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>
This commit is contained in:
Jason Elbaum 2023-03-30 19:03:56 +03:00 committed by GitHub
parent 971b177fa3
commit 1f76bb17dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 12023 additions and 9896 deletions

View File

@ -336,18 +336,18 @@ QUIET_INSTALL = @printf ' %b %b\n' $(LINKCOLOR)INSTALL$(ENDCOLOR) $(BINCOLOR)
endif
ifneq (, $(findstring LOG_REQ_RES, $(REDIS_CFLAGS)))
COMMANDS_FILENAME=commands_with_reply_schema
COMMANDS_DEF_FILENAME=commands_with_reply_schema
GEN_COMMANDS_FLAGS=--with-reply-schema
else
COMMANDS_FILENAME=commands
COMMANDS_DEF_FILENAME=commands
GEN_COMMANDS_FLAGS=
endif
REDIS_SERVER_NAME=redis-server$(PROG_SUFFIX)
REDIS_SENTINEL_NAME=redis-sentinel$(PROG_SUFFIX)
REDIS_SERVER_OBJ=adlist.o quicklist.o ae.o anet.o dict.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o $(COMMANDS_FILENAME).o strl.o connection.o unix.o logreqres.o
REDIS_SERVER_OBJ=adlist.o quicklist.o ae.o anet.o dict.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o commands.o strl.o connection.o unix.o logreqres.o
REDIS_CLI_NAME=redis-cli$(PROG_SUFFIX)
REDIS_CLI_OBJ=anet.o adlist.o dict.o redis-cli.o zmalloc.o release.o ae.o redisassert.o crcspeed.o crc64.o siphash.o crc16.o monotonic.o cli_common.o mt19937-64.o strl.o
REDIS_CLI_OBJ=anet.o adlist.o dict.o redis-cli.o zmalloc.o release.o ae.o redisassert.o crcspeed.o crc64.o siphash.o crc16.o monotonic.o cli_common.o mt19937-64.o strl.o cli_commands.o
REDIS_BENCHMARK_NAME=redis-benchmark$(PROG_SUFFIX)
REDIS_BENCHMARK_OBJ=ae.o anet.o redis-benchmark.o adlist.o dict.o zmalloc.o redisassert.o release.o crcspeed.o crc64.o siphash.o crc16.o monotonic.o cli_common.o mt19937-64.o strl.o
REDIS_CHECK_RDB_NAME=redis-check-rdb$(PROG_SUFFIX)
@ -435,13 +435,15 @@ DEP = $(REDIS_SERVER_OBJ:%.o=%.d) $(REDIS_CLI_OBJ:%.o=%.d) $(REDIS_BENCHMARK_OBJ
%.o: %.c .make-prerequisites
$(REDIS_CC) -MMD -o $@ -c $<
# The file commands.c is checked in and doesn't normally need to be rebuilt. It
# The file commands.def is checked in and doesn't normally need to be rebuilt. It
# is built only if python is available and its prereqs are modified.
ifneq (,$(PYTHON))
$(COMMANDS_FILENAME).c: commands/*.json ../utils/generate-command-code.py
$(COMMANDS_DEF_FILENAME).def: commands/*.json ../utils/generate-command-code.py
$(QUIET_GEN)$(PYTHON) ../utils/generate-command-code.py $(GEN_COMMANDS_FLAGS)
endif
commands.c: $(COMMANDS_DEF_FILENAME).def
clean:
rm -rf $(REDIS_SERVER_NAME) $(REDIS_SENTINEL_NAME) $(REDIS_CLI_NAME) $(REDIS_BENCHMARK_NAME) $(REDIS_CHECK_RDB_NAME) $(REDIS_CHECK_AOF_NAME) *.o *.gcda *.gcno *.gcov redis.info lcov-html Makefile.dep *.so
rm -f $(DEP)

13
src/cli_commands.c Normal file
View File

@ -0,0 +1,13 @@
#include <stddef.h>
#include "cli_commands.h"
/* Definitions to configure commands.c to generate the above structs. */
#define MAKE_CMD(name,summary,complexity,since,doc_flags,replaced,deprecated,group,group_enum,history,num_history,tips,num_tips,function,arity,flags,acl,key_specs,key_specs_num,get_keys,numargs) name,summary,group,since,numargs
#define MAKE_ARG(name,type,key_spec_index,token,summary,since,flags,numsubargs,deprecated_since) name,type,token,since,flags,numsubargs
#define COMMAND_ARG cliCommandArg
#define COMMAND_STRUCT commandDocs
#define SKIP_CMD_HISTORY_TABLE
#define SKIP_CMD_TIPS_TABLE
#define SKIP_CMD_KEY_SPECS_TABLE
#include "commands.def"

46
src/cli_commands.h Normal file
View File

@ -0,0 +1,46 @@
/* This file is used by redis-cli in place of server.h when including commands.c
* It contains alternative structs which omit the parts of the commands table
* that are not suitable for redis-cli, e.g. the command proc. */
#ifndef __REDIS_CLI_COMMANDS_H
#define __REDIS_CLI_COMMANDS_H
#include <stddef.h>
#include "commands.h"
/* Syntax specifications for a command argument. */
typedef struct cliCommandArg {
char *name;
redisCommandArgType type;
char *token;
char *since;
int flags;
int numsubargs;
struct cliCommandArg *subargs;
const char *display_text;
/*
* For use at runtime.
* Fields used to keep track of input word matches for command-line hinting.
*/
int matched; /* How many input words have been matched by this argument? */
int matched_token; /* Has the token been matched? */
int matched_name; /* Has the name been matched? */
int matched_all; /* Has the whole argument been consumed (no hint needed)? */
} cliCommandArg;
/* Command documentation info used for help output */
struct commandDocs {
char *name;
char *summary;
char *group;
char *since;
int numargs;
cliCommandArg *args; /* An array of the command arguments. */
struct commandDocs *subcommands;
char *params; /* A string describing the syntax of the command arguments. */
};
extern struct commandDocs redisCommandTable[];
#endif

File diff suppressed because it is too large Load Diff

10833
src/commands.def Normal file

File diff suppressed because it is too large Load Diff

40
src/commands.h Normal file
View File

@ -0,0 +1,40 @@
#ifndef __REDIS_COMMANDS_H
#define __REDIS_COMMANDS_H
/* Must be synced with ARG_TYPE_STR and generate-command-code.py */
typedef enum {
ARG_TYPE_STRING,
ARG_TYPE_INTEGER,
ARG_TYPE_DOUBLE,
ARG_TYPE_KEY, /* A string, but represents a keyname */
ARG_TYPE_PATTERN,
ARG_TYPE_UNIX_TIME,
ARG_TYPE_PURE_TOKEN,
ARG_TYPE_ONEOF, /* Has subargs */
ARG_TYPE_BLOCK /* Has subargs */
} redisCommandArgType;
#define CMD_ARG_NONE (0)
#define CMD_ARG_OPTIONAL (1<<0)
#define CMD_ARG_MULTIPLE (1<<1)
#define CMD_ARG_MULTIPLE_TOKEN (1<<2)
/* WARNING! This struct must match RedisModuleCommandArg */
typedef struct redisCommandArg {
const char *name;
redisCommandArgType type;
int key_spec_index;
const char *token;
const char *summary;
const char *since;
int flags;
const char *deprecated_since;
int num_args;
struct redisCommandArg *subargs;
const char *display_text;
} redisCommandArg;
/* Returns the command group name by group number. */
const char *commandGroupStr(int index);
#endif

1884
src/help.h

File diff suppressed because it is too large Load Diff

View File

@ -1295,10 +1295,9 @@ RedisModuleCommand *moduleCreateCommandProxy(struct RedisModule *module, sds dec
cp->rediscmd->proc = RedisModuleCommandDispatcher;
cp->rediscmd->flags = flags | CMD_MODULE;
cp->rediscmd->module_cmd = cp;
cp->rediscmd->key_specs_max = STATIC_KEY_SPECS_NUM;
cp->rediscmd->key_specs = cp->rediscmd->key_specs_static;
if (firstkey != 0) {
cp->rediscmd->key_specs_num = 1;
cp->rediscmd->key_specs = zcalloc(sizeof(keySpec));
cp->rediscmd->key_specs[0].flags = CMD_KEY_FULL_ACCESS;
if (flags & CMD_MODULE_GETKEYS)
cp->rediscmd->key_specs[0].flags |= CMD_KEY_VARIABLE_FLAGS;
@ -1310,6 +1309,7 @@ RedisModuleCommand *moduleCreateCommandProxy(struct RedisModule *module, sds dec
cp->rediscmd->key_specs[0].fk.range.limit = 0;
} else {
cp->rediscmd->key_specs_num = 0;
cp->rediscmd->key_specs = NULL;
}
populateCommandLegacyRangeSpec(cp->rediscmd);
cp->rediscmd->microseconds = 0;
@ -1425,6 +1425,21 @@ moduleCmdArgAt(const RedisModuleCommandInfoVersion *version,
return (RedisModuleCommandArg *)((char *)(args) + offset);
}
/* Recursively populate the args structure (setting num_args to the number of
* subargs) and return the number of args. */
int populateArgsStructure(struct redisCommandArg *args) {
if (!args)
return 0;
int count = 0;
while (args->name) {
serverAssert(count < INT_MAX);
args->num_args = populateArgsStructure(args->subargs);
count++;
args++;
}
return count;
}
/* Helper for categoryFlagsFromString(). Attempts to find an acl flag representing the provided flag string
* and adds that flag to acl_categories_flags if a match is found.
*
@ -1797,7 +1812,7 @@ int RM_SetCommandInfo(RedisModuleCommand *command, const RedisModuleCommandInfo
cmd->tips || cmd->args ||
!(cmd->key_specs_num == 0 ||
/* Allow key spec populated from legacy (first,last,step) to exist. */
(cmd->key_specs_num == 1 && cmd->key_specs == cmd->key_specs_static &&
(cmd->key_specs_num == 1 &&
cmd->key_specs[0].begin_search_type == KSPEC_BS_INDEX &&
cmd->key_specs[0].find_keys_type == KSPEC_FK_RANGE))) {
errno = EEXIST;
@ -1848,13 +1863,8 @@ int RM_SetCommandInfo(RedisModuleCommand *command, const RedisModuleCommandInfo
while (moduleCmdKeySpecAt(version, info->key_specs, count)->begin_search_type)
count++;
serverAssert(count < INT_MAX);
if (count <= STATIC_KEY_SPECS_NUM) {
cmd->key_specs_max = STATIC_KEY_SPECS_NUM;
cmd->key_specs = cmd->key_specs_static;
} else {
cmd->key_specs_max = count;
cmd->key_specs = zmalloc(sizeof(keySpec) * count);
}
zfree(cmd->key_specs);
cmd->key_specs = zmalloc(sizeof(keySpec) * count);
/* Copy the contents of the RedisModuleCommandKeySpec array. */
cmd->key_specs_num = count;
@ -11926,8 +11936,7 @@ int moduleFreeCommand(struct RedisModule *module, struct redisCommand *cmd) {
if (cmd->key_specs[j].begin_search_type == KSPEC_BS_KEYWORD)
zfree((char *)cmd->key_specs[j].bs.keyword.keyword);
}
if (cmd->key_specs != cmd->key_specs_static)
zfree(cmd->key_specs);
zfree(cmd->key_specs);
for (int j = 0; cmd->tips && cmd->tips[j]; j++)
zfree((char *)cmd->tips[j]);
zfree(cmd->tips);

File diff suppressed because it is too large Load Diff

View File

@ -2943,21 +2943,6 @@ void setImplicitACLCategories(struct redisCommand *c) {
c->acl_categories |= ACL_CATEGORY_SLOW;
}
/* Recursively populate the args structure (setting num_args to the number of
* subargs) and return the number of args. */
int populateArgsStructure(struct redisCommandArg *args) {
if (!args)
return 0;
int count = 0;
while (args->name) {
serverAssert(count < INT_MAX);
args->num_args = populateArgsStructure(args->subargs);
count++;
args++;
}
return count;
}
/* Recursively populate the command structure.
*
* On success, the function return C_OK. Otherwise C_ERR is returned and we won't
@ -2975,28 +2960,10 @@ int populateCommandStructure(struct redisCommand *c) {
* set of flags. */
setImplicitACLCategories(c);
/* Redis commands don't need more args than STATIC_KEY_SPECS_NUM (Number of keys
* specs can be greater than STATIC_KEY_SPECS_NUM only for module commands) */
c->key_specs = c->key_specs_static;
c->key_specs_max = STATIC_KEY_SPECS_NUM;
/* We start with an unallocated histogram and only allocate memory when a command
* has been issued for the first time */
c->latency_histogram = NULL;
for (int i = 0; i < STATIC_KEY_SPECS_NUM; i++) {
if (c->key_specs[i].begin_search_type == KSPEC_BS_INVALID)
break;
c->key_specs_num++;
}
/* Count things so we don't have to use deferred reply in COMMAND reply. */
while (c->history && c->history[c->num_history].since)
c->num_history++;
while (c->tips && c->tips[c->num_tips])
c->num_tips++;
c->num_args = populateArgsStructure(c->args);
/* Handle the legacy range spec and the "movablekeys" flag (must be done after populating all key specs). */
populateCommandLegacyRangeSpec(c);
@ -4860,28 +4827,6 @@ void addReplyCommandSubCommands(client *c, struct redisCommand *cmd, void (*repl
dictReleaseIterator(di);
}
/* Must match redisCommandGroup */
const char *COMMAND_GROUP_STR[] = {
"generic",
"string",
"list",
"set",
"sorted-set",
"hash",
"pubsub",
"transactions",
"connection",
"server",
"scripting",
"hyperloglog",
"cluster",
"sentinel",
"geo",
"stream",
"bitmap",
"module"
};
/* Output the representation of a Redis command. Used by the COMMAND command and COMMAND INFO. */
void addReplyCommandInfo(client *c, struct redisCommand *cmd) {
if (!cmd) {
@ -4940,7 +4885,7 @@ void addReplyCommandDocs(client *c, struct redisCommand *cmd) {
/* Always have the group, for module commands the group is always "module". */
addReplyBulkCString(c, "group");
addReplyBulkCString(c, COMMAND_GROUP_STR[cmd->group]);
addReplyBulkCString(c, commandGroupStr(cmd->group));
if (cmd->complexity) {
addReplyBulkCString(c, "complexity");

View File

@ -35,6 +35,7 @@
#include "solarisfixes.h"
#include "rio.h"
#include "atomicvar.h"
#include "commands.h"
#include <assert.h>
#include <stdio.h>
@ -2135,43 +2136,6 @@ typedef struct {
} fk;
} keySpec;
/* Number of static key specs */
#define STATIC_KEY_SPECS_NUM 4
/* Must be synced with ARG_TYPE_STR and generate-command-code.py */
typedef enum {
ARG_TYPE_STRING,
ARG_TYPE_INTEGER,
ARG_TYPE_DOUBLE,
ARG_TYPE_KEY, /* A string, but represents a keyname */
ARG_TYPE_PATTERN,
ARG_TYPE_UNIX_TIME,
ARG_TYPE_PURE_TOKEN,
ARG_TYPE_ONEOF, /* Has subargs */
ARG_TYPE_BLOCK /* Has subargs */
} redisCommandArgType;
#define CMD_ARG_NONE (0)
#define CMD_ARG_OPTIONAL (1<<0)
#define CMD_ARG_MULTIPLE (1<<1)
#define CMD_ARG_MULTIPLE_TOKEN (1<<2)
/* WARNING! This struct must match RedisModuleCommandArg */
typedef struct redisCommandArg {
const char *name;
redisCommandArgType type;
int key_spec_index;
const char *token;
const char *summary;
const char *since;
int flags;
const char *deprecated_since;
struct redisCommandArg *subargs;
const char *display_text;
/* runtime populated data */
int num_args;
} redisCommandArg;
#ifdef LOG_REQ_RES
/* Must be synced with generate-command-code.py */
@ -2341,15 +2305,19 @@ struct redisCommand {
const char *deprecated_since; /* In case the command is deprecated, when did it happen? */
redisCommandGroup group; /* Command group */
commandHistory *history; /* History of the command */
int num_history;
const char **tips; /* An array of strings that are meant to be tips for clients/proxies regarding this command */
int num_tips;
redisCommandProc *proc; /* Command implementation */
int arity; /* Number of arguments, it is possible to use -N to say >= N */
uint64_t flags; /* Command flags, see CMD_*. */
uint64_t acl_categories; /* ACl categories, see ACL_CATEGORY_*. */
keySpec key_specs_static[STATIC_KEY_SPECS_NUM]; /* Key specs. See keySpec */
keySpec *key_specs;
int key_specs_num;
/* Use a function to determine keys arguments in a command line.
* Used for Redis Cluster redirect (may be NULL) */
redisGetKeysProc *getkeys_proc;
int num_args; /* Length of args array. */
/* Array of subcommands (may be NULL) */
struct redisCommand *subcommands;
/* Array of arguments (may be NULL) */
@ -2368,16 +2336,10 @@ struct redisCommand {
bit set in the bitmap of allowed commands. */
sds fullname; /* A SDS string representing the command fullname. */
struct hdr_histogram* latency_histogram; /*points to the command latency command histogram (unit of time nanosecond) */
keySpec *key_specs;
keySpec legacy_range_key_spec; /* The legacy (first,last,step) key spec is
* still maintained (if applicable) so that
* we can still support the reply format of
* COMMAND INFO and COMMAND GETKEYS */
int num_args;
int num_history;
int num_tips;
int key_specs_num;
int key_specs_max;
dict *subcommands_dict; /* A dictionary that holds the subcommands, the key is the subcommand sds name
* (not the fullname), and the value is the redisCommand structure pointer. */
struct redisCommand *parent;
@ -2484,7 +2446,6 @@ extern dict *modules;
/* Command metadata */
void populateCommandLegacyRangeSpec(struct redisCommand *c);
int populateArgsStructure(struct redisCommandArg *args);
/* Modules */
void moduleInitModulesSystem(void);

View File

@ -0,0 +1,111 @@
# Test suite for redis-cli command-line hinting mechanism.
# Each test case consists of two strings: a (partial) input command line, and the expected hint string.
# Command with one arg: GET key
"GET " "key"
"GET abc " ""
# Command with two args: DECRBY key decrement
"DECRBY xyz 2 " ""
"DECRBY xyz " "decrement"
"DECRBY " "key decrement"
# Command with optional arg: LPOP key [count]
"LPOP key " "[count]"
"LPOP key 3 " ""
# Command with optional token arg: XRANGE key start end [COUNT count]
"XRANGE " "key start end [COUNT count]"
"XRANGE k 4 2 " "[COUNT count]"
"XRANGE k 4 2 COU" "[COUNT count]"
"XRANGE k 4 2 COUNT" "[COUNT count]"
"XRANGE k 4 2 COUNT " "count"
# Command with optional token block arg: BITFIELD_RO key [GET encoding offset [GET encoding offset ...]]
"BITFIELD_RO k " "[GET encoding offset [GET encoding offset ...]]"
"BITFIELD_RO k GE" "[GET encoding offset [GET encoding offset ...]]"
"BITFIELD_RO k GET" "[GET encoding offset [GET encoding offset ...]]"
# TODO: The following hints end with an unbalanced "]" which shouldn't be there.
"BITFIELD_RO k GET " "encoding offset [GET encoding offset ...]]"
"BITFIELD_RO k GET xyz " "offset [GET encoding offset ...]]"
"BITFIELD_RO k GET xyz 12 " "[GET encoding offset ...]]"
"BITFIELD_RO k GET xyz 12 GET " "encoding offset [GET encoding offset ...]]"
"BITFIELD_RO k GET enc1 12 GET enc2 " "offset [GET encoding offset ...]]"
"BITFIELD_RO k GET enc1 12 GET enc2 34 " "[GET encoding offset ...]]"
# Two-word command with multiple non-token block args: CONFIG SET parameter value [parameter value ...]
"CONFIG SET param " "value [parameter value ...]"
"CONFIG SET param val " "[parameter value ...]"
"CONFIG SET param val parm2 val2 " "[parameter value ...]"
# Command with nested optional args: ZRANDMEMBER key [count [WITHSCORES]]
"ZRANDMEMBER k " "[count [WITHSCORES]]"
"ZRANDMEMBER k 3 " "[WITHSCORES]"
"ZRANDMEMBER k 3 WI" "[WITHSCORES]"
"ZRANDMEMBER k 3 WITHSCORES " ""
# Wrong data type: count must be an integer. Hinting fails.
"ZRANDMEMBER k cnt " ""
# Command ends with repeated arg: MGET key [key ...]
"MGET " "key [key ...]"
"MGET k " "[key ...]"
"MGET k k " "[key ...]"
# Optional args can be in any order: SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
"SCAN 2 MATCH " "pattern [COUNT count] [TYPE type]"
"SCAN 2 COUNT " "count [MATCH pattern] [TYPE type]"
# One-of choices: BLMOVE source destination LEFT|RIGHT LEFT|RIGHT timeout
"BLMOVE src dst LEFT " "LEFT|RIGHT timeout"
# Optional args can be in any order: ZRANGE key min max [BYSCORE|BYLEX] [REV] [LIMIT offset count] [WITHSCORES]
"ZRANGE k 1 2 " "[BYSCORE|BYLEX] [REV] [LIMIT offset count] [WITHSCORES]"
"ZRANGE k 1 2 bylex " "[REV] [LIMIT offset count] [WITHSCORES]"
"ZRANGE k 1 2 bylex rev " "[LIMIT offset count] [WITHSCORES]"
"ZRANGE k 1 2 limit 2 4 " "[BYSCORE|BYLEX] [REV] [WITHSCORES]"
"ZRANGE k 1 2 bylex rev limit 2 4 WITHSCORES " ""
"ZRANGE k 1 2 rev " "[BYSCORE|BYLEX] [LIMIT offset count] [WITHSCORES]"
"ZRANGE k 1 2 WITHSCORES " "[BYSCORE|BYLEX] [REV] [LIMIT offset count]"
# Optional one-of args with parameters: SET key value [NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]
"SET key value " "[NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value EX" "[NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value EX " "seconds [NX|XX] [GET]"
"SET key value EX 23 " "[NX|XX] [GET]"
"SET key value EXAT" "[NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value EXAT " "unix-time-seconds [NX|XX] [GET]"
"SET key value PX" "[NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value PX " "milliseconds [NX|XX] [GET]"
"SET key value PXAT" "[NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value PXAT " "unix-time-milliseconds [NX|XX] [GET]"
"SET key value KEEPTTL " "[NX|XX] [GET]"
"SET key value XX " "[GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
# If an input word can't be matched, stop hinting.
"SET key value FOOBAR " ""
# Incorrect type for EX 'seconds' parameter - stop hinting.
"SET key value EX sec " ""
# Reordering partially-matched optional argument: GEORADIUS key longitude latitude radius M|KM|FT|MI [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]
"GEORADIUS key " "longitude latitude radius M|KM|FT|MI [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]"
"GEORADIUS key 1 2 3 M " "[WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]"
"GEORADIUS key 1 2 3 M COUNT " "count [ANY] [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [STORE key] [STOREDIST key]"
"GEORADIUS key 1 2 3 M COUNT 12 " "[ANY] [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [STORE key] [STOREDIST key]"
"GEORADIUS key 1 2 3 M COUNT 12 " "[ANY] [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [STORE key] [STOREDIST key]"
"GEORADIUS key 1 -2.345 3 M COUNT 12 " "[ANY] [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [STORE key] [STOREDIST key]"" ""
# Wrong data type: latitude must be a double. Hinting fails.
"GEORADIUS key 1 X " ""
# Once the next optional argument is started, the [ANY] hint completing the COUNT argument disappears.
"GEORADIUS key 1 2 3 M COUNT 12 ASC " "[WITHCOORD] [WITHDIST] [WITHHASH] [STORE key] [STOREDIST key]"
# Incorrect argument type for double-valued token parameter.
"GEOSEARCH k FROMLONLAT " "longitude latitude BYRADIUS radius M|KM|FT|MI|BYBOX width height M|KM|FT|MI [ASC|DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH]"
"GEOSEARCH k FROMLONLAT 2.34 4.45 BYRADIUS badvalue " ""
# Optional parameters followed by mandatory params: ZADD key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...]
"ZADD key " "[NX|XX] [GT|LT] [CH] [INCR] score member [score member ...]"
"ZADD key CH LT " "[NX|XX] [INCR] score member [score member ...]"
"ZADD key 0 " "member [score member ...]"
# Empty-valued token argument represented as a pair of double-quotes.
"MIGRATE " "host port key|\"\" destination-db timeout [COPY] [REPLACE] [AUTH password|AUTH2 username password] [KEYS key [key ...]]"

View File

@ -423,6 +423,27 @@ if {!$::tls} { ;# fake_redis_node doesn't support TLS
file delete $tmpfile
}
test_nontty_cli "Test command-line hinting - latest server" {
# cli will connect to the running server and will use COMMAND DOCS
catch {run_cli --test_hint_file tests/assets/test_cli_hint_suite.txt} output
assert_match "*SUCCESS*" $output
}
test_nontty_cli "Test command-line hinting - no server" {
# cli will fail to connect to the server and will use the cached commands.c
catch {run_cli -p 123 --test_hint_file tests/assets/test_cli_hint_suite.txt} output
assert_match "*SUCCESS*" $output
}
test_nontty_cli "Test command-line hinting - old server" {
# cli will connect to the server but will not use COMMAND DOCS,
# and complete the missing info from the cached commands.c
r ACL setuser clitest on nopass +@all -command|docs
catch {run_cli --user clitest -a nopass --no-auth-warning --test_hint_file tests/assets/test_cli_hint_suite.txt} output
assert_match "*SUCCESS*" $output
r acl deluser clitest
}
proc test_redis_cli_rdb_dump {functions_only} {
r flushdb
r function flush

View File

@ -196,7 +196,7 @@ class Argument(object):
def struct_code(self):
"""
Output example:
"expiration",ARG_TYPE_ONEOF,NULL,NULL,NULL,CMD_ARG_OPTIONAL,.value.subargs=SET_expiration_Subargs
MAKE_ARG("expiration",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,5,NULL),.subargs=GETEX_expiration_Subargs
"""
def _flags_code():
@ -210,7 +210,7 @@ class Argument(object):
s += "CMD_ARG_MULTIPLE_TOKEN|"
return s[:-1] if s else "CMD_ARG_NONE"
s = "\"%s\",%s,%d,%s,%s,%s,%s" % (
s = "MAKE_ARG(\"%s\",%s,%d,%s,%s,%s,%s,%d,%s)" % (
self.name,
ARG_TYPES[self.type],
self.desc.get("key_spec_index", -1),
@ -218,9 +218,9 @@ class Argument(object):
get_optional_desc_string(self.desc, "summary"),
get_optional_desc_string(self.desc, "since"),
_flags_code(),
len(self.subargs),
get_optional_desc_string(self.desc, "deprecated_since"),
)
if "deprecated_since" in self.desc:
s += ",.deprecated_since=\"%s\"" % self.desc["deprecated_since"]
if "display" in self.desc:
s += ",.display_text=\"%s\"" % self.desc["display"].lower()
if self.subargs:
@ -234,10 +234,9 @@ class Argument(object):
subarg.write_internal_structs(f)
f.write("/* %s argument table */\n" % self.fullname())
f.write("struct redisCommandArg %s[] = {\n" % self.subarg_table_name())
f.write("struct COMMAND_ARG %s[] = {\n" % self.subarg_table_name())
for subarg in self.subargs:
f.write("{%s},\n" % subarg.struct_code())
f.write("{0}\n")
f.write("};\n\n")
@ -339,11 +338,14 @@ class Command(object):
return "%s_History" % (self.fullname().replace(" ", "_"))
def tips_table_name(self):
return "%s_tips" % (self.fullname().replace(" ", "_"))
return "%s_Tips" % (self.fullname().replace(" ", "_"))
def arg_table_name(self):
return "%s_Args" % (self.fullname().replace(" ", "_"))
def key_specs_table_name(self):
return "%s_Keyspecs" % (self.fullname().replace(" ", "_"))
def reply_schema_name(self):
return "%s_ReplySchema" % (self.fullname().replace(" ", "_"))
@ -356,22 +358,37 @@ class Command(object):
s = ""
for tupl in self.desc["history"]:
s += "{\"%s\",\"%s\"},\n" % (tupl[0], tupl[1])
s += "{0}"
return s
def num_history(self):
if not self.desc.get("history"):
return 0
return len(self.desc["history"])
def tips_code(self):
if not self.desc.get("command_tips"):
return ""
s = ""
for hint in self.desc["command_tips"]:
s += "\"%s\",\n" % hint.lower()
s += "NULL"
return s
def num_tips(self):
if not self.desc.get("command_tips"):
return 0
return len(self.desc["command_tips"])
def key_specs_code(self):
s = ""
for spec in self.key_specs:
s += "{%s}," % KeySpec(spec).struct_code()
return s[:-1]
def struct_code(self):
"""
Output example:
"set","Set the string value of a key","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_STRING,SET_History,SET_tips,setCommand,-3,"write denyoom @string",{{"write read",KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=SET_Args
MAKE_CMD("set","Set the string value of a key","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,SET_History,4,SET_Tips,0,setCommand,-3,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,SET_Keyspecs,1,setGetKeys,5),.args=SET_Args
"""
def _flags_code():
@ -392,13 +409,7 @@ class Command(object):
s += "CMD_DOC_%s|" % flag
return s[:-1] if s else "CMD_DOC_NONE"
def _key_specs_code():
s = ""
for spec in self.key_specs:
s += "{%s}," % KeySpec(spec).struct_code()
return s[:-1]
s = "\"%s\",%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%d,%s,%s," % (
s = "MAKE_CMD(\"%s\",%s,%s,%s,%s,%s,%s,%s,%s,%s,%d,%s,%d,%s,%d,%s,%s,%s,%d,%s,%d)," % (
self.name.lower(),
get_optional_desc_string(self.desc, "summary"),
get_optional_desc_string(self.desc, "complexity"),
@ -406,22 +417,22 @@ class Command(object):
_doc_flags_code(),
get_optional_desc_string(self.desc, "replaced_by"),
get_optional_desc_string(self.desc, "deprecated_since"),
"\"%s\"" % self.group,
GROUPS[self.group],
self.history_table_name(),
self.num_history(),
self.tips_table_name(),
self.num_tips(),
self.desc.get("function", "NULL"),
self.desc["arity"],
_flags_code(),
_acl_categories_code()
_acl_categories_code(),
self.key_specs_table_name(),
len(self.key_specs),
self.desc.get("get_keys_function", "NULL"),
len(self.args),
)
specs = _key_specs_code()
if specs:
s += "{%s}," % specs
if self.desc.get("get_keys_function"):
s += "%s," % self.desc["get_keys_function"]
if self.subcommands:
s += ".subcommands=%s," % self.subcommand_table_name()
@ -440,7 +451,7 @@ class Command(object):
subcommand.write_internal_structs(f)
f.write("/* %s command table */\n" % self.fullname())
f.write("struct redisCommand %s[] = {\n" % self.subcommand_table_name())
f.write("struct COMMAND_STRUCT %s[] = {\n" % self.subcommand_table_name())
for subcommand in subcommand_list:
f.write("{%s},\n" % subcommand.struct_code())
f.write("{0}\n")
@ -448,33 +459,47 @@ class Command(object):
f.write("/********** %s ********************/\n\n" % self.fullname())
f.write("#ifndef SKIP_CMD_HISTORY_TABLE\n")
f.write("/* %s history */\n" % self.fullname())
code = self.history_code()
if code:
f.write("commandHistory %s[] = {\n" % self.history_table_name())
f.write("%s\n" % code)
f.write("};\n\n")
f.write("%s" % code)
f.write("};\n")
else:
f.write("#define %s NULL\n\n" % self.history_table_name())
f.write("#define %s NULL\n" % self.history_table_name())
f.write("#endif\n\n")
f.write("#ifndef SKIP_CMD_TIPS_TABLE\n")
f.write("/* %s tips */\n" % self.fullname())
code = self.tips_code()
if code:
f.write("const char *%s[] = {\n" % self.tips_table_name())
f.write("%s\n" % code)
f.write("};\n\n")
f.write("%s" % code)
f.write("};\n")
else:
f.write("#define %s NULL\n\n" % self.tips_table_name())
f.write("#define %s NULL\n" % self.tips_table_name())
f.write("#endif\n\n")
f.write("#ifndef SKIP_CMD_KEY_SPECS_TABLE\n")
f.write("/* %s key specs */\n" % self.fullname())
code = self.key_specs_code()
if code:
f.write("keySpec %s[%d] = {\n" % (self.key_specs_table_name(), len(self.key_specs)))
f.write("%s\n" % code)
f.write("};\n")
else:
f.write("#define %s NULL\n" % self.key_specs_table_name())
f.write("#endif\n\n")
if self.args:
for arg in self.args:
arg.write_internal_structs(f)
f.write("/* %s argument table */\n" % self.fullname())
f.write("struct redisCommandArg %s[] = {\n" % self.arg_table_name())
f.write("struct COMMAND_ARG %s[] = {\n" % self.arg_table_name())
for arg in self.args:
f.write("{%s},\n" % arg.struct_code())
f.write("{0}\n")
f.write("};\n\n")
if self.reply_schema and args.with_reply_schema:
@ -543,15 +568,40 @@ if check_command_error_counter != 0:
exit(1)
commands_filename = "commands_with_reply_schema" if args.with_reply_schema else "commands"
print("Generating %s.c..." % commands_filename)
with open("%s/%s.c" % (srcdir, commands_filename), "w") as f:
print("Generating %s.def..." % commands_filename)
with open("%s/%s.def" % (srcdir, commands_filename), "w") as f:
f.write("/* Automatically generated by %s, do not edit. */\n\n" % os.path.basename(__file__))
f.write("#include \"server.h\"\n")
f.write(
"""
/* We have fabulous commands from
* the fantastic
* Redis Command Table! */\n
* Redis Command Table! */
/* Must match redisCommandGroup */
const char *COMMAND_GROUP_STR[] = {
"generic",
"string",
"list",
"set",
"sorted-set",
"hash",
"pubsub",
"transactions",
"connection",
"server",
"scripting",
"hyperloglog",
"cluster",
"sentinel",
"geo",
"stream",
"bitmap",
"module"
};
const char *commandGroupStr(int index) {
return COMMAND_GROUP_STR[index];
}
"""
)
@ -560,7 +610,7 @@ with open("%s/%s.c" % (srcdir, commands_filename), "w") as f:
command.write_internal_structs(f)
f.write("/* Main command table */\n")
f.write("struct redisCommand redisCommandTable[] = {\n")
f.write("struct COMMAND_STRUCT redisCommandTable[] = {\n")
curr_group = None
for command in command_list:
if curr_group != command.group:

View File

@ -1,151 +0,0 @@
#!/usr/bin/env ruby -w
# Usage: generate-command-help.r [path/to/commands.json]
# or: generate-commands-json.py | generate-command-help.rb -
#
# Defaults to downloading commands.json from the redis-doc repo if not provided
# or STDINed.
GROUPS = [
"generic",
"string",
"list",
"set",
"sorted-set",
"hash",
"pubsub",
"transactions",
"connection",
"server",
"scripting",
"hyperloglog",
"cluster",
"geo",
"stream",
"bitmap"
].freeze
GROUPS_BY_NAME = Hash[*
GROUPS.each_with_index.map do |n,i|
[n,i]
end.flatten
].freeze
def argument arg
if "block" == arg["type"]
name = arg["arguments"].map do |entry|
argument entry
end.join " "
elsif "oneof" == arg["type"]
name = arg["arguments"].map do |entry|
argument entry
end.join "|"
elsif "pure-token" == arg["type"]
name = nil # prepended later
else
name = arg["name"].is_a?(Array) ? arg["name"].join(" ") : arg["name"]
end
if arg["multiple"]
if arg["multiple_token"]
name = "#{name} [#{arg["token"]} #{name} ...]"
else
name = "#{name} [#{name} ...]"
end
end
if arg["token"]
name = [arg["token"], name].compact.join " "
end
if arg["optional"]
name = "[#{name}]"
end
name
end
def arguments command
return "" unless command["arguments"]
command["arguments"].map do |arg|
argument arg
end.join " "
end
def commands
return @commands if @commands
require "rubygems"
require "net/http"
require "net/https"
require "json"
require "uri"
if ARGV.length > 0
if ARGV[0] == '-'
data = STDIN.read
elsif FileTest.exist? ARGV[0]
data = File.read(ARGV[0])
else
raise Exception.new "File not found: #{ARGV[0]}"
end
else
url = URI.parse "https://raw.githubusercontent.com/redis/redis-doc/master/commands.json"
client = Net::HTTP.new url.host, url.port
client.use_ssl = true
response = client.get url.path
if !response.is_a?(Net::HTTPSuccess)
response.error!
return
else
data = response.body
end
end
@commands = JSON.parse(data)
end
def generate_groups
GROUPS.map do |n|
"\"#{n}\""
end.join(",\n ");
end
def generate_commands
commands.to_a.sort do |x,y|
x[0] <=> y[0]
end.map do |key, command|
group = GROUPS_BY_NAME[command["group"]]
if group.nil?
STDERR.puts "Please update groups array in #{__FILE__}"
raise "Unknown group #{command["group"]}"
end
ret = <<-SPEC
{ "#{key}",
"#{arguments(command)}",
"#{command["summary"]}",
#{group},
"#{command["since"]}" }
SPEC
ret.strip
end.join(",\n ")
end
# Write to stdout
puts <<-HELP_H
/* Automatically generated by #{__FILE__}, do not edit. */
#ifndef __REDIS_HELP_H
#define __REDIS_HELP_H
static char *commandGroups[] = {
#{generate_groups}
};
struct commandHelp {
char *name;
char *params;
char *summary;
int group;
char *since;
} commandHelp[] = {
#{generate_commands}
};
#endif
HELP_H