summaryrefslogtreecommitdiff
path: root/src/redis-cli.c
diff options
context:
space:
mode:
authorJason Elbaum <jason.elbaum@gmail.com>2022-02-05 16:54:16 +0200
committerGitHub <noreply@github.com>2022-02-05 16:54:16 +0200
commit5b17909c4f69830e93a569b6e4994c9775657e35 (patch)
treeef5ad8f9329cf1cb242099826449739f2c52021c /src/redis-cli.c
parente662f6835e325e8a3e485828eacddc6bfdb91660 (diff)
downloadredis-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.c395
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);