Unverified Commit 1f76bb17 authored by Jason Elbaum's avatar Jason Elbaum Committed by GitHub
Browse files

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: default avatarOran Agra <oran@redislabs.com>
Co-authored-by: default avatarViktor Söderqvist <viktor.soderqvist@est.tech>
parent 971b177f
...@@ -336,18 +336,18 @@ QUIET_INSTALL = @printf ' %b %b\n' $(LINKCOLOR)INSTALL$(ENDCOLOR) $(BINCOLOR) ...@@ -336,18 +336,18 @@ QUIET_INSTALL = @printf ' %b %b\n' $(LINKCOLOR)INSTALL$(ENDCOLOR) $(BINCOLOR)
endif endif
ifneq (, $(findstring LOG_REQ_RES, $(REDIS_CFLAGS))) 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 GEN_COMMANDS_FLAGS=--with-reply-schema
else else
COMMANDS_FILENAME=commands COMMANDS_DEF_FILENAME=commands
GEN_COMMANDS_FLAGS= GEN_COMMANDS_FLAGS=
endif endif
REDIS_SERVER_NAME=redis-server$(PROG_SUFFIX) REDIS_SERVER_NAME=redis-server$(PROG_SUFFIX)
REDIS_SENTINEL_NAME=redis-sentinel$(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_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_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_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) 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 ...@@ -435,13 +435,15 @@ DEP = $(REDIS_SERVER_OBJ:%.o=%.d) $(REDIS_CLI_OBJ:%.o=%.d) $(REDIS_BENCHMARK_OBJ
%.o: %.c .make-prerequisites %.o: %.c .make-prerequisites
$(REDIS_CC) -MMD -o $@ -c $< $(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. # is built only if python is available and its prereqs are modified.
ifneq (,$(PYTHON)) 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) $(QUIET_GEN)$(PYTHON) ../utils/generate-command-code.py $(GEN_COMMANDS_FLAGS)
endif endif
commands.c: $(COMMANDS_DEF_FILENAME).def
clean: 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 -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) rm -f $(DEP)
......
#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"
/* 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
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
#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
/* Automatically generated by utils/generate-command-help.rb, do not edit. */
#ifndef __REDIS_HELP_H
#define __REDIS_HELP_H
static char *commandGroups[] = {
"generic",
"string",
"list",
"set",
"sorted-set",
"hash",
"pubsub",
"transactions",
"connection",
"server",
"scripting",
"hyperloglog",
"cluster",
"geo",
"stream",
"bitmap"
};
struct commandHelp {
char *name;
char *params;
char *summary;
int group;
char *since;
} commandHelp[] = {
{ "ACL",
"",
"A container for Access List Control commands ",
9,
"6.0.0" },
{ "ACL CAT",
"[categoryname]",
"List the ACL categories or the commands inside a category",
9,
"6.0.0" },
{ "ACL DELUSER",
"username [username ...]",
"Remove the specified ACL users and the associated rules",
9,
"6.0.0" },
{ "ACL DRYRUN",
"username command [arg [arg ...]]",
"Returns whether the user can execute the given command without executing the command.",
9,
"7.0.0" },
{ "ACL GENPASS",
"[bits]",
"Generate a pseudorandom secure password to use for ACL users",
9,
"6.0.0" },
{ "ACL GETUSER",
"username",
"Get the rules for a specific ACL user",
9,
"6.0.0" },
{ "ACL HELP",
"",
"Show helpful text about the different subcommands",
9,
"6.0.0" },
{ "ACL LIST",
"",
"List the current ACL rules in ACL config file format",
9,
"6.0.0" },
{ "ACL LOAD",
"",
"Reload the ACLs from the configured ACL file",
9,
"6.0.0" },
{ "ACL LOG",
"[count|RESET]",
"List latest events denied because of ACLs in place",
9,
"6.0.0" },
{ "ACL SAVE",
"",
"Save the current ACL rules in the configured ACL file",
9,
"6.0.0" },
{ "ACL SETUSER",
"username [rule [rule ...]]",
"Modify or create the rules for a specific ACL user",
9,
"6.0.0" },
{ "ACL USERS",
"",
"List the username of all the configured ACL rules",
9,
"6.0.0" },
{ "ACL WHOAMI",
"",
"Return the name of the user associated to the current connection",
9,
"6.0.0" },
{ "APPEND",
"key value",
"Append a value to a key",
1,
"2.0.0" },
{ "ASKING",
"",
"Sent by cluster clients after an -ASK redirect",
12,
"3.0.0" },
{ "AUTH",
"[username] password",
"Authenticate to the server",
8,
"1.0.0" },
{ "BGREWRITEAOF",
"",
"Asynchronously rewrite the append-only file",
9,
"1.0.0" },
{ "BGSAVE",
"[SCHEDULE]",
"Asynchronously save the dataset to disk",
9,
"1.0.0" },
{ "BITCOUNT",
"key [start end [BYTE|BIT]]",
"Count set bits in a string",
15,
"2.6.0" },
{ "BITFIELD",
"key [GET encoding offset|[OVERFLOW WRAP|SAT|FAIL] SET encoding offset value|INCRBY encoding offset increment [GET encoding offset|[OVERFLOW WRAP|SAT|FAIL] SET encoding offset value|INCRBY encoding offset increment ...]]",
"Perform arbitrary bitfield integer operations on strings",
15,
"3.2.0" },
{ "BITFIELD_RO",
"key [GET encoding offset [GET encoding offset ...]]",
"Perform arbitrary bitfield integer operations on strings. Read-only variant of BITFIELD",
15,
"6.0.0" },
{ "BITOP",
"AND|OR|XOR|NOT destkey key [key ...]",
"Perform bitwise operations between strings",
15,
"2.6.0" },
{ "BITPOS",
"key bit [start [end [BYTE|BIT]]]",
"Find first bit set or clear in a string",
15,
"2.8.7" },
{ "BLMOVE",
"source destination LEFT|RIGHT LEFT|RIGHT timeout",
"Pop an element from a list, push it to another list and return it; or block until one is available",
2,
"6.2.0" },
{ "BLMPOP",
"timeout numkeys key [key ...] LEFT|RIGHT [COUNT count]",
"Pop elements from a list, or block until one is available",
2,
"7.0.0" },
{ "BLPOP",
"key [key ...] timeout",
"Remove and get the first element in a list, or block until one is available",
2,
"2.0.0" },
{ "BRPOP",
"key [key ...] timeout",
"Remove and get the last element in a list, or block until one is available",
2,
"2.0.0" },
{ "BRPOPLPUSH",
"source destination timeout",
"Pop an element from a list, push it to another list and return it; or block until one is available",
2,
"2.2.0" },
{ "BZMPOP",
"timeout numkeys key [key ...] MIN|MAX [COUNT count]",
"Remove and return members with scores in a sorted set or block until one is available",
4,
"7.0.0" },
{ "BZPOPMAX",
"key [key ...] timeout",
"Remove and return the member with the highest score from one or more sorted sets, or block until one is available",
4,
"5.0.0" },
{ "BZPOPMIN",
"key [key ...] timeout",
"Remove and return the member with the lowest score from one or more sorted sets, or block until one is available",
4,
"5.0.0" },
{ "CLIENT",
"",
"A container for client connection commands",
8,
"2.4.0" },
{ "CLIENT CACHING",
"YES|NO",
"Instruct the server about tracking or not keys in the next request",
8,
"6.0.0" },
{ "CLIENT GETNAME",
"",
"Get the current connection name",
8,
"2.6.9" },
{ "CLIENT GETREDIR",
"",
"Get tracking notifications redirection client ID if any",
8,
"6.0.0" },
{ "CLIENT HELP",
"",
"Show helpful text about the different subcommands",
8,
"5.0.0" },
{ "CLIENT ID",
"",
"Returns the client ID for the current connection",
8,
"5.0.0" },
{ "CLIENT INFO",
"",
"Returns information about the current client connection.",
8,
"6.2.0" },
{ "CLIENT KILL",
"old-format|[ID client-id]|[TYPE NORMAL|MASTER|SLAVE|REPLICA|PUBSUB]|[USER username]|[ADDR addr]|[LADDR laddr]|[SKIPME YES|NO] [[ID client-id]|[TYPE NORMAL|MASTER|SLAVE|REPLICA|PUBSUB]|[USER username]|[ADDR addr]|[LADDR laddr]|[SKIPME YES|NO] ...]",
"Kill the connection of a client",
8,
"2.4.0" },
{ "CLIENT LIST",
"[TYPE NORMAL|MASTER|REPLICA|PUBSUB] [ID client-id [client-id ...]]",
"Get the list of client connections",
8,
"2.4.0" },
{ "CLIENT NO-EVICT",
"ON|OFF",
"Set client eviction mode for the current connection",
8,
"7.0.0" },
{ "CLIENT NO-TOUCH",
"ON|OFF",
"Controls whether commands sent by the client will alter the LRU/LFU of the keys they access.",
8,
"7.2.0" },
{ "CLIENT PAUSE",
"timeout [WRITE|ALL]",
"Stop processing commands from clients for some time",
8,
"3.0.0" },
{ "CLIENT REPLY",
"ON|OFF|SKIP",
"Instruct the server whether to reply to commands",
8,
"3.2.0" },
{ "CLIENT SETINFO",
"LIB-NAME libname|LIB-VER libver",
"Set client or connection specific info",
8,
"7.2.0" },
{ "CLIENT SETNAME",
"connection-name",
"Set the current connection name",
8,
"2.6.9" },
{ "CLIENT TRACKING",
"ON|OFF [REDIRECT client-id] [PREFIX prefix [PREFIX prefix ...]] [BCAST] [OPTIN] [OPTOUT] [NOLOOP]",
"Enable or disable server assisted client side caching support",
8,
"6.0.0" },
{ "CLIENT TRACKINGINFO",
"",
"Return information about server assisted client side caching for the current connection",
8,
"6.2.0" },
{ "CLIENT UNBLOCK",
"client-id [TIMEOUT|ERROR]",
"Unblock a client blocked in a blocking command from a different connection",
8,
"5.0.0" },
{ "CLIENT UNPAUSE",
"",
"Resume processing of clients that were paused",
8,
"6.2.0" },
{ "CLUSTER",
"",
"A container for cluster commands",
12,
"3.0.0" },
{ "CLUSTER ADDSLOTS",
"slot [slot ...]",
"Assign new hash slots to receiving node",
12,
"3.0.0" },
{ "CLUSTER ADDSLOTSRANGE",
"start-slot end-slot [start-slot end-slot ...]",
"Assign new hash slots to receiving node",
12,
"7.0.0" },
{ "CLUSTER BUMPEPOCH",
"",
"Advance the cluster config epoch",
12,
"3.0.0" },
{ "CLUSTER COUNT-FAILURE-REPORTS",
"node-id",
"Return the number of failure reports active for a given node",
12,
"3.0.0" },
{ "CLUSTER COUNTKEYSINSLOT",
"slot",
"Return the number of local keys in the specified hash slot",
12,
"3.0.0" },
{ "CLUSTER DELSLOTS",
"slot [slot ...]",
"Set hash slots as unbound in receiving node",
12,
"3.0.0" },
{ "CLUSTER DELSLOTSRANGE",
"start-slot end-slot [start-slot end-slot ...]",
"Set hash slots as unbound in receiving node",
12,
"7.0.0" },
{ "CLUSTER FAILOVER",
"[FORCE|TAKEOVER]",
"Forces a replica to perform a manual failover of its master.",
12,
"3.0.0" },
{ "CLUSTER FLUSHSLOTS",
"",
"Delete a node's own slots information",
12,
"3.0.0" },
{ "CLUSTER FORGET",
"node-id",
"Remove a node from the nodes table",
12,
"3.0.0" },
{ "CLUSTER GETKEYSINSLOT",
"slot count",
"Return local key names in the specified hash slot",
12,
"3.0.0" },
{ "CLUSTER HELP",
"",
"Show helpful text about the different subcommands",
12,
"5.0.0" },
{ "CLUSTER INFO",
"",
"Provides info about Redis Cluster node state",
12,
"3.0.0" },
{ "CLUSTER KEYSLOT",
"key",
"Returns the hash slot of the specified key",
12,
"3.0.0" },
{ "CLUSTER LINKS",
"",
"Returns a list of all TCP links to and from peer nodes in cluster",
12,
"7.0.0" },
{ "CLUSTER MEET",
"ip port [cluster-bus-port]",
"Force a node cluster to handshake with another node",
12,
"3.0.0" },
{ "CLUSTER MYID",
"",
"Return the node id",
12,
"3.0.0" },
{ "CLUSTER MYSHARDID",
"",
"Return the node shard id",
12,
"7.2.0" },
{ "CLUSTER NODES",
"",
"Get Cluster config for the node",
12,
"3.0.0" },
{ "CLUSTER REPLICAS",
"node-id",
"List replica nodes of the specified master node",
12,
"5.0.0" },
{ "CLUSTER REPLICATE",
"node-id",
"Reconfigure a node as a replica of the specified master node",
12,
"3.0.0" },
{ "CLUSTER RESET",
"[HARD|SOFT]",
"Reset a Redis Cluster node",
12,
"3.0.0" },
{ "CLUSTER SAVECONFIG",
"",
"Forces the node to save cluster state on disk",
12,
"3.0.0" },
{ "CLUSTER SET-CONFIG-EPOCH",
"config-epoch",
"Set the configuration epoch in a new node",
12,
"3.0.0" },
{ "CLUSTER SETSLOT",
"slot IMPORTING importing|MIGRATING migrating|NODE node|STABLE",
"Bind a hash slot to a specific node",
12,
"3.0.0" },
{ "CLUSTER SHARDS",
"",
"Get array of cluster slots to node mappings",
12,
"7.0.0" },
{ "CLUSTER SLAVES",
"node-id",
"List replica nodes of the specified master node",
12,
"3.0.0" },
{ "CLUSTER SLOTS",
"",
"Get array of Cluster slot to node mappings",
12,
"3.0.0" },
{ "COMMAND",
"",
"Get array of Redis command details",
9,
"2.8.13" },
{ "COMMAND COUNT",
"",
"Get total number of Redis commands",
9,
"2.8.13" },
{ "COMMAND DOCS",
"[command-name [command-name ...]]",
"Get array of specific Redis command documentation",
9,
"7.0.0" },
{ "COMMAND GETKEYS",
"command [arg [arg ...]]",
"Extract keys given a full Redis command",
9,
"2.8.13" },
{ "COMMAND GETKEYSANDFLAGS",
"command [arg [arg ...]]",
"Extract keys and access flags given a full Redis command",
9,
"7.0.0" },
{ "COMMAND HELP",
"",
"Show helpful text about the different subcommands",
9,
"5.0.0" },
{ "COMMAND INFO",
"[command-name [command-name ...]]",
"Get array of specific Redis command details, or all when no argument is given.",
9,
"2.8.13" },
{ "COMMAND LIST",
"[FILTERBY MODULE module-name|ACLCAT category|PATTERN pattern]",
"Get an array of Redis command names",
9,
"7.0.0" },
{ "CONFIG",
"",
"A container for server configuration commands",
9,
"2.0.0" },
{ "CONFIG GET",
"parameter [parameter ...]",
"Get the values of configuration parameters",
9,
"2.0.0" },
{ "CONFIG HELP",
"",
"Show helpful text about the different subcommands",
9,
"5.0.0" },
{ "CONFIG RESETSTAT",
"",
"Reset the stats returned by INFO",
9,
"2.0.0" },
{ "CONFIG REWRITE",
"",
"Rewrite the configuration file with the in memory configuration",
9,
"2.8.0" },
{ "CONFIG SET",
"parameter value [parameter value ...]",
"Set configuration parameters to the given values",
9,
"2.0.0" },
{ "COPY",
"source destination [DB destination-db] [REPLACE]",
"Copy a key",
0,
"6.2.0" },
{ "DBSIZE",
"",
"Return the number of keys in the selected database",
9,
"1.0.0" },
{ "DEBUG",
"",
"A container for debugging commands",
9,
"1.0.0" },
{ "DECR",
"key",
"Decrement the integer value of a key by one",
1,
"1.0.0" },
{ "DECRBY",
"key decrement",
"Decrement the integer value of a key by the given number",
1,
"1.0.0" },
{ "DEL",
"key [key ...]",
"Delete a key",
0,
"1.0.0" },
{ "DISCARD",
"",
"Discard all commands issued after MULTI",
7,
"2.0.0" },
{ "DUMP",
"key",
"Return a serialized version of the value stored at the specified key.",
0,
"2.6.0" },
{ "ECHO",
"message",
"Echo the given string",
8,
"1.0.0" },
{ "EVAL",
"script numkeys [key [key ...]] [arg [arg ...]]",
"Execute a Lua script server side",
10,
"2.6.0" },
{ "EVALSHA",
"sha1 numkeys [key [key ...]] [arg [arg ...]]",
"Execute a Lua script server side",
10,
"2.6.0" },
{ "EVALSHA_RO",
"sha1 numkeys [key [key ...]] [arg [arg ...]]",
"Execute a read-only Lua script server side",
10,
"7.0.0" },
{ "EVAL_RO",
"script numkeys [key [key ...]] [arg [arg ...]]",
"Execute a read-only Lua script server side",
10,
"7.0.0" },
{ "EXEC",
"",
"Execute all commands issued after MULTI",
7,
"1.2.0" },
{ "EXISTS",
"key [key ...]",
"Determine if a key exists",
0,
"1.0.0" },
{ "EXPIRE",
"key seconds [NX|XX|GT|LT]",
"Set a key's time to live in seconds",
0,
"1.0.0" },
{ "EXPIREAT",
"key unix-time-seconds [NX|XX|GT|LT]",
"Set the expiration for a key as a UNIX timestamp",
0,
"1.2.0" },
{ "EXPIRETIME",
"key",
"Get the expiration Unix timestamp for a key",
0,
"7.0.0" },
{ "FAILOVER",
"[TO host port [FORCE]] [ABORT] [TIMEOUT milliseconds]",
"Start a coordinated failover between this server and one of its replicas.",
9,
"6.2.0" },
{ "FCALL",
"function numkeys [key [key ...]] [arg [arg ...]]",
"Invoke a function",
10,
"7.0.0" },
{ "FCALL_RO",
"function numkeys [key [key ...]] [arg [arg ...]]",
"Invoke a read-only function",
10,
"7.0.0" },
{ "FLUSHALL",
"[ASYNC|SYNC]",
"Remove all keys from all databases",
9,
"1.0.0" },
{ "FLUSHDB",
"[ASYNC|SYNC]",
"Remove all keys from the current database",
9,
"1.0.0" },
{ "FUNCTION",
"",
"A container for function commands",
10,
"7.0.0" },
{ "FUNCTION DELETE",
"library-name",
"Delete a function by name",
10,
"7.0.0" },
{ "FUNCTION DUMP",
"",
"Dump all functions into a serialized binary payload",
10,
"7.0.0" },
{ "FUNCTION FLUSH",
"[ASYNC|SYNC]",
"Deleting all functions",
10,
"7.0.0" },
{ "FUNCTION HELP",
"",
"Show helpful text about the different subcommands",
10,
"7.0.0" },
{ "FUNCTION KILL",
"",
"Kill the function currently in execution.",
10,
"7.0.0" },
{ "FUNCTION LIST",
"[LIBRARYNAME library-name-pattern] [WITHCODE]",
"List information about all the functions",
10,
"7.0.0" },
{ "FUNCTION LOAD",
"[REPLACE] function-code",
"Create a function with the given arguments (name, code, description)",
10,
"7.0.0" },
{ "FUNCTION RESTORE",
"serialized-value [FLUSH|APPEND|REPLACE]",
"Restore all the functions on the given payload",
10,
"7.0.0" },
{ "FUNCTION STATS",
"",
"Return information about the function currently running (name, description, duration)",
10,
"7.0.0" },
{ "GEOADD",
"key [NX|XX] [CH] longitude latitude member [longitude latitude member ...]",
"Add one or more geospatial items in the geospatial index represented using a sorted set",
13,
"3.2.0" },
{ "GEODIST",
"key member1 member2 [M|KM|FT|MI]",
"Returns the distance between two members of a geospatial index",
13,
"3.2.0" },
{ "GEOHASH",
"key [member [member ...]]",
"Returns members of a geospatial index as standard geohash strings",
13,
"3.2.0" },
{ "GEOPOS",
"key [member [member ...]]",
"Returns longitude and latitude of members of a geospatial index",
13,
"3.2.0" },
{ "GEORADIUS",
"key longitude latitude radius M|KM|FT|MI [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE storekey] [STOREDIST storedistkey]",
"Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point",
13,
"3.2.0" },
{ "GEORADIUSBYMEMBER",
"key member radius M|KM|FT|MI [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE storekey] [STOREDIST storedistkey]",
"Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member",
13,
"3.2.0" },
{ "GEORADIUSBYMEMBER_RO",
"key member radius M|KM|FT|MI [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC]",
"A read-only variant for GEORADIUSBYMEMBER",
13,
"3.2.10" },
{ "GEORADIUS_RO",
"key longitude latitude radius M|KM|FT|MI [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC]",
"A read-only variant for GEORADIUS",
13,
"3.2.10" },
{ "GEOSEARCH",
"key FROMMEMBER member|FROMLONLAT longitude latitude BYRADIUS radius M|KM|FT|MI|BYBOX width height M|KM|FT|MI [ASC|DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH]",
"Query a sorted set representing a geospatial index to fetch members inside an area of a box or a circle.",
13,
"6.2.0" },
{ "GEOSEARCHSTORE",
"destination source FROMMEMBER member|FROMLONLAT longitude latitude BYRADIUS radius M|KM|FT|MI|BYBOX width height M|KM|FT|MI [ASC|DESC] [COUNT count [ANY]] [STOREDIST]",
"Query a sorted set representing a geospatial index to fetch members inside an area of a box or a circle, and store the result in another key.",
13,
"6.2.0" },
{ "GET",
"key",
"Get the value of a key",
1,
"1.0.0" },
{ "GETBIT",
"key offset",
"Returns the bit value at offset in the string value stored at key",
15,
"2.2.0" },
{ "GETDEL",
"key",
"Get the value of a key and delete the key",
1,
"6.2.0" },
{ "GETEX",
"key [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|PERSIST]",
"Get the value of a key and optionally set its expiration",
1,
"6.2.0" },
{ "GETRANGE",
"key start end",
"Get a substring of the string stored at a key",
1,
"2.4.0" },
{ "GETSET",
"key value",
"Set the string value of a key and return its old value",
1,
"1.0.0" },
{ "HDEL",
"key field [field ...]",
"Delete one or more hash fields",
5,
"2.0.0" },
{ "HELLO",
"[protover [AUTH username password] [SETNAME clientname]]",
"Handshake with Redis",
8,
"6.0.0" },
{ "HEXISTS",
"key field",
"Determine if a hash field exists",
5,
"2.0.0" },
{ "HGET",
"key field",
"Get the value of a hash field",
5,
"2.0.0" },
{ "HGETALL",
"key",
"Get all the fields and values in a hash",
5,
"2.0.0" },
{ "HINCRBY",
"key field increment",
"Increment the integer value of a hash field by the given number",
5,
"2.0.0" },
{ "HINCRBYFLOAT",
"key field increment",
"Increment the float value of a hash field by the given amount",
5,
"2.6.0" },
{ "HKEYS",
"key",
"Get all the fields in a hash",
5,
"2.0.0" },
{ "HLEN",
"key",
"Get the number of fields in a hash",
5,
"2.0.0" },
{ "HMGET",
"key field [field ...]",
"Get the values of all the given hash fields",
5,
"2.0.0" },
{ "HMSET",
"key field value [field value ...]",
"Set multiple hash fields to multiple values",
5,
"2.0.0" },
{ "HRANDFIELD",
"key [count [WITHVALUES]]",
"Get one or multiple random fields from a hash",
5,
"6.2.0" },
{ "HSCAN",
"key cursor [MATCH pattern] [COUNT count]",
"Incrementally iterate hash fields and associated values",
5,
"2.8.0" },
{ "HSET",
"key field value [field value ...]",
"Set the string value of a hash field",
5,
"2.0.0" },
{ "HSETNX",
"key field value",
"Set the value of a hash field, only if the field does not exist",
5,
"2.0.0" },
{ "HSTRLEN",
"key field",
"Get the length of the value of a hash field",
5,
"3.2.0" },
{ "HVALS",
"key",
"Get all the values in a hash",
5,
"2.0.0" },
{ "INCR",
"key",
"Increment the integer value of a key by one",
1,
"1.0.0" },
{ "INCRBY",
"key increment",
"Increment the integer value of a key by the given amount",
1,
"1.0.0" },
{ "INCRBYFLOAT",
"key increment",
"Increment the float value of a key by the given amount",
1,
"2.6.0" },
{ "INFO",
"[section [section ...]]",
"Get information and statistics about the server",
9,
"1.0.0" },
{ "KEYS",
"pattern",
"Find all keys matching the given pattern",
0,
"1.0.0" },
{ "LASTSAVE",
"",
"Get the UNIX time stamp of the last successful save to disk",
9,
"1.0.0" },
{ "LATENCY",
"",
"A container for latency diagnostics commands",
9,
"2.8.13" },
{ "LATENCY DOCTOR",
"",
"Return a human readable latency analysis report.",
9,
"2.8.13" },
{ "LATENCY GRAPH",
"event",
"Return a latency graph for the event.",
9,
"2.8.13" },
{ "LATENCY HELP",
"",
"Show helpful text about the different subcommands.",
9,
"2.8.13" },
{ "LATENCY HISTOGRAM",
"[command [command ...]]",
"Return the cumulative distribution of latencies of a subset of commands or all.",
9,
"7.0.0" },
{ "LATENCY HISTORY",
"event",
"Return timestamp-latency samples for the event.",
9,
"2.8.13" },
{ "LATENCY LATEST",
"",
"Return the latest latency samples for all events.",
9,
"2.8.13" },
{ "LATENCY RESET",
"[event [event ...]]",
"Reset latency data for one or more events.",
9,
"2.8.13" },
{ "LCS",
"key1 key2 [LEN] [IDX] [MINMATCHLEN min-match-len] [WITHMATCHLEN]",
"Find longest common substring",
1,
"7.0.0" },
{ "LINDEX",
"key index",
"Get an element from a list by its index",
2,
"1.0.0" },
{ "LINSERT",
"key BEFORE|AFTER pivot element",
"Insert an element before or after another element in a list",
2,
"2.2.0" },
{ "LLEN",
"key",
"Get the length of a list",
2,
"1.0.0" },
{ "LMOVE",
"source destination LEFT|RIGHT LEFT|RIGHT",
"Pop an element from a list, push it to another list and return it",
2,
"6.2.0" },
{ "LMPOP",
"numkeys key [key ...] LEFT|RIGHT [COUNT count]",
"Pop elements from a list",
2,
"7.0.0" },
{ "LOLWUT",
"[VERSION version]",
"Display some computer art and the Redis version",
9,
"5.0.0" },
{ "LPOP",
"key [count]",
"Remove and get the first elements in a list",
2,
"1.0.0" },
{ "LPOS",
"key element [RANK rank] [COUNT num-matches] [MAXLEN len]",
"Return the index of matching elements on a list",
2,
"6.0.6" },
{ "LPUSH",
"key element [element ...]",
"Prepend one or multiple elements to a list",
2,
"1.0.0" },
{ "LPUSHX",
"key element [element ...]",
"Prepend an element to a list, only if the list exists",
2,
"2.2.0" },
{ "LRANGE",
"key start stop",
"Get a range of elements from a list",
2,
"1.0.0" },
{ "LREM",
"key count element",
"Remove elements from a list",
2,
"1.0.0" },
{ "LSET",
"key index element",
"Set the value of an element in a list by its index",
2,
"1.0.0" },
{ "LTRIM",
"key start stop",
"Trim a list to the specified range",
2,
"1.0.0" },
{ "MEMORY",
"",
"A container for memory diagnostics commands",
9,
"4.0.0" },
{ "MEMORY DOCTOR",
"",
"Outputs memory problems report",
9,
"4.0.0" },
{ "MEMORY HELP",
"",
"Show helpful text about the different subcommands",
9,
"4.0.0" },
{ "MEMORY MALLOC-STATS",
"",
"Show allocator internal stats",
9,
"4.0.0" },
{ "MEMORY PURGE",
"",
"Ask the allocator to release memory",
9,
"4.0.0" },
{ "MEMORY STATS",
"",
"Show memory usage details",
9,
"4.0.0" },
{ "MEMORY USAGE",
"key [SAMPLES count]",
"Estimate the memory usage of a key",
9,
"4.0.0" },
{ "MGET",
"key [key ...]",
"Get the values of all the given keys",
1,
"1.0.0" },
{ "MIGRATE",
"host port key| destination-db timeout [COPY] [REPLACE] [AUTH auth|AUTH2 username password] [KEYS keys [keys ...]]",
"Atomically transfer a key from a Redis instance to another one.",
0,
"2.6.0" },
{ "MODULE",
"",
"A container for module commands",
9,
"4.0.0" },
{ "MODULE HELP",
"",
"Show helpful text about the different subcommands",
9,
"5.0.0" },
{ "MODULE LIST",
"",
"List all modules loaded by the server",
9,
"4.0.0" },
{ "MODULE LOAD",
"path [arg [arg ...]]",
"Load a module",
9,
"4.0.0" },
{ "MODULE LOADEX",
"path [CONFIG name value [CONFIG name value ...]] [ARGS args [args ...]]",
"Load a module with extended parameters",
9,
"7.0.0" },
{ "MODULE UNLOAD",
"name",
"Unload a module",
9,
"4.0.0" },
{ "MONITOR",
"",
"Listen for all requests received by the server in real time",
9,
"1.0.0" },
{ "MOVE",
"key db",
"Move a key to another database",
0,
"1.0.0" },
{ "MSET",
"key value [key value ...]",
"Set multiple keys to multiple values",
1,
"1.0.1" },
{ "MSETNX",
"key value [key value ...]",
"Set multiple keys to multiple values, only if none of the keys exist",
1,
"1.0.1" },
{ "MULTI",
"",
"Mark the start of a transaction block",
7,
"1.2.0" },
{ "OBJECT",
"",
"A container for object introspection commands",
0,
"2.2.3" },
{ "OBJECT ENCODING",
"key",
"Inspect the internal encoding of a Redis object",
0,
"2.2.3" },
{ "OBJECT FREQ",
"key",
"Get the logarithmic access frequency counter of a Redis object",
0,
"4.0.0" },
{ "OBJECT HELP",
"",
"Show helpful text about the different subcommands",
0,
"6.2.0" },
{ "OBJECT IDLETIME",
"key",
"Get the time since a Redis object was last accessed",
0,
"2.2.3" },
{ "OBJECT REFCOUNT",
"key",
"Get the number of references to the value of the key",
0,
"2.2.3" },
{ "PERSIST",
"key",
"Remove the expiration from a key",
0,
"2.2.0" },
{ "PEXPIRE",
"key milliseconds [NX|XX|GT|LT]",
"Set a key's time to live in milliseconds",
0,
"2.6.0" },
{ "PEXPIREAT",
"key unix-time-milliseconds [NX|XX|GT|LT]",
"Set the expiration for a key as a UNIX timestamp specified in milliseconds",
0,
"2.6.0" },
{ "PEXPIRETIME",
"key",
"Get the expiration Unix timestamp for a key in milliseconds",
0,
"7.0.0" },
{ "PFADD",
"key [element [element ...]]",
"Adds the specified elements to the specified HyperLogLog.",
11,
"2.8.9" },
{ "PFCOUNT",
"key [key ...]",
"Return the approximated cardinality of the set(s) observed by the HyperLogLog at key(s).",
11,
"2.8.9" },
{ "PFDEBUG",
"subcommand key",
"Internal commands for debugging HyperLogLog values",
11,
"2.8.9" },
{ "PFMERGE",
"destkey [sourcekey [sourcekey ...]]",
"Merge N different HyperLogLogs into a single one.",
11,
"2.8.9" },
{ "PFSELFTEST",
"",
"An internal command for testing HyperLogLog values",
11,
"2.8.9" },
{ "PING",
"[message]",
"Ping the server",
8,
"1.0.0" },
{ "PSETEX",
"key milliseconds value",
"Set the value and expiration in milliseconds of a key",
1,
"2.6.0" },
{ "PSUBSCRIBE",
"pattern [pattern ...]",
"Listen for messages published to channels matching the given patterns",
6,
"2.0.0" },
{ "PSYNC",
"replicationid offset",
"Internal command used for replication",
9,
"2.8.0" },
{ "PTTL",
"key",
"Get the time to live for a key in milliseconds",
0,
"2.6.0" },
{ "PUBLISH",
"channel message",
"Post a message to a channel",
6,
"2.0.0" },
{ "PUBSUB",
"",
"A container for Pub/Sub commands",
6,
"2.8.0" },
{ "PUBSUB CHANNELS",
"[pattern]",
"List active channels",
6,
"2.8.0" },
{ "PUBSUB HELP",
"",
"Show helpful text about the different subcommands",
6,
"6.2.0" },
{ "PUBSUB NUMPAT",
"",
"Get the count of unique patterns pattern subscriptions",
6,
"2.8.0" },
{ "PUBSUB NUMSUB",
"[channel [channel ...]]",
"Get the count of subscribers for channels",
6,
"2.8.0" },
{ "PUBSUB SHARDCHANNELS",
"[pattern]",
"List active shard channels",
6,
"7.0.0" },
{ "PUBSUB SHARDNUMSUB",
"[shardchannel [shardchannel ...]]",
"Get the count of subscribers for shard channels",
6,
"7.0.0" },
{ "PUNSUBSCRIBE",
"[pattern [pattern ...]]",
"Stop listening for messages posted to channels matching the given patterns",
6,
"2.0.0" },
{ "QUIT",
"",
"Close the connection",
8,
"1.0.0" },
{ "RANDOMKEY",
"",
"Return a random key from the keyspace",
0,
"1.0.0" },
{ "READONLY",
"",
"Enables read queries for a connection to a cluster replica node",
12,
"3.0.0" },
{ "READWRITE",
"",
"Disables read queries for a connection to a cluster replica node",
12,
"3.0.0" },
{ "RENAME",
"key newkey",
"Rename a key",
0,
"1.0.0" },
{ "RENAMENX",
"key newkey",
"Rename a key, only if the new key does not exist",
0,
"1.0.0" },
{ "REPLCONF",
"",
"An internal command for configuring the replication stream",
9,
"3.0.0" },
{ "REPLICAOF",
"host port",
"Make the server a replica of another instance, or promote it as master.",
9,
"5.0.0" },
{ "RESET",
"",
"Reset the connection",
8,
"6.2.0" },
{ "RESTORE",
"key ttl serialized-value [REPLACE] [ABSTTL] [IDLETIME seconds] [FREQ frequency]",
"Create a key using the provided serialized value, previously obtained using DUMP.",
0,
"2.6.0" },
{ "RESTORE-ASKING",
"key ttl serialized-value [REPLACE] [ABSTTL] [IDLETIME seconds] [FREQ frequency]",
"An internal command for migrating keys in a cluster",
9,
"3.0.0" },
{ "ROLE",
"",
"Return the role of the instance in the context of replication",
9,
"2.8.12" },
{ "RPOP",
"key [count]",
"Remove and get the last elements in a list",
2,
"1.0.0" },
{ "RPOPLPUSH",
"source destination",
"Remove the last element in a list, prepend it to another list and return it",
2,
"1.2.0" },
{ "RPUSH",
"key element [element ...]",
"Append one or multiple elements to a list",
2,
"1.0.0" },
{ "RPUSHX",
"key element [element ...]",
"Append an element to a list, only if the list exists",
2,
"2.2.0" },
{ "SADD",
"key member [member ...]",
"Add one or more members to a set",
3,
"1.0.0" },
{ "SAVE",
"",
"Synchronously save the dataset to disk",
9,
"1.0.0" },
{ "SCAN",
"cursor [MATCH pattern] [COUNT count] [TYPE type]",
"Incrementally iterate the keys space",
0,
"2.8.0" },
{ "SCARD",
"key",
"Get the number of members in a set",
3,
"1.0.0" },
{ "SCRIPT",
"",
"A container for Lua scripts management commands",
10,
"2.6.0" },
{ "SCRIPT DEBUG",
"YES|SYNC|NO",
"Set the debug mode for executed scripts.",
10,
"3.2.0" },
{ "SCRIPT EXISTS",
"sha1 [sha1 ...]",
"Check existence of scripts in the script cache.",
10,
"2.6.0" },
{ "SCRIPT FLUSH",
"[ASYNC|SYNC]",
"Remove all the scripts from the script cache.",
10,
"2.6.0" },
{ "SCRIPT HELP",
"",
"Show helpful text about the different subcommands",
10,
"5.0.0" },
{ "SCRIPT KILL",
"",
"Kill the script currently in execution.",
10,
"2.6.0" },
{ "SCRIPT LOAD",
"script",
"Load the specified Lua script into the script cache.",
10,
"2.6.0" },
{ "SDIFF",
"key [key ...]",
"Subtract multiple sets",
3,
"1.0.0" },
{ "SDIFFSTORE",
"destination key [key ...]",
"Subtract multiple sets and store the resulting set in a key",
3,
"1.0.0" },
{ "SELECT",
"index",
"Change the selected database for the current connection",
8,
"1.0.0" },
{ "SET",
"key value [NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]",
"Set the string value of a key",
1,
"1.0.0" },
{ "SETBIT",
"key offset value",
"Sets or clears the bit at offset in the string value stored at key",
15,
"2.2.0" },
{ "SETEX",
"key seconds value",
"Set the value and expiration of a key",
1,
"2.0.0" },
{ "SETNX",
"key value",
"Set the value of a key, only if the key does not exist",
1,
"1.0.0" },
{ "SETRANGE",
"key offset value",
"Overwrite part of a string at key starting at the specified offset",
1,
"2.2.0" },
{ "SHUTDOWN",
"[NOSAVE|SAVE] [NOW] [FORCE] [ABORT]",
"Synchronously save the dataset to disk and then shut down the server",
9,
"1.0.0" },
{ "SINTER",
"key [key ...]",
"Intersect multiple sets",
3,
"1.0.0" },
{ "SINTERCARD",
"numkeys key [key ...] [LIMIT limit]",
"Intersect multiple sets and return the cardinality of the result",
3,
"7.0.0" },
{ "SINTERSTORE",
"destination key [key ...]",
"Intersect multiple sets and store the resulting set in a key",
3,
"1.0.0" },
{ "SISMEMBER",
"key member",
"Determine if a given value is a member of a set",
3,
"1.0.0" },
{ "SLAVEOF",
"host port",
"Make the server a replica of another instance, or promote it as master.",
9,
"1.0.0" },
{ "SLOWLOG",
"",
"A container for slow log commands",
9,
"2.2.12" },
{ "SLOWLOG GET",
"[count]",
"Get the slow log's entries",
9,
"2.2.12" },
{ "SLOWLOG HELP",
"",
"Show helpful text about the different subcommands",
9,
"6.2.0" },
{ "SLOWLOG LEN",
"",
"Get the slow log's length",
9,
"2.2.12" },
{ "SLOWLOG RESET",
"",
"Clear all entries from the slow log",
9,
"2.2.12" },
{ "SMEMBERS",
"key",
"Get all the members in a set",
3,
"1.0.0" },
{ "SMISMEMBER",
"key member [member ...]",
"Returns the membership associated with the given elements for a set",
3,
"6.2.0" },
{ "SMOVE",
"source destination member",
"Move a member from one set to another",
3,
"1.0.0" },
{ "SORT",
"key [BY by-pattern] [LIMIT offset count] [GET get-pattern [GET get-pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]",
"Sort the elements in a list, set or sorted set",
0,
"1.0.0" },
{ "SORT_RO",
"key [BY by-pattern] [LIMIT offset count] [GET get-pattern [GET get-pattern ...]] [ASC|DESC] [ALPHA]",
"Sort the elements in a list, set or sorted set. Read-only variant of SORT.",
0,
"7.0.0" },
{ "SPOP",
"key [count]",
"Remove and return one or multiple random members from a set",
3,
"1.0.0" },
{ "SPUBLISH",
"shardchannel message",
"Post a message to a shard channel",
6,
"7.0.0" },
{ "SRANDMEMBER",
"key [count]",
"Get one or multiple random members from a set",
3,
"1.0.0" },
{ "SREM",
"key member [member ...]",
"Remove one or more members from a set",
3,
"1.0.0" },
{ "SSCAN",
"key cursor [MATCH pattern] [COUNT count]",
"Incrementally iterate Set elements",
3,
"2.8.0" },
{ "SSUBSCRIBE",
"shardchannel [shardchannel ...]",
"Listen for messages published to the given shard channels",
6,
"7.0.0" },
{ "STRLEN",
"key",
"Get the length of the value stored in a key",
1,
"2.2.0" },
{ "SUBSCRIBE",
"channel [channel ...]",
"Listen for messages published to the given channels",
6,
"2.0.0" },
{ "SUBSTR",
"key start end",
"Get a substring of the string stored at a key",
1,
"1.0.0" },
{ "SUNION",
"key [key ...]",
"Add multiple sets",
3,
"1.0.0" },
{ "SUNIONSTORE",
"destination key [key ...]",
"Add multiple sets and store the resulting set in a key",
3,
"1.0.0" },
{ "SUNSUBSCRIBE",
"[shardchannel [shardchannel ...]]",
"Stop listening for messages posted to the given shard channels",
6,
"7.0.0" },
{ "SWAPDB",
"index1 index2",
"Swaps two Redis databases",
9,
"4.0.0" },
{ "SYNC",
"",
"Internal command used for replication",
9,
"1.0.0" },
{ "TIME",
"",
"Return the current server time",
9,
"2.6.0" },
{ "TOUCH",
"key [key ...]",
"Alters the last access time of a key(s). Returns the number of existing keys specified.",
0,
"3.2.1" },
{ "TTL",
"key",
"Get the time to live for a key in seconds",
0,
"1.0.0" },
{ "TYPE",
"key",
"Determine the type stored at key",
0,
"1.0.0" },
{ "UNLINK",
"key [key ...]",
"Delete a key asynchronously in another thread. Otherwise it is just as DEL, but non blocking.",
0,
"4.0.0" },
{ "UNSUBSCRIBE",
"[channel [channel ...]]",
"Stop listening for messages posted to the given channels",
6,
"2.0.0" },
{ "UNWATCH",
"",
"Forget about all watched keys",
7,
"2.2.0" },
{ "WAIT",
"numreplicas timeout",
"Wait for the synchronous replication of all the write commands sent in the context of the current connection",
0,
"3.0.0" },
{ "WAITAOF",
"numlocal numreplicas timeout",
"Wait for all write commands sent in the context of the current connection to be synced to AOF of local host and/or replicas",
0,
"7.2.0" },
{ "WATCH",
"key [key ...]",
"Watch the given keys to determine execution of the MULTI/EXEC block",
7,
"2.2.0" },
{ "XACK",
"key group id [id ...]",
"Marks a pending message as correctly processed, effectively removing it from the pending entries list of the consumer group. Return value of the command is the number of messages successfully acknowledged, that is, the IDs we were actually able to resolve in the PEL.",
14,
"5.0.0" },
{ "XADD",
"key [NOMKSTREAM] [MAXLEN|MINID [=|~] threshold [LIMIT count]] *|id field value [field value ...]",
"Appends a new entry to a stream",
14,
"5.0.0" },
{ "XAUTOCLAIM",
"key group consumer min-idle-time start [COUNT count] [JUSTID]",
"Changes (or acquires) ownership of messages in a consumer group, as if the messages were delivered to the specified consumer.",
14,
"6.2.0" },
{ "XCLAIM",
"key group consumer min-idle-time id [id ...] [IDLE ms] [TIME unix-time-milliseconds] [RETRYCOUNT count] [FORCE] [JUSTID] [LASTID lastid]",
"Changes (or acquires) ownership of a message in a consumer group, as if the message was delivered to the specified consumer.",
14,
"5.0.0" },
{ "XDEL",
"key id [id ...]",
"Removes the specified entries from the stream. Returns the number of items actually deleted, that may be different from the number of IDs passed in case certain IDs do not exist.",
14,
"5.0.0" },
{ "XGROUP",
"",
"A container for consumer groups commands",
14,
"5.0.0" },
{ "XGROUP CREATE",
"key group id|$ [MKSTREAM] [ENTRIESREAD entries-read]",
"Create a consumer group.",
14,
"5.0.0" },
{ "XGROUP CREATECONSUMER",
"key group consumer",
"Create a consumer in a consumer group.",
14,
"6.2.0" },
{ "XGROUP DELCONSUMER",
"key group consumer",
"Delete a consumer from a consumer group.",
14,
"5.0.0" },
{ "XGROUP DESTROY",
"key group",
"Destroy a consumer group.",
14,
"5.0.0" },
{ "XGROUP HELP",
"",
"Show helpful text about the different subcommands",
14,
"5.0.0" },
{ "XGROUP SETID",
"key group id|$ [ENTRIESREAD entriesread]",
"Set a consumer group to an arbitrary last delivered ID value.",
14,
"5.0.0" },
{ "XINFO",
"",
"A container for stream introspection commands",
14,
"5.0.0" },
{ "XINFO CONSUMERS",
"key group",
"List the consumers in a consumer group",
14,
"5.0.0" },
{ "XINFO GROUPS",
"key",
"List the consumer groups of a stream",
14,
"5.0.0" },
{ "XINFO HELP",
"",
"Show helpful text about the different subcommands",
14,
"5.0.0" },
{ "XINFO STREAM",
"key [FULL [COUNT count]]",
"Get information about a stream",
14,
"5.0.0" },
{ "XLEN",
"key",
"Return the number of entries in a stream",
14,
"5.0.0" },
{ "XPENDING",
"key group [[IDLE min-idle-time] start end count [consumer]]",
"Return information and entries from a stream consumer group pending entries list, that are messages fetched but never acknowledged.",
14,
"5.0.0" },
{ "XRANGE",
"key start end [COUNT count]",
"Return a range of elements in a stream, with IDs matching the specified IDs interval",
14,
"5.0.0" },
{ "XREAD",
"[COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]",
"Return never seen elements in multiple streams, with IDs greater than the ones reported by the caller for each stream. Can block.",
14,
"5.0.0" },
{ "XREADGROUP",
"GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] id [id ...]",
"Return new entries from a stream using a consumer group, or access the history of the pending entries for a given consumer. Can block.",
14,
"5.0.0" },
{ "XREVRANGE",
"key end start [COUNT count]",
"Return a range of elements in a stream, with IDs matching the specified IDs interval, in reverse order (from greater to smaller IDs) compared to XRANGE",
14,
"5.0.0" },
{ "XSETID",
"key last-id [ENTRIESADDED entries-added] [MAXDELETEDID max-deleted-id]",
"An internal command for replicating stream values",
14,
"5.0.0" },
{ "XTRIM",
"key MAXLEN|MINID [=|~] threshold [LIMIT count]",
"Trims the stream to (approximately if '~' is passed) a certain size",
14,
"5.0.0" },
{ "ZADD",
"key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...]",
"Add one or more members to a sorted set, or update its score if it already exists",
4,
"1.2.0" },
{ "ZCARD",
"key",
"Get the number of members in a sorted set",
4,
"1.2.0" },
{ "ZCOUNT",
"key min max",
"Count the members in a sorted set with scores within the given values",
4,
"2.0.0" },
{ "ZDIFF",
"numkeys key [key ...] [WITHSCORES]",
"Subtract multiple sorted sets",
4,
"6.2.0" },
{ "ZDIFFSTORE",
"destination numkeys key [key ...]",
"Subtract multiple sorted sets and store the resulting sorted set in a new key",
4,
"6.2.0" },
{ "ZINCRBY",
"key increment member",
"Increment the score of a member in a sorted set",
4,
"1.2.0" },
{ "ZINTER",
"numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] [WITHSCORES]",
"Intersect multiple sorted sets",
4,
"6.2.0" },
{ "ZINTERCARD",
"numkeys key [key ...] [LIMIT limit]",
"Intersect multiple sorted sets and return the cardinality of the result",
4,
"7.0.0" },
{ "ZINTERSTORE",
"destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]",
"Intersect multiple sorted sets and store the resulting sorted set in a new key",
4,
"2.0.0" },
{ "ZLEXCOUNT",
"key min max",
"Count the number of members in a sorted set between a given lexicographical range",
4,
"2.8.9" },
{ "ZMPOP",
"numkeys key [key ...] MIN|MAX [COUNT count]",
"Remove and return members with scores in a sorted set",
4,
"7.0.0" },
{ "ZMSCORE",
"key member [member ...]",
"Get the score associated with the given members in a sorted set",
4,
"6.2.0" },
{ "ZPOPMAX",
"key [count]",
"Remove and return members with the highest scores in a sorted set",
4,
"5.0.0" },
{ "ZPOPMIN",
"key [count]",
"Remove and return members with the lowest scores in a sorted set",
4,
"5.0.0" },
{ "ZRANDMEMBER",
"key [count [WITHSCORES]]",
"Get one or multiple random elements from a sorted set",
4,
"6.2.0" },
{ "ZRANGE",
"key start stop [BYSCORE|BYLEX] [REV] [LIMIT offset count] [WITHSCORES]",
"Return a range of members in a sorted set",
4,
"1.2.0" },
{ "ZRANGEBYLEX",
"key min max [LIMIT offset count]",
"Return a range of members in a sorted set, by lexicographical range",
4,
"2.8.9" },
{ "ZRANGEBYSCORE",
"key min max [WITHSCORES] [LIMIT offset count]",
"Return a range of members in a sorted set, by score",
4,
"1.0.5" },
{ "ZRANGESTORE",
"dst src min max [BYSCORE|BYLEX] [REV] [LIMIT offset count]",
"Store a range of members from sorted set into another key",
4,
"6.2.0" },
{ "ZRANK",
"key member [WITHSCORE]",
"Determine the index of a member in a sorted set",
4,
"2.0.0" },
{ "ZREM",
"key member [member ...]",
"Remove one or more members from a sorted set",
4,
"1.2.0" },
{ "ZREMRANGEBYLEX",
"key min max",
"Remove all members in a sorted set between the given lexicographical range",
4,
"2.8.9" },
{ "ZREMRANGEBYRANK",
"key start stop",
"Remove all members in a sorted set within the given indexes",
4,
"2.0.0" },
{ "ZREMRANGEBYSCORE",
"key min max",
"Remove all members in a sorted set within the given scores",
4,
"1.2.0" },
{ "ZREVRANGE",
"key start stop [WITHSCORES]",
"Return a range of members in a sorted set, by index, with scores ordered from high to low",
4,
"1.2.0" },
{ "ZREVRANGEBYLEX",
"key max min [LIMIT offset count]",
"Return a range of members in a sorted set, by lexicographical range, ordered from higher to lower strings.",
4,
"2.8.9" },
{ "ZREVRANGEBYSCORE",
"key max min [WITHSCORES] [LIMIT offset count]",
"Return a range of members in a sorted set, by score, with scores ordered from high to low",
4,
"2.2.0" },
{ "ZREVRANK",
"key member [WITHSCORE]",
"Determine the index of a member in a sorted set, with scores ordered from high to low",
4,
"2.0.0" },
{ "ZSCAN",
"key cursor [MATCH pattern] [COUNT count]",
"Incrementally iterate sorted sets elements and associated scores",
4,
"2.8.0" },
{ "ZSCORE",
"key member",
"Get the score associated with the given member in a sorted set",
4,
"1.2.0" },
{ "ZUNION",
"numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] [WITHSCORES]",
"Add multiple sorted sets",
4,
"6.2.0" },
{ "ZUNIONSTORE",
"destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]",
"Add multiple sorted sets and store the resulting sorted set in a new key",
4,
"2.0.0" }
};
#endif
...@@ -1295,10 +1295,9 @@ RedisModuleCommand *moduleCreateCommandProxy(struct RedisModule *module, sds dec ...@@ -1295,10 +1295,9 @@ RedisModuleCommand *moduleCreateCommandProxy(struct RedisModule *module, sds dec
cp->rediscmd->proc = RedisModuleCommandDispatcher; cp->rediscmd->proc = RedisModuleCommandDispatcher;
cp->rediscmd->flags = flags | CMD_MODULE; cp->rediscmd->flags = flags | CMD_MODULE;
cp->rediscmd->module_cmd = cp; 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) { if (firstkey != 0) {
cp->rediscmd->key_specs_num = 1; cp->rediscmd->key_specs_num = 1;
cp->rediscmd->key_specs = zcalloc(sizeof(keySpec));
cp->rediscmd->key_specs[0].flags = CMD_KEY_FULL_ACCESS; cp->rediscmd->key_specs[0].flags = CMD_KEY_FULL_ACCESS;
if (flags & CMD_MODULE_GETKEYS) if (flags & CMD_MODULE_GETKEYS)
cp->rediscmd->key_specs[0].flags |= CMD_KEY_VARIABLE_FLAGS; cp->rediscmd->key_specs[0].flags |= CMD_KEY_VARIABLE_FLAGS;
...@@ -1310,6 +1309,7 @@ RedisModuleCommand *moduleCreateCommandProxy(struct RedisModule *module, sds dec ...@@ -1310,6 +1309,7 @@ RedisModuleCommand *moduleCreateCommandProxy(struct RedisModule *module, sds dec
cp->rediscmd->key_specs[0].fk.range.limit = 0; cp->rediscmd->key_specs[0].fk.range.limit = 0;
} else { } else {
cp->rediscmd->key_specs_num = 0; cp->rediscmd->key_specs_num = 0;
cp->rediscmd->key_specs = NULL;
} }
populateCommandLegacyRangeSpec(cp->rediscmd); populateCommandLegacyRangeSpec(cp->rediscmd);
cp->rediscmd->microseconds = 0; cp->rediscmd->microseconds = 0;
...@@ -1425,6 +1425,21 @@ moduleCmdArgAt(const RedisModuleCommandInfoVersion *version, ...@@ -1425,6 +1425,21 @@ moduleCmdArgAt(const RedisModuleCommandInfoVersion *version,
return (RedisModuleCommandArg *)((char *)(args) + offset); 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 /* 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. * and adds that flag to acl_categories_flags if a match is found.
* *
...@@ -1797,7 +1812,7 @@ int RM_SetCommandInfo(RedisModuleCommand *command, const RedisModuleCommandInfo ...@@ -1797,7 +1812,7 @@ int RM_SetCommandInfo(RedisModuleCommand *command, const RedisModuleCommandInfo
cmd->tips || cmd->args || cmd->tips || cmd->args ||
!(cmd->key_specs_num == 0 || !(cmd->key_specs_num == 0 ||
/* Allow key spec populated from legacy (first,last,step) to exist. */ /* 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].begin_search_type == KSPEC_BS_INDEX &&
cmd->key_specs[0].find_keys_type == KSPEC_FK_RANGE))) { cmd->key_specs[0].find_keys_type == KSPEC_FK_RANGE))) {
errno = EEXIST; errno = EEXIST;
...@@ -1848,13 +1863,8 @@ int RM_SetCommandInfo(RedisModuleCommand *command, const RedisModuleCommandInfo ...@@ -1848,13 +1863,8 @@ int RM_SetCommandInfo(RedisModuleCommand *command, const RedisModuleCommandInfo
while (moduleCmdKeySpecAt(version, info->key_specs, count)->begin_search_type) while (moduleCmdKeySpecAt(version, info->key_specs, count)->begin_search_type)
count++; count++;
serverAssert(count < INT_MAX); serverAssert(count < INT_MAX);
if (count <= STATIC_KEY_SPECS_NUM) { zfree(cmd->key_specs);
cmd->key_specs_max = STATIC_KEY_SPECS_NUM; cmd->key_specs = zmalloc(sizeof(keySpec) * count);
cmd->key_specs = cmd->key_specs_static;
} else {
cmd->key_specs_max = count;
cmd->key_specs = zmalloc(sizeof(keySpec) * count);
}
   
/* Copy the contents of the RedisModuleCommandKeySpec array. */ /* Copy the contents of the RedisModuleCommandKeySpec array. */
cmd->key_specs_num = count; cmd->key_specs_num = count;
...@@ -11926,8 +11936,7 @@ int moduleFreeCommand(struct RedisModule *module, struct redisCommand *cmd) { ...@@ -11926,8 +11936,7 @@ int moduleFreeCommand(struct RedisModule *module, struct redisCommand *cmd) {
if (cmd->key_specs[j].begin_search_type == KSPEC_BS_KEYWORD) if (cmd->key_specs[j].begin_search_type == KSPEC_BS_KEYWORD)
zfree((char *)cmd->key_specs[j].bs.keyword.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++) for (int j = 0; cmd->tips && cmd->tips[j]; j++)
zfree((char *)cmd->tips[j]); zfree((char *)cmd->tips[j]);
zfree(cmd->tips); zfree(cmd->tips);
......
...@@ -59,13 +59,14 @@ ...@@ -59,13 +59,14 @@
#include "adlist.h" #include "adlist.h"
#include "zmalloc.h" #include "zmalloc.h"
#include "linenoise.h" #include "linenoise.h"
#include "help.h" /* Used for backwards-compatibility with pre-7.0 servers that don't support COMMAND DOCS. */
#include "anet.h" #include "anet.h"
#include "ae.h" #include "ae.h"
#include "connection.h" #include "connection.h"
#include "cli_common.h" #include "cli_common.h"
#include "mt19937-64.h" #include "mt19937-64.h"
#include "cli_commands.h"
#define UNUSED(V) ((void) V) #define UNUSED(V) ((void) V)
#define OUTPUT_STANDARD 0 #define OUTPUT_STANDARD 0
...@@ -183,15 +184,6 @@ static int dictSdsKeyCompare(dict *d, const void *key1, ...@@ -183,15 +184,6 @@ static int dictSdsKeyCompare(dict *d, const void *key1,
static void dictSdsDestructor(dict *d, void *val); static void dictSdsDestructor(dict *d, void *val);
static void dictListDestructor(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 */ /* Cluster Manager Command Info */
typedef struct clusterManagerCommand { typedef struct clusterManagerCommand {
char *name; char *name;
...@@ -281,6 +273,9 @@ static struct config { ...@@ -281,6 +273,9 @@ static struct config {
int current_resp3; /* 1 if we have RESP3 right now in the current connection. */ int current_resp3; /* 1 if we have RESP3 right now in the current connection. */
int in_multi; int in_multi;
int pre_multi_dbnum; int pre_multi_dbnum;
char *server_version;
char *test_hint;
char *test_hint_file;
} config; } config;
/* User preferences. */ /* User preferences. */
...@@ -422,7 +417,7 @@ typedef struct { ...@@ -422,7 +417,7 @@ typedef struct {
sds full; sds full;
/* Only used for help on commands */ /* Only used for help on commands */
struct commandDocs org; struct commandDocs docs;
} helpEntry; } helpEntry;
static helpEntry *helpEntries = NULL; static helpEntry *helpEntries = NULL;
...@@ -442,50 +437,13 @@ static sds cliVersion(void) { ...@@ -442,50 +437,13 @@ static sds cliVersion(void) {
return version; return version;
} }
/* For backwards compatibility with pre-7.0 servers. Initializes command help. */
static void cliOldInitHelp(void) {
int commandslen = sizeof(commandHelp)/sizeof(struct commandHelp);
int groupslen = sizeof(commandGroups)/sizeof(char*);
int i, len, pos = 0;
helpEntry tmp;
helpEntriesLen = len = commandslen+groupslen;
helpEntries = zmalloc(sizeof(helpEntry)*len);
for (i = 0; i < groupslen; i++) {
tmp.argc = 1;
tmp.argv = zmalloc(sizeof(sds));
tmp.argv[0] = sdscatprintf(sdsempty(),"@%s",commandGroups[i]);
tmp.full = tmp.argv[0];
tmp.type = CLI_HELP_GROUP;
tmp.org.name = NULL;
tmp.org.params = NULL;
tmp.org.summary = NULL;
tmp.org.since = NULL;
tmp.org.group = NULL;
helpEntries[pos++] = tmp;
}
for (i = 0; i < commandslen; i++) {
tmp.argv = sdssplitargs(commandHelp[i].name,&tmp.argc);
tmp.full = sdsnew(commandHelp[i].name);
tmp.type = CLI_HELP_COMMAND;
tmp.org.name = commandHelp[i].name;
tmp.org.params = commandHelp[i].params;
tmp.org.summary = commandHelp[i].summary;
tmp.org.since = commandHelp[i].since;
tmp.org.group = commandGroups[commandHelp[i].group];
helpEntries[pos++] = tmp;
}
}
/* For backwards compatibility with pre-7.0 servers. /* For backwards compatibility with pre-7.0 servers.
* cliOldInitHelp() setups the helpEntries array with the command and group * cliLegacyInitHelp() sets up the helpEntries array with the command and group
* names from the help.h file. However the Redis instance we are connecting * names from the commands.c file. However the Redis instance we are connecting
* to may support more commands, so this function integrates the previous * to may support more commands, so this function integrates the previous
* entries with additional entries obtained using the COMMAND command * entries with additional entries obtained using the COMMAND command
* available in recent versions of Redis. */ * available in recent versions of Redis. */
static void cliOldIntegrateHelp(void) { static void cliLegacyIntegrateHelp(void) {
if (cliConnect(CC_QUIET) == REDIS_ERR) return; if (cliConnect(CC_QUIET) == REDIS_ERR) return;
redisReply *reply = redisCommand(context, "COMMAND"); redisReply *reply = redisCommand(context, "COMMAND");
...@@ -520,75 +478,88 @@ static void cliOldIntegrateHelp(void) { ...@@ -520,75 +478,88 @@ static void cliOldIntegrateHelp(void) {
new->type = CLI_HELP_COMMAND; new->type = CLI_HELP_COMMAND;
sdstoupper(new->argv[0]); sdstoupper(new->argv[0]);
new->org.name = new->argv[0]; new->docs.name = new->argv[0];
new->org.params = sdsempty(); new->docs.args = NULL;
new->docs.numargs = 0;
new->docs.params = sdsempty();
int args = llabs(entry->element[1]->integer); int args = llabs(entry->element[1]->integer);
args--; /* Remove the command name itself. */ args--; /* Remove the command name itself. */
if (entry->element[3]->integer == 1) { if (entry->element[3]->integer == 1) {
new->org.params = sdscat(new->org.params,"key "); new->docs.params = sdscat(new->docs.params,"key ");
args--; args--;
} }
while(args-- > 0) new->org.params = sdscat(new->org.params,"arg "); while(args-- > 0) new->docs.params = sdscat(new->docs.params,"arg ");
if (entry->element[1]->integer < 0) if (entry->element[1]->integer < 0)
new->org.params = sdscat(new->org.params,"...options..."); new->docs.params = sdscat(new->docs.params,"...options...");
new->org.summary = "Help not available"; new->docs.summary = "Help not available";
new->org.since = "Not known"; new->docs.since = "Not known";
new->org.group = commandGroups[0]; new->docs.group = "generic";
} }
freeReplyObject(reply); freeReplyObject(reply);
} }
/* Concatenate a string to an sds string, but if it's empty substitute double quote marks. */ /* Concatenate a string to an sds string, but if it's empty substitute double quote marks. */
static sds sdscat_orempty(sds params, char *value) { static sds sdscat_orempty(sds params, const char *value) {
if (value[0] == '\0') { if (value[0] == '\0') {
return sdscat(params, "\"\""); return sdscat(params, "\"\"");
} }
return sdscat(params, value); return sdscat(params, value);
} }
static sds cliAddArgument(sds params, redisReply *argMap); static sds makeHint(char **inputargv, int inputargc, int cmdlen, struct commandDocs docs);
static void cliAddCommandDocArg(cliCommandArg *cmdArg, redisReply *argMap);
/* Concatenate a list of arguments to the parameter string, separated by a separator string. */ static void cliMakeCommandDocArgs(redisReply *arguments, cliCommandArg *result) {
static sds cliConcatArguments(sds params, redisReply *arguments, char *separator) {
for (size_t j = 0; j < arguments->elements; j++) { for (size_t j = 0; j < arguments->elements; j++) {
params = cliAddArgument(params, arguments->element[j]); cliAddCommandDocArg(&result[j], arguments->element[j]);
if (j != arguments->elements - 1) {
params = sdscat(params, separator);
}
} }
return params;
} }
/* Add an argument to the parameter string. */ static void cliAddCommandDocArg(cliCommandArg *cmdArg, redisReply *argMap) {
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) { if (argMap->type != REDIS_REPLY_MAP && argMap->type != REDIS_REPLY_ARRAY) {
return params; return;
} }
for (size_t i = 0; i < argMap->elements; i += 2) { for (size_t i = 0; i < argMap->elements; i += 2) {
assert(argMap->element[i]->type == REDIS_REPLY_STRING); assert(argMap->element[i]->type == REDIS_REPLY_STRING);
char *key = argMap->element[i]->str; char *key = argMap->element[i]->str;
if (!strcmp(key, "name")) { if (!strcmp(key, "name")) {
assert(argMap->element[i + 1]->type == REDIS_REPLY_STRING); assert(argMap->element[i + 1]->type == REDIS_REPLY_STRING);
name = argMap->element[i + 1]->str; cmdArg->name = sdsnew(argMap->element[i + 1]->str);
} else if (!strcmp(key, "display_text")) {
assert(argMap->element[i + 1]->type == REDIS_REPLY_STRING);
cmdArg->display_text = sdsnew(argMap->element[i + 1]->str);
} else if (!strcmp(key, "token")) { } else if (!strcmp(key, "token")) {
assert(argMap->element[i + 1]->type == REDIS_REPLY_STRING); assert(argMap->element[i + 1]->type == REDIS_REPLY_STRING);
char *token = argMap->element[i + 1]->str; cmdArg->token = sdsnew(argMap->element[i + 1]->str);
tokenPart = sdscat_orempty(tokenPart, token);
} else if (!strcmp(key, "type")) { } else if (!strcmp(key, "type")) {
assert(argMap->element[i + 1]->type == REDIS_REPLY_STRING); assert(argMap->element[i + 1]->type == REDIS_REPLY_STRING);
type = argMap->element[i + 1]->str; char *type = argMap->element[i + 1]->str;
if (!strcmp(type, "string")) {
cmdArg->type = ARG_TYPE_STRING;
} else if (!strcmp(type, "integer")) {
cmdArg->type = ARG_TYPE_INTEGER;
} else if (!strcmp(type, "double")) {
cmdArg->type = ARG_TYPE_DOUBLE;
} else if (!strcmp(type, "key")) {
cmdArg->type = ARG_TYPE_KEY;
} else if (!strcmp(type, "pattern")) {
cmdArg->type = ARG_TYPE_PATTERN;
} else if (!strcmp(type, "unix-time")) {
cmdArg->type = ARG_TYPE_UNIX_TIME;
} else if (!strcmp(type, "pure-token")) {
cmdArg->type = ARG_TYPE_PURE_TOKEN;
} else if (!strcmp(type, "oneof")) {
cmdArg->type = ARG_TYPE_ONEOF;
} else if (!strcmp(type, "block")) {
cmdArg->type = ARG_TYPE_BLOCK;
}
} else if (!strcmp(key, "arguments")) { } else if (!strcmp(key, "arguments")) {
arguments = argMap->element[i + 1]; redisReply *arguments = argMap->element[i + 1];
cmdArg->subargs = zcalloc(arguments->elements * sizeof(cliCommandArg));
cmdArg->numsubargs = arguments->elements;
cliMakeCommandDocArgs(arguments, cmdArg->subargs);
} else if (!strcmp(key, "flags")) { } else if (!strcmp(key, "flags")) {
redisReply *flags = argMap->element[i + 1]; redisReply *flags = argMap->element[i + 1];
assert(flags->type == REDIS_REPLY_SET || flags->type == REDIS_REPLY_ARRAY); assert(flags->type == REDIS_REPLY_SET || flags->type == REDIS_REPLY_ARRAY);
...@@ -596,57 +567,15 @@ static sds cliAddArgument(sds params, redisReply *argMap) { ...@@ -596,57 +567,15 @@ static sds cliAddArgument(sds params, redisReply *argMap) {
assert(flags->element[j]->type == REDIS_REPLY_STATUS); assert(flags->element[j]->type == REDIS_REPLY_STATUS);
char *flag = flags->element[j]->str; char *flag = flags->element[j]->str;
if (!strcmp(flag, "optional")) { if (!strcmp(flag, "optional")) {
optional = 1; cmdArg->flags |= CMD_ARG_OPTIONAL;
} else if (!strcmp(flag, "multiple")) { } else if (!strcmp(flag, "multiple")) {
multiple = 1; cmdArg->flags |= CMD_ARG_MULTIPLE;
} else if (!strcmp(flag, "multiple_token")) { } else if (!strcmp(flag, "multiple_token")) {
multipleToken = 1; cmdArg->flags |= CMD_ARG_MULTIPLE_TOKEN;
} }
} }
} }
} }
/* Then build the "repeating part" of the argument string. */
if (!strcmp(type, "key") ||
!strcmp(type, "string") ||
!strcmp(type, "integer") ||
!strcmp(type, "double") ||
!strcmp(type, "pattern") ||
!strcmp(type, "unix-time") ||
!strcmp(type, "token"))
{
repeatPart = sdscat_orempty(repeatPart, name);
} else if (!strcmp(type, "oneof")) {
repeatPart = cliConcatArguments(repeatPart, arguments, "|");
} else if (!strcmp(type, "block")) {
repeatPart = cliConcatArguments(repeatPart, arguments, " ");
} else if (strcmp(type, "pure-token") != 0) {
fprintf(stderr, "Unknown type '%s' set for argument '%s'\n", type, name);
}
/* Finally, build the parameter string. */
if (tokenPart[0] != '\0' && strcmp(type, "pure-token") != 0) {
tokenPart = sdscat(tokenPart, " ");
}
if (optional) {
params = sdscat(params, "[");
}
params = sdscat(params, tokenPart);
params = sdscat(params, repeatPart);
if (multiple) {
params = sdscat(params, " [");
if (multipleToken) {
params = sdscat(params, tokenPart);
}
params = sdscat(params, repeatPart);
params = sdscat(params, " ...]");
}
if (optional) {
params = sdscat(params, "]");
}
sdsfree(tokenPart);
sdsfree(repeatPart);
return params;
} }
/* Fill in the fields of a help entry for the command/subcommand name. */ /* Fill in the fields of a help entry for the command/subcommand name. */
...@@ -656,8 +585,13 @@ static void cliFillInCommandHelpEntry(helpEntry *help, char *cmdname, char *subc ...@@ -656,8 +585,13 @@ static void cliFillInCommandHelpEntry(helpEntry *help, char *cmdname, char *subc
help->argv[0] = sdsnew(cmdname); help->argv[0] = sdsnew(cmdname);
sdstoupper(help->argv[0]); sdstoupper(help->argv[0]);
if (subcommandname) { if (subcommandname) {
/* Subcommand name is two words separated by a pipe character. */ /* Subcommand name may be two words separated by a pipe character. */
help->argv[1] = sdsnew(strchr(subcommandname, '|') + 1); char *pipe = strchr(subcommandname, '|');
if (pipe != NULL) {
help->argv[1] = sdsnew(pipe + 1);
} else {
help->argv[1] = sdsnew(subcommandname);
}
sdstoupper(help->argv[1]); sdstoupper(help->argv[1]);
} }
sds fullname = sdsnew(help->argv[0]); sds fullname = sdsnew(help->argv[0]);
...@@ -668,9 +602,11 @@ static void cliFillInCommandHelpEntry(helpEntry *help, char *cmdname, char *subc ...@@ -668,9 +602,11 @@ static void cliFillInCommandHelpEntry(helpEntry *help, char *cmdname, char *subc
help->full = fullname; help->full = fullname;
help->type = CLI_HELP_COMMAND; help->type = CLI_HELP_COMMAND;
help->org.name = help->full; help->docs.name = help->full;
help->org.params = sdsempty(); help->docs.params = NULL;
help->org.since = NULL; help->docs.args = NULL;
help->docs.numargs = 0;
help->docs.since = NULL;
} }
/* Initialize a command help entry for the command/subcommand described in 'specs'. /* Initialize a command help entry for the command/subcommand described in 'specs'.
...@@ -692,23 +628,26 @@ static helpEntry *cliInitCommandHelpEntry(char *cmdname, char *subcommandname, ...@@ -692,23 +628,26 @@ static helpEntry *cliInitCommandHelpEntry(char *cmdname, char *subcommandname,
if (!strcmp(key, "summary")) { if (!strcmp(key, "summary")) {
redisReply *reply = specs->element[j + 1]; redisReply *reply = specs->element[j + 1];
assert(reply->type == REDIS_REPLY_STRING); assert(reply->type == REDIS_REPLY_STRING);
help->org.summary = sdsnew(reply->str); help->docs.summary = sdsnew(reply->str);
} else if (!strcmp(key, "since")) { } else if (!strcmp(key, "since")) {
redisReply *reply = specs->element[j + 1]; redisReply *reply = specs->element[j + 1];
assert(reply->type == REDIS_REPLY_STRING); assert(reply->type == REDIS_REPLY_STRING);
help->org.since = sdsnew(reply->str); help->docs.since = sdsnew(reply->str);
} else if (!strcmp(key, "group")) { } else if (!strcmp(key, "group")) {
redisReply *reply = specs->element[j + 1]; redisReply *reply = specs->element[j + 1];
assert(reply->type == REDIS_REPLY_STRING); assert(reply->type == REDIS_REPLY_STRING);
help->org.group = sdsnew(reply->str); help->docs.group = sdsnew(reply->str);
sds group = sdsdup(help->org.group); sds group = sdsdup(help->docs.group);
if (dictAdd(groups, group, NULL) != DICT_OK) { if (dictAdd(groups, group, NULL) != DICT_OK) {
sdsfree(group); sdsfree(group);
} }
} else if (!strcmp(key, "arguments")) { } else if (!strcmp(key, "arguments")) {
redisReply *args = specs->element[j + 1]; redisReply *arguments = specs->element[j + 1];
assert(args->type == REDIS_REPLY_ARRAY); assert(arguments->type == REDIS_REPLY_ARRAY);
help->org.params = cliConcatArguments(help->org.params, args, " "); help->docs.args = zcalloc(arguments->elements * sizeof(cliCommandArg));
help->docs.numargs = arguments->elements;
cliMakeCommandDocArgs(arguments, help->docs.args);
help->docs.params = makeHint(NULL, 0, 0, help->docs);
} else if (!strcmp(key, "subcommands")) { } else if (!strcmp(key, "subcommands")) {
redisReply *subcommands = specs->element[j + 1]; redisReply *subcommands = specs->element[j + 1];
assert(subcommands->type == REDIS_REPLY_MAP || subcommands->type == REDIS_REPLY_ARRAY); assert(subcommands->type == REDIS_REPLY_MAP || subcommands->type == REDIS_REPLY_ARRAY);
...@@ -774,11 +713,13 @@ void cliInitGroupHelpEntries(dict *groups) { ...@@ -774,11 +713,13 @@ void cliInitGroupHelpEntries(dict *groups) {
tmp.argv[0] = sdscatprintf(sdsempty(),"@%s",(char *)dictGetKey(entry)); tmp.argv[0] = sdscatprintf(sdsempty(),"@%s",(char *)dictGetKey(entry));
tmp.full = tmp.argv[0]; tmp.full = tmp.argv[0];
tmp.type = CLI_HELP_GROUP; tmp.type = CLI_HELP_GROUP;
tmp.org.name = NULL; tmp.docs.name = NULL;
tmp.org.params = NULL; tmp.docs.params = NULL;
tmp.org.summary = NULL; tmp.docs.args = NULL;
tmp.org.since = NULL; tmp.docs.numargs = 0;
tmp.org.group = NULL; tmp.docs.summary = NULL;
tmp.docs.since = NULL;
tmp.docs.group = NULL;
helpEntries[pos++] = tmp; helpEntries[pos++] = tmp;
} }
dictReleaseIterator(iter); dictReleaseIterator(iter);
...@@ -798,6 +739,164 @@ void cliInitCommandHelpEntries(redisReply *commandTable, dict *groups) { ...@@ -798,6 +739,164 @@ void cliInitCommandHelpEntries(redisReply *commandTable, dict *groups) {
} }
} }
/* Does the server version support a command/argument only available "since" some version?
* Returns 1 when supported, or 0 when the "since" version is newer than "version". */
static int versionIsSupported(sds version, sds since) {
int i;
char *versionPos = version;
char *sincePos = since;
if (!since) {
return 1;
}
for (i = 0; i != 3; i++) {
int versionPart = atoi(versionPos);
int sincePart = atoi(sincePos);
if (versionPart > sincePart) {
return 1;
} else if (sincePart > versionPart) {
return 0;
}
versionPos = strchr(versionPos, '.');
sincePos = strchr(sincePos, '.');
if (!versionPos || !sincePos)
return 0;
versionPos++;
sincePos++;
}
return 0;
}
static void removeUnsupportedArgs(struct cliCommandArg *args, int *numargs, sds version) {
int i = 0, j;
while (i != *numargs) {
if (versionIsSupported(version, args[i].since)) {
if (args[i].subargs) {
removeUnsupportedArgs(args[i].subargs, &args[i].numsubargs, version);
}
i++;
continue;
}
for (j = i; j != *numargs; j++) {
args[j] = args[j + 1];
}
(*numargs)--;
}
}
static helpEntry *cliLegacyInitCommandHelpEntry(char *cmdname, char *subcommandname,
helpEntry *next, struct commandDocs *command,
dict *groups, sds version) {
helpEntry *help = next++;
cliFillInCommandHelpEntry(help, cmdname, subcommandname);
help->docs.summary = sdsnew(command->summary);
help->docs.since = sdsnew(command->since);
help->docs.group = sdsnew(command->group);
sds group = sdsdup(help->docs.group);
if (dictAdd(groups, group, NULL) != DICT_OK) {
sdsfree(group);
}
if (command->args != NULL) {
help->docs.args = command->args;
help->docs.numargs = command->numargs;
if (version)
removeUnsupportedArgs(help->docs.args, &help->docs.numargs, version);
help->docs.params = makeHint(NULL, 0, 0, help->docs);
}
if (command->subcommands != NULL) {
for (size_t i = 0; command->subcommands[i].name != NULL; i++) {
if (!version || versionIsSupported(version, command->subcommands[i].since)) {
char *subcommandname = command->subcommands[i].name;
next = cliLegacyInitCommandHelpEntry(
cmdname, subcommandname, next, &command->subcommands[i], groups, version);
}
}
}
return next;
}
int cliLegacyInitCommandHelpEntries(struct commandDocs *commands, dict *groups, sds version) {
helpEntry *next = helpEntries;
for (size_t i = 0; commands[i].name != NULL; i++) {
if (!version || versionIsSupported(version, commands[i].since)) {
next = cliLegacyInitCommandHelpEntry(commands[i].name, NULL, next, &commands[i], groups, version);
}
}
return next - helpEntries;
}
/* Returns the total number of commands and subcommands in the command docs table,
* filtered by server version (if provided).
*/
static size_t cliLegacyCountCommands(struct commandDocs *commands, sds version) {
int numCommands = 0;
for (size_t i = 0; commands[i].name != NULL; i++) {
if (version && !versionIsSupported(version, commands[i].since)) {
continue;
}
numCommands++;
if (commands[i].subcommands != NULL) {
numCommands += cliLegacyCountCommands(commands[i].subcommands, version);
}
}
return numCommands;
}
/* Gets the server version string by calling INFO SERVER.
* Stores the result in config.server_version.
* When not connected, or not possible, returns NULL. */
static sds cliGetServerVersion() {
static const char *key = "\nredis_version:";
redisReply *serverInfo = NULL;
char *pos;
if (config.server_version != NULL) {
return config.server_version;
}
if (!context) return NULL;
serverInfo = redisCommand(context, "INFO SERVER");
if (serverInfo == NULL || serverInfo->type == REDIS_REPLY_ERROR) {
freeReplyObject(serverInfo);
return sdsempty();
}
assert(serverInfo->type == REDIS_REPLY_STRING || serverInfo->type == REDIS_REPLY_VERB);
sds info = serverInfo->str;
/* Finds the first appearance of "redis_version" in the INFO SERVER reply. */
pos = strstr(info, key);
if (pos) {
pos += strlen(key);
char *end = strchr(pos, '\r');
if (end) {
sds version = sdsnewlen(pos, end - pos);
freeReplyObject(serverInfo);
config.server_version = version;
return version;
}
}
freeReplyObject(serverInfo);
return NULL;
}
static void cliLegacyInitHelp(dict *groups) {
sds serverVersion = cliGetServerVersion();
/* Scan the commandDocs array and fill in the entries */
helpEntriesLen = cliLegacyCountCommands(redisCommandTable, serverVersion);
helpEntries = zmalloc(sizeof(helpEntry)*helpEntriesLen);
helpEntriesLen = cliLegacyInitCommandHelpEntries(redisCommandTable, groups, serverVersion);
cliInitGroupHelpEntries(groups);
qsort(helpEntries, helpEntriesLen, sizeof(helpEntry), helpEntryCompare);
dictRelease(groups);
}
/* cliInitHelp() sets up the helpEntries array with the command and group /* cliInitHelp() sets up the helpEntries array with the command and group
* names and command descriptions obtained using the COMMAND DOCS command. * names and command descriptions obtained using the COMMAND DOCS command.
*/ */
...@@ -817,16 +916,20 @@ static void cliInitHelp(void) { ...@@ -817,16 +916,20 @@ static void cliInitHelp(void) {
if (cliConnect(CC_QUIET) == REDIS_ERR) { if (cliConnect(CC_QUIET) == REDIS_ERR) {
/* Can not connect to the server, but we still want to provide /* Can not connect to the server, but we still want to provide
* help, generate it only from the old help.h data instead. */ * help, generate it only from the static cli_commands.c data instead. */
cliOldInitHelp(); groups = dictCreate(&groupsdt);
cliLegacyInitHelp(groups);
return; return;
} }
commandTable = redisCommand(context, "COMMAND DOCS"); commandTable = redisCommand(context, "COMMAND DOCS");
if (commandTable == NULL || commandTable->type == REDIS_REPLY_ERROR) { if (commandTable == NULL || commandTable->type == REDIS_REPLY_ERROR) {
/* New COMMAND DOCS subcommand not supported - generate help from old help.h data instead. */ /* New COMMAND DOCS subcommand not supported - generate help from
* static cli_commands.c data instead. */
freeReplyObject(commandTable); freeReplyObject(commandTable);
cliOldInitHelp();
cliOldIntegrateHelp(); groups = dictCreate(&groupsdt);
cliLegacyInitHelp(groups);
cliLegacyIntegrateHelp();
return; return;
}; };
if (commandTable->type != REDIS_REPLY_MAP && commandTable->type != REDIS_REPLY_ARRAY) return; if (commandTable->type != REDIS_REPLY_MAP && commandTable->type != REDIS_REPLY_ARRAY) return;
...@@ -901,7 +1004,7 @@ static void cliOutputHelp(int argc, char **argv) { ...@@ -901,7 +1004,7 @@ static void cliOutputHelp(int argc, char **argv) {
entry = &helpEntries[i]; entry = &helpEntries[i];
if (entry->type != CLI_HELP_COMMAND) continue; if (entry->type != CLI_HELP_COMMAND) continue;
help = &entry->org; help = &entry->docs;
if (group == NULL) { if (group == NULL) {
/* Compare all arguments */ /* Compare all arguments */
if (argc <= entry->argc) { if (argc <= entry->argc) {
...@@ -948,36 +1051,429 @@ static void completionCallback(const char *buf, linenoiseCompletions *lc) { ...@@ -948,36 +1051,429 @@ static void completionCallback(const char *buf, linenoiseCompletions *lc) {
} }
} }
/* Linenoise hints callback. */ static sds addHintForArgument(sds hint, cliCommandArg *arg);
static char *hintsCallback(const char *buf, int *color, int *bold) {
if (!pref.hints) return NULL;
int i, rawargc, argc, buflen = strlen(buf), matchlen = 0, shift = 0; /* Adds a separator character between words of a string under construction.
sds *rawargv, *argv = sdssplitargs(buf,&argc); * A separator is added if the string length is greater than its previously-recorded
int endspace = buflen && isspace(buf[buflen-1]); * length (*len), which is then updated, and it's not the last word to be added.
helpEntry *entry = NULL; */
static sds addSeparator(sds str, size_t *len, char *separator, int is_last) {
if (sdslen(str) > *len && !is_last) {
str = sdscat(str, separator);
*len = sdslen(str);
}
return str;
}
/* Check if the argument list is empty and return ASAP. */ /* Recursively zeros the matched* fields of all arguments. */
if (argc == 0) { static void clearMatchedArgs(cliCommandArg *args, int numargs) {
sdsfreesplitres(argv,argc); for (int i = 0; i != numargs; ++i) {
return NULL; args[i].matched = 0;
args[i].matched_token = 0;
args[i].matched_name = 0;
args[i].matched_all = 0;
if (args[i].subargs) {
clearMatchedArgs(args[i].subargs, args[i].numsubargs);
}
} }
}
if (argc > 3 && (!strcasecmp(argv[0], "acl") && !strcasecmp(argv[1], "dryrun"))) { /* Builds a completion hint string describing the arguments, skipping parts already matched.
shift = 3; * Hints for all arguments are added to the input 'hint' parameter, separated by 'separator'.
} else if (argc > 2 && (!strcasecmp(argv[0], "command") && */
(!strcasecmp(argv[1], "getkeys") || !strcasecmp(argv[1], "getkeysandflags")))) static sds addHintForArguments(sds hint, cliCommandArg *args, int numargs, char *separator) {
{ int i, j, incomplete;
shift = 2; size_t len=sdslen(hint);
for (i = 0; i < numargs; i++) {
if (!(args[i].flags & CMD_ARG_OPTIONAL)) {
hint = addHintForArgument(hint, &args[i]);
hint = addSeparator(hint, &len, separator, i == numargs-1);
continue;
}
/* The rule is that successive "optional" arguments can appear in any order.
* But if they are followed by a required argument, no more of those optional arguments
* can appear after that.
*
* This code handles all successive optional args together. This lets us show the
* completion of the currently-incomplete optional arg first, if there is one.
*/
for (j = i, incomplete = -1; j < numargs; j++) {
if (!(args[j].flags & CMD_ARG_OPTIONAL)) break;
if (args[j].matched != 0 && args[j].matched_all == 0) {
/* User has started typing this arg; show its completion first. */
hint = addHintForArgument(hint, &args[j]);
hint = addSeparator(hint, &len, separator, i == numargs-1);
incomplete = j;
}
}
/* If the following non-optional arg has not been matched, add hints for
* any remaining optional args in this group.
*/
if (j == numargs || args[j].matched == 0) {
for (; i < j; i++) {
if (incomplete != i) {
hint = addHintForArgument(hint, &args[i]);
hint = addSeparator(hint, &len, separator, i == numargs-1);
}
}
}
i = j - 1;
}
return hint;
}
/* Adds the "repeating" section of the hint string for a multiple-typed argument: [ABC def ...]
* The repeating part is a fixed unit; we don't filter matched elements from it.
*/
static sds addHintForRepeatedArgument(sds hint, cliCommandArg *arg) {
if (!(arg->flags & CMD_ARG_MULTIPLE)) {
return hint;
}
/* The repeating part is always shown at the end of the argument's hint,
* so we can safely clear its matched flags before printing it.
*/
clearMatchedArgs(arg, 1);
if (hint[0] != '\0') {
hint = sdscat(hint, " ");
}
hint = sdscat(hint, "[");
if (arg->flags & CMD_ARG_MULTIPLE_TOKEN) {
hint = sdscat_orempty(hint, arg->token);
if (arg->type != ARG_TYPE_PURE_TOKEN) {
hint = sdscat(hint, " ");
}
}
switch (arg->type) {
case ARG_TYPE_ONEOF:
hint = addHintForArguments(hint, arg->subargs, arg->numsubargs, "|");
break;
case ARG_TYPE_BLOCK:
hint = addHintForArguments(hint, arg->subargs, arg->numsubargs, " ");
break;
case ARG_TYPE_PURE_TOKEN:
break;
default:
hint = sdscat_orempty(hint, arg->display_text ? arg->display_text : arg->name);
break;
}
hint = sdscat(hint, " ...]");
return hint;
}
/* Adds hint string for one argument, if not already matched. */
static sds addHintForArgument(sds hint, cliCommandArg *arg) {
if (arg->matched_all) {
return hint;
}
/* Surround an optional arg with brackets, unless it's partially matched. */
if ((arg->flags & CMD_ARG_OPTIONAL) && !arg->matched) {
hint = sdscat(hint, "[");
}
/* Start with the token, if present and not matched. */
if (arg->token != NULL && !arg->matched_token) {
hint = sdscat_orempty(hint, arg->token);
if (arg->type != ARG_TYPE_PURE_TOKEN) {
hint = sdscat(hint, " ");
}
}
/* Add the body of the syntax string. */
switch (arg->type) {
case ARG_TYPE_ONEOF:
if (arg->matched == 0) {
hint = addHintForArguments(hint, arg->subargs, arg->numsubargs, "|");
} else {
int i;
for (i = 0; i < arg->numsubargs; i++) {
if (arg->subargs[i].matched != 0) {
hint = addHintForArgument(hint, &arg->subargs[i]);
}
}
}
break;
case ARG_TYPE_BLOCK:
hint = addHintForArguments(hint, arg->subargs, arg->numsubargs, " ");
break;
case ARG_TYPE_PURE_TOKEN:
break;
default:
if (!arg->matched_name) {
hint = sdscat_orempty(hint, arg->display_text ? arg->display_text : arg->name);
}
break;
}
hint = addHintForRepeatedArgument(hint, arg);
if ((arg->flags & CMD_ARG_OPTIONAL) && !arg->matched) {
hint = sdscat(hint, "]");
}
return hint;
}
static int matchArg(char **nextword, int numwords, cliCommandArg *arg);
static int matchArgs(char **words, int numwords, cliCommandArg *args, int numargs);
/* Tries to match the next words of the input against an argument. */
static int matchNoTokenArg(char **nextword, int numwords, cliCommandArg *arg) {
int i;
switch (arg->type) {
case ARG_TYPE_BLOCK: {
arg->matched += matchArgs(nextword, numwords, arg->subargs, arg->numsubargs);
/* All the subargs must be matched for the block to match. */
arg->matched_all = 1;
for (i = 0; i < arg->numsubargs; i++) {
if (arg->subargs[i].matched_all == 0) {
arg->matched_all = 0;
}
}
break;
}
case ARG_TYPE_ONEOF: {
for (i = 0; i < arg->numsubargs; i++) {
if (matchArg(nextword, numwords, &arg->subargs[i])) {
arg->matched += arg->subargs[i].matched;
arg->matched_all = arg->subargs[i].matched_all;
break;
}
}
break;
}
case ARG_TYPE_INTEGER:
case ARG_TYPE_UNIX_TIME: {
long long value;
if (sscanf(*nextword, "%lld", &value)) {
arg->matched += 1;
arg->matched_name = 1;
arg->matched_all = 1;
} else {
/* Matching failed due to incorrect arg type. */
arg->matched = 0;
arg->matched_name = 0;
}
break;
}
case ARG_TYPE_DOUBLE: {
double value;
if (sscanf(*nextword, "%lf", &value)) {
arg->matched += 1;
arg->matched_name = 1;
arg->matched_all = 1;
} else {
/* Matching failed due to incorrect arg type. */
arg->matched = 0;
arg->matched_name = 0;
}
break;
}
default:
arg->matched += 1;
arg->matched_name = 1;
arg->matched_all = 1;
break;
} }
argc -= shift; return arg->matched;
argv += shift; }
/* Tries to match the next word of the input against a token literal. */
static int matchToken(char **nextword, cliCommandArg *arg) {
if (strcasecmp(arg->token, nextword[0]) != 0) {
return 0;
}
arg->matched_token = 1;
arg->matched = 1;
return 1;
}
/* Tries to match the next words of the input against the next argument.
* If the arg is repeated ("multiple"), it will be matched only once.
* If the next input word(s) can't be matched, returns 0 for failure.
*/
static int matchArgOnce(char **nextword, int numwords, cliCommandArg *arg) {
/* First match the token, if present. */
if (arg->token != NULL) {
if (!matchToken(nextword, arg)) {
return 0;
}
if (arg->type == ARG_TYPE_PURE_TOKEN) {
arg->matched_all = 1;
return 1;
}
if (numwords == 1) {
return 1;
}
nextword++;
numwords--;
}
/* Then match the rest of the argument. */
if (!matchNoTokenArg(nextword, numwords, arg)) {
return 0;
}
return arg->matched;
}
/* Tries to match the next words of the input against the next argument.
* If the arg is repeated ("multiple"), it will be matched as many times as possible.
*/
static int matchArg(char **nextword, int numwords, cliCommandArg *arg) {
int matchedWords = 0;
int matchedOnce = matchArgOnce(nextword, numwords, arg);
if (!(arg->flags & CMD_ARG_MULTIPLE)) {
return matchedOnce;
}
/* Found one match; now match a "multiple" argument as many times as possible. */
matchedWords += matchedOnce;
while (arg->matched_all && matchedWords < numwords) {
clearMatchedArgs(arg, 1);
if (arg->token != NULL && !(arg->flags & CMD_ARG_MULTIPLE_TOKEN)) {
/* The token only appears the first time; the rest of the times,
* pretend we saw it so we don't hint it.
*/
matchedOnce = matchNoTokenArg(nextword + matchedWords, numwords - matchedWords, arg);
if (arg->matched) {
arg->matched_token = 1;
}
} else {
matchedOnce = matchArgOnce(nextword + matchedWords, numwords - matchedWords, arg);
}
matchedWords += matchedOnce;
}
arg->matched_all = 0; /* Because more repetitions are still possible. */
return matchedWords;
}
/* Tries to match the next words of the input against
* any one of a consecutive set of optional arguments.
*/
static int matchOneOptionalArg(char **words, int numwords, cliCommandArg *args, int numargs, int *matchedarg) {
for (int nextword = 0, nextarg = 0; nextword != numwords && nextarg != numargs; ++nextarg) {
if (args[nextarg].matched) {
/* Already matched this arg. */
continue;
}
int matchedWords = matchArg(&words[nextword], numwords - nextword, &args[nextarg]);
if (matchedWords != 0) {
*matchedarg = nextarg;
return matchedWords;
}
}
return 0;
}
/* Matches as many input words as possible against a set of consecutive optional arguments. */
static int matchOptionalArgs(char **words, int numwords, cliCommandArg *args, int numargs) {
int nextword = 0;
int matchedarg = -1, lastmatchedarg = -1;
while (nextword != numwords) {
int matchedWords = matchOneOptionalArg(&words[nextword], numwords - nextword, args, numargs, &matchedarg);
if (matchedWords == 0) {
break;
}
/* Successfully matched an optional arg; mark any previous match as completed
* so it won't be partially hinted.
*/
if (lastmatchedarg != -1) {
args[lastmatchedarg].matched_all = 1;
}
lastmatchedarg = matchedarg;
nextword += matchedWords;
}
return nextword;
}
/* Matches as many input words as possible against command arguments. */
static int matchArgs(char **words, int numwords, cliCommandArg *args, int numargs) {
int nextword, nextarg, matchedWords;
for (nextword = 0, nextarg = 0; nextword != numwords && nextarg != numargs; ++nextarg) {
/* Optional args can occur in any order. Collect a range of consecutive optional args
* and try to match them as a group against the next input words.
*/
if (args[nextarg].flags & CMD_ARG_OPTIONAL) {
int lastoptional;
for (lastoptional = nextarg; lastoptional < numargs; lastoptional++) {
if (!(args[lastoptional].flags & CMD_ARG_OPTIONAL)) break;
}
matchedWords = matchOptionalArgs(&words[nextword], numwords - nextword, &args[nextarg], lastoptional - nextarg);
nextarg = lastoptional - 1;
} else {
matchedWords = matchArg(&words[nextword], numwords - nextword, &args[nextarg]);
if (matchedWords == 0) {
/* Couldn't match a required word - matching fails! */
return 0;
}
}
nextword += matchedWords;
}
return nextword;
}
/* Compute the linenoise hint for the input prefix in inputargv/inputargc.
* cmdlen is the number of words from the start of the input that make up the command.
* If docs.args exists, dynamically creates a hint string by matching the arg specs
* against the input words.
*/
static sds makeHint(char **inputargv, int inputargc, int cmdlen, struct commandDocs docs) {
sds hint;
if (docs.args) {
/* Remove arguments from the returned hint to show only the
* ones the user did not yet type. */
clearMatchedArgs(docs.args, docs.numargs);
hint = sdsempty();
int matchedWords = 0;
if (inputargv && inputargc)
matchedWords = matchArgs(inputargv + cmdlen, inputargc - cmdlen, docs.args, docs.numargs);
if (matchedWords == inputargc - cmdlen) {
hint = addHintForArguments(hint, docs.args, docs.numargs, " ");
}
return hint;
}
/* If arg specs are not available, show the hint string until the user types something. */
if (inputargc <= cmdlen) {
hint = sdsnew(docs.params);
} else {
hint = sdsempty();
}
return hint;
}
/* Search for a command matching the longest possible prefix of input words. */
static helpEntry* findHelpEntry(int argc, char **argv) {
helpEntry *entry = NULL;
int i, rawargc, matchlen = 0;
sds *rawargv;
/* Search longest matching prefix command */
for (i = 0; i < helpEntriesLen; i++) { for (i = 0; i < helpEntriesLen; i++) {
if (!(helpEntries[i].type & CLI_HELP_COMMAND)) continue; if (!(helpEntries[i].type & CLI_HELP_COMMAND)) continue;
rawargv = sdssplitargs(helpEntries[i].full,&rawargc); rawargv = helpEntries[i].argv;
rawargc = helpEntries[i].argc;
if (rawargc <= argc) { if (rawargc <= argc) {
int j; int j;
for (j = 0; j < rawargc; j++) { for (j = 0; j < rawargc; j++) {
...@@ -990,35 +1486,51 @@ static char *hintsCallback(const char *buf, int *color, int *bold) { ...@@ -990,35 +1486,51 @@ static char *hintsCallback(const char *buf, int *color, int *bold) {
entry = &helpEntries[i]; entry = &helpEntries[i];
} }
} }
sdsfreesplitres(rawargv,rawargc);
} }
sdsfreesplitres(argv - shift,argc + shift); return entry;
}
/* Returns the command-line hint string for a given partial input. */
static sds getHintForInput(const char *charinput) {
sds hint = NULL;
int inputargc, inputlen = strlen(charinput);
sds *inputargv = sdssplitargs(charinput, &inputargc);
int endspace = inputlen && isspace(charinput[inputlen-1]);
/* Don't match the last word until the user has typed a space after it. */
int matchargc = endspace ? inputargc : inputargc - 1;
helpEntry *entry = findHelpEntry(matchargc, inputargv);
if (entry) { if (entry) {
*color = 90; hint = makeHint(inputargv, matchargc, entry->argc, entry->docs);
*bold = 0; }
sds hint = sdsnew(entry->org.params); sdsfreesplitres(inputargv, inputargc);
return hint;
}
/* Remove arguments from the returned hint to show only the /* Linenoise hints callback. */
* ones the user did not yet type. */ static char *hintsCallback(const char *buf, int *color, int *bold) {
int toremove = argc-matchlen; if (!pref.hints) return NULL;
while(toremove > 0 && sdslen(hint)) {
if (hint[0] == '[') break;
if (hint[0] == ' ') toremove--;
sdsrange(hint,1,-1);
}
/* Add an initial space if needed. */ sds hint = getHintForInput(buf);
if (!endspace) { if (hint == NULL) {
sds newhint = sdsnewlen(" ",1); return NULL;
newhint = sdscatsds(newhint,hint); }
sdsfree(hint);
hint = newhint;
}
return hint; *color = 90;
*bold = 0;
/* Add an initial space if needed. */
int len = strlen(buf);
int endspace = len && isspace(buf[len-1]);
if (!endspace) {
sds newhint = sdsnewlen(" ",1);
newhint = sdscatsds(newhint,hint);
sdsfree(hint);
hint = newhint;
} }
return NULL;
return hint;
} }
static void freeHintsCallback(void *ptr) { static void freeHintsCallback(void *ptr) {
...@@ -1119,6 +1631,16 @@ static int cliSwitchProto(void) { ...@@ -1119,6 +1631,16 @@ static int cliSwitchProto(void) {
result = REDIS_OK; result = REDIS_OK;
} }
} }
/* Retrieve server version string for later use. */
for (size_t i = 0; i < reply->elements; i += 2) {
assert(reply->element[i]->type == REDIS_REPLY_STRING);
char *key = reply->element[i]->str;
if (!strcmp(key, "version")) {
assert(reply->element[i + 1]->type == REDIS_REPLY_STRING);
config.server_version = sdsnew(reply->element[i + 1]->str);
}
}
freeReplyObject(reply); freeReplyObject(reply);
config.current_resp3 = 1; config.current_resp3 = 1;
return result; return result;
...@@ -2341,6 +2863,10 @@ static int parseOptions(int argc, char **argv) { ...@@ -2341,6 +2863,10 @@ static int parseOptions(int argc, char **argv) {
} else if (!strcmp(argv[i],"--cluster-fix-with-unreachable-masters")) { } else if (!strcmp(argv[i],"--cluster-fix-with-unreachable-masters")) {
config.cluster_manager_command.flags |= config.cluster_manager_command.flags |=
CLUSTER_MANAGER_CMD_FLAG_FIX_WITH_UNREACHABLE_MASTERS; CLUSTER_MANAGER_CMD_FLAG_FIX_WITH_UNREACHABLE_MASTERS;
} else if (!strcmp(argv[i],"--test_hint") && !lastarg) {
config.test_hint = argv[++i];
} else if (!strcmp(argv[i],"--test_hint_file") && !lastarg) {
config.test_hint_file = argv[++i];
#ifdef USE_OPENSSL #ifdef USE_OPENSSL
} else if (!strcmp(argv[i],"--tls")) { } else if (!strcmp(argv[i],"--tls")) {
config.tls = 1; config.tls = 1;
...@@ -9119,6 +9645,90 @@ static sds askPassword(const char *msg) { ...@@ -9119,6 +9645,90 @@ static sds askPassword(const char *msg) {
return auth; return auth;
} }
/* Prints out the hint completion string for a given input prefix string. */
void testHint(const char *input) {
cliInitHelp();
sds hint = getHintForInput(input);
printf("%s\n", hint);
exit(0);
}
sds readHintSuiteLine(char buf[], size_t size, FILE *fp) {
while (fgets(buf, size, fp) != NULL) {
if (buf[0] != '#') {
sds input = sdsnew(buf);
/* Strip newline. */
input = sdstrim(input, "\n");
return input;
}
}
return NULL;
}
/* Runs a suite of hint completion tests contained in a file. */
void testHintSuite(char *filename) {
FILE *fp;
char buf[256];
sds line, input, expected, hint;
int pass=0, fail=0;
int argc;
char **argv;
fp = fopen(filename, "r");
if (!fp) {
fprintf(stderr,
"Can't open file '%s': %s\n", filename, strerror(errno));
exit(-1);
}
cliInitHelp();
while (1) {
line = readHintSuiteLine(buf, sizeof(buf), fp);
if (line == NULL) break;
argv = sdssplitargs(line, &argc);
sdsfree(line);
if (argc == 0) {
sdsfreesplitres(argv, argc);
continue;
}
if (argc == 1) {
fprintf(stderr,
"Missing expected hint for input '%s'\n", argv[0]);
exit(-1);
}
input = argv[0];
expected = argv[1];
hint = getHintForInput(input);
if (config.verbose) {
printf("Input: '%s', Expected: '%s', Hint: '%s'\n", input, expected, hint);
}
/* Strip trailing spaces from hint - they don't matter. */
while (hint != NULL && sdslen(hint) > 0 && hint[sdslen(hint) - 1] == ' ') {
sdssetlen(hint, sdslen(hint) - 1);
hint[sdslen(hint)] = '\0';
}
if (hint == NULL || strcmp(hint, expected) != 0) {
fprintf(stderr, "Test case '%s' FAILED: expected '%s', got '%s'\n", input, expected, hint);
++fail;
}
else {
++pass;
}
sdsfreesplitres(argv, argc);
sdsfree(hint);
}
fclose(fp);
printf("%s: %d/%d passed\n", fail == 0 ? "SUCCESS" : "FAILURE", pass, pass + fail);
exit(fail);
}
/*------------------------------------------------------------------------------ /*------------------------------------------------------------------------------
* Program main() * Program main()
*--------------------------------------------------------------------------- */ *--------------------------------------------------------------------------- */
...@@ -9176,6 +9786,7 @@ int main(int argc, char **argv) { ...@@ -9176,6 +9786,7 @@ int main(int argc, char **argv) {
config.set_errcode = 0; config.set_errcode = 0;
config.no_auth_warning = 0; config.no_auth_warning = 0;
config.in_multi = 0; config.in_multi = 0;
config.server_version = NULL;
config.cluster_manager_command.name = NULL; config.cluster_manager_command.name = NULL;
config.cluster_manager_command.argc = 0; config.cluster_manager_command.argc = 0;
config.cluster_manager_command.argv = NULL; config.cluster_manager_command.argv = NULL;
...@@ -9321,6 +9932,15 @@ int main(int argc, char **argv) { ...@@ -9321,6 +9932,15 @@ int main(int argc, char **argv) {
/* Intrinsic latency mode */ /* Intrinsic latency mode */
if (config.intrinsic_latency_mode) intrinsicLatencyMode(); if (config.intrinsic_latency_mode) intrinsicLatencyMode();
/* Print command-line hint for an input prefix string */
if (config.test_hint) {
testHint(config.test_hint);
}
/* Run test suite for command-line hints */
if (config.test_hint_file) {
testHintSuite(config.test_hint_file);
}
/* Start interactive mode when no command is provided */ /* Start interactive mode when no command is provided */
if (argc == 0 && !config.eval) { if (argc == 0 && !config.eval) {
/* Ignore SIGPIPE in interactive mode to force a reconnect */ /* Ignore SIGPIPE in interactive mode to force a reconnect */
......
...@@ -2943,21 +2943,6 @@ void setImplicitACLCategories(struct redisCommand *c) { ...@@ -2943,21 +2943,6 @@ void setImplicitACLCategories(struct redisCommand *c) {
c->acl_categories |= ACL_CATEGORY_SLOW; 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. /* Recursively populate the command structure.
* *
* On success, the function return C_OK. Otherwise C_ERR is returned and we won't * 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) { ...@@ -2975,28 +2960,10 @@ int populateCommandStructure(struct redisCommand *c) {
* set of flags. */ * set of flags. */
setImplicitACLCategories(c); 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 /* We start with an unallocated histogram and only allocate memory when a command
* has been issued for the first time */ * has been issued for the first time */
c->latency_histogram = NULL; 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). */ /* Handle the legacy range spec and the "movablekeys" flag (must be done after populating all key specs). */
populateCommandLegacyRangeSpec(c); populateCommandLegacyRangeSpec(c);
...@@ -4860,28 +4827,6 @@ void addReplyCommandSubCommands(client *c, struct redisCommand *cmd, void (*repl ...@@ -4860,28 +4827,6 @@ void addReplyCommandSubCommands(client *c, struct redisCommand *cmd, void (*repl
dictReleaseIterator(di); 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. */ /* Output the representation of a Redis command. Used by the COMMAND command and COMMAND INFO. */
void addReplyCommandInfo(client *c, struct redisCommand *cmd) { void addReplyCommandInfo(client *c, struct redisCommand *cmd) {
if (!cmd) { if (!cmd) {
...@@ -4940,7 +4885,7 @@ void addReplyCommandDocs(client *c, struct redisCommand *cmd) { ...@@ -4940,7 +4885,7 @@ void addReplyCommandDocs(client *c, struct redisCommand *cmd) {
/* Always have the group, for module commands the group is always "module". */ /* Always have the group, for module commands the group is always "module". */
addReplyBulkCString(c, "group"); addReplyBulkCString(c, "group");
addReplyBulkCString(c, COMMAND_GROUP_STR[cmd->group]); addReplyBulkCString(c, commandGroupStr(cmd->group));
if (cmd->complexity) { if (cmd->complexity) {
addReplyBulkCString(c, "complexity"); addReplyBulkCString(c, "complexity");
......
...@@ -35,6 +35,7 @@ ...@@ -35,6 +35,7 @@
#include "solarisfixes.h" #include "solarisfixes.h"
#include "rio.h" #include "rio.h"
#include "atomicvar.h" #include "atomicvar.h"
#include "commands.h"
#include <assert.h> #include <assert.h>
#include <stdio.h> #include <stdio.h>
...@@ -2135,43 +2136,6 @@ typedef struct { ...@@ -2135,43 +2136,6 @@ typedef struct {
} fk; } fk;
} keySpec; } 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 #ifdef LOG_REQ_RES
/* Must be synced with generate-command-code.py */ /* Must be synced with generate-command-code.py */
...@@ -2341,15 +2305,19 @@ struct redisCommand { ...@@ -2341,15 +2305,19 @@ struct redisCommand {
const char *deprecated_since; /* In case the command is deprecated, when did it happen? */ const char *deprecated_since; /* In case the command is deprecated, when did it happen? */
redisCommandGroup group; /* Command group */ redisCommandGroup group; /* Command group */
commandHistory *history; /* History of the command */ 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 */ 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 */ redisCommandProc *proc; /* Command implementation */
int arity; /* Number of arguments, it is possible to use -N to say >= N */ int arity; /* Number of arguments, it is possible to use -N to say >= N */
uint64_t flags; /* Command flags, see CMD_*. */ uint64_t flags; /* Command flags, see CMD_*. */
uint64_t acl_categories; /* ACl categories, see ACL_CATEGORY_*. */ 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. /* Use a function to determine keys arguments in a command line.
* Used for Redis Cluster redirect (may be NULL) */ * Used for Redis Cluster redirect (may be NULL) */
redisGetKeysProc *getkeys_proc; redisGetKeysProc *getkeys_proc;
int num_args; /* Length of args array. */
/* Array of subcommands (may be NULL) */ /* Array of subcommands (may be NULL) */
struct redisCommand *subcommands; struct redisCommand *subcommands;
/* Array of arguments (may be NULL) */ /* Array of arguments (may be NULL) */
...@@ -2368,16 +2336,10 @@ struct redisCommand { ...@@ -2368,16 +2336,10 @@ struct redisCommand {
bit set in the bitmap of allowed commands. */ bit set in the bitmap of allowed commands. */
sds fullname; /* A SDS string representing the command fullname. */ 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) */ 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 keySpec legacy_range_key_spec; /* The legacy (first,last,step) key spec is
* still maintained (if applicable) so that * still maintained (if applicable) so that
* we can still support the reply format of * we can still support the reply format of
* COMMAND INFO and COMMAND GETKEYS */ * 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 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. */ * (not the fullname), and the value is the redisCommand structure pointer. */
struct redisCommand *parent; struct redisCommand *parent;
...@@ -2484,7 +2446,6 @@ extern dict *modules; ...@@ -2484,7 +2446,6 @@ extern dict *modules;
/* Command metadata */ /* Command metadata */
void populateCommandLegacyRangeSpec(struct redisCommand *c); void populateCommandLegacyRangeSpec(struct redisCommand *c);
int populateArgsStructure(struct redisCommandArg *args);
/* Modules */ /* Modules */
void moduleInitModulesSystem(void); void moduleInitModulesSystem(void);
......
# 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 ...]]"
...@@ -423,6 +423,27 @@ if {!$::tls} { ;# fake_redis_node doesn't support TLS ...@@ -423,6 +423,27 @@ if {!$::tls} { ;# fake_redis_node doesn't support TLS
file delete $tmpfile 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} { proc test_redis_cli_rdb_dump {functions_only} {
r flushdb r flushdb
r function flush r function flush
......
...@@ -196,7 +196,7 @@ class Argument(object): ...@@ -196,7 +196,7 @@ class Argument(object):
def struct_code(self): def struct_code(self):
""" """
Output example: 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(): def _flags_code():
...@@ -210,7 +210,7 @@ class Argument(object): ...@@ -210,7 +210,7 @@ class Argument(object):
s += "CMD_ARG_MULTIPLE_TOKEN|" s += "CMD_ARG_MULTIPLE_TOKEN|"
return s[:-1] if s else "CMD_ARG_NONE" 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, self.name,
ARG_TYPES[self.type], ARG_TYPES[self.type],
self.desc.get("key_spec_index", -1), self.desc.get("key_spec_index", -1),
...@@ -218,9 +218,9 @@ class Argument(object): ...@@ -218,9 +218,9 @@ class Argument(object):
get_optional_desc_string(self.desc, "summary"), get_optional_desc_string(self.desc, "summary"),
get_optional_desc_string(self.desc, "since"), get_optional_desc_string(self.desc, "since"),
_flags_code(), _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: if "display" in self.desc:
s += ",.display_text=\"%s\"" % self.desc["display"].lower() s += ",.display_text=\"%s\"" % self.desc["display"].lower()
if self.subargs: if self.subargs:
...@@ -234,10 +234,9 @@ class Argument(object): ...@@ -234,10 +234,9 @@ class Argument(object):
subarg.write_internal_structs(f) subarg.write_internal_structs(f)
f.write("/* %s argument table */\n" % self.fullname()) 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: for subarg in self.subargs:
f.write("{%s},\n" % subarg.struct_code()) f.write("{%s},\n" % subarg.struct_code())
f.write("{0}\n")
f.write("};\n\n") f.write("};\n\n")
...@@ -339,11 +338,14 @@ class Command(object): ...@@ -339,11 +338,14 @@ class Command(object):
return "%s_History" % (self.fullname().replace(" ", "_")) return "%s_History" % (self.fullname().replace(" ", "_"))
def tips_table_name(self): def tips_table_name(self):
return "%s_tips" % (self.fullname().replace(" ", "_")) return "%s_Tips" % (self.fullname().replace(" ", "_"))
def arg_table_name(self): def arg_table_name(self):
return "%s_Args" % (self.fullname().replace(" ", "_")) return "%s_Args" % (self.fullname().replace(" ", "_"))
def key_specs_table_name(self):
return "%s_Keyspecs" % (self.fullname().replace(" ", "_"))
def reply_schema_name(self): def reply_schema_name(self):
return "%s_ReplySchema" % (self.fullname().replace(" ", "_")) return "%s_ReplySchema" % (self.fullname().replace(" ", "_"))
...@@ -356,22 +358,37 @@ class Command(object): ...@@ -356,22 +358,37 @@ class Command(object):
s = "" s = ""
for tupl in self.desc["history"]: for tupl in self.desc["history"]:
s += "{\"%s\",\"%s\"},\n" % (tupl[0], tupl[1]) s += "{\"%s\",\"%s\"},\n" % (tupl[0], tupl[1])
s += "{0}"
return s return s
def num_history(self):
if not self.desc.get("history"):
return 0
return len(self.desc["history"])
def tips_code(self): def tips_code(self):
if not self.desc.get("command_tips"): if not self.desc.get("command_tips"):
return "" return ""
s = "" s = ""
for hint in self.desc["command_tips"]: for hint in self.desc["command_tips"]:
s += "\"%s\",\n" % hint.lower() s += "\"%s\",\n" % hint.lower()
s += "NULL"
return s 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): def struct_code(self):
""" """
Output example: 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(): def _flags_code():
...@@ -392,13 +409,7 @@ class Command(object): ...@@ -392,13 +409,7 @@ class Command(object):
s += "CMD_DOC_%s|" % flag s += "CMD_DOC_%s|" % flag
return s[:-1] if s else "CMD_DOC_NONE" return s[:-1] if s else "CMD_DOC_NONE"
def _key_specs_code(): s = "MAKE_CMD(\"%s\",%s,%s,%s,%s,%s,%s,%s,%s,%s,%d,%s,%d,%s,%d,%s,%s,%s,%d,%s,%d)," % (
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," % (
self.name.lower(), self.name.lower(),
get_optional_desc_string(self.desc, "summary"), get_optional_desc_string(self.desc, "summary"),
get_optional_desc_string(self.desc, "complexity"), get_optional_desc_string(self.desc, "complexity"),
...@@ -406,22 +417,22 @@ class Command(object): ...@@ -406,22 +417,22 @@ class Command(object):
_doc_flags_code(), _doc_flags_code(),
get_optional_desc_string(self.desc, "replaced_by"), get_optional_desc_string(self.desc, "replaced_by"),
get_optional_desc_string(self.desc, "deprecated_since"), get_optional_desc_string(self.desc, "deprecated_since"),
"\"%s\"" % self.group,
GROUPS[self.group], GROUPS[self.group],
self.history_table_name(), self.history_table_name(),
self.num_history(),
self.tips_table_name(), self.tips_table_name(),
self.num_tips(),
self.desc.get("function", "NULL"), self.desc.get("function", "NULL"),
self.desc["arity"], self.desc["arity"],
_flags_code(), _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: if self.subcommands:
s += ".subcommands=%s," % self.subcommand_table_name() s += ".subcommands=%s," % self.subcommand_table_name()
...@@ -440,7 +451,7 @@ class Command(object): ...@@ -440,7 +451,7 @@ class Command(object):
subcommand.write_internal_structs(f) subcommand.write_internal_structs(f)
f.write("/* %s command table */\n" % self.fullname()) 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: for subcommand in subcommand_list:
f.write("{%s},\n" % subcommand.struct_code()) f.write("{%s},\n" % subcommand.struct_code())
f.write("{0}\n") f.write("{0}\n")
...@@ -448,33 +459,47 @@ class Command(object): ...@@ -448,33 +459,47 @@ class Command(object):
f.write("/********** %s ********************/\n\n" % self.fullname()) f.write("/********** %s ********************/\n\n" % self.fullname())
f.write("#ifndef SKIP_CMD_HISTORY_TABLE\n")
f.write("/* %s history */\n" % self.fullname()) f.write("/* %s history */\n" % self.fullname())
code = self.history_code() code = self.history_code()
if code: if code:
f.write("commandHistory %s[] = {\n" % self.history_table_name()) f.write("commandHistory %s[] = {\n" % self.history_table_name())
f.write("%s\n" % code) f.write("%s" % code)
f.write("};\n\n") f.write("};\n")
else: 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()) f.write("/* %s tips */\n" % self.fullname())
code = self.tips_code() code = self.tips_code()
if code: if code:
f.write("const char *%s[] = {\n" % self.tips_table_name()) f.write("const char *%s[] = {\n" % self.tips_table_name())
f.write("%s" % code)
f.write("};\n")
else:
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("%s\n" % code)
f.write("};\n\n") f.write("};\n")
else: else:
f.write("#define %s NULL\n\n" % self.tips_table_name()) f.write("#define %s NULL\n" % self.key_specs_table_name())
f.write("#endif\n\n")
if self.args: if self.args:
for arg in self.args: for arg in self.args:
arg.write_internal_structs(f) arg.write_internal_structs(f)
f.write("/* %s argument table */\n" % self.fullname()) 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: for arg in self.args:
f.write("{%s},\n" % arg.struct_code()) f.write("{%s},\n" % arg.struct_code())
f.write("{0}\n")
f.write("};\n\n") f.write("};\n\n")
if self.reply_schema and args.with_reply_schema: if self.reply_schema and args.with_reply_schema:
...@@ -543,15 +568,40 @@ if check_command_error_counter != 0: ...@@ -543,15 +568,40 @@ if check_command_error_counter != 0:
exit(1) exit(1)
commands_filename = "commands_with_reply_schema" if args.with_reply_schema else "commands" commands_filename = "commands_with_reply_schema" if args.with_reply_schema else "commands"
print("Generating %s.c..." % commands_filename) print("Generating %s.def..." % commands_filename)
with open("%s/%s.c" % (srcdir, commands_filename), "w") as f: 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("/* Automatically generated by %s, do not edit. */\n\n" % os.path.basename(__file__))
f.write("#include \"server.h\"\n")
f.write( f.write(
""" """
/* We have fabulous commands from /* We have fabulous commands from
* the fantastic * 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: ...@@ -560,7 +610,7 @@ with open("%s/%s.c" % (srcdir, commands_filename), "w") as f:
command.write_internal_structs(f) command.write_internal_structs(f)
f.write("/* Main command table */\n") f.write("/* Main command table */\n")
f.write("struct redisCommand redisCommandTable[] = {\n") f.write("struct COMMAND_STRUCT redisCommandTable[] = {\n")
curr_group = None curr_group = None
for command in command_list: for command in command_list:
if curr_group != command.group: if curr_group != command.group:
......
#!/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
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment