summaryrefslogtreecommitdiff
path: root/src/redis-cli.c
diff options
context:
space:
mode:
authorJason Elbaum <Jason.elbaum@redis.com>2023-03-30 19:03:56 +0300
committerGitHub <noreply@github.com>2023-03-30 19:03:56 +0300
commit1f76bb17ddcb2adc484bf82f1b839c45e264524f (patch)
treeec1f4d09166d1c2bd783c3668f2a4913aa8bcf5e /src/redis-cli.c
parent971b177fa338fe06cb67a930c6e54467d29ec44f (diff)
downloadredis-1f76bb17ddcb2adc484bf82f1b839c45e264524f.tar.gz
Reimplement cli hints based on command arg docs (#10515)
Now that the command argument specs are available at runtime (#9656), this PR addresses #8084 by implementing a complete solution for command-line hinting in `redis-cli`. It correctly handles nearly every case in Redis's complex command argument definitions, including `BLOCK` and `ONEOF` arguments, reordering of optional arguments, and repeated arguments (even when followed by mandatory arguments). It also validates numerically-typed arguments. It may not correctly handle all possible combinations of those, but overall it is quite robust. Arguments are only matched after the space bar is typed, so partial word matching is not supported - that proved to be more confusing than helpful. When the user's current input cannot be matched against the argument specs, hinting is disabled. Partial support has been implemented for legacy (pre-7.0) servers that do not support `COMMAND DOCS`, by falling back to a statically-compiled command argument table. On startup, if the server does not support `COMMAND DOCS`, `redis-cli` will now issue an `INFO SERVER` command to retrieve the server version (unless `HELLO` has already been sent, in which case the server version will be extracted from the reply to `HELLO`). The server version will be used to filter the commands and arguments in the command table, removing those not supported by that version of the server. However, the static table only includes core Redis commands, so with a legacy server hinting will not be supported for module commands. The auto generated help.h and the scripts that generates it are gone. Command and argument tables for the server and CLI use different structs, due primarily to the need to support different runtime data. In order to generate code for both, macros have been added to `commands.def` (previously `commands.c`) to make it possible to configure the code generation differently for different use cases (one linked with redis-server, and one with redis-cli). Also adding a basic testing framework for the command hints based on new (undocumented) command line options to `redis-cli`: `--test_hint 'INPUT'` prints out the command-line hint for a given input string, and `--test_hint_file <filename>` runs a suite of test cases for the hinting mechanism. The test suite is in `tests/assets/test_cli_hint_suite.txt`, and it is run from `tests/integration/redis-cli.tcl`. Co-authored-by: Oran Agra <oran@redislabs.com> Co-authored-by: Viktor Söderqvist <viktor.soderqvist@est.tech>
Diffstat (limited to 'src/redis-cli.c')
-rw-r--r--src/redis-cli.c1014
1 files changed, 817 insertions, 197 deletions
diff --git a/src/redis-cli.c b/src/redis-cli.c
index d8e6b966a..d92fcb01a 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;
@@ -281,6 +273,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 +417,7 @@ typedef struct {
sds full;
/* Only used for help on commands */
- struct commandDocs org;
+ struct commandDocs docs;
} helpEntry;
static helpEntry *helpEntries = NULL;
@@ -442,50 +437,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 +478,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);
+
+static void cliAddCommandDocArg(cliCommandArg *cmdArg, redisReply *argMap);
-/* 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 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 +567,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 +585,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 +602,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 +628,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 +713,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 +739,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() {
+ 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 +916,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 +1004,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 +1051,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 +1486,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 +1631,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;
@@ -2341,6 +2863,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;
@@ -9119,6 +9645,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()
*--------------------------------------------------------------------------- */
@@ -9176,6 +9786,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 +9932,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 */