diff options
Diffstat (limited to 'src/redis-cli.c')
-rw-r--r-- | src/redis-cli.c | 1098 |
1 files changed, 866 insertions, 232 deletions
diff --git a/src/redis-cli.c b/src/redis-cli.c index d8e6b966a..3f5827fbe 100644 --- a/src/redis-cli.c +++ b/src/redis-cli.c @@ -59,13 +59,14 @@ #include "adlist.h" #include "zmalloc.h" #include "linenoise.h" -#include "help.h" /* Used for backwards-compatibility with pre-7.0 servers that don't support COMMAND DOCS. */ #include "anet.h" #include "ae.h" #include "connection.h" #include "cli_common.h" #include "mt19937-64.h" +#include "cli_commands.h" + #define UNUSED(V) ((void) V) #define OUTPUT_STANDARD 0 @@ -183,15 +184,6 @@ static int dictSdsKeyCompare(dict *d, const void *key1, static void dictSdsDestructor(dict *d, void *val); static void dictListDestructor(dict *d, void *val); -/* Command documentation info used for help output */ -struct commandDocs { - char *name; - char *params; /* A string describing the syntax of the command arguments. */ - char *summary; - char *group; - char *since; -}; - /* Cluster Manager Command Info */ typedef struct clusterManagerCommand { char *name; @@ -247,6 +239,7 @@ static struct config { int get_functions_rdb_mode; int stat_mode; int scan_mode; + int count; int intrinsic_latency_mode; int intrinsic_latency_duration; sds pattern; @@ -281,6 +274,9 @@ static struct config { int current_resp3; /* 1 if we have RESP3 right now in the current connection. */ int in_multi; int pre_multi_dbnum; + char *server_version; + char *test_hint; + char *test_hint_file; } config; /* User preferences. */ @@ -422,7 +418,7 @@ typedef struct { sds full; /* Only used for help on commands */ - struct commandDocs org; + struct commandDocs docs; } helpEntry; static helpEntry *helpEntries = NULL; @@ -442,50 +438,13 @@ static sds cliVersion(void) { return version; } -/* For backwards compatibility with pre-7.0 servers. Initializes command help. */ -static void cliOldInitHelp(void) { - int commandslen = sizeof(commandHelp)/sizeof(struct commandHelp); - int groupslen = sizeof(commandGroups)/sizeof(char*); - int i, len, pos = 0; - helpEntry tmp; - - helpEntriesLen = len = commandslen+groupslen; - helpEntries = zmalloc(sizeof(helpEntry)*len); - - for (i = 0; i < groupslen; i++) { - tmp.argc = 1; - tmp.argv = zmalloc(sizeof(sds)); - tmp.argv[0] = sdscatprintf(sdsempty(),"@%s",commandGroups[i]); - tmp.full = tmp.argv[0]; - tmp.type = CLI_HELP_GROUP; - tmp.org.name = NULL; - tmp.org.params = NULL; - tmp.org.summary = NULL; - tmp.org.since = NULL; - tmp.org.group = NULL; - helpEntries[pos++] = tmp; - } - - for (i = 0; i < commandslen; i++) { - tmp.argv = sdssplitargs(commandHelp[i].name,&tmp.argc); - tmp.full = sdsnew(commandHelp[i].name); - tmp.type = CLI_HELP_COMMAND; - tmp.org.name = commandHelp[i].name; - tmp.org.params = commandHelp[i].params; - tmp.org.summary = commandHelp[i].summary; - tmp.org.since = commandHelp[i].since; - tmp.org.group = commandGroups[commandHelp[i].group]; - helpEntries[pos++] = tmp; - } -} - /* For backwards compatibility with pre-7.0 servers. - * cliOldInitHelp() setups the helpEntries array with the command and group - * names from the help.h file. However the Redis instance we are connecting + * cliLegacyInitHelp() sets up the helpEntries array with the command and group + * names from the commands.c file. However the Redis instance we are connecting * to may support more commands, so this function integrates the previous * entries with additional entries obtained using the COMMAND command * available in recent versions of Redis. */ -static void cliOldIntegrateHelp(void) { +static void cliLegacyIntegrateHelp(void) { if (cliConnect(CC_QUIET) == REDIS_ERR) return; redisReply *reply = redisCommand(context, "COMMAND"); @@ -520,75 +479,88 @@ static void cliOldIntegrateHelp(void) { new->type = CLI_HELP_COMMAND; sdstoupper(new->argv[0]); - new->org.name = new->argv[0]; - new->org.params = sdsempty(); + new->docs.name = new->argv[0]; + new->docs.args = NULL; + new->docs.numargs = 0; + new->docs.params = sdsempty(); int args = llabs(entry->element[1]->integer); args--; /* Remove the command name itself. */ if (entry->element[3]->integer == 1) { - new->org.params = sdscat(new->org.params,"key "); + new->docs.params = sdscat(new->docs.params,"key "); args--; } - while(args-- > 0) new->org.params = sdscat(new->org.params,"arg "); + while(args-- > 0) new->docs.params = sdscat(new->docs.params,"arg "); if (entry->element[1]->integer < 0) - new->org.params = sdscat(new->org.params,"...options..."); - new->org.summary = "Help not available"; - new->org.since = "Not known"; - new->org.group = commandGroups[0]; + new->docs.params = sdscat(new->docs.params,"...options..."); + new->docs.summary = "Help not available"; + new->docs.since = "Not known"; + new->docs.group = "generic"; } freeReplyObject(reply); } /* Concatenate a string to an sds string, but if it's empty substitute double quote marks. */ -static sds sdscat_orempty(sds params, char *value) { +static sds sdscat_orempty(sds params, const char *value) { if (value[0] == '\0') { return sdscat(params, "\"\""); } return sdscat(params, value); } -static sds cliAddArgument(sds params, redisReply *argMap); +static sds makeHint(char **inputargv, int inputargc, int cmdlen, struct commandDocs docs); -/* Concatenate a list of arguments to the parameter string, separated by a separator string. */ -static sds cliConcatArguments(sds params, redisReply *arguments, char *separator) { +static void cliAddCommandDocArg(cliCommandArg *cmdArg, redisReply *argMap); + +static void cliMakeCommandDocArgs(redisReply *arguments, cliCommandArg *result) { for (size_t j = 0; j < arguments->elements; j++) { - params = cliAddArgument(params, arguments->element[j]); - if (j != arguments->elements - 1) { - params = sdscat(params, separator); - } + cliAddCommandDocArg(&result[j], arguments->element[j]); } - return params; } -/* Add an argument to the parameter string. */ -static sds cliAddArgument(sds params, redisReply *argMap) { - char *name = NULL; - char *type = NULL; - int optional = 0; - int multiple = 0; - int multipleToken = 0; - redisReply *arguments = NULL; - sds tokenPart = sdsempty(); - sds repeatPart = sdsempty(); - - /* First read the fields describing the argument. */ +static void cliAddCommandDocArg(cliCommandArg *cmdArg, redisReply *argMap) { if (argMap->type != REDIS_REPLY_MAP && argMap->type != REDIS_REPLY_ARRAY) { - return params; + return; } + for (size_t i = 0; i < argMap->elements; i += 2) { assert(argMap->element[i]->type == REDIS_REPLY_STRING); char *key = argMap->element[i]->str; if (!strcmp(key, "name")) { assert(argMap->element[i + 1]->type == REDIS_REPLY_STRING); - name = argMap->element[i + 1]->str; + cmdArg->name = sdsnew(argMap->element[i + 1]->str); + } else if (!strcmp(key, "display_text")) { + assert(argMap->element[i + 1]->type == REDIS_REPLY_STRING); + cmdArg->display_text = sdsnew(argMap->element[i + 1]->str); } else if (!strcmp(key, "token")) { assert(argMap->element[i + 1]->type == REDIS_REPLY_STRING); - char *token = argMap->element[i + 1]->str; - tokenPart = sdscat_orempty(tokenPart, token); + cmdArg->token = sdsnew(argMap->element[i + 1]->str); } else if (!strcmp(key, "type")) { assert(argMap->element[i + 1]->type == REDIS_REPLY_STRING); - type = argMap->element[i + 1]->str; + char *type = argMap->element[i + 1]->str; + if (!strcmp(type, "string")) { + cmdArg->type = ARG_TYPE_STRING; + } else if (!strcmp(type, "integer")) { + cmdArg->type = ARG_TYPE_INTEGER; + } else if (!strcmp(type, "double")) { + cmdArg->type = ARG_TYPE_DOUBLE; + } else if (!strcmp(type, "key")) { + cmdArg->type = ARG_TYPE_KEY; + } else if (!strcmp(type, "pattern")) { + cmdArg->type = ARG_TYPE_PATTERN; + } else if (!strcmp(type, "unix-time")) { + cmdArg->type = ARG_TYPE_UNIX_TIME; + } else if (!strcmp(type, "pure-token")) { + cmdArg->type = ARG_TYPE_PURE_TOKEN; + } else if (!strcmp(type, "oneof")) { + cmdArg->type = ARG_TYPE_ONEOF; + } else if (!strcmp(type, "block")) { + cmdArg->type = ARG_TYPE_BLOCK; + } } else if (!strcmp(key, "arguments")) { - arguments = argMap->element[i + 1]; + redisReply *arguments = argMap->element[i + 1]; + cmdArg->subargs = zcalloc(arguments->elements * sizeof(cliCommandArg)); + cmdArg->numsubargs = arguments->elements; + cliMakeCommandDocArgs(arguments, cmdArg->subargs); } else if (!strcmp(key, "flags")) { redisReply *flags = argMap->element[i + 1]; assert(flags->type == REDIS_REPLY_SET || flags->type == REDIS_REPLY_ARRAY); @@ -596,57 +568,15 @@ static sds cliAddArgument(sds params, redisReply *argMap) { assert(flags->element[j]->type == REDIS_REPLY_STATUS); char *flag = flags->element[j]->str; if (!strcmp(flag, "optional")) { - optional = 1; + cmdArg->flags |= CMD_ARG_OPTIONAL; } else if (!strcmp(flag, "multiple")) { - multiple = 1; + cmdArg->flags |= CMD_ARG_MULTIPLE; } else if (!strcmp(flag, "multiple_token")) { - multipleToken = 1; + cmdArg->flags |= CMD_ARG_MULTIPLE_TOKEN; } } } } - - /* Then build the "repeating part" of the argument string. */ - if (!strcmp(type, "key") || - !strcmp(type, "string") || - !strcmp(type, "integer") || - !strcmp(type, "double") || - !strcmp(type, "pattern") || - !strcmp(type, "unix-time") || - !strcmp(type, "token")) - { - repeatPart = sdscat_orempty(repeatPart, name); - } else if (!strcmp(type, "oneof")) { - repeatPart = cliConcatArguments(repeatPart, arguments, "|"); - } else if (!strcmp(type, "block")) { - repeatPart = cliConcatArguments(repeatPart, arguments, " "); - } else if (strcmp(type, "pure-token") != 0) { - fprintf(stderr, "Unknown type '%s' set for argument '%s'\n", type, name); - } - - /* Finally, build the parameter string. */ - if (tokenPart[0] != '\0' && strcmp(type, "pure-token") != 0) { - tokenPart = sdscat(tokenPart, " "); - } - if (optional) { - params = sdscat(params, "["); - } - params = sdscat(params, tokenPart); - params = sdscat(params, repeatPart); - if (multiple) { - params = sdscat(params, " ["); - if (multipleToken) { - params = sdscat(params, tokenPart); - } - params = sdscat(params, repeatPart); - params = sdscat(params, " ...]"); - } - if (optional) { - params = sdscat(params, "]"); - } - sdsfree(tokenPart); - sdsfree(repeatPart); - return params; } /* Fill in the fields of a help entry for the command/subcommand name. */ @@ -656,8 +586,13 @@ static void cliFillInCommandHelpEntry(helpEntry *help, char *cmdname, char *subc help->argv[0] = sdsnew(cmdname); sdstoupper(help->argv[0]); if (subcommandname) { - /* Subcommand name is two words separated by a pipe character. */ - help->argv[1] = sdsnew(strchr(subcommandname, '|') + 1); + /* Subcommand name may be two words separated by a pipe character. */ + char *pipe = strchr(subcommandname, '|'); + if (pipe != NULL) { + help->argv[1] = sdsnew(pipe + 1); + } else { + help->argv[1] = sdsnew(subcommandname); + } sdstoupper(help->argv[1]); } sds fullname = sdsnew(help->argv[0]); @@ -668,9 +603,11 @@ static void cliFillInCommandHelpEntry(helpEntry *help, char *cmdname, char *subc help->full = fullname; help->type = CLI_HELP_COMMAND; - help->org.name = help->full; - help->org.params = sdsempty(); - help->org.since = NULL; + help->docs.name = help->full; + help->docs.params = NULL; + help->docs.args = NULL; + help->docs.numargs = 0; + help->docs.since = NULL; } /* Initialize a command help entry for the command/subcommand described in 'specs'. @@ -692,23 +629,26 @@ static helpEntry *cliInitCommandHelpEntry(char *cmdname, char *subcommandname, if (!strcmp(key, "summary")) { redisReply *reply = specs->element[j + 1]; assert(reply->type == REDIS_REPLY_STRING); - help->org.summary = sdsnew(reply->str); + help->docs.summary = sdsnew(reply->str); } else if (!strcmp(key, "since")) { redisReply *reply = specs->element[j + 1]; assert(reply->type == REDIS_REPLY_STRING); - help->org.since = sdsnew(reply->str); + help->docs.since = sdsnew(reply->str); } else if (!strcmp(key, "group")) { redisReply *reply = specs->element[j + 1]; assert(reply->type == REDIS_REPLY_STRING); - help->org.group = sdsnew(reply->str); - sds group = sdsdup(help->org.group); + help->docs.group = sdsnew(reply->str); + sds group = sdsdup(help->docs.group); if (dictAdd(groups, group, NULL) != DICT_OK) { sdsfree(group); } } else if (!strcmp(key, "arguments")) { - redisReply *args = specs->element[j + 1]; - assert(args->type == REDIS_REPLY_ARRAY); - help->org.params = cliConcatArguments(help->org.params, args, " "); + redisReply *arguments = specs->element[j + 1]; + assert(arguments->type == REDIS_REPLY_ARRAY); + help->docs.args = zcalloc(arguments->elements * sizeof(cliCommandArg)); + help->docs.numargs = arguments->elements; + cliMakeCommandDocArgs(arguments, help->docs.args); + help->docs.params = makeHint(NULL, 0, 0, help->docs); } else if (!strcmp(key, "subcommands")) { redisReply *subcommands = specs->element[j + 1]; assert(subcommands->type == REDIS_REPLY_MAP || subcommands->type == REDIS_REPLY_ARRAY); @@ -774,11 +714,13 @@ void cliInitGroupHelpEntries(dict *groups) { tmp.argv[0] = sdscatprintf(sdsempty(),"@%s",(char *)dictGetKey(entry)); tmp.full = tmp.argv[0]; tmp.type = CLI_HELP_GROUP; - tmp.org.name = NULL; - tmp.org.params = NULL; - tmp.org.summary = NULL; - tmp.org.since = NULL; - tmp.org.group = NULL; + tmp.docs.name = NULL; + tmp.docs.params = NULL; + tmp.docs.args = NULL; + tmp.docs.numargs = 0; + tmp.docs.summary = NULL; + tmp.docs.since = NULL; + tmp.docs.group = NULL; helpEntries[pos++] = tmp; } dictReleaseIterator(iter); @@ -798,6 +740,164 @@ void cliInitCommandHelpEntries(redisReply *commandTable, dict *groups) { } } +/* Does the server version support a command/argument only available "since" some version? + * Returns 1 when supported, or 0 when the "since" version is newer than "version". */ +static int versionIsSupported(sds version, sds since) { + int i; + char *versionPos = version; + char *sincePos = since; + if (!since) { + return 1; + } + + for (i = 0; i != 3; i++) { + int versionPart = atoi(versionPos); + int sincePart = atoi(sincePos); + if (versionPart > sincePart) { + return 1; + } else if (sincePart > versionPart) { + return 0; + } + versionPos = strchr(versionPos, '.'); + sincePos = strchr(sincePos, '.'); + if (!versionPos || !sincePos) + return 0; + versionPos++; + sincePos++; + } + return 0; +} + +static void removeUnsupportedArgs(struct cliCommandArg *args, int *numargs, sds version) { + int i = 0, j; + while (i != *numargs) { + if (versionIsSupported(version, args[i].since)) { + if (args[i].subargs) { + removeUnsupportedArgs(args[i].subargs, &args[i].numsubargs, version); + } + i++; + continue; + } + for (j = i; j != *numargs; j++) { + args[j] = args[j + 1]; + } + (*numargs)--; + } +} + +static helpEntry *cliLegacyInitCommandHelpEntry(char *cmdname, char *subcommandname, + helpEntry *next, struct commandDocs *command, + dict *groups, sds version) { + helpEntry *help = next++; + cliFillInCommandHelpEntry(help, cmdname, subcommandname); + + help->docs.summary = sdsnew(command->summary); + help->docs.since = sdsnew(command->since); + help->docs.group = sdsnew(command->group); + sds group = sdsdup(help->docs.group); + if (dictAdd(groups, group, NULL) != DICT_OK) { + sdsfree(group); + } + + if (command->args != NULL) { + help->docs.args = command->args; + help->docs.numargs = command->numargs; + if (version) + removeUnsupportedArgs(help->docs.args, &help->docs.numargs, version); + help->docs.params = makeHint(NULL, 0, 0, help->docs); + } + + if (command->subcommands != NULL) { + for (size_t i = 0; command->subcommands[i].name != NULL; i++) { + if (!version || versionIsSupported(version, command->subcommands[i].since)) { + char *subcommandname = command->subcommands[i].name; + next = cliLegacyInitCommandHelpEntry( + cmdname, subcommandname, next, &command->subcommands[i], groups, version); + } + } + } + return next; +} + +int cliLegacyInitCommandHelpEntries(struct commandDocs *commands, dict *groups, sds version) { + helpEntry *next = helpEntries; + for (size_t i = 0; commands[i].name != NULL; i++) { + if (!version || versionIsSupported(version, commands[i].since)) { + next = cliLegacyInitCommandHelpEntry(commands[i].name, NULL, next, &commands[i], groups, version); + } + } + return next - helpEntries; +} + +/* Returns the total number of commands and subcommands in the command docs table, + * filtered by server version (if provided). + */ +static size_t cliLegacyCountCommands(struct commandDocs *commands, sds version) { + int numCommands = 0; + for (size_t i = 0; commands[i].name != NULL; i++) { + if (version && !versionIsSupported(version, commands[i].since)) { + continue; + } + numCommands++; + if (commands[i].subcommands != NULL) { + numCommands += cliLegacyCountCommands(commands[i].subcommands, version); + } + } + return numCommands; +} + +/* Gets the server version string by calling INFO SERVER. + * Stores the result in config.server_version. + * When not connected, or not possible, returns NULL. */ +static sds cliGetServerVersion(void) { + static const char *key = "\nredis_version:"; + redisReply *serverInfo = NULL; + char *pos; + + if (config.server_version != NULL) { + return config.server_version; + } + + if (!context) return NULL; + serverInfo = redisCommand(context, "INFO SERVER"); + if (serverInfo == NULL || serverInfo->type == REDIS_REPLY_ERROR) { + freeReplyObject(serverInfo); + return sdsempty(); + } + + assert(serverInfo->type == REDIS_REPLY_STRING || serverInfo->type == REDIS_REPLY_VERB); + sds info = serverInfo->str; + + /* Finds the first appearance of "redis_version" in the INFO SERVER reply. */ + pos = strstr(info, key); + if (pos) { + pos += strlen(key); + char *end = strchr(pos, '\r'); + if (end) { + sds version = sdsnewlen(pos, end - pos); + freeReplyObject(serverInfo); + config.server_version = version; + return version; + } + } + freeReplyObject(serverInfo); + return NULL; +} + +static void cliLegacyInitHelp(dict *groups) { + sds serverVersion = cliGetServerVersion(); + + /* Scan the commandDocs array and fill in the entries */ + helpEntriesLen = cliLegacyCountCommands(redisCommandTable, serverVersion); + helpEntries = zmalloc(sizeof(helpEntry)*helpEntriesLen); + + helpEntriesLen = cliLegacyInitCommandHelpEntries(redisCommandTable, groups, serverVersion); + cliInitGroupHelpEntries(groups); + + qsort(helpEntries, helpEntriesLen, sizeof(helpEntry), helpEntryCompare); + dictRelease(groups); +} + /* cliInitHelp() sets up the helpEntries array with the command and group * names and command descriptions obtained using the COMMAND DOCS command. */ @@ -817,16 +917,20 @@ static void cliInitHelp(void) { if (cliConnect(CC_QUIET) == REDIS_ERR) { /* Can not connect to the server, but we still want to provide - * help, generate it only from the old help.h data instead. */ - cliOldInitHelp(); + * help, generate it only from the static cli_commands.c data instead. */ + groups = dictCreate(&groupsdt); + cliLegacyInitHelp(groups); return; } commandTable = redisCommand(context, "COMMAND DOCS"); if (commandTable == NULL || commandTable->type == REDIS_REPLY_ERROR) { - /* New COMMAND DOCS subcommand not supported - generate help from old help.h data instead. */ + /* New COMMAND DOCS subcommand not supported - generate help from + * static cli_commands.c data instead. */ freeReplyObject(commandTable); - cliOldInitHelp(); - cliOldIntegrateHelp(); + + groups = dictCreate(&groupsdt); + cliLegacyInitHelp(groups); + cliLegacyIntegrateHelp(); return; }; if (commandTable->type != REDIS_REPLY_MAP && commandTable->type != REDIS_REPLY_ARRAY) return; @@ -901,7 +1005,7 @@ static void cliOutputHelp(int argc, char **argv) { entry = &helpEntries[i]; if (entry->type != CLI_HELP_COMMAND) continue; - help = &entry->org; + help = &entry->docs; if (group == NULL) { /* Compare all arguments */ if (argc <= entry->argc) { @@ -948,36 +1052,429 @@ static void completionCallback(const char *buf, linenoiseCompletions *lc) { } } -/* Linenoise hints callback. */ -static char *hintsCallback(const char *buf, int *color, int *bold) { - if (!pref.hints) return NULL; +static sds addHintForArgument(sds hint, cliCommandArg *arg); - int i, rawargc, argc, buflen = strlen(buf), matchlen = 0, shift = 0; - sds *rawargv, *argv = sdssplitargs(buf,&argc); - int endspace = buflen && isspace(buf[buflen-1]); - helpEntry *entry = NULL; +/* Adds a separator character between words of a string under construction. + * A separator is added if the string length is greater than its previously-recorded + * length (*len), which is then updated, and it's not the last word to be added. + */ +static sds addSeparator(sds str, size_t *len, char *separator, int is_last) { + if (sdslen(str) > *len && !is_last) { + str = sdscat(str, separator); + *len = sdslen(str); + } + return str; +} - /* Check if the argument list is empty and return ASAP. */ - if (argc == 0) { - sdsfreesplitres(argv,argc); - return NULL; +/* Recursively zeros the matched* fields of all arguments. */ +static void clearMatchedArgs(cliCommandArg *args, int numargs) { + for (int i = 0; i != numargs; ++i) { + args[i].matched = 0; + args[i].matched_token = 0; + args[i].matched_name = 0; + args[i].matched_all = 0; + if (args[i].subargs) { + clearMatchedArgs(args[i].subargs, args[i].numsubargs); + } } +} - if (argc > 3 && (!strcasecmp(argv[0], "acl") && !strcasecmp(argv[1], "dryrun"))) { - shift = 3; - } else if (argc > 2 && (!strcasecmp(argv[0], "command") && - (!strcasecmp(argv[1], "getkeys") || !strcasecmp(argv[1], "getkeysandflags")))) - { - shift = 2; +/* Builds a completion hint string describing the arguments, skipping parts already matched. + * Hints for all arguments are added to the input 'hint' parameter, separated by 'separator'. + */ +static sds addHintForArguments(sds hint, cliCommandArg *args, int numargs, char *separator) { + int i, j, incomplete; + size_t len=sdslen(hint); + for (i = 0; i < numargs; i++) { + if (!(args[i].flags & CMD_ARG_OPTIONAL)) { + hint = addHintForArgument(hint, &args[i]); + hint = addSeparator(hint, &len, separator, i == numargs-1); + continue; + } + + /* The rule is that successive "optional" arguments can appear in any order. + * But if they are followed by a required argument, no more of those optional arguments + * can appear after that. + * + * This code handles all successive optional args together. This lets us show the + * completion of the currently-incomplete optional arg first, if there is one. + */ + for (j = i, incomplete = -1; j < numargs; j++) { + if (!(args[j].flags & CMD_ARG_OPTIONAL)) break; + if (args[j].matched != 0 && args[j].matched_all == 0) { + /* User has started typing this arg; show its completion first. */ + hint = addHintForArgument(hint, &args[j]); + hint = addSeparator(hint, &len, separator, i == numargs-1); + incomplete = j; + } + } + + /* If the following non-optional arg has not been matched, add hints for + * any remaining optional args in this group. + */ + if (j == numargs || args[j].matched == 0) { + for (; i < j; i++) { + if (incomplete != i) { + hint = addHintForArgument(hint, &args[i]); + hint = addSeparator(hint, &len, separator, i == numargs-1); + } + } + } + + i = j - 1; + } + return hint; +} + +/* Adds the "repeating" section of the hint string for a multiple-typed argument: [ABC def ...] + * The repeating part is a fixed unit; we don't filter matched elements from it. + */ +static sds addHintForRepeatedArgument(sds hint, cliCommandArg *arg) { + if (!(arg->flags & CMD_ARG_MULTIPLE)) { + return hint; + } + + /* The repeating part is always shown at the end of the argument's hint, + * so we can safely clear its matched flags before printing it. + */ + clearMatchedArgs(arg, 1); + + if (hint[0] != '\0') { + hint = sdscat(hint, " "); + } + hint = sdscat(hint, "["); + + if (arg->flags & CMD_ARG_MULTIPLE_TOKEN) { + hint = sdscat_orempty(hint, arg->token); + if (arg->type != ARG_TYPE_PURE_TOKEN) { + hint = sdscat(hint, " "); + } + } + + switch (arg->type) { + case ARG_TYPE_ONEOF: + hint = addHintForArguments(hint, arg->subargs, arg->numsubargs, "|"); + break; + + case ARG_TYPE_BLOCK: + hint = addHintForArguments(hint, arg->subargs, arg->numsubargs, " "); + break; + + case ARG_TYPE_PURE_TOKEN: + break; + + default: + hint = sdscat_orempty(hint, arg->display_text ? arg->display_text : arg->name); + break; + } + + hint = sdscat(hint, " ...]"); + return hint; +} + +/* Adds hint string for one argument, if not already matched. */ +static sds addHintForArgument(sds hint, cliCommandArg *arg) { + if (arg->matched_all) { + return hint; + } + + /* Surround an optional arg with brackets, unless it's partially matched. */ + if ((arg->flags & CMD_ARG_OPTIONAL) && !arg->matched) { + hint = sdscat(hint, "["); + } + + /* Start with the token, if present and not matched. */ + if (arg->token != NULL && !arg->matched_token) { + hint = sdscat_orempty(hint, arg->token); + if (arg->type != ARG_TYPE_PURE_TOKEN) { + hint = sdscat(hint, " "); + } + } + + /* Add the body of the syntax string. */ + switch (arg->type) { + case ARG_TYPE_ONEOF: + if (arg->matched == 0) { + hint = addHintForArguments(hint, arg->subargs, arg->numsubargs, "|"); + } else { + int i; + for (i = 0; i < arg->numsubargs; i++) { + if (arg->subargs[i].matched != 0) { + hint = addHintForArgument(hint, &arg->subargs[i]); + } + } + } + break; + + case ARG_TYPE_BLOCK: + hint = addHintForArguments(hint, arg->subargs, arg->numsubargs, " "); + break; + + case ARG_TYPE_PURE_TOKEN: + break; + + default: + if (!arg->matched_name) { + hint = sdscat_orempty(hint, arg->display_text ? arg->display_text : arg->name); + } + break; + } + + hint = addHintForRepeatedArgument(hint, arg); + + if ((arg->flags & CMD_ARG_OPTIONAL) && !arg->matched) { + hint = sdscat(hint, "]"); + } + + return hint; +} + +static int matchArg(char **nextword, int numwords, cliCommandArg *arg); +static int matchArgs(char **words, int numwords, cliCommandArg *args, int numargs); + +/* Tries to match the next words of the input against an argument. */ +static int matchNoTokenArg(char **nextword, int numwords, cliCommandArg *arg) { + int i; + switch (arg->type) { + case ARG_TYPE_BLOCK: { + arg->matched += matchArgs(nextword, numwords, arg->subargs, arg->numsubargs); + + /* All the subargs must be matched for the block to match. */ + arg->matched_all = 1; + for (i = 0; i < arg->numsubargs; i++) { + if (arg->subargs[i].matched_all == 0) { + arg->matched_all = 0; + } + } + break; + } + case ARG_TYPE_ONEOF: { + for (i = 0; i < arg->numsubargs; i++) { + if (matchArg(nextword, numwords, &arg->subargs[i])) { + arg->matched += arg->subargs[i].matched; + arg->matched_all = arg->subargs[i].matched_all; + break; + } + } + break; + } + + case ARG_TYPE_INTEGER: + case ARG_TYPE_UNIX_TIME: { + long long value; + if (sscanf(*nextword, "%lld", &value)) { + arg->matched += 1; + arg->matched_name = 1; + arg->matched_all = 1; + } else { + /* Matching failed due to incorrect arg type. */ + arg->matched = 0; + arg->matched_name = 0; + } + break; + } + + case ARG_TYPE_DOUBLE: { + double value; + if (sscanf(*nextword, "%lf", &value)) { + arg->matched += 1; + arg->matched_name = 1; + arg->matched_all = 1; + } else { + /* Matching failed due to incorrect arg type. */ + arg->matched = 0; + arg->matched_name = 0; + } + break; + } + + default: + arg->matched += 1; + arg->matched_name = 1; + arg->matched_all = 1; + break; } - argc -= shift; - argv += shift; + return arg->matched; +} + +/* Tries to match the next word of the input against a token literal. */ +static int matchToken(char **nextword, cliCommandArg *arg) { + if (strcasecmp(arg->token, nextword[0]) != 0) { + return 0; + } + arg->matched_token = 1; + arg->matched = 1; + return 1; +} + +/* Tries to match the next words of the input against the next argument. + * If the arg is repeated ("multiple"), it will be matched only once. + * If the next input word(s) can't be matched, returns 0 for failure. + */ +static int matchArgOnce(char **nextword, int numwords, cliCommandArg *arg) { + /* First match the token, if present. */ + if (arg->token != NULL) { + if (!matchToken(nextword, arg)) { + return 0; + } + if (arg->type == ARG_TYPE_PURE_TOKEN) { + arg->matched_all = 1; + return 1; + } + if (numwords == 1) { + return 1; + } + nextword++; + numwords--; + } + + /* Then match the rest of the argument. */ + if (!matchNoTokenArg(nextword, numwords, arg)) { + return 0; + } + return arg->matched; +} + +/* Tries to match the next words of the input against the next argument. + * If the arg is repeated ("multiple"), it will be matched as many times as possible. + */ +static int matchArg(char **nextword, int numwords, cliCommandArg *arg) { + int matchedWords = 0; + int matchedOnce = matchArgOnce(nextword, numwords, arg); + if (!(arg->flags & CMD_ARG_MULTIPLE)) { + return matchedOnce; + } + + /* Found one match; now match a "multiple" argument as many times as possible. */ + matchedWords += matchedOnce; + while (arg->matched_all && matchedWords < numwords) { + clearMatchedArgs(arg, 1); + if (arg->token != NULL && !(arg->flags & CMD_ARG_MULTIPLE_TOKEN)) { + /* The token only appears the first time; the rest of the times, + * pretend we saw it so we don't hint it. + */ + matchedOnce = matchNoTokenArg(nextword + matchedWords, numwords - matchedWords, arg); + if (arg->matched) { + arg->matched_token = 1; + } + } else { + matchedOnce = matchArgOnce(nextword + matchedWords, numwords - matchedWords, arg); + } + matchedWords += matchedOnce; + } + arg->matched_all = 0; /* Because more repetitions are still possible. */ + return matchedWords; +} + +/* Tries to match the next words of the input against + * any one of a consecutive set of optional arguments. + */ +static int matchOneOptionalArg(char **words, int numwords, cliCommandArg *args, int numargs, int *matchedarg) { + for (int nextword = 0, nextarg = 0; nextword != numwords && nextarg != numargs; ++nextarg) { + if (args[nextarg].matched) { + /* Already matched this arg. */ + continue; + } + + int matchedWords = matchArg(&words[nextword], numwords - nextword, &args[nextarg]); + if (matchedWords != 0) { + *matchedarg = nextarg; + return matchedWords; + } + } + return 0; +} + +/* Matches as many input words as possible against a set of consecutive optional arguments. */ +static int matchOptionalArgs(char **words, int numwords, cliCommandArg *args, int numargs) { + int nextword = 0; + int matchedarg = -1, lastmatchedarg = -1; + while (nextword != numwords) { + int matchedWords = matchOneOptionalArg(&words[nextword], numwords - nextword, args, numargs, &matchedarg); + if (matchedWords == 0) { + break; + } + /* Successfully matched an optional arg; mark any previous match as completed + * so it won't be partially hinted. + */ + if (lastmatchedarg != -1) { + args[lastmatchedarg].matched_all = 1; + } + lastmatchedarg = matchedarg; + nextword += matchedWords; + } + return nextword; +} + +/* Matches as many input words as possible against command arguments. */ +static int matchArgs(char **words, int numwords, cliCommandArg *args, int numargs) { + int nextword, nextarg, matchedWords; + for (nextword = 0, nextarg = 0; nextword != numwords && nextarg != numargs; ++nextarg) { + /* Optional args can occur in any order. Collect a range of consecutive optional args + * and try to match them as a group against the next input words. + */ + if (args[nextarg].flags & CMD_ARG_OPTIONAL) { + int lastoptional; + for (lastoptional = nextarg; lastoptional < numargs; lastoptional++) { + if (!(args[lastoptional].flags & CMD_ARG_OPTIONAL)) break; + } + matchedWords = matchOptionalArgs(&words[nextword], numwords - nextword, &args[nextarg], lastoptional - nextarg); + nextarg = lastoptional - 1; + } else { + matchedWords = matchArg(&words[nextword], numwords - nextword, &args[nextarg]); + if (matchedWords == 0) { + /* Couldn't match a required word - matching fails! */ + return 0; + } + } + + nextword += matchedWords; + } + return nextword; +} + +/* Compute the linenoise hint for the input prefix in inputargv/inputargc. + * cmdlen is the number of words from the start of the input that make up the command. + * If docs.args exists, dynamically creates a hint string by matching the arg specs + * against the input words. + */ +static sds makeHint(char **inputargv, int inputargc, int cmdlen, struct commandDocs docs) { + sds hint; + + if (docs.args) { + /* Remove arguments from the returned hint to show only the + * ones the user did not yet type. */ + clearMatchedArgs(docs.args, docs.numargs); + hint = sdsempty(); + int matchedWords = 0; + if (inputargv && inputargc) + matchedWords = matchArgs(inputargv + cmdlen, inputargc - cmdlen, docs.args, docs.numargs); + if (matchedWords == inputargc - cmdlen) { + hint = addHintForArguments(hint, docs.args, docs.numargs, " "); + } + return hint; + } + + /* If arg specs are not available, show the hint string until the user types something. */ + if (inputargc <= cmdlen) { + hint = sdsnew(docs.params); + } else { + hint = sdsempty(); + } + return hint; +} + +/* Search for a command matching the longest possible prefix of input words. */ +static helpEntry* findHelpEntry(int argc, char **argv) { + helpEntry *entry = NULL; + int i, rawargc, matchlen = 0; + sds *rawargv; - /* Search longest matching prefix command */ for (i = 0; i < helpEntriesLen; i++) { if (!(helpEntries[i].type & CLI_HELP_COMMAND)) continue; - rawargv = sdssplitargs(helpEntries[i].full,&rawargc); + rawargv = helpEntries[i].argv; + rawargc = helpEntries[i].argc; if (rawargc <= argc) { int j; for (j = 0; j < rawargc; j++) { @@ -990,35 +1487,51 @@ static char *hintsCallback(const char *buf, int *color, int *bold) { entry = &helpEntries[i]; } } - sdsfreesplitres(rawargv,rawargc); } - sdsfreesplitres(argv - shift,argc + shift); + return entry; +} + +/* Returns the command-line hint string for a given partial input. */ +static sds getHintForInput(const char *charinput) { + sds hint = NULL; + int inputargc, inputlen = strlen(charinput); + sds *inputargv = sdssplitargs(charinput, &inputargc); + int endspace = inputlen && isspace(charinput[inputlen-1]); + /* Don't match the last word until the user has typed a space after it. */ + int matchargc = endspace ? inputargc : inputargc - 1; + + helpEntry *entry = findHelpEntry(matchargc, inputargv); if (entry) { - *color = 90; - *bold = 0; - sds hint = sdsnew(entry->org.params); + hint = makeHint(inputargv, matchargc, entry->argc, entry->docs); + } + sdsfreesplitres(inputargv, inputargc); + return hint; +} - /* Remove arguments from the returned hint to show only the - * ones the user did not yet type. */ - int toremove = argc-matchlen; - while(toremove > 0 && sdslen(hint)) { - if (hint[0] == '[') break; - if (hint[0] == ' ') toremove--; - sdsrange(hint,1,-1); - } +/* Linenoise hints callback. */ +static char *hintsCallback(const char *buf, int *color, int *bold) { + if (!pref.hints) return NULL; - /* Add an initial space if needed. */ - if (!endspace) { - sds newhint = sdsnewlen(" ",1); - newhint = sdscatsds(newhint,hint); - sdsfree(hint); - hint = newhint; - } + sds hint = getHintForInput(buf); + if (hint == NULL) { + return NULL; + } - return hint; + *color = 90; + *bold = 0; + + /* Add an initial space if needed. */ + int len = strlen(buf); + int endspace = len && isspace(buf[len-1]); + if (!endspace) { + sds newhint = sdsnewlen(" ",1); + newhint = sdscatsds(newhint,hint); + sdsfree(hint); + hint = newhint; } - return NULL; + + return hint; } static void freeHintsCallback(void *ptr) { @@ -1119,6 +1632,16 @@ static int cliSwitchProto(void) { result = REDIS_OK; } } + + /* Retrieve server version string for later use. */ + for (size_t i = 0; i < reply->elements; i += 2) { + assert(reply->element[i]->type == REDIS_REPLY_STRING); + char *key = reply->element[i]->str; + if (!strcmp(key, "version")) { + assert(reply->element[i + 1]->type == REDIS_REPLY_STRING); + config.server_version = sdsnew(reply->element[i + 1]->str); + } + } freeReplyObject(reply); config.current_resp3 = 1; return result; @@ -1202,7 +1725,7 @@ static int cliConnect(int flags) { /* In cluster, if server replies ASK, we will redirect to a different node. * Before sending the real command, we need to send ASKING command first. */ -static int cliSendAsking() { +static int cliSendAsking(void) { redisReply *reply; config.cluster_send_asking = 0; @@ -1797,7 +2320,7 @@ static int cliReadReply(int output_raw_strings) { } /* Simultaneously wait for pubsub messages from redis and input on stdin. */ -static void cliWaitForMessagesOrStdin() { +static void cliWaitForMessagesOrStdin(void) { int show_info = config.output != OUTPUT_RAW && (isatty(STDOUT_FILENO) || getenv("FAKETTY")); int use_color = show_info && isColorTerm(); @@ -2205,6 +2728,8 @@ static int parseOptions(int argc, char **argv) { } else if (!strcmp(argv[i],"--pattern") && !lastarg) { sdsfree(config.pattern); config.pattern = sdsnew(argv[++i]); + } else if (!strcmp(argv[i],"--count") && !lastarg) { + config.count = atoi(argv[++i]); } else if (!strcmp(argv[i],"--quoted-pattern") && !lastarg) { sdsfree(config.pattern); config.pattern = unquoteCString(argv[++i]); @@ -2341,6 +2866,10 @@ static int parseOptions(int argc, char **argv) { } else if (!strcmp(argv[i],"--cluster-fix-with-unreachable-masters")) { config.cluster_manager_command.flags |= CLUSTER_MANAGER_CMD_FLAG_FIX_WITH_UNREACHABLE_MASTERS; + } else if (!strcmp(argv[i],"--test_hint") && !lastarg) { + config.test_hint = argv[++i]; + } else if (!strcmp(argv[i],"--test_hint_file") && !lastarg) { + config.test_hint_file = argv[++i]; #ifdef USE_OPENSSL } else if (!strcmp(argv[i],"--tls")) { config.tls = 1; @@ -2439,7 +2968,7 @@ static int parseOptions(int argc, char **argv) { return i; } -static void parseEnv() { +static void parseEnv(void) { /* Set auth from env, but do not overwrite CLI arguments if passed */ char *auth = getenv(REDIS_CLI_AUTH_ENV); if (auth != NULL && config.conn_info.auth == NULL) { @@ -2455,6 +2984,29 @@ static void parseEnv() { static void usage(int err) { sds version = cliVersion(); FILE *target = err ? stderr: stdout; + const char *tls_usage = +#ifdef USE_OPENSSL +" --tls Establish a secure TLS connection.\n" +" --sni <host> Server name indication for TLS.\n" +" --cacert <file> CA Certificate file to verify with.\n" +" --cacertdir <dir> Directory where trusted CA certificates are stored.\n" +" If neither cacert nor cacertdir are specified, the default\n" +" system-wide trusted root certs configuration will apply.\n" +" --insecure Allow insecure TLS connection by skipping cert validation.\n" +" --cert <file> Client certificate to authenticate with.\n" +" --key <file> Private key file to authenticate with.\n" +" --tls-ciphers <list> Sets the list of preferred ciphers (TLSv1.2 and below)\n" +" in order of preference from highest to lowest separated by colon (\":\").\n" +" See the ciphers(1ssl) manpage for more information about the syntax of this string.\n" +#ifdef TLS1_3_VERSION +" --tls-ciphersuites <list> Sets the list of preferred ciphersuites (TLSv1.3)\n" +" in order of preference from highest to lowest separated by colon (\":\").\n" +" See the ciphers(1ssl) manpage for more information about the syntax of this string,\n" +" and specifically for TLSv1.3 ciphersuites.\n" +#endif +#endif +""; + fprintf(target, "redis-cli %s\n" "\n" @@ -2486,26 +3038,7 @@ static void usage(int err) { " -D <delimiter> Delimiter between responses for raw formatting (default: \\n).\n" " -c Enable cluster mode (follow -ASK and -MOVED redirections).\n" " -e Return exit error code when command execution fails.\n" -#ifdef USE_OPENSSL -" --tls Establish a secure TLS connection.\n" -" --sni <host> Server name indication for TLS.\n" -" --cacert <file> CA Certificate file to verify with.\n" -" --cacertdir <dir> Directory where trusted CA certificates are stored.\n" -" If neither cacert nor cacertdir are specified, the default\n" -" system-wide trusted root certs configuration will apply.\n" -" --insecure Allow insecure TLS connection by skipping cert validation.\n" -" --cert <file> Client certificate to authenticate with.\n" -" --key <file> Private key file to authenticate with.\n" -" --tls-ciphers <list> Sets the list of preferred ciphers (TLSv1.2 and below)\n" -" in order of preference from highest to lowest separated by colon (\":\").\n" -" See the ciphers(1ssl) manpage for more information about the syntax of this string.\n" -#ifdef TLS1_3_VERSION -" --tls-ciphersuites <list> Sets the list of preferred ciphersuites (TLSv1.3)\n" -" in order of preference from highest to lowest separated by colon (\":\").\n" -" See the ciphers(1ssl) manpage for more information about the syntax of this string,\n" -" and specifically for TLSv1.3 ciphersuites.\n" -#endif -#endif +"%s" " --raw Use raw formatting for replies (default when STDOUT is\n" " not a tty).\n" " --no-raw Force formatted output even when STDOUT is not a tty.\n" @@ -2515,7 +3048,8 @@ static void usage(int err) { " --quoted-json Same as --json, but produce ASCII-safe quoted strings, not Unicode.\n" " --show-pushes <yn> Whether to print RESP3 PUSH messages. Enabled by default when\n" " STDOUT is a tty but can be overridden with --show-pushes no.\n" -" --stat Print rolling stats about server: mem, clients, ...\n",version); +" --stat Print rolling stats about server: mem, clients, ...\n", +version,tls_usage); fprintf(target, " --latency Enter a special mode continuously sampling latency.\n" @@ -2550,6 +3084,7 @@ static void usage(int err) { " --scan List all keys using the SCAN command.\n" " --pattern <pat> Keys pattern when using the --scan, --bigkeys or --hotkeys\n" " options (default: *).\n" +" --count <count> Count option when using the --scan, --bigkeys or --hotkeys (default: 10).\n" " --quoted-pattern <pat> Same as --pattern, but the specified string can be\n" " quoted, in order to pass an otherwise non binary-safe string.\n" " --intrinsic-latency <sec> Run a test to measure intrinsic system latency.\n" @@ -2580,6 +3115,7 @@ static void usage(int err) { " redis-cli --quoted-input set '\"null-\\x00-separated\"' value\n" " redis-cli --eval myscript.lua key1 key2 , arg1 arg2 arg3\n" " redis-cli --scan --pattern '*:12345*'\n" +" redis-cli --scan --pattern '*:12345*' --count 100\n" "\n" " (Note: when using --eval the comma separates KEYS[] from ARGV[] items)\n" "\n" @@ -2735,12 +3271,15 @@ static int isSensitiveCommand(int argc, char **argv) { return 1; } else if (argc > 2 && !strcasecmp(argv[0],"config") && - !strcasecmp(argv[1],"set") && ( - !strcasecmp(argv[2],"masterauth") || - !strcasecmp(argv[2],"masteruser") || - !strcasecmp(argv[2],"requirepass"))) - { - return 1; + !strcasecmp(argv[1],"set")) { + for (int j = 2; j < argc; j = j+2) { + if (!strcasecmp(argv[j],"masterauth") || + !strcasecmp(argv[j],"masteruser") || + !strcasecmp(argv[j],"requirepass")) { + return 1; + } + } + return 0; /* HELLO [protover [AUTH username password] [SETNAME clientname]] */ } else if (argc > 4 && !strcasecmp(argv[0],"hello")) { for (int j = 2; j < argc; j++) { @@ -5336,7 +5875,7 @@ static clusterManagerNode * clusterManagerGetNodeWithMostKeysInSlot(list *nodes, * in the cluster. If there are multiple masters with the same smaller * number of replicas, one at random is returned. */ -static clusterManagerNode *clusterManagerNodeWithLeastReplicas() { +static clusterManagerNode *clusterManagerNodeWithLeastReplicas(void) { clusterManagerNode *node = NULL; int lowest_count = 0; listIter li; @@ -5355,7 +5894,7 @@ static clusterManagerNode *clusterManagerNodeWithLeastReplicas() { /* This function returns a random master node, return NULL if none */ -static clusterManagerNode *clusterManagerNodeMasterRandom() { +static clusterManagerNode *clusterManagerNodeMasterRandom(void) { int master_count = 0; int idx; listIter li; @@ -7857,7 +8396,7 @@ int sendReplconf(const char* arg1, const char* arg2) { return res; } -void sendCapa() { +void sendCapa(void) { sendReplconf("capa", "eof"); } @@ -8292,8 +8831,8 @@ static redisReply *sendScan(unsigned long long *it) { redisReply *reply; if (config.pattern) - reply = redisCommand(context, "SCAN %llu MATCH %b", - *it, config.pattern, sdslen(config.pattern)); + reply = redisCommand(context, "SCAN %llu MATCH %b COUNT %d", + *it, config.pattern, sdslen(config.pattern), config.count); else reply = redisCommand(context,"SCAN %llu",*it); @@ -9119,6 +9658,90 @@ static sds askPassword(const char *msg) { return auth; } +/* Prints out the hint completion string for a given input prefix string. */ +void testHint(const char *input) { + cliInitHelp(); + + sds hint = getHintForInput(input); + printf("%s\n", hint); + exit(0); +} + +sds readHintSuiteLine(char buf[], size_t size, FILE *fp) { + while (fgets(buf, size, fp) != NULL) { + if (buf[0] != '#') { + sds input = sdsnew(buf); + + /* Strip newline. */ + input = sdstrim(input, "\n"); + return input; + } + } + return NULL; +} + +/* Runs a suite of hint completion tests contained in a file. */ +void testHintSuite(char *filename) { + FILE *fp; + char buf[256]; + sds line, input, expected, hint; + int pass=0, fail=0; + int argc; + char **argv; + + fp = fopen(filename, "r"); + if (!fp) { + fprintf(stderr, + "Can't open file '%s': %s\n", filename, strerror(errno)); + exit(-1); + } + + cliInitHelp(); + + while (1) { + line = readHintSuiteLine(buf, sizeof(buf), fp); + if (line == NULL) break; + argv = sdssplitargs(line, &argc); + sdsfree(line); + if (argc == 0) { + sdsfreesplitres(argv, argc); + continue; + } + + if (argc == 1) { + fprintf(stderr, + "Missing expected hint for input '%s'\n", argv[0]); + exit(-1); + } + input = argv[0]; + expected = argv[1]; + hint = getHintForInput(input); + if (config.verbose) { + printf("Input: '%s', Expected: '%s', Hint: '%s'\n", input, expected, hint); + } + + /* Strip trailing spaces from hint - they don't matter. */ + while (hint != NULL && sdslen(hint) > 0 && hint[sdslen(hint) - 1] == ' ') { + sdssetlen(hint, sdslen(hint) - 1); + hint[sdslen(hint)] = '\0'; + } + + if (hint == NULL || strcmp(hint, expected) != 0) { + fprintf(stderr, "Test case '%s' FAILED: expected '%s', got '%s'\n", input, expected, hint); + ++fail; + } + else { + ++pass; + } + sdsfreesplitres(argv, argc); + sdsfree(hint); + } + fclose(fp); + + printf("%s: %d/%d passed\n", fail == 0 ? "SUCCESS" : "FAILURE", pass, pass + fail); + exit(fail); +} + /*------------------------------------------------------------------------------ * Program main() *--------------------------------------------------------------------------- */ @@ -9152,6 +9775,7 @@ int main(int argc, char **argv) { config.get_functions_rdb_mode = 0; config.stat_mode = 0; config.scan_mode = 0; + config.count = 10; config.intrinsic_latency_mode = 0; config.pattern = NULL; config.rdb_filename = NULL; @@ -9176,6 +9800,7 @@ int main(int argc, char **argv) { config.set_errcode = 0; config.no_auth_warning = 0; config.in_multi = 0; + config.server_version = NULL; config.cluster_manager_command.name = NULL; config.cluster_manager_command.argc = 0; config.cluster_manager_command.argv = NULL; @@ -9321,6 +9946,15 @@ int main(int argc, char **argv) { /* Intrinsic latency mode */ if (config.intrinsic_latency_mode) intrinsicLatencyMode(); + /* Print command-line hint for an input prefix string */ + if (config.test_hint) { + testHint(config.test_hint); + } + /* Run test suite for command-line hints */ + if (config.test_hint_file) { + testHintSuite(config.test_hint_file); + } + /* Start interactive mode when no command is provided */ if (argc == 0 && !config.eval) { /* Ignore SIGPIPE in interactive mode to force a reconnect */ |