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>
This commit is contained in:
Jason Elbaum 2022-02-05 16:54:16 +02:00 committed by GitHub
parent e662f6835e
commit 5b17909c4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 353 additions and 44 deletions

View File

@ -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);

View File

@ -647,7 +647,7 @@ proc latencyrstat_percentiles {cmd r} {
proc generate_fuzzy_traffic_on_key {key duration} {
# Commands per type, blocking commands removed
# TODO: extract these from help.h or elsewhere, and improve to include other types
# TODO: extract these from COMMAND DOCS, and improve to include other types
set string_commands {APPEND BITCOUNT BITFIELD BITOP BITPOS DECR DECRBY GET GETBIT GETRANGE GETSET INCR INCRBY INCRBYFLOAT MGET MSET MSETNX PSETEX SET SETBIT SETEX SETNX SETRANGE LCS STRLEN}
set hash_commands {HDEL HEXISTS HGET HGETALL HINCRBY HINCRBYFLOAT HKEYS HLEN HMGET HMSET HSCAN HSET HSETNX HSTRLEN HVALS HRANDFIELD}
set zset_commands {ZADD ZCARD ZCOUNT ZINCRBY ZINTERSTORE ZLEXCOUNT ZPOPMAX ZPOPMIN ZRANGE ZRANGEBYLEX ZRANGEBYSCORE ZRANK ZREM ZREMRANGEBYLEX ZREMRANGEBYRANK ZREMRANGEBYSCORE ZREVRANGE ZREVRANGEBYLEX ZREVRANGEBYSCORE ZREVRANK ZSCAN ZSCORE ZUNIONSTORE ZRANDMEMBER}

0
utils/generate-command-help.rb Executable file → Normal file
View File