diff options
author | Jason Elbaum <jason.elbaum@gmail.com> | 2022-02-05 16:54:16 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-02-05 16:54:16 +0200 |
commit | 5b17909c4f69830e93a569b6e4994c9775657e35 (patch) | |
tree | ef5ad8f9329cf1cb242099826449739f2c52021c /src/redis-cli.c | |
parent | e662f6835e325e8a3e485828eacddc6bfdb91660 (diff) | |
download | redis-5b17909c4f69830e93a569b6e4994c9775657e35.tar.gz |
redis-cli generates command help tables from the results of COMMAND (#10043)
This is a followup to #9656 and implements the following step mentioned in that PR:
* When possible, extract all the help and completion tips from COMMAND DOCS (Redis 7.0 and up)
* If COMMAND DOCS fails, use the static help.h compiled into redis-cli.
* Supplement additional command names from COMMAND (pre-Redis 7.0)
The last step is needed to add module command and other non-standard commands.
This PR does not change the interactive hinting mechanism, which still uses only the param
strings to provide somewhat unreliable and inconsistent command hints (see #8084).
That task is left for a future PR.
Co-authored-by: Oran Agra <oran@redislabs.com>
Diffstat (limited to 'src/redis-cli.c')
-rw-r--r-- | src/redis-cli.c | 395 |
1 files changed, 352 insertions, 43 deletions
diff --git a/src/redis-cli.c b/src/redis-cli.c index 31a2973c7..bbbe6d6ec 100644 --- a/src/redis-cli.c +++ b/src/redis-cli.c @@ -58,7 +58,7 @@ #include "adlist.h" #include "zmalloc.h" #include "linenoise.h" -#include "help.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 "cli_common.h" @@ -167,13 +167,21 @@ int *spectrum_palette; int spectrum_palette_size; /* Dict Helpers */ - static uint64_t dictSdsHash(const void *key); static int dictSdsKeyCompare(dict *d, const void *key1, const void *key2); 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; @@ -398,11 +406,11 @@ typedef struct { sds full; /* Only used for help on commands */ - struct commandHelp *org; + struct commandDocs org; } helpEntry; -static helpEntry *helpEntries; -static int helpEntriesLen; +static helpEntry *helpEntries = NULL; +static int helpEntriesLen = 0; static sds cliVersion(void) { sds version; @@ -418,7 +426,8 @@ static sds cliVersion(void) { return version; } -static void cliInitHelp(void) { +/* 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; @@ -433,7 +442,11 @@ static void cliInitHelp(void) { tmp.argv[0] = sdscatprintf(sdsempty(),"@%s",commandGroups[i]); tmp.full = tmp.argv[0]; tmp.type = CLI_HELP_GROUP; - tmp.org = NULL; + tmp.org.name = NULL; + tmp.org.params = NULL; + tmp.org.summary = NULL; + tmp.org.since = NULL; + tmp.org.group = NULL; helpEntries[pos++] = tmp; } @@ -441,17 +454,22 @@ static void cliInitHelp(void) { tmp.argv = sdssplitargs(commandHelp[i].name,&tmp.argc); tmp.full = sdsnew(commandHelp[i].name); tmp.type = CLI_HELP_COMMAND; - tmp.org = &commandHelp[i]; + 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; } } -/* cliInitHelp() setups the helpEntries array with the command and group +/* 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 * 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 cliIntegrateHelp(void) { +static void cliOldIntegrateHelp(void) { if (cliConnect(CC_QUIET) == REDIS_ERR) return; redisReply *reply = redisCommand(context, "COMMAND"); @@ -486,33 +504,334 @@ static void cliIntegrateHelp(void) { new->type = CLI_HELP_COMMAND; sdstoupper(new->argv[0]); - struct commandHelp *ch = zmalloc(sizeof(*ch)); - ch->name = new->argv[0]; - ch->params = sdsempty(); + new->org.name = new->argv[0]; + new->org.params = sdsempty(); int args = llabs(entry->element[1]->integer); args--; /* Remove the command name itself. */ if (entry->element[3]->integer == 1) { - ch->params = sdscat(ch->params,"key "); + new->org.params = sdscat(new->org.params,"key "); args--; } - while(args-- > 0) ch->params = sdscat(ch->params,"arg "); + while(args-- > 0) new->org.params = sdscat(new->org.params,"arg "); if (entry->element[1]->integer < 0) - ch->params = sdscat(ch->params,"...options..."); - ch->summary = "Help not available"; - ch->group = 0; - ch->since = "not known"; - new->org = ch; + new->org.params = sdscat(new->org.params,"...options..."); + new->org.summary = "Help not available"; + new->org.since = "Not known"; + new->org.group = commandGroups[0]; } 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) { + if (value[0] == '\0') { + return sdscat(params, "\"\""); + } + return sdscat(params, value); +} + +static sds cliAddArgument(sds params, 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) { + for (size_t j = 0; j < arguments->elements; j++) { + params = cliAddArgument(params, arguments->element[j]); + if (j != arguments->elements - 1) { + params = sdscat(params, separator); + } + } + 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. */ + if (argMap->type != REDIS_REPLY_MAP && argMap->type != REDIS_REPLY_ARRAY) { + return params; + } + 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; + } 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); + } else if (!strcmp(key, "type")) { + assert(argMap->element[i + 1]->type == REDIS_REPLY_STRING); + type = argMap->element[i + 1]->str; + } else if (!strcmp(key, "arguments")) { + arguments = argMap->element[i + 1]; + } else if (!strcmp(key, "flags")) { + redisReply *flags = argMap->element[i + 1]; + assert(flags->type == REDIS_REPLY_SET || flags->type == REDIS_REPLY_ARRAY); + for (size_t j = 0; j < flags->elements; j++) { + assert(flags->element[j]->type == REDIS_REPLY_STATUS); + char *flag = flags->element[j]->str; + if (!strcmp(flag, "optional")) { + optional = 1; + } else if (!strcmp(flag, "multiple")) { + multiple = 1; + } else if (!strcmp(flag, "multiple_token")) { + multipleToken = 1; + } + } + } + } + + /* 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. */ +static void cliFillInCommandHelpEntry(helpEntry *help, char *cmdname, char *subcommandname) { + help->argc = subcommandname ? 2 : 1; + help->argv = zmalloc(sizeof(sds) * help->argc); + 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); + sdstoupper(help->argv[1]); + } + sds fullname = sdsnew(help->argv[0]); + if (subcommandname) { + fullname = sdscat(fullname, " "); + fullname = sdscat(fullname, help->argv[1]); + } + help->full = fullname; + help->type = CLI_HELP_COMMAND; + + help->org.name = help->full; + help->org.params = sdsempty(); + help->org.since = NULL; +} + +/* Initialize a command help entry for the command/subcommand described in 'specs'. + * 'next' points to the next help entry to be filled in. + * 'groups' is a set of command group names to be filled in. + * Returns a pointer to the next available position in the help entries table. + * If the command has subcommands, this is called recursively for the subcommands. + */ +static helpEntry *cliInitCommandHelpEntry(char *cmdname, char *subcommandname, + helpEntry *next, redisReply *specs, + dict *groups) { + helpEntry *help = next++; + cliFillInCommandHelpEntry(help, cmdname, subcommandname); + + assert(specs->type == REDIS_REPLY_MAP || specs->type == REDIS_REPLY_ARRAY); + for (size_t j = 0; j < specs->elements; j += 2) { + assert(specs->element[j]->type == REDIS_REPLY_STRING); + char *key = specs->element[j]->str; + if (!strcmp(key, "summary")) { + redisReply *reply = specs->element[j + 1]; + assert(reply->type == REDIS_REPLY_STRING); + help->org.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); + } 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); + 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, " "); + } else if (!strcmp(key, "subcommands")) { + redisReply *subcommands = specs->element[j + 1]; + assert(subcommands->type == REDIS_REPLY_MAP || subcommands->type == REDIS_REPLY_ARRAY); + for (size_t i = 0; i < subcommands->elements; i += 2) { + assert(subcommands->element[i]->type == REDIS_REPLY_STRING); + char *subcommandname = subcommands->element[i]->str; + redisReply *subcommand = subcommands->element[i + 1]; + assert(subcommand->type == REDIS_REPLY_MAP || subcommand->type == REDIS_REPLY_ARRAY); + next = cliInitCommandHelpEntry(cmdname, subcommandname, next, subcommand, groups); + } + } + } + return next; +} + +/* Returns the total number of commands and subcommands in the command docs table. */ +static size_t cliCountCommands(redisReply* commandTable) { + size_t numCommands = commandTable->elements / 2; + + /* The command docs table maps command names to a map of their specs. */ + for (size_t i = 0; i < commandTable->elements; i += 2) { + assert(commandTable->element[i]->type == REDIS_REPLY_STRING); /* Command name. */ + assert(commandTable->element[i + 1]->type == REDIS_REPLY_MAP || + commandTable->element[i + 1]->type == REDIS_REPLY_ARRAY); + redisReply *map = commandTable->element[i + 1]; + for (size_t j = 0; j < map->elements; j += 2) { + assert(map->element[j]->type == REDIS_REPLY_STRING); + char *key = map->element[j]->str; + if (!strcmp(key, "subcommands")) { + redisReply *subcommands = map->element[j + 1]; + assert(subcommands->type == REDIS_REPLY_MAP || subcommands->type == REDIS_REPLY_ARRAY); + numCommands += subcommands->elements / 2; + } + } + } + return numCommands; +} + +/* Comparator for sorting help table entries. */ +int helpEntryCompare(const void *entry1, const void *entry2) { + helpEntry *i1 = (helpEntry *)entry1; + helpEntry *i2 = (helpEntry *)entry2; + return strcmp(i1->full, i2->full); +} + +/* Initializes command help entries for command groups. + * Called after the command help entries have already been filled in. + * Extends the help table with new entries for the command groups. + */ +void cliInitGroupHelpEntries(dict *groups) { + dictIterator *iter = dictGetIterator(groups); + dictEntry *entry; + helpEntry tmp; + + int numGroups = dictSize(groups); + int pos = helpEntriesLen; + helpEntriesLen += numGroups; + helpEntries = zrealloc(helpEntries, sizeof(helpEntry)*helpEntriesLen); + + for (entry = dictNext(iter); entry != NULL; entry = dictNext(iter)) { + tmp.argc = 1; + tmp.argv = zmalloc(sizeof(sds)); + tmp.argv[0] = sdscatprintf(sdsempty(),"@%s",(char *)entry->key); + 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; + } + dictReleaseIterator(iter); +} + +/* Initializes help entries for all commands in the COMMAND DOCS reply. */ +void cliInitCommandHelpEntries(redisReply *commandTable, dict *groups) { + helpEntry *next = helpEntries; + for (size_t i = 0; i < commandTable->elements; i += 2) { + assert(commandTable->element[i]->type == REDIS_REPLY_STRING); + char *cmdname = commandTable->element[i]->str; + + assert(commandTable->element[i + 1]->type == REDIS_REPLY_MAP || + commandTable->element[i + 1]->type == REDIS_REPLY_ARRAY); + redisReply *cmdspecs = commandTable->element[i + 1]; + next = cliInitCommandHelpEntry(cmdname, NULL, next, cmdspecs, groups); + } +} + +/* cliInitHelp() sets up the helpEntries array with the command and group + * names and command descriptions obtained using the COMMAND DOCS command. + */ +static void cliInitHelp(void) { + /* Dict type for a set of strings, used to collect names of command groups. */ + dictType groupsdt = { + dictSdsHash, /* hash function */ + NULL, /* key dup */ + NULL, /* val dup */ + dictSdsKeyCompare, /* key compare */ + dictSdsDestructor, /* key destructor */ + NULL, /* val destructor */ + NULL /* allow to expand */ + }; + redisReply *commandTable; + dict *groups; + + if (cliConnect(CC_QUIET) == REDIS_ERR) 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. */ + freeReplyObject(commandTable); + cliOldInitHelp(); + cliOldIntegrateHelp(); + return; + }; + if (commandTable->type != REDIS_REPLY_MAP && commandTable->type != REDIS_REPLY_ARRAY) return; + + /* Scan the array reported by COMMAND DOCS and fill in the entries */ + helpEntriesLen = cliCountCommands(commandTable); + helpEntries = zmalloc(sizeof(helpEntry)*helpEntriesLen); + + groups = dictCreate(&groupsdt); + cliInitCommandHelpEntries(commandTable, groups); + cliInitGroupHelpEntries(groups); + + qsort(helpEntries, helpEntriesLen, sizeof(helpEntry), helpEntryCompare); + freeReplyObject(commandTable); + dictRelease(groups); +} + /* Output command help to stdout. */ -static void cliOutputCommandHelp(struct commandHelp *help, int group) { +static void cliOutputCommandHelp(struct commandDocs *help, int group) { printf("\r\n \x1b[1m%s\x1b[0m \x1b[90m%s\x1b[0m\r\n", help->name, help->params); printf(" \x1b[33msummary:\x1b[0m %s\r\n", help->summary); - printf(" \x1b[33msince:\x1b[0m %s\r\n", help->since); + if (help->since != NULL) { + printf(" \x1b[33msince:\x1b[0m %s\r\n", help->since); + } if (group) { - printf(" \x1b[33mgroup:\x1b[0m %s\r\n", commandGroups[help->group]); + printf(" \x1b[33mgroup:\x1b[0m %s\r\n", help->group); } } @@ -538,22 +857,16 @@ static void cliOutputGenericHelp(void) { /* Output all command help, filtering by group or command name. */ static void cliOutputHelp(int argc, char **argv) { - int i, j, len; - int group = -1; + int i, j; + char *group = NULL; helpEntry *entry; - struct commandHelp *help; + struct commandDocs *help; if (argc == 0) { cliOutputGenericHelp(); return; } else if (argc > 0 && argv[0][0] == '@') { - len = sizeof(commandGroups)/sizeof(char*); - for (i = 0; i < len; i++) { - if (strcasecmp(argv[0]+1,commandGroups[i]) == 0) { - group = i; - break; - } - } + group = argv[0]+1; } assert(argc > 0); @@ -561,8 +874,8 @@ static void cliOutputHelp(int argc, char **argv) { entry = &helpEntries[i]; if (entry->type != CLI_HELP_COMMAND) continue; - help = entry->org; - if (group == -1) { + help = &entry->org; + if (group == NULL) { /* Compare all arguments */ if (argc <= entry->argc) { for (j = 0; j < argc; j++) { @@ -572,10 +885,8 @@ static void cliOutputHelp(int argc, char **argv) { cliOutputCommandHelp(help,1); } } - } else { - if (group == help->group) { - cliOutputCommandHelp(help,0); - } + } else if (strcasecmp(group, help->group) == 0) { + cliOutputCommandHelp(help,0); } } printf("\r\n"); @@ -649,7 +960,7 @@ static char *hintsCallback(const char *buf, int *color, int *bold) { if (entry) { *color = 90; *bold = 0; - sds hint = sdsnew(entry->org->params); + sds hint = sdsnew(entry->org.params); /* Remove arguments from the returned hint to show only the * ones the user did not yet type. */ @@ -2229,10 +2540,8 @@ static void repl(void) { int argc; sds *argv; - /* Initialize the help and, if possible, use the COMMAND command in order - * to retrieve missing entries. */ + /* Initialize the help using the results of the COMMAND command. */ cliInitHelp(); - cliIntegrateHelp(); config.interactive = 1; linenoiseSetMultiLine(1); |