src/redis-cli.c C 11,581 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 11,581.
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.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.

Generated by searchcode.com · powered by code_get_file + code_get_findings