1/* Redis CLI (command line interface)2 *3 * Copyright (c) 2009-Present, Redis Ltd.4 * All rights reserved.5 *6 * Licensed under your choice of (a) the Redis Source Available License 2.07 * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the8 * GNU Affero General Public License v3 (AGPLv3).9 */1011#include "fmacros.h"1213#include <stdarg.h>14#include <stdio.h>15#include <string.h>16#include <stdlib.h>17#include <signal.h>18#include <unistd.h>19#include <time.h>20#include <ctype.h>21#include <errno.h>22#include <sys/stat.h>23#include <sys/time.h>24#include <assert.h>25#include <fcntl.h>26#include <limits.h>27#include <math.h>28#include <termios.h>2930#include <hiredis.h>31#ifdef USE_OPENSSL32#include <openssl/ssl.h>33#include <openssl/err.h>34#include <hiredis_ssl.h>35#endif36#include <sdscompat.h> /* Use hiredis' sds compat header that maps sds calls to their hi_ variants */37#include <sds.h> /* use sds.h from hiredis, so that only one set of sds functions will be present in the binary */38#include "dict.h"39#include "adlist.h"40#include "zmalloc.h"41#include "linenoise.h"42#include "anet.h"43#include "ae.h"44#include "connection.h"45#include "cli_common.h"46#include "mt19937-64.h"47#include "cli_commands.h"48#include "hdr_histogram.h"4950#define UNUSED(V) ((void) V)5152#define OUTPUT_STANDARD 053#define OUTPUT_RAW 154#define OUTPUT_CSV 255#define OUTPUT_JSON 356#define OUTPUT_QUOTED_JSON 457#define REDIS_CLI_KEEPALIVE_INTERVAL 15 /* seconds */58#define REDIS_CLI_DEFAULT_PIPE_TIMEOUT 30 /* seconds */59#define REDIS_CLI_HISTFILE_ENV "REDISCLI_HISTFILE"60#define REDIS_CLI_HISTFILE_DEFAULT ".rediscli_history"61#define REDIS_CLI_RCFILE_ENV "REDISCLI_RCFILE"62#define REDIS_CLI_RCFILE_DEFAULT ".redisclirc"63#define REDIS_CLI_AUTH_ENV "REDISCLI_AUTH"64#define REDIS_CLI_CLUSTER_YES_ENV "REDISCLI_CLUSTER_YES"6566#define CLUSTER_MANAGER_SLOTS 1638467#define CLUSTER_MANAGER_PORT_INCR 10000 /* same as CLUSTER_PORT_INCR */68#define CLUSTER_MANAGER_MIGRATE_TIMEOUT 6000069#define CLUSTER_MANAGER_ASM_MIGRATE_TIMEOUT 3600000 /* 60 minutes */70#define CLUSTER_MANAGER_MIGRATE_PIPELINE 1071#define CLUSTER_MANAGER_REBALANCE_THRESHOLD 272/* CLUSTER MIGRATION, used by ASM, is available starting with Redis 8.4.0. */73#define CLUSTER_MANAGER_ASM_MIN_VERSION "8.4.0"7475#define CLUSTER_MANAGER_INVALID_HOST_ARG \76 "[ERR] Invalid arguments: you need to pass either a valid " \77 "address (ie. 120.0.0.1:7000 or [::1]:7000) or space " \78 "separated IP and port (ie. 120.0.0.1 7000)\n"79#define CLUSTER_MANAGER_MODE() (config.cluster_manager_command.name != NULL)80#define CLUSTER_MANAGER_MASTERS_COUNT(nodes, replicas) ((nodes)/((replicas) + 1))81#define CLUSTER_MANAGER_COMMAND(n,...) \82 (redisCommand((n)->context, __VA_ARGS__))8384#define CLUSTER_MANAGER_NODE_ARRAY_FREE(array) zfree((array)->alloc)8586#define CLUSTER_MANAGER_PRINT_REPLY_ERROR(n, err) \87 clusterManagerLogErr("Node %s:%d replied with error:\n%s\n", \88 (n)->ip, (n)->port, (err));8990#define clusterManagerLogInfo(...) \91 clusterManagerLog(CLUSTER_MANAGER_LOG_LVL_INFO,__VA_ARGS__)9293#define clusterManagerLogErr(...) \94 clusterManagerLog(CLUSTER_MANAGER_LOG_LVL_ERR,__VA_ARGS__)9596#define clusterManagerLogWarn(...) \97 clusterManagerLog(CLUSTER_MANAGER_LOG_LVL_WARN,__VA_ARGS__)9899#define clusterManagerLogOk(...) \100 clusterManagerLog(CLUSTER_MANAGER_LOG_LVL_SUCCESS,__VA_ARGS__)101102#define CLUSTER_MANAGER_FLAG_MYSELF 1 << 0103#define CLUSTER_MANAGER_FLAG_SLAVE 1 << 1104#define CLUSTER_MANAGER_FLAG_FRIEND 1 << 2105#define CLUSTER_MANAGER_FLAG_NOADDR 1 << 3106#define CLUSTER_MANAGER_FLAG_DISCONNECT 1 << 4107#define CLUSTER_MANAGER_FLAG_FAIL 1 << 5108109#define CLUSTER_MANAGER_CMD_FLAG_FIX 1 << 0110#define CLUSTER_MANAGER_CMD_FLAG_SLAVE 1 << 1111#define CLUSTER_MANAGER_CMD_FLAG_YES 1 << 2112#define CLUSTER_MANAGER_CMD_FLAG_AUTOWEIGHTS 1 << 3113#define CLUSTER_MANAGER_CMD_FLAG_EMPTYMASTER 1 << 4114#define CLUSTER_MANAGER_CMD_FLAG_SIMULATE 1 << 5115#define CLUSTER_MANAGER_CMD_FLAG_REPLACE 1 << 6116#define CLUSTER_MANAGER_CMD_FLAG_COPY 1 << 7117#define CLUSTER_MANAGER_CMD_FLAG_COLOR 1 << 8118#define CLUSTER_MANAGER_CMD_FLAG_CHECK_OWNERS 1 << 9119#define CLUSTER_MANAGER_CMD_FLAG_FIX_WITH_UNREACHABLE_MASTERS 1 << 10120#define CLUSTER_MANAGER_CMD_FLAG_MASTERS_ONLY 1 << 11121#define CLUSTER_MANAGER_CMD_FLAG_SLAVES_ONLY 1 << 12122#define CLUSTER_MANAGER_CMD_FLAG_TIMEOUT 1 << 13123124#define CLUSTER_MANAGER_OPT_GETFRIENDS 1 << 0125#define CLUSTER_MANAGER_OPT_COLD 1 << 1126#define CLUSTER_MANAGER_OPT_UPDATE 1 << 2127#define CLUSTER_MANAGER_OPT_QUIET 1 << 6128#define CLUSTER_MANAGER_OPT_VERBOSE 1 << 7129130#define CLUSTER_MANAGER_LOG_LVL_INFO 1131#define CLUSTER_MANAGER_LOG_LVL_WARN 2132#define CLUSTER_MANAGER_LOG_LVL_ERR 3133#define CLUSTER_MANAGER_LOG_LVL_SUCCESS 4134135#define CLUSTER_JOIN_CHECK_AFTER 20136137#define LOG_COLOR_BOLD "29;1m"138#define LOG_COLOR_RED "31;1m"139#define LOG_COLOR_GREEN "32;1m"140#define LOG_COLOR_YELLOW "33;1m"141#define LOG_COLOR_RESET "0m"142143/* cliConnect() flags. */144#define CC_FORCE (1<<0) /* Re-connect if already connected. */145#define CC_QUIET (1<<1) /* Don't log connecting errors. */146147/* DNS lookup */148#define NET_IP_STR_LEN 46 /* INET6_ADDRSTRLEN is 46 */149150#define REFRESH_INTERVAL 300 /* milliseconds */151152#define IS_TTY_OR_FAKETTY() (isatty(STDOUT_FILENO) || getenv("FAKETTY"))153154/* --latency-dist palettes. */155int spectrum_palette_color_size = 19;156int spectrum_palette_color[] = {0,233,234,235,237,239,241,243,245,247,144,143,142,184,226,214,208,202,196};157158int spectrum_palette_mono_size = 13;159int spectrum_palette_mono[] = {0,233,234,235,237,239,241,243,245,247,249,251,253};160161/* The actual palette in use. */162int *spectrum_palette;163int spectrum_palette_size;164165static int orig_termios_saved = 0;166static struct termios orig_termios; /* To restore terminal at exit.*/167168/* Dict Helpers */169static uint64_t dictSdsHash(const void *key);170static int dictSdsKeyCompare(dictCmpCache *cache, const void *key1,171 const void *key2);172static void dictSdsDestructor(dict *d, void *val);173static void dictListDestructor(dict *d, void *val);174175/* Cluster Manager Command Info */176typedef struct clusterManagerCommand {177 char *name;178 int argc;179 char **argv;180 sds stdin_arg; /* arg from stdin. (-X option) */181 int flags;182 int replicas;183 char *from;184 char *to;185 char **weight;186 int weight_argc;187 char *master_id;188 int slots;189 int timeout;190 int pipeline;191 float threshold;192 char *backup_dir;193 char *from_user;194 char *from_pass;195 int from_askpass;196} clusterManagerCommand;197198static int createClusterManagerCommand(char *cmdname, int argc, char **argv);199200201static redisContext *context;202static struct config {203 cliConnInfo conn_info;204 struct timeval connect_timeout;205 char *hostsocket;206 int tls;207 cliSSLconfig sslconfig;208 long repeat;209 long interval;210 int dbnum; /* db num currently selected */211 int interactive;212 int shutdown;213 int monitor_mode;214 int pubsub_mode;215 int blocking_state_aborted; /* used to abort monitor_mode and pubsub_mode. */216 int vset_recall_mode;217 sds vset_recall_key;218 int vset_recall_ele_count;219 int vset_recall_vsim_count;220 int vset_recall_vsim_ef;221 int latency_mode;222 int latency_dist_mode;223 int latency_history;224 double *latency_percentiles; /* Percentiles to report, e.g. 50,99,99.9. */225 char **latency_percentiles_labels; /* Original tokens, used for display. */226 int latency_percentiles_count;227 int lru_test_mode;228 long long lru_test_sample_size;229 int cluster_mode;230 int cluster_reissue_command;231 int cluster_send_asking;232 int slave_mode;233 int pipe_mode;234 int pipe_timeout;235 int getrdb_mode;236 int get_functions_rdb_mode;237 int stat_mode;238 int scan_mode;239 int count;240 int intrinsic_latency_mode;241 int intrinsic_latency_duration;242 sds pattern;243 char *rdb_filename;244 int bigkeys;245 int memkeys;246 long long memkeys_samples;247 int hotkeys;248 int keystats;249 unsigned long long cursor;250 unsigned long top_sizes_limit;251 int stdin_lastarg; /* get last arg from stdin. (-x option) */252 int stdin_tag_arg; /* get <tag> arg from stdin. (-X option) */253 char *stdin_tag_name; /* Placeholder(tag name) for user input. */254 int askpass;255 int quoted_input; /* Force input args to be treated as quoted strings */256 int output; /* output mode, see OUTPUT_* defines */257 int push_output; /* Should we display spontaneous PUSH replies */258 sds mb_delim;259 sds cmd_delim;260 char prompt[128];261 char *eval;262 int eval_ldb;263 int eval_ldb_sync; /* Ask for synchronous mode of the Lua debugger. */264 int eval_ldb_end; /* Lua debugging session ended. */265 int enable_ldb_on_eval; /* Handle manual SCRIPT DEBUG + EVAL commands. */266 int last_cmd_type;267 redisReply *last_reply;268 int verbose;269 int set_errcode;270 clusterManagerCommand cluster_manager_command;271 int no_auth_warning;272 int resp2; /* value of 1: specified explicitly with option -2 */273 int resp3; /* value of 1: specified explicitly, value of 2: implicit like --json option */274 int current_resp3; /* 1 if we have RESP3 right now in the current connection. */275 int in_multi;276 int pre_multi_dbnum;277 char *server_version;278 char *test_hint;279 char *test_hint_file;280 char *client_name;281 int prefer_ipv4; /* Prefer IPv4 over IPv6 on DNS lookup. */282 int prefer_ipv6; /* Prefer IPv6 over IPv4 on DNS lookup. */283} config;284285/* User preferences. */286static struct pref {287 int hints;288} pref;289290static volatile sig_atomic_t force_cancel_loop = 0;291static void usage(int err);292static void slaveMode(int send_sync);293static int cliConnect(int flags);294295static char *getInfoField(char *info, char *field);296static long getLongInfoField(char *info, char *field);297298/*------------------------------------------------------------------------------299 * Utility functions300 *--------------------------------------------------------------------------- */301size_t redis_strlcpy(char *dst, const char *src, size_t dsize);302303static void cliPushHandler(void *, void *);304305uint16_t crc16(const char *buf, int len);306307static long long ustime(void) {308 struct timeval tv;309 long long ust;310311 gettimeofday(&tv, NULL);312 ust = ((long long)tv.tv_sec)*1000000;313 ust += tv.tv_usec;314 return ust;315}316317static long long mstime(void) {318 return ustime()/1000;319}320321static void cliRefreshPrompt(void) {322 if (config.eval_ldb) return;323324 sds prompt = sdsempty();325 if (config.hostsocket != NULL) {326 prompt = sdscatfmt(prompt,"redis %s",config.hostsocket);327 } else {328 char addr[256];329 formatAddr(addr, sizeof(addr), config.conn_info.hostip, config.conn_info.hostport);330 prompt = sdscatlen(prompt,addr,strlen(addr));331 }332333 /* Add [dbnum] if needed */334 if (config.dbnum != 0)335 prompt = sdscatfmt(prompt,"[%i]",config.dbnum);336337 /* Add TX if in transaction state*/338 if (config.in_multi) 339 prompt = sdscatlen(prompt,"(TX)",4);340341 if (config.pubsub_mode)342 prompt = sdscatfmt(prompt,"(subscribed mode)");343344 /* Copy the prompt in the static buffer. */345 prompt = sdscatlen(prompt,"> ",2);346 snprintf(config.prompt,sizeof(config.prompt),"%s",prompt);347 sdsfree(prompt);348}349350/* Return the name of the dotfile for the specified 'dotfilename'.351 * Normally it just concatenates user $HOME to the file specified352 * in 'dotfilename'. However if the environment variable 'envoverride'353 * is set, its value is taken as the path.354 *355 * The function returns NULL (if the file is /dev/null or cannot be356 * obtained for some error), or an SDS string that must be freed by357 * the user. */358static sds getDotfilePath(char *envoverride, char *dotfilename) {359 char *path = NULL;360 sds dotPath = NULL;361362 /* Check the env for a dotfile override. */363 path = getenv(envoverride);364 if (path != NULL && *path != '\0') {365 if (!strcmp("/dev/null", path)) {366 return NULL;367 }368369 /* If the env is set, return it. */370 dotPath = sdsnew(path);371 } else {372 char *home = getenv("HOME");373 if (home != NULL && *home != '\0') {374 /* If no override is set use $HOME/<dotfilename>. */375 dotPath = sdscatprintf(sdsempty(), "%s/%s", home, dotfilename);376 }377 }378 return dotPath;379}380381static uint64_t dictSdsHash(const void *key) {382 return dictGenHashFunction((unsigned char*)key, sdslen((char*)key));383}384385static int dictSdsKeyCompare(dictCmpCache *cache, const void *key1, const void *key2)386{387 int l1,l2;388 UNUSED(cache);389390 l1 = sdslen((sds)key1);391 l2 = sdslen((sds)key2);392 if (l1 != l2) return 0;393 return memcmp(key1, key2, l1) == 0;394}395396static void dictSdsDestructor(dict *d, void *val)397{398 UNUSED(d);399 sdsfree(val);400}401402void dictListDestructor(dict *d, void *val)403{404 UNUSED(d);405 listRelease((list*)val);406}407408/* Erase the lines before printing, and returns the number of lines printed */409int cleanPrintfln(char *fmt, ...) {410 va_list args;411 char buf[1024]; /* limitation */412 int char_count, line_count = 0;413414 /* Clear the line if in TTY */415 if (IS_TTY_OR_FAKETTY()) {416 printf("\033[2K\r");417 }418419 va_start(args, fmt);420 char_count = vsnprintf(buf, sizeof(buf), fmt, args);421 va_end(args);422423 if (char_count >= (int)sizeof(buf)) {424 fprintf(stderr, "Warning: String was trimmed in cleanPrintln\n");425 }426427 char *position, *string = buf;428 while ((position = strchr(string, '\n')) != NULL) {429 int line_length = (int)(position - string);430 printf("%.*s\n", line_length, string);431 string = position + 1;432 line_count++;433 }434435 printf("%s\n", string);436 return line_count + 1;437}438439/*------------------------------------------------------------------------------440 * Help functions441 *--------------------------------------------------------------------------- */442443#define CLI_HELP_COMMAND 1444#define CLI_HELP_GROUP 2445446typedef struct {447 int type;448 int argc;449 sds *argv;450 sds full;451452 /* Only used for help on commands */453 struct commandDocs docs;454} helpEntry;455456static helpEntry *helpEntries = NULL;457static int helpEntriesLen = 0;458459/* For backwards compatibility with pre-7.0 servers.460 * cliLegacyInitHelp() sets up the helpEntries array with the command and group461 * names from the commands.c file. However the Redis instance we are connecting462 * to may support more commands, so this function integrates the previous463 * entries with additional entries obtained using the COMMAND command464 * available in recent versions of Redis. */465static void cliLegacyIntegrateHelp(void) {466 if (cliConnect(CC_QUIET) == REDIS_ERR) return;467468 redisReply *reply = redisCommand(context, "COMMAND");469 if (reply == NULL) return;470 if (reply->type != REDIS_REPLY_ARRAY) {471 freeReplyObject(reply);472 return;473 }474475 /* Scan the array reported by COMMAND and fill only the entries that476 * don't already match what we have. */477 for (size_t j = 0; j < reply->elements; j++) {478 redisReply *entry = reply->element[j];479 if (entry->type != REDIS_REPLY_ARRAY || entry->elements < 4 ||480 entry->element[0]->type != REDIS_REPLY_STRING ||481 entry->element[1]->type != REDIS_REPLY_INTEGER ||482 entry->element[3]->type != REDIS_REPLY_INTEGER) break;483 char *cmdname = entry->element[0]->str;484 int i;485486 for (i = 0; i < helpEntriesLen; i++) {487 helpEntry *he = helpEntries+i;488 if (!strcasecmp(he->argv[0],cmdname))489 break;490 }491 if (i != helpEntriesLen) continue;492493 helpEntriesLen++;494 helpEntries = zrealloc(helpEntries,sizeof(helpEntry)*helpEntriesLen);495 helpEntry *new = helpEntries+(helpEntriesLen-1);496497 new->argc = 1;498 new->argv = zmalloc(sizeof(sds));499 new->argv[0] = sdsnew(cmdname);500 new->full = new->argv[0];501 new->type = CLI_HELP_COMMAND;502 sdstoupper(new->argv[0]);503504 new->docs.name = new->argv[0];505 new->docs.args = NULL;506 new->docs.numargs = 0;507 new->docs.params = sdsempty();508 int args = llabs(entry->element[1]->integer);509 args--; /* Remove the command name itself. */510 if (entry->element[3]->integer == 1) {511 new->docs.params = sdscat(new->docs.params,"key ");512 args--;513 }514 while(args-- > 0) new->docs.params = sdscat(new->docs.params,"arg ");515 if (entry->element[1]->integer < 0)516 new->docs.params = sdscat(new->docs.params,"...options...");517 new->docs.summary = "Help not available";518 new->docs.since = "Not known";519 new->docs.group = "generic";520 }521 freeReplyObject(reply);522}523524/* Concatenate a string to an sds string, but if it's empty substitute double quote marks. */525static sds sdscat_orempty(sds params, const char *value) {526 if (value[0] == '\0') {527 return sdscat(params, "\"\"");528 }529 return sdscat(params, value);530}531532static sds makeHint(char **inputargv, int inputargc, int cmdlen, struct commandDocs docs);533534static void cliAddCommandDocArg(cliCommandArg *cmdArg, redisReply *argMap);535536static void cliMakeCommandDocArgs(redisReply *arguments, cliCommandArg *result) {537 for (size_t j = 0; j < arguments->elements; j++) {538 cliAddCommandDocArg(&result[j], arguments->element[j]);539 }540}541542static void cliAddCommandDocArg(cliCommandArg *cmdArg, redisReply *argMap) {543 if (argMap->type != REDIS_REPLY_MAP && argMap->type != REDIS_REPLY_ARRAY) {544 return;545 }546547 for (size_t i = 0; i < argMap->elements; i += 2) {548 assert(argMap->element[i]->type == REDIS_REPLY_STRING);549 char *key = argMap->element[i]->str;550 if (!strcmp(key, "name")) {551 assert(argMap->element[i + 1]->type == REDIS_REPLY_STRING);552 cmdArg->name = sdsnew(argMap->element[i + 1]->str);553 } else if (!strcmp(key, "display_text")) {554 assert(argMap->element[i + 1]->type == REDIS_REPLY_STRING);555 cmdArg->display_text = sdsnew(argMap->element[i + 1]->str);556 } else if (!strcmp(key, "token")) {557 assert(argMap->element[i + 1]->type == REDIS_REPLY_STRING);558 cmdArg->token = sdsnew(argMap->element[i + 1]->str);559 } else if (!strcmp(key, "type")) {560 assert(argMap->element[i + 1]->type == REDIS_REPLY_STRING);561 char *type = argMap->element[i + 1]->str;562 if (!strcmp(type, "string")) {563 cmdArg->type = ARG_TYPE_STRING;564 } else if (!strcmp(type, "integer")) {565 cmdArg->type = ARG_TYPE_INTEGER;566 } else if (!strcmp(type, "double")) {567 cmdArg->type = ARG_TYPE_DOUBLE;568 } else if (!strcmp(type, "key")) {569 cmdArg->type = ARG_TYPE_KEY;570 } else if (!strcmp(type, "pattern")) {571 cmdArg->type = ARG_TYPE_PATTERN;572 } else if (!strcmp(type, "unix-time")) {573 cmdArg->type = ARG_TYPE_UNIX_TIME;574 } else if (!strcmp(type, "pure-token")) {575 cmdArg->type = ARG_TYPE_PURE_TOKEN;576 } else if (!strcmp(type, "oneof")) {577 cmdArg->type = ARG_TYPE_ONEOF;578 } else if (!strcmp(type, "block")) {579 cmdArg->type = ARG_TYPE_BLOCK;580 }581 } else if (!strcmp(key, "arguments")) {582 redisReply *arguments = argMap->element[i + 1];583 cmdArg->subargs = zcalloc(arguments->elements * sizeof(cliCommandArg));584 cmdArg->numsubargs = arguments->elements;585 cliMakeCommandDocArgs(arguments, cmdArg->subargs);586 } else if (!strcmp(key, "flags")) {587 redisReply *flags = argMap->element[i + 1];588 assert(flags->type == REDIS_REPLY_SET || flags->type == REDIS_REPLY_ARRAY);589 for (size_t j = 0; j < flags->elements; j++) {590 assert(flags->element[j]->type == REDIS_REPLY_STATUS);591 char *flag = flags->element[j]->str;592 if (!strcmp(flag, "optional")) {593 cmdArg->flags |= CMD_ARG_OPTIONAL;594 } else if (!strcmp(flag, "multiple")) {595 cmdArg->flags |= CMD_ARG_MULTIPLE;596 } else if (!strcmp(flag, "multiple_token")) {597 cmdArg->flags |= CMD_ARG_MULTIPLE_TOKEN;598 }599 }600 }601 }602}603604/* Fill in the fields of a help entry for the command/subcommand name. */605static void cliFillInCommandHelpEntry(helpEntry *help, char *cmdname, char *subcommandname) {606 help->argc = subcommandname ? 2 : 1;607 help->argv = zmalloc(sizeof(sds) * help->argc);608 help->argv[0] = sdsnew(cmdname);609 sdstoupper(help->argv[0]);610 if (subcommandname) {611 /* Subcommand name may be two words separated by a pipe character. */612 char *pipe = strchr(subcommandname, '|');613 if (pipe != NULL) {614 help->argv[1] = sdsnew(pipe + 1);615 } else {616 help->argv[1] = sdsnew(subcommandname);617 }618 sdstoupper(help->argv[1]);619 }620 sds fullname = sdsnew(help->argv[0]);621 if (subcommandname) {622 fullname = sdscat(fullname, " ");623 fullname = sdscat(fullname, help->argv[1]);624 }625 help->full = fullname;626 help->type = CLI_HELP_COMMAND;627628 help->docs.name = help->full;629 help->docs.params = NULL;630 help->docs.args = NULL;631 help->docs.numargs = 0;632 help->docs.since = NULL;633}634635/* Initialize a command help entry for the command/subcommand described in 'specs'.636 * 'next' points to the next help entry to be filled in.637 * 'groups' is a set of command group names to be filled in.638 * Returns a pointer to the next available position in the help entries table.639 * If the command has subcommands, this is called recursively for the subcommands.640 */641static helpEntry *cliInitCommandHelpEntry(char *cmdname, char *subcommandname,642 helpEntry *next, redisReply *specs,643 dict *groups) {644 helpEntry *help = next++;645 cliFillInCommandHelpEntry(help, cmdname, subcommandname);646647 assert(specs->type == REDIS_REPLY_MAP || specs->type == REDIS_REPLY_ARRAY);648 for (size_t j = 0; j < specs->elements; j += 2) {649 assert(specs->element[j]->type == REDIS_REPLY_STRING);650 char *key = specs->element[j]->str;651 if (!strcmp(key, "summary")) {652 redisReply *reply = specs->element[j + 1];653 assert(reply->type == REDIS_REPLY_STRING);654 help->docs.summary = sdsnew(reply->str);655 } else if (!strcmp(key, "since")) {656 redisReply *reply = specs->element[j + 1];657 assert(reply->type == REDIS_REPLY_STRING);658 help->docs.since = sdsnew(reply->str);659 } else if (!strcmp(key, "group")) {660 redisReply *reply = specs->element[j + 1];661 assert(reply->type == REDIS_REPLY_STRING);662 help->docs.group = sdsnew(reply->str);663 sds group = sdsdup(help->docs.group);664 if (dictAdd(groups, group, NULL) != DICT_OK) {665 sdsfree(group);666 }667 } else if (!strcmp(key, "arguments")) {668 redisReply *arguments = specs->element[j + 1];669 assert(arguments->type == REDIS_REPLY_ARRAY);670 help->docs.args = zcalloc(arguments->elements * sizeof(cliCommandArg));671 help->docs.numargs = arguments->elements;672 cliMakeCommandDocArgs(arguments, help->docs.args);673 help->docs.params = makeHint(NULL, 0, 0, help->docs);674 } else if (!strcmp(key, "subcommands")) {675 redisReply *subcommands = specs->element[j + 1];676 assert(subcommands->type == REDIS_REPLY_MAP || subcommands->type == REDIS_REPLY_ARRAY);677 for (size_t i = 0; i < subcommands->elements; i += 2) {678 assert(subcommands->element[i]->type == REDIS_REPLY_STRING);679 char *subcommandname = subcommands->element[i]->str;680 redisReply *subcommand = subcommands->element[i + 1];681 assert(subcommand->type == REDIS_REPLY_MAP || subcommand->type == REDIS_REPLY_ARRAY);682 next = cliInitCommandHelpEntry(cmdname, subcommandname, next, subcommand, groups);683 }684 }685 }686 return next;687}688689/* Returns the total number of commands and subcommands in the command docs table. */690static size_t cliCountCommands(redisReply* commandTable) {691 size_t numCommands = commandTable->elements / 2;692693 /* The command docs table maps command names to a map of their specs. */ 694 for (size_t i = 0; i < commandTable->elements; i += 2) {695 assert(commandTable->element[i]->type == REDIS_REPLY_STRING); /* Command name. */696 assert(commandTable->element[i + 1]->type == REDIS_REPLY_MAP ||697 commandTable->element[i + 1]->type == REDIS_REPLY_ARRAY);698 redisReply *map = commandTable->element[i + 1];699 for (size_t j = 0; j < map->elements; j += 2) {700 assert(map->element[j]->type == REDIS_REPLY_STRING);701 char *key = map->element[j]->str;702 if (!strcmp(key, "subcommands")) {703 redisReply *subcommands = map->element[j + 1];704 assert(subcommands->type == REDIS_REPLY_MAP || subcommands->type == REDIS_REPLY_ARRAY);705 numCommands += subcommands->elements / 2;706 }707 }708 }709 return numCommands;710}711712/* Comparator for sorting help table entries. */713int helpEntryCompare(const void *entry1, const void *entry2) {714 helpEntry *i1 = (helpEntry *)entry1;715 helpEntry *i2 = (helpEntry *)entry2;716 return strcmp(i1->full, i2->full);717}718719/* Initializes command help entries for command groups.720 * Called after the command help entries have already been filled in.721 * Extends the help table with new entries for the command groups.722 */723void cliInitGroupHelpEntries(dict *groups) {724 dictIterator iter;725 dictEntry *entry;726 helpEntry tmp;727728 int numGroups = dictSize(groups);729 int pos = helpEntriesLen;730 helpEntriesLen += numGroups;731 helpEntries = zrealloc(helpEntries, sizeof(helpEntry)*helpEntriesLen);732733 dictInitIterator(&iter, groups);734 for (entry = dictNext(&iter); entry != NULL; entry = dictNext(&iter)) {735 tmp.argc = 1;736 tmp.argv = zmalloc(sizeof(sds));737 tmp.argv[0] = sdscatprintf(sdsempty(),"@%s",(char *)dictGetKey(entry));738 tmp.full = tmp.argv[0];739 tmp.type = CLI_HELP_GROUP;740 tmp.docs.name = NULL;741 tmp.docs.params = NULL;742 tmp.docs.args = NULL;743 tmp.docs.numargs = 0;744 tmp.docs.summary = NULL;745 tmp.docs.since = NULL;746 tmp.docs.group = NULL;747 helpEntries[pos++] = tmp;748 }749 dictResetIterator(&iter);750}751752/* Initializes help entries for all commands in the COMMAND DOCS reply. */753void cliInitCommandHelpEntries(redisReply *commandTable, dict *groups) {754 helpEntry *next = helpEntries;755 for (size_t i = 0; i < commandTable->elements; i += 2) {756 assert(commandTable->element[i]->type == REDIS_REPLY_STRING);757 char *cmdname = commandTable->element[i]->str;758759 assert(commandTable->element[i + 1]->type == REDIS_REPLY_MAP ||760 commandTable->element[i + 1]->type == REDIS_REPLY_ARRAY);761 redisReply *cmdspecs = commandTable->element[i + 1];762 next = cliInitCommandHelpEntry(cmdname, NULL, next, cmdspecs, groups);763 }764}765766/* Does the server version support a command/argument only available "since" some version?767 * Returns 1 when supported, or 0 when the "since" version is newer than "version". */768static int versionIsSupported(sds version, sds since) {769 int i;770 char *versionPos = version;771 char *sincePos = since;772 if (!since) {773 return 1;774 }775776 for (i = 0; i != 3; i++) {777 int versionPart = atoi(versionPos);778 int sincePart = atoi(sincePos);779 if (versionPart > sincePart) {780 return 1;781 } else if (sincePart > versionPart) {782 return 0;783 }784 versionPos = strchr(versionPos, '.');785 sincePos = strchr(sincePos, '.');786787 /* If we finished to parse both `version` and `since`, it means they are equal */788 if (!versionPos && !sincePos) return 1;789790 /* Different number of digits considered as not supported */791 if (!versionPos || !sincePos) return 0;792793 versionPos++;794 sincePos++;795 }796 return 0;797}798799static void removeUnsupportedArgs(struct cliCommandArg *args, int *numargs, sds version) {800 int i = 0, j;801 while (i != *numargs) {802 if (versionIsSupported(version, args[i].since)) {803 if (args[i].subargs) {804 removeUnsupportedArgs(args[i].subargs, &args[i].numsubargs, version);805 }806 i++;807 continue;808 }809 for (j = i; j != *numargs - 1; j++) {810 args[j] = args[j + 1];811 }812 (*numargs)--;813 }814}815816static helpEntry *cliLegacyInitCommandHelpEntry(char *cmdname, char *subcommandname,817 helpEntry *next, struct commandDocs *command,818 dict *groups, sds version) {819 helpEntry *help = next++;820 cliFillInCommandHelpEntry(help, cmdname, subcommandname);821 822 help->docs.summary = sdsnew(command->summary);823 help->docs.since = sdsnew(command->since);824 help->docs.group = sdsnew(command->group);825 sds group = sdsdup(help->docs.group);826 if (dictAdd(groups, group, NULL) != DICT_OK) {827 sdsfree(group);828 }829830 if (command->args != NULL) {831 help->docs.args = command->args;832 help->docs.numargs = command->numargs;833 if (version)834 removeUnsupportedArgs(help->docs.args, &help->docs.numargs, version);835 help->docs.params = makeHint(NULL, 0, 0, help->docs);836 }837838 if (command->subcommands != NULL) {839 for (size_t i = 0; command->subcommands[i].name != NULL; i++) {840 if (!version || versionIsSupported(version, command->subcommands[i].since)) {841 char *subcommandname = command->subcommands[i].name;842 next = cliLegacyInitCommandHelpEntry(843 cmdname, subcommandname, next, &command->subcommands[i], groups, version);844 }845 }846 }847 return next;848}849850int cliLegacyInitCommandHelpEntries(struct commandDocs *commands, dict *groups, sds version) {851 helpEntry *next = helpEntries;852 for (size_t i = 0; commands[i].name != NULL; i++) {853 if (!version || versionIsSupported(version, commands[i].since)) {854 next = cliLegacyInitCommandHelpEntry(commands[i].name, NULL, next, &commands[i], groups, version);855 }856 }857 return next - helpEntries;858}859860/* Returns the total number of commands and subcommands in the command docs table,861 * filtered by server version (if provided).862 */863static size_t cliLegacyCountCommands(struct commandDocs *commands, sds version) {864 int numCommands = 0;865 for (size_t i = 0; commands[i].name != NULL; i++) {866 if (version && !versionIsSupported(version, commands[i].since)) {867 continue;868 }869 numCommands++;870 if (commands[i].subcommands != NULL) {871 numCommands += cliLegacyCountCommands(commands[i].subcommands, version);872 }873 }874 return numCommands;875}876877/* Gets the server version string from the given context by calling INFO SERVER.878 * The caller owns the returned SDS. When not connected, or not possible,879 * returns NULL. */880static sds cliGetServerVersionFromContext(redisContext *ctx) {881 static const char *key = "\nredis_version:";882 redisReply *serverInfo = NULL;883 char *pos;884885 if (!ctx) return NULL;886 serverInfo = redisCommand(ctx, "INFO SERVER");887 if (serverInfo == NULL || serverInfo->type == REDIS_REPLY_ERROR) {888 freeReplyObject(serverInfo);889 return sdsempty();890 }891892 assert(serverInfo->type == REDIS_REPLY_STRING || serverInfo->type == REDIS_REPLY_VERB);893 sds info = serverInfo->str;894895 /* Finds the first appearance of "redis_version" in the INFO SERVER reply. */896 pos = strstr(info, key);897 if (pos) {898 pos += strlen(key);899 char *end = strchr(pos, '\r');900 if (end) {901 sds version = sdsnewlen(pos, end - pos);902 freeReplyObject(serverInfo);903 return version;904 }905 }906 freeReplyObject(serverInfo);907 return NULL;908}909910/* Gets the server version string by calling INFO SERVER.911 * Stores the result in config.server_version.912 * When not connected, or not possible, returns NULL. */913static sds cliGetServerVersion(void) {914 if (config.server_version != NULL) {915 return config.server_version;916 }917918 sds version = cliGetServerVersionFromContext(context);919 if (version != NULL && sdslen(version) != 0) {920 config.server_version = version;921 }922 return version;923}924925static void cliLegacyInitHelp(dict *groups) {926 sds serverVersion = cliGetServerVersion();927 928 /* Scan the commandDocs array and fill in the entries */929 helpEntriesLen = cliLegacyCountCommands(redisCommandTable, serverVersion);930 helpEntries = zmalloc(sizeof(helpEntry)*helpEntriesLen);931932 helpEntriesLen = cliLegacyInitCommandHelpEntries(redisCommandTable, groups, serverVersion);933 cliInitGroupHelpEntries(groups);934935 qsort(helpEntries, helpEntriesLen, sizeof(helpEntry), helpEntryCompare);936 dictRelease(groups);937}938939/* cliInitHelp() sets up the helpEntries array with the command and group940 * names and command descriptions obtained using the COMMAND DOCS command.941 */942static void cliInitHelp(void) {943 /* Dict type for a set of strings, used to collect names of command groups. */944 dictType groupsdt = {945 dictSdsHash, /* hash function */946 NULL, /* key dup */947 NULL, /* val dup */948 dictSdsKeyCompare, /* key compare */949 dictSdsDestructor, /* key destructor */950 NULL, /* val destructor */951 NULL /* allow to expand */952 };953 redisReply *commandTable;954 dict *groups;955956 if (cliConnect(CC_QUIET) == REDIS_ERR) {957 /* Can not connect to the server, but we still want to provide958 * help, generate it only from the static cli_commands.c data instead. */959 groups = dictCreate(&groupsdt);960 cliLegacyInitHelp(groups);961 return;962 }963 commandTable = redisCommand(context, "COMMAND DOCS");964 if (commandTable == NULL || commandTable->type == REDIS_REPLY_ERROR) {965 /* New COMMAND DOCS subcommand not supported - generate help from966 * static cli_commands.c data instead. */967 freeReplyObject(commandTable);968969 groups = dictCreate(&groupsdt);970 cliLegacyInitHelp(groups);971 cliLegacyIntegrateHelp();972 return;973 };974 if (commandTable->type != REDIS_REPLY_MAP && commandTable->type != REDIS_REPLY_ARRAY) {975 freeReplyObject(commandTable);976 return;977 }978979 /* Scan the array reported by COMMAND DOCS and fill in the entries */980 helpEntriesLen = cliCountCommands(commandTable);981 helpEntries = zmalloc(sizeof(helpEntry)*helpEntriesLen);982983 groups = dictCreate(&groupsdt);984 cliInitCommandHelpEntries(commandTable, groups);985 cliInitGroupHelpEntries(groups);986987 qsort(helpEntries, helpEntriesLen, sizeof(helpEntry), helpEntryCompare);988 freeReplyObject(commandTable);989 dictRelease(groups);990}991992/* Output command help to stdout. */993static void cliOutputCommandHelp(struct commandDocs *help, int group) {994 printf("\r\n \x1b[1m%s\x1b[0m \x1b[90m%s\x1b[0m\r\n", help->name, help->params);995 printf(" \x1b[33msummary:\x1b[0m %s\r\n", help->summary);996 if (help->since != NULL) {997 printf(" \x1b[33msince:\x1b[0m %s\r\n", help->since);998 }999 if (group) {1000 printf(" \x1b[33mgroup:\x1b[0m %s\r\n", help->group);1001 }1002}10031004/* Print generic help. */1005static void cliOutputGenericHelp(void) {1006 sds version = cliVersion();1007 printf(1008 "redis-cli %s\n"1009 "To get help about Redis commands type:\n"1010 " \"help @<group>\" to get a list of commands in <group>\n"1011 " \"help <command>\" for help on <command>\n"1012 " \"help <tab>\" to get a list of possible help topics\n"1013 " \"quit\" to exit\n"1014 "\n"1015 "To set redis-cli preferences:\n"1016 " \":set hints\" enable online hints\n"1017 " \":set nohints\" disable online hints\n"1018 "Set your preferences in ~/.redisclirc\n",1019 version1020 );1021 sdsfree(version);1022}10231024/* Output all command help, filtering by group or command name. */1025static void cliOutputHelp(int argc, char **argv) {1026 int i, j;1027 char *group = NULL;1028 helpEntry *entry;1029 struct commandDocs *help;10301031 if (argc == 0) {1032 cliOutputGenericHelp();1033 return;1034 } else if (argc > 0 && argv[0][0] == '@') {1035 group = argv[0]+1;1036 }10371038 if (helpEntries == NULL) {1039 /* Initialize the help using the results of the COMMAND command.1040 * In case we are using redis-cli help XXX, we need to init it. */1041 cliInitHelp();1042 }10431044 assert(argc > 0);1045 for (i = 0; i < helpEntriesLen; i++) {1046 entry = &helpEntries[i];1047 if (entry->type != CLI_HELP_COMMAND) continue;10481049 help = &entry->docs;1050 if (group == NULL) {1051 /* Compare all arguments */1052 if (argc <= entry->argc) {1053 for (j = 0; j < argc; j++) {1054 if (strcasecmp(argv[j],entry->argv[j]) != 0) break;1055 }1056 if (j == argc) {1057 cliOutputCommandHelp(help,1);1058 }1059 }1060 } else if (strcasecmp(group, help->group) == 0) {1061 cliOutputCommandHelp(help,0);1062 }1063 }1064 printf("\r\n");1065}10661067/* Linenoise completion callback. */1068static void completionCallback(const char *buf, linenoiseCompletions *lc) {1069 size_t startpos = 0;1070 int mask;1071 int i;1072 size_t matchlen;1073 sds tmp;10741075 if (strncasecmp(buf,"help ",5) == 0) {1076 startpos = 5;1077 while (isspace(buf[startpos])) startpos++;1078 mask = CLI_HELP_COMMAND | CLI_HELP_GROUP;1079 } else {1080 mask = CLI_HELP_COMMAND;1081 }10821083 for (i = 0; i < helpEntriesLen; i++) {1084 if (!(helpEntries[i].type & mask)) continue;10851086 matchlen = strlen(buf+startpos);1087 if (strncasecmp(buf+startpos,helpEntries[i].full,matchlen) == 0) {1088 tmp = sdsnewlen(buf,startpos);1089 tmp = sdscat(tmp,helpEntries[i].full);1090 linenoiseAddCompletion(lc,tmp);1091 sdsfree(tmp);1092 }1093 }1094}10951096static sds addHintForArgument(sds hint, cliCommandArg *arg);10971098/* Adds a separator character between words of a string under construction.1099 * A separator is added if the string length is greater than its previously-recorded1100 * length (*len), which is then updated, and it's not the last word to be added.1101 */1102static sds addSeparator(sds str, size_t *len, char *separator, int is_last) {1103 if (sdslen(str) > *len && !is_last) {1104 str = sdscat(str, separator);1105 *len = sdslen(str);1106 }1107 return str;1108}11091110/* Recursively zeros the matched* fields of all arguments. */1111static void clearMatchedArgs(cliCommandArg *args, int numargs) {1112 for (int i = 0; i != numargs; ++i) {1113 args[i].matched = 0;1114 args[i].matched_token = 0;1115 args[i].matched_name = 0;1116 args[i].matched_all = 0;1117 if (args[i].subargs) {1118 clearMatchedArgs(args[i].subargs, args[i].numsubargs);1119 }1120 }1121}11221123/* Builds a completion hint string describing the arguments, skipping parts already matched.1124 * Hints for all arguments are added to the input 'hint' parameter, separated by 'separator'.1125 */1126static sds addHintForArguments(sds hint, cliCommandArg *args, int numargs, char *separator) {1127 int i, j, incomplete;1128 size_t len=sdslen(hint);1129 for (i = 0; i < numargs; i++) {1130 if (!(args[i].flags & CMD_ARG_OPTIONAL)) {1131 hint = addHintForArgument(hint, &args[i]);1132 hint = addSeparator(hint, &len, separator, i == numargs-1);1133 continue;1134 }11351136 /* The rule is that successive "optional" arguments can appear in any order.1137 * But if they are followed by a required argument, no more of those optional arguments1138 * can appear after that.1139 * 1140 * This code handles all successive optional args together. This lets us show the1141 * completion of the currently-incomplete optional arg first, if there is one.1142 */1143 for (j = i, incomplete = -1; j < numargs; j++) {1144 if (!(args[j].flags & CMD_ARG_OPTIONAL)) break;1145 if (args[j].matched != 0 && args[j].matched_all == 0) {1146 /* User has started typing this arg; show its completion first. */1147 hint = addHintForArgument(hint, &args[j]);1148 hint = addSeparator(hint, &len, separator, i == numargs-1);1149 incomplete = j;1150 }1151 }11521153 /* If the following non-optional arg has not been matched, add hints for1154 * any remaining optional args in this group. 1155 */1156 if (j == numargs || args[j].matched == 0) {1157 for (; i < j; i++) {1158 if (incomplete != i) {1159 hint = addHintForArgument(hint, &args[i]);1160 hint = addSeparator(hint, &len, separator, i == numargs-1);1161 }1162 }1163 }11641165 i = j - 1;1166 }1167 return hint;1168}11691170/* Adds the "repeating" section of the hint string for a multiple-typed argument: [ABC def ...]1171 * The repeating part is a fixed unit; we don't filter matched elements from it.1172 */1173static sds addHintForRepeatedArgument(sds hint, cliCommandArg *arg) {1174 if (!(arg->flags & CMD_ARG_MULTIPLE)) {1175 return hint;1176 }11771178 /* The repeating part is always shown at the end of the argument's hint,1179 * so we can safely clear its matched flags before printing it.1180 */1181 clearMatchedArgs(arg, 1);1182 1183 if (hint[0] != '\0') {1184 hint = sdscat(hint, " ");1185 }1186 hint = sdscat(hint, "[");11871188 if (arg->flags & CMD_ARG_MULTIPLE_TOKEN) {1189 hint = sdscat_orempty(hint, arg->token);1190 if (arg->type != ARG_TYPE_PURE_TOKEN) {1191 hint = sdscat(hint, " ");1192 }1193 }11941195 switch (arg->type) {1196 case ARG_TYPE_ONEOF:1197 hint = addHintForArguments(hint, arg->subargs, arg->numsubargs, "|");1198 break;11991200 case ARG_TYPE_BLOCK:1201 hint = addHintForArguments(hint, arg->subargs, arg->numsubargs, " ");1202 break;12031204 case ARG_TYPE_PURE_TOKEN:1205 break;12061207 default:1208 hint = sdscat_orempty(hint, arg->display_text ? arg->display_text : arg->name);1209 break;1210 }12111212 hint = sdscat(hint, " ...]");1213 return hint;1214}12151216/* Adds hint string for one argument, if not already matched. */1217static sds addHintForArgument(sds hint, cliCommandArg *arg) {1218 if (arg->matched_all) {1219 return hint;1220 }12211222 /* Surround an optional arg with brackets, unless it's partially matched. */1223 if ((arg->flags & CMD_ARG_OPTIONAL) && !arg->matched) {1224 hint = sdscat(hint, "[");1225 }12261227 /* Start with the token, if present and not matched. */1228 if (arg->token != NULL && !arg->matched_token) {1229 hint = sdscat_orempty(hint, arg->token);1230 if (arg->type != ARG_TYPE_PURE_TOKEN) {1231 hint = sdscat(hint, " ");1232 }1233 }12341235 /* Add the body of the syntax string. */1236 switch (arg->type) {1237 case ARG_TYPE_ONEOF:1238 if (arg->matched == 0) {1239 hint = addHintForArguments(hint, arg->subargs, arg->numsubargs, "|");1240 } else {1241 int i;1242 for (i = 0; i < arg->numsubargs; i++) {1243 if (arg->subargs[i].matched != 0) {1244 hint = addHintForArgument(hint, &arg->subargs[i]);1245 }1246 }1247 }1248 break;12491250 case ARG_TYPE_BLOCK:1251 hint = addHintForArguments(hint, arg->subargs, arg->numsubargs, " ");1252 break;12531254 case ARG_TYPE_PURE_TOKEN:1255 break;12561257 default:1258 if (!arg->matched_name) {1259 hint = sdscat_orempty(hint, arg->display_text ? arg->display_text : arg->name);1260 }1261 break;1262 }12631264 hint = addHintForRepeatedArgument(hint, arg);12651266 if ((arg->flags & CMD_ARG_OPTIONAL) && !arg->matched) {1267 hint = sdscat(hint, "]");1268 }12691270 return hint;1271}12721273static int matchArg(char **nextword, int numwords, cliCommandArg *arg);1274static int matchArgs(char **words, int numwords, cliCommandArg *args, int numargs);12751276/* Tries to match the next words of the input against an argument. */1277static int matchNoTokenArg(char **nextword, int numwords, cliCommandArg *arg) {1278 int i;1279 switch (arg->type) {1280 case ARG_TYPE_BLOCK: {1281 arg->matched += matchArgs(nextword, numwords, arg->subargs, arg->numsubargs);12821283 /* All the subargs must be matched for the block to match. */1284 arg->matched_all = 1;1285 for (i = 0; i < arg->numsubargs; i++) {1286 if (arg->subargs[i].matched_all == 0) {1287 arg->matched_all = 0;1288 }1289 }1290 break;1291 }1292 case ARG_TYPE_ONEOF: {1293 for (i = 0; i < arg->numsubargs; i++) {1294 if (matchArg(nextword, numwords, &arg->subargs[i])) {1295 arg->matched += arg->subargs[i].matched;1296 arg->matched_all = arg->subargs[i].matched_all;1297 break;1298 }1299 }1300 break;1301 }13021303 case ARG_TYPE_INTEGER:1304 case ARG_TYPE_UNIX_TIME: {1305 long long value;1306 if (sscanf(*nextword, "%lld", &value) == 1) {1307 arg->matched += 1;1308 arg->matched_name = 1;1309 arg->matched_all = 1;1310 } else {1311 /* Matching failed due to incorrect arg type. */1312 arg->matched = 0;1313 arg->matched_name = 0;1314 }1315 break;1316 }13171318 case ARG_TYPE_DOUBLE: {1319 double value;1320 if (sscanf(*nextword, "%lf", &value) == 1) {1321 arg->matched += 1;1322 arg->matched_name = 1;1323 arg->matched_all = 1;1324 } else {1325 /* Matching failed due to incorrect arg type. */1326 arg->matched = 0;1327 arg->matched_name = 0;1328 }1329 break;1330 }13311332 default:1333 arg->matched += 1;1334 arg->matched_name = 1;1335 arg->matched_all = 1;1336 break;1337 }1338 return arg->matched;1339}13401341/* Tries to match the next word of the input against a token literal. */1342static int matchToken(char **nextword, cliCommandArg *arg) {1343 if (strcasecmp(arg->token, nextword[0]) != 0) {1344 return 0;1345 }1346 arg->matched_token = 1;1347 arg->matched = 1;1348 return 1;1349}13501351/* Tries to match the next words of the input against the next argument.1352 * If the arg is repeated ("multiple"), it will be matched only once.1353 * If the next input word(s) can't be matched, returns 0 for failure.1354 */1355static int matchArgOnce(char **nextword, int numwords, cliCommandArg *arg) {1356 /* First match the token, if present. */1357 if (arg->token != NULL) {1358 if (!matchToken(nextword, arg)) {1359 return 0;1360 }1361 if (arg->type == ARG_TYPE_PURE_TOKEN) {1362 arg->matched_all = 1;1363 return 1;1364 }1365 if (numwords == 1) {1366 return 1;1367 }1368 nextword++;1369 numwords--;1370 }13711372 /* Then match the rest of the argument. */1373 if (!matchNoTokenArg(nextword, numwords, arg)) {1374 return 0;1375 }1376 return arg->matched;1377}13781379/* Tries to match the next words of the input against the next argument.1380 * If the arg is repeated ("multiple"), it will be matched as many times as possible.1381 */1382static int matchArg(char **nextword, int numwords, cliCommandArg *arg) {1383 int matchedWords = 0;1384 int matchedOnce = matchArgOnce(nextword, numwords, arg);1385 if (!(arg->flags & CMD_ARG_MULTIPLE)) {1386 return matchedOnce;1387 }13881389 /* Found one match; now match a "multiple" argument as many times as possible. */1390 matchedWords += matchedOnce;1391 while (arg->matched_all && matchedWords < numwords) {1392 clearMatchedArgs(arg, 1);1393 if (arg->token != NULL && !(arg->flags & CMD_ARG_MULTIPLE_TOKEN)) {1394 /* The token only appears the first time; the rest of the times,1395 * pretend we saw it so we don't hint it.1396 */1397 matchedOnce = matchNoTokenArg(nextword + matchedWords, numwords - matchedWords, arg);1398 if (arg->matched) {1399 arg->matched_token = 1;1400 }1401 } else {1402 matchedOnce = matchArgOnce(nextword + matchedWords, numwords - matchedWords, arg);1403 }1404 matchedWords += matchedOnce;1405 }1406 arg->matched_all = 0; /* Because more repetitions are still possible. */1407 return matchedWords;1408}14091410/* Tries to match the next words of the input against1411 * any one of a consecutive set of optional arguments.1412 */1413static int matchOneOptionalArg(char **words, int numwords, cliCommandArg *args, int numargs, int *matchedarg) {1414 for (int nextword = 0, nextarg = 0; nextword != numwords && nextarg != numargs; ++nextarg) {1415 if (args[nextarg].matched) {1416 /* Already matched this arg. */1417 continue;1418 }14191420 int matchedWords = matchArg(&words[nextword], numwords - nextword, &args[nextarg]);1421 if (matchedWords != 0) {1422 *matchedarg = nextarg;1423 return matchedWords;1424 }1425 }1426 return 0;1427}14281429/* Matches as many input words as possible against a set of consecutive optional arguments. */1430static int matchOptionalArgs(char **words, int numwords, cliCommandArg *args, int numargs) {1431 int nextword = 0;1432 int matchedarg = -1, lastmatchedarg = -1;1433 while (nextword != numwords) {1434 int matchedWords = matchOneOptionalArg(&words[nextword], numwords - nextword, args, numargs, &matchedarg);1435 if (matchedWords == 0) {1436 break;1437 }1438 /* Successfully matched an optional arg; mark any previous match as completed1439 * so it won't be partially hinted.1440 */1441 if (lastmatchedarg != -1) {1442 args[lastmatchedarg].matched_all = 1;1443 }1444 lastmatchedarg = matchedarg;1445 nextword += matchedWords;1446 }1447 return nextword;1448}14491450/* Matches as many input words as possible against command arguments. */1451static int matchArgs(char **words, int numwords, cliCommandArg *args, int numargs) {1452 int nextword, nextarg, matchedWords;1453 for (nextword = 0, nextarg = 0; nextword != numwords && nextarg != numargs; ++nextarg) {1454 /* Optional args can occur in any order. Collect a range of consecutive optional args1455 * and try to match them as a group against the next input words.1456 */1457 if (args[nextarg].flags & CMD_ARG_OPTIONAL) {1458 int lastoptional;1459 for (lastoptional = nextarg; lastoptional < numargs; lastoptional++) {1460 if (!(args[lastoptional].flags & CMD_ARG_OPTIONAL)) break;1461 }1462 matchedWords = matchOptionalArgs(&words[nextword], numwords - nextword, &args[nextarg], lastoptional - nextarg);1463 nextarg = lastoptional - 1;1464 } else {1465 matchedWords = matchArg(&words[nextword], numwords - nextword, &args[nextarg]);1466 if (matchedWords == 0) {1467 /* Couldn't match a required word - matching fails! */1468 return 0;1469 }1470 }14711472 nextword += matchedWords;1473 }1474 return nextword;1475}14761477/* Compute the linenoise hint for the input prefix in inputargv/inputargc.1478 * cmdlen is the number of words from the start of the input that make up the command.1479 * If docs.args exists, dynamically creates a hint string by matching the arg specs1480 * against the input words.1481 */1482static sds makeHint(char **inputargv, int inputargc, int cmdlen, struct commandDocs docs) {1483 sds hint;14841485 if (docs.args) {1486 /* Remove arguments from the returned hint to show only the1487 * ones the user did not yet type. */1488 clearMatchedArgs(docs.args, docs.numargs);1489 hint = sdsempty();1490 int matchedWords = 0;1491 if (inputargv && inputargc)1492 matchedWords = matchArgs(inputargv + cmdlen, inputargc - cmdlen, docs.args, docs.numargs);1493 if (matchedWords == inputargc - cmdlen) {1494 hint = addHintForArguments(hint, docs.args, docs.numargs, " ");1495 }1496 return hint;1497 }14981499 /* If arg specs are not available, show the hint string until the user types something. */1500 if (inputargc <= cmdlen) {1501 hint = sdsnew(docs.params);1502 } else {1503 hint = sdsempty();1504 }1505 return hint;1506}15071508/* Search for a command matching the longest possible prefix of input words. */1509static helpEntry* findHelpEntry(int argc, char **argv) {1510 helpEntry *entry = NULL;1511 int i, rawargc, matchlen = 0;1512 sds *rawargv;15131514 for (i = 0; i < helpEntriesLen; i++) {1515 if (!(helpEntries[i].type & CLI_HELP_COMMAND)) continue;15161517 rawargv = helpEntries[i].argv;1518 rawargc = helpEntries[i].argc;1519 if (rawargc <= argc) {1520 int j;1521 for (j = 0; j < rawargc; j++) {1522 if (strcasecmp(rawargv[j],argv[j])) {1523 break;1524 }1525 }1526 if (j == rawargc && rawargc > matchlen) {1527 matchlen = rawargc;1528 entry = &helpEntries[i];1529 }1530 }1531 }1532 return entry;1533}15341535/* Returns the command-line hint string for a given partial input. */1536static sds getHintForInput(const char *charinput) {1537 sds hint = NULL;1538 int inputargc, inputlen = strlen(charinput);1539 sds *inputargv = sdssplitargs(charinput, &inputargc);1540 int endspace = inputlen && isspace(charinput[inputlen-1]);15411542 /* Don't match the last word until the user has typed a space after it. */1543 int matchargc = endspace ? inputargc : inputargc - 1;15441545 helpEntry *entry = findHelpEntry(matchargc, inputargv);1546 if (entry) {1547 hint = makeHint(inputargv, matchargc, entry->argc, entry->docs);1548 }1549 sdsfreesplitres(inputargv, inputargc);1550 return hint;1551}15521553/* Linenoise hints callback. */1554static char *hintsCallback(const char *buf, int *color, int *bold) {1555 if (!pref.hints) return NULL;15561557 sds hint = getHintForInput(buf);1558 if (hint == NULL) {1559 return NULL;1560 }15611562 *color = 90;1563 *bold = 0;15641565 /* Add an initial space if needed. */1566 int len = strlen(buf);1567 int endspace = len && isspace(buf[len-1]);1568 if (!endspace) {1569 sds newhint = sdsnewlen(" ",1);1570 newhint = sdscatsds(newhint,hint);1571 sdsfree(hint);1572 hint = newhint;1573 }15741575 return hint;1576}15771578static void freeHintsCallback(void *ptr) {1579 sdsfree(ptr);1580}15811582/*------------------------------------------------------------------------------1583 * TTY manipulation1584 *--------------------------------------------------------------------------- */15851586/* Restore terminal if we've changed it. */1587void cliRestoreTTY(void) {1588 if (orig_termios_saved)1589 tcsetattr(STDIN_FILENO, TCSANOW, &orig_termios);1590}15911592/* Put the terminal in "press any key" mode */1593static void cliPressAnyKeyTTY(void) {1594 if (!isatty(STDIN_FILENO)) return;1595 if (!orig_termios_saved) {1596 if (tcgetattr(STDIN_FILENO, &orig_termios) == -1) return;1597 atexit(cliRestoreTTY);1598 orig_termios_saved = 1;1599 }1600 struct termios mode = orig_termios;1601 mode.c_lflag &= ~(ECHO | ICANON); /* echoing off, canonical off */1602 tcsetattr(STDIN_FILENO, TCSANOW, &mode);1603}16041605/*------------------------------------------------------------------------------1606 * Networking / parsing1607 *--------------------------------------------------------------------------- */16081609/* Send AUTH command to the server */1610static int cliAuth(redisContext *ctx, char *user, char *auth) {1611 redisReply *reply;1612 if (auth == NULL) return REDIS_OK;16131614 if (user == NULL)1615 reply = redisCommand(ctx,"AUTH %s",auth);1616 else1617 reply = redisCommand(ctx,"AUTH %s %s",user,auth);16181619 if (reply == NULL) {1620 fprintf(stderr, "\nI/O error\n");1621 return REDIS_ERR;1622 }16231624 int result = REDIS_OK;1625 if (reply->type == REDIS_REPLY_ERROR) {1626 result = REDIS_ERR;1627 fprintf(stderr, "AUTH failed: %s\n", reply->str);1628 }1629 freeReplyObject(reply);1630 return result;1631}16321633/* Send SELECT input_dbnum to the server */1634static int cliSelect(void) {1635 redisReply *reply;1636 if (config.conn_info.input_dbnum == config.dbnum) return REDIS_OK;16371638 reply = redisCommand(context,"SELECT %d",config.conn_info.input_dbnum);1639 if (reply == NULL) {1640 fprintf(stderr, "\nI/O error\n");1641 return REDIS_ERR;1642 }16431644 int result = REDIS_OK;1645 if (reply->type == REDIS_REPLY_ERROR) {1646 result = REDIS_ERR;1647 fprintf(stderr,"SELECT %d failed: %s\n",config.conn_info.input_dbnum,reply->str);1648 } else {1649 config.dbnum = config.conn_info.input_dbnum;1650 cliRefreshPrompt();1651 }1652 freeReplyObject(reply);1653 return result;1654}16551656/* Select RESP3 mode if redis-cli was started with the -3 option. */1657static int cliSwitchProto(void) {1658 redisReply *reply;1659 if (!config.resp3 || config.resp2) return REDIS_OK;16601661 reply = redisCommand(context,"HELLO 3");1662 if (reply == NULL) {1663 fprintf(stderr, "\nI/O error\n");1664 return REDIS_ERR;1665 }16661667 int result = REDIS_OK;1668 if (reply->type == REDIS_REPLY_ERROR) {1669 fprintf(stderr,"HELLO 3 failed: %s\n",reply->str);1670 if (config.resp3 == 1) {1671 result = REDIS_ERR;1672 } else if (config.resp3 == 2) {1673 result = REDIS_OK;1674 }1675 }16761677 /* Retrieve server version string for later use. */1678 for (size_t i = 0; i < reply->elements; i += 2) {1679 assert(reply->element[i]->type == REDIS_REPLY_STRING);1680 char *key = reply->element[i]->str;1681 if (!strcmp(key, "version")) {1682 assert(reply->element[i + 1]->type == REDIS_REPLY_STRING);1683 config.server_version = sdsnew(reply->element[i + 1]->str);1684 }1685 }1686 freeReplyObject(reply);1687 config.current_resp3 = 1;1688 return result;1689}16901691/* Set the client name if configured. */1692static int cliSetName(void) {1693 if (config.client_name == NULL) return REDIS_OK;16941695 redisReply *reply = redisCommand(context,"CLIENT SETNAME %s", config.client_name);1696 if (reply == NULL) {1697 fprintf(stderr, "\nI/O error\n");1698 return REDIS_ERR;1699 }1700 int result = REDIS_OK;1701 if (reply->type == REDIS_REPLY_ERROR) {1702 fprintf(stderr,"CLIENT SETNAME failed: %s\n", reply->str);1703 result = REDIS_ERR;1704 }1705 freeReplyObject(reply);1706 return result;1707}17081709/* Connect to the server. It is possible to pass certain flags to the function:1710 * CC_FORCE: The connection is performed even if there is already1711 * a connected socket.1712 * CC_QUIET: Don't print errors if connection fails. */1713static int cliConnect(int flags) {1714 if (context == NULL || flags & CC_FORCE) {1715 if (context != NULL) {1716 redisFree(context);1717 config.dbnum = 0;1718 config.in_multi = 0;1719 config.pubsub_mode = 0;1720 cliRefreshPrompt();1721 }17221723 /* Do not use hostsocket when we got redirected in cluster mode */1724 if (config.hostsocket == NULL ||1725 (config.cluster_mode && config.cluster_reissue_command)) {1726 context = redisConnectWrapper(config.conn_info.hostip, config.conn_info.hostport,1727 config.connect_timeout);1728 } else {1729 context = redisConnectUnixWrapper(config.hostsocket, config.connect_timeout);1730 }17311732 if (!context->err && config.tls) {1733 const char *err = NULL;1734 if (cliSecureConnection(context, config.sslconfig, &err) == REDIS_ERR && err) {1735 fprintf(stderr, "Could not negotiate a TLS connection: %s\n", err);1736 redisFree(context);1737 context = NULL;1738 return REDIS_ERR;1739 }1740 }17411742 if (context->err) {1743 if (!(flags & CC_QUIET)) {1744 fprintf(stderr,"Could not connect to Redis at ");1745 if (config.hostsocket == NULL ||1746 (config.cluster_mode && config.cluster_reissue_command))1747 {1748 fprintf(stderr, "%s:%d: %s\n",1749 config.conn_info.hostip,config.conn_info.hostport,context->errstr);1750 } else {1751 fprintf(stderr,"%s: %s\n",1752 config.hostsocket,context->errstr);1753 }1754 }1755 redisFree(context);1756 context = NULL;1757 return REDIS_ERR;1758 }175917601761 /* Set aggressive KEEP_ALIVE socket option in the Redis context socket1762 * in order to prevent timeouts caused by the execution of long1763 * commands. At the same time this improves the detection of real1764 * errors. */1765 anetKeepAlive(NULL, context->fd, REDIS_CLI_KEEPALIVE_INTERVAL);17661767 /* State of the current connection. */1768 config.current_resp3 = 0;17691770 /* Do AUTH, select the right DB, switch to RESP3 if needed. */1771 if (cliAuth(context, config.conn_info.user, config.conn_info.auth) != REDIS_OK)1772 return REDIS_ERR;1773 if (cliSelect() != REDIS_OK)1774 return REDIS_ERR;1775 if (cliSwitchProto() != REDIS_OK)1776 return REDIS_ERR;1777 if (cliSetName() != REDIS_OK)1778 return REDIS_ERR;1779 }17801781 /* Set a PUSH handler if configured to do so. */1782 if (config.push_output) {1783 redisSetPushCallback(context, cliPushHandler);1784 }17851786 return REDIS_OK;1787}17881789/* In cluster, if server replies ASK, we will redirect to a different node.1790 * Before sending the real command, we need to send ASKING command first. */1791static int cliSendAsking(void) {1792 redisReply *reply;17931794 config.cluster_send_asking = 0;1795 if (context == NULL) {1796 return REDIS_ERR;1797 }1798 reply = redisCommand(context,"ASKING");1799 if (reply == NULL) {1800 fprintf(stderr, "\nI/O error\n");1801 return REDIS_ERR;1802 }1803 int result = REDIS_OK;1804 if (reply->type == REDIS_REPLY_ERROR) {1805 result = REDIS_ERR;1806 fprintf(stderr,"ASKING failed: %s\n",reply->str);1807 }1808 freeReplyObject(reply);1809 return result;1810}18111812static void cliPrintContextError(void) {1813 if (context == NULL) return;1814 fprintf(stderr,"Error: %s\n",context->errstr);1815}18161817static int isInvalidateReply(redisReply *reply) {1818 return reply->type == REDIS_REPLY_PUSH && reply->elements == 2 &&1819 reply->element[0]->type == REDIS_REPLY_STRING &&1820 !strncmp(reply->element[0]->str, "invalidate", 10) &&1821 reply->element[1]->type == REDIS_REPLY_ARRAY;1822}18231824/* Special display handler for RESP3 'invalidate' messages.1825 * This function does not validate the reply, so it should1826 * already be confirmed correct */1827static sds cliFormatInvalidateTTY(redisReply *r) {1828 sds out = sdsnew("-> invalidate: ");18291830 for (size_t i = 0; i < r->element[1]->elements; i++) {1831 redisReply *key = r->element[1]->element[i];1832 assert(key->type == REDIS_REPLY_STRING);18331834 out = sdscatfmt(out, "'%s'", key->str, key->len);1835 if (i < r->element[1]->elements - 1)1836 out = sdscatlen(out, ", ", 2);1837 }18381839 return sdscatlen(out, "\n", 1);1840}18411842/* Returns non-zero if cliFormatReplyTTY renders the reply in multiple lines. */1843static int cliIsMultilineValueTTY(redisReply *r) {1844 switch (r->type) {1845 case REDIS_REPLY_ARRAY:1846 case REDIS_REPLY_SET:1847 case REDIS_REPLY_PUSH:1848 if (r->elements == 0) return 0;1849 if (r->elements > 1) return 1;1850 return cliIsMultilineValueTTY(r->element[0]);1851 case REDIS_REPLY_MAP:1852 if (r->elements == 0) return 0;1853 if (r->elements > 2) return 1;1854 return cliIsMultilineValueTTY(r->element[1]);1855 default:1856 return 0;1857 }1858}18591860static sds cliFormatReplyTTY(redisReply *r, char *prefix) {1861 sds out = sdsempty();1862 switch (r->type) {1863 case REDIS_REPLY_ERROR:1864 out = sdscatprintf(out,"(error) %s\n", r->str);1865 break;1866 case REDIS_REPLY_STATUS:1867 out = sdscat(out,r->str);1868 out = sdscat(out,"\n");1869 break;1870 case REDIS_REPLY_INTEGER:1871 out = sdscatprintf(out,"(integer) %lld\n",r->integer);1872 break;1873 case REDIS_REPLY_DOUBLE:1874 out = sdscatprintf(out,"(double) %s\n",r->str);1875 break;1876 case REDIS_REPLY_STRING:1877 case REDIS_REPLY_VERB:1878 /* If you are producing output for the standard output we want1879 * a more interesting output with quoted characters and so forth,1880 * unless it's a verbatim string type. */1881 if (r->type == REDIS_REPLY_STRING) {1882 out = sdscatrepr(out,r->str,r->len);1883 out = sdscat(out,"\n");1884 } else {1885 out = sdscatlen(out,r->str,r->len);1886 out = sdscat(out,"\n");1887 }1888 break;1889 case REDIS_REPLY_NIL:1890 out = sdscat(out,"(nil)\n");1891 break;1892 case REDIS_REPLY_BOOL:1893 out = sdscat(out,r->integer ? "(true)\n" : "(false)\n");1894 break;1895 case REDIS_REPLY_ARRAY:1896 case REDIS_REPLY_MAP:1897 case REDIS_REPLY_SET:1898 case REDIS_REPLY_PUSH:1899 if (r->elements == 0) {1900 if (r->type == REDIS_REPLY_ARRAY)1901 out = sdscat(out,"(empty array)\n");1902 else if (r->type == REDIS_REPLY_MAP)1903 out = sdscat(out,"(empty hash)\n");1904 else if (r->type == REDIS_REPLY_SET)1905 out = sdscat(out,"(empty set)\n");1906 else if (r->type == REDIS_REPLY_PUSH)1907 out = sdscat(out,"(empty push)\n");1908 else1909 out = sdscat(out,"(empty aggregate type)\n");1910 } else {1911 unsigned int i, idxlen = 0;1912 char _prefixlen[16];1913 char _prefixfmt[16];1914 sds _prefix;1915 sds tmp;19161917 /* Calculate chars needed to represent the largest index */1918 i = r->elements;1919 if (r->type == REDIS_REPLY_MAP) i /= 2;1920 do {1921 idxlen++;1922 i /= 10;1923 } while(i);19241925 /* Prefix for nested multi bulks should grow with idxlen+2 spaces */1926 memset(_prefixlen,' ',idxlen+2);1927 _prefixlen[idxlen+2] = '\0';1928 _prefix = sdscat(sdsnew(prefix),_prefixlen);19291930 /* Setup prefix format for every entry */1931 char numsep;1932 if (r->type == REDIS_REPLY_SET) numsep = '~';1933 else if (r->type == REDIS_REPLY_MAP) numsep = '#';1934 /* TODO: this would be a breaking change for scripts, do that in a major version. */1935 /* else if (r->type == REDIS_REPLY_PUSH) numsep = '>'; */1936 else numsep = ')';1937 snprintf(_prefixfmt,sizeof(_prefixfmt),"%%s%%%ud%c ",idxlen,numsep);19381939 for (i = 0; i < r->elements; i++) {1940 unsigned int human_idx = (r->type == REDIS_REPLY_MAP) ?1941 i/2 : i;1942 human_idx++; /* Make it 1-based. */19431944 /* Don't use the prefix for the first element, as the parent1945 * caller already prepended the index number. */1946 out = sdscatprintf(out,_prefixfmt,i == 0 ? "" : prefix,human_idx);19471948 /* Format the multi bulk entry */1949 tmp = cliFormatReplyTTY(r->element[i],_prefix);1950 out = sdscatlen(out,tmp,sdslen(tmp));1951 sdsfree(tmp);19521953 /* For maps, format the value as well. */1954 if (r->type == REDIS_REPLY_MAP) {1955 i++;1956 sdsrange(out,0,-2);1957 out = sdscat(out," => ");1958 if (cliIsMultilineValueTTY(r->element[i])) {1959 /* linebreak before multiline value to fix alignment */1960 out = sdscat(out, "\n");1961 out = sdscat(out, _prefix);1962 }1963 tmp = cliFormatReplyTTY(r->element[i],_prefix);1964 out = sdscatlen(out,tmp,sdslen(tmp));1965 sdsfree(tmp);1966 }1967 }1968 sdsfree(_prefix);1969 }1970 break;1971 default:1972 fprintf(stderr,"Unknown reply type: %d\n", r->type);1973 exit(1);1974 }1975 return out;1976}19771978/* Returns 1 if the reply is a pubsub pushed reply. */1979int isPubsubPush(redisReply *r) {1980 if (r == NULL ||1981 r->type != (config.current_resp3 ? REDIS_REPLY_PUSH : REDIS_REPLY_ARRAY) ||1982 r->elements < 3 ||1983 r->element[0]->type != REDIS_REPLY_STRING)1984 {1985 return 0;1986 }1987 char *str = r->element[0]->str;1988 size_t len = r->element[0]->len;1989 /* Check if it is [p|s][un]subscribe or [p|s]message, but even simpler, we1990 * just check that it ends with "message" or "subscribe". */1991 return ((len >= strlen("message") &&1992 !strcmp(str + len - strlen("message"), "message")) ||1993 (len >= strlen("subscribe") &&1994 !strcmp(str + len - strlen("subscribe"), "subscribe")));1995}19961997int isColorTerm(void) {1998 char *t = getenv("TERM");1999 return t != NULL && strstr(t,"xterm") != NULL;2000}
Findings
✓ No findings reported for this file.