]> git.saurik.com Git - redis.git/commitdiff
Merge branch 'master' into brpoplpush
authorPieter Noordhuis <pcnoordhuis@gmail.com>
Mon, 6 Dec 2010 11:31:56 +0000 (12:31 +0100)
committerPieter Noordhuis <pcnoordhuis@gmail.com>
Mon, 6 Dec 2010 11:31:56 +0000 (12:31 +0100)
Makefile
deps/linenoise/example.c
deps/linenoise/linenoise.c
deps/linenoise/linenoise.h
src/help.h [new file with mode: 0644]
src/redis-cli.c
utils/generate-command-help.rb [new file with mode: 0755]

index 185aab8d2038f404c6f5f8143859ca2e03bed2a2..b72faa1b18c88b8a491deca9d9ebe5e3c1a95f64 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -16,4 +16,7 @@ clean:
 $(TARGETS):
        cd src && $(MAKE) $@
 
+src/help.h:
+       @./utils/generate-command-help.rb > $@
+
 dummy:
index b3f9e9e3517fce46a3a3719b7b3be85e69e4034a..ea0b515c1fce3a1f2100a4f3315d1613444dc56f 100644 (file)
@@ -2,9 +2,18 @@
 #include <stdlib.h>
 #include "linenoise.h"
 
+
+void completion(const char *buf, linenoiseCompletions *lc) {
+    if (buf[0] == 'h') {
+        linenoiseAddCompletion(lc,"hello");
+        linenoiseAddCompletion(lc,"hello there");
+    }
+}
+
 int main(void) {
     char *line;
 
+    linenoiseSetCompletionCallback(completion);
     linenoiseHistoryLoad("history.txt"); /* Load the history at startup */
     while((line = linenoise("hello> ")) != NULL) {
         if (line[0] != '\0') {
index 045862e7881b7f4c0026f94c86a88a1368428490..dd43413661ca9677c51667ca657daeca50706307 100644 (file)
@@ -9,6 +9,8 @@
  * the 2010 UNIX computers around.
  *
  * Copyright (c) 2010, Salvatore Sanfilippo <antirez at gmail dot com>
+ * Copyright (c) 2010, Pieter Noordhuis <pcnoordhuis at gmail dot com>
+ *
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * CUF (CUrsor Forward)
  *    Sequence: ESC [ n C
  *    Effect: moves cursor forward of n chars
+ *
+ * The following are used to clear the screen: ESC [ H ESC [ 2 J
+ * This is actually composed of two sequences:
+ *
+ * cursorhome
+ *    Sequence: ESC [ H
+ *    Effect: moves the cursor to upper left corner
+ *
+ * ED2 (Clear entire screen)
+ *    Sequence: ESC [ 2 J
+ *    Effect: clear the whole screen
  * 
  */
 
 #include <sys/types.h>
 #include <sys/ioctl.h>
 #include <unistd.h>
+#include "linenoise.h"
 
 #define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100
 #define LINENOISE_MAX_LINE 4096
 static char *unsupported_term[] = {"dumb","cons25",NULL};
+static linenoiseCompletionCallback *completionCallback = NULL;
 
 static struct termios orig_termios; /* in order to restore at exit */
 static int rawmode = 0; /* for atexit() function to check if restore is needed*/
@@ -195,6 +210,78 @@ static void refreshLine(int fd, const char *prompt, char *buf, size_t len, size_
     if (write(fd,seq,strlen(seq)) == -1) return;
 }
 
+static void beep() {
+    fprintf(stderr, "\x7");
+    fflush(stderr);
+}
+
+static void freeCompletions(linenoiseCompletions *lc) {
+    size_t i;
+    for (i = 0; i < lc->len; i++)
+        free(lc->cvec[i]);
+    if (lc->cvec != NULL)
+        free(lc->cvec);
+}
+
+static int completeLine(int fd, const char *prompt, char *buf, size_t buflen, size_t *len, size_t *pos, size_t cols) {
+    linenoiseCompletions lc = { 0, NULL };
+    int nread, nwritten;
+    char c = 0;
+
+    completionCallback(buf,&lc);
+    if (lc.len == 0) {
+        beep();
+    } else {
+        size_t stop = 0, i = 0;
+        size_t clen;
+
+        while(!stop) {
+            /* Show completion or original buffer */
+            if (i < lc.len) {
+                clen = strlen(lc.cvec[i]);
+                refreshLine(fd,prompt,lc.cvec[i],clen,clen,cols);
+            } else {
+                refreshLine(fd,prompt,buf,*len,*pos,cols);
+            }
+
+            nread = read(fd,&c,1);
+            if (nread <= 0) {
+                freeCompletions(&lc);
+                return -1;
+            }
+
+            switch(c) {
+                case 9: /* tab */
+                    i = (i+1) % (lc.len+1);
+                    if (i == lc.len) beep();
+                    break;
+                case 27: /* escape */
+                    /* Re-show original buffer */
+                    if (i < lc.len) {
+                        refreshLine(fd,prompt,buf,*len,*pos,cols);
+                    }
+                    stop = 1;
+                    break;
+                default:
+                    /* Update buffer and return */
+                    if (i < lc.len) {
+                        nwritten = snprintf(buf,buflen,"%s",lc.cvec[i]);
+                        *len = *pos = nwritten;
+                    }
+                    stop = 1;
+                    break;
+            }
+        }
+    }
+
+    freeCompletions(&lc);
+    return c; /* Return last read character */
+}
+
+void linenoiseClearScreen(void) {
+    write(STDIN_FILENO,"\x1b[H\x1b[2J",7);
+}
+
 static int linenoisePrompt(int fd, char *buf, size_t buflen, const char *prompt) {
     size_t plen = strlen(prompt);
     size_t pos = 0;
@@ -217,6 +304,18 @@ static int linenoisePrompt(int fd, char *buf, size_t buflen, const char *prompt)
 
         nread = read(fd,&c,1);
         if (nread <= 0) return len;
+
+        /* Only autocomplete when the callback is set. It returns < 0 when
+         * there was an error reading from fd. Otherwise it will return the
+         * character that should be handled next. */
+        if (c == 9 && completionCallback != NULL) {
+            c = completeLine(fd,prompt,buf,buflen,&len,&pos,cols);
+            /* Return on errors */
+            if (c < 0) return len;
+            /* Read next character when 0 */
+            if (c == 0) continue;
+        }
+
         switch(c) {
         case 13:    /* enter */
         case 4:     /* ctrl-d */
@@ -350,6 +449,9 @@ up_down_arrow:
             pos = len;
             refreshLine(fd,prompt,buf,len,pos,cols);
             break;
+        case 12: /* ctrl+l, clear screen */
+            linenoiseClearScreen();
+            refreshLine(fd,prompt,buf,len,pos,cols);
         }
     }
     return len;
@@ -402,6 +504,19 @@ char *linenoise(const char *prompt) {
     }
 }
 
+/* Register a callback function to be called for tab-completion. */
+void linenoiseSetCompletionCallback(linenoiseCompletionCallback *fn) {
+    completionCallback = fn;
+}
+
+void linenoiseAddCompletion(linenoiseCompletions *lc, char *str) {
+    size_t len = strlen(str);
+    char *copy = malloc(len+1);
+    memcpy(copy,str,len+1);
+    lc->cvec = realloc(lc->cvec,sizeof(char*)*(lc->len+1));
+    lc->cvec[lc->len++] = copy;
+}
+
 /* Using a circular buffer is smarter, but a bit more complex to handle. */
 int linenoiseHistoryAdd(const char *line) {
     char *linecopy;
index 0d76aea9cb0e89133a4b0a766a4b71e6b7cc9bbb..76a703c28480d91dade14fe0b7f6456813dc8893 100644 (file)
@@ -4,6 +4,8 @@
  * See linenoise.c for more information.
  *
  * Copyright (c) 2010, Salvatore Sanfilippo <antirez at gmail dot com>
+ * Copyright (c) 2010, Pieter Noordhuis <pcnoordhuis at gmail dot com>
+ *
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
 #ifndef __LINENOISE_H
 #define __LINENOISE_H
 
+typedef struct linenoiseCompletions {
+  size_t len;
+  char **cvec;
+} linenoiseCompletions;
+
+typedef void(linenoiseCompletionCallback)(const char *, linenoiseCompletions *);
+void linenoiseSetCompletionCallback(linenoiseCompletionCallback *);
+void linenoiseAddCompletion(linenoiseCompletions *, char *);
+
 char *linenoise(const char *prompt);
 int linenoiseHistoryAdd(const char *line);
 int linenoiseHistorySetMaxLen(int len);
 int linenoiseHistorySave(char *filename);
 int linenoiseHistoryLoad(char *filename);
+void linenoiseClearScreen(void);
 
 #endif /* __LINENOISE_H */
diff --git a/src/help.h b/src/help.h
new file mode 100644 (file)
index 0000000..121d9df
--- /dev/null
@@ -0,0 +1,608 @@
+/* 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"
+};
+
+struct commandHelp {
+  char *name;
+  char *params;
+  char *summary;
+  int group;
+  char *since;
+} commandHelp[] = {
+    { "APPEND",
+    "key value",
+    "Append a value to a key",
+    1,
+    "1.3.3" },
+    { "AUTH",
+    "password",
+    "Authenticate to the server",
+    8,
+    "0.08" },
+    { "BGREWRITEAOF",
+    "-",
+    "Asynchronously rewrite the append-only file",
+    9,
+    "1.07" },
+    { "BGSAVE",
+    "-",
+    "Asynchronously save the dataset to disk",
+    9,
+    "0.07" },
+    { "BLPOP",
+    "key [key ...] timeout",
+    "Remove and get the first element in a list, or block until one is available",
+    2,
+    "1.3.1" },
+    { "BRPOP",
+    "key [key ...] timeout",
+    "Remove and get the last element in a list, or block until one is available",
+    2,
+    "1.3.1" },
+    { "CONFIG GET",
+    "parameter",
+    "Get the value of a configuration parameter",
+    9,
+    "2.0" },
+    { "CONFIG SET",
+    "parameter value",
+    "Set a configuration parameter to the given value",
+    9,
+    "2.0" },
+    { "DBSIZE",
+    "-",
+    "Return the number of keys in the selected database",
+    9,
+    "0.07" },
+    { "DEBUG OBJECT",
+    "key",
+    "Get debugging information about a key",
+    9,
+    "0.101" },
+    { "DEBUG SEGFAULT",
+    "-",
+    "Make the server crash",
+    9,
+    "0.101" },
+    { "DECR",
+    "key decrement",
+    "Decrement the integer value of a key by one",
+    1,
+    "0.07" },
+    { "DECRBY",
+    "key decrement",
+    "Decrement the integer value of a key by the given number",
+    1,
+    "0.07" },
+    { "DEL",
+    "key [key ...]",
+    "Delete a key",
+    0,
+    "0.07" },
+    { "DISCARD",
+    "-",
+    "Discard all commands issued after MULTI",
+    7,
+    "1.3.3" },
+    { "ECHO",
+    "message",
+    "Echo the given string",
+    8,
+    "0.07" },
+    { "EXEC",
+    "-",
+    "Execute all commands issued after MULTI",
+    7,
+    "1.1.95" },
+    { "EXISTS",
+    "key",
+    "Determine if a key exists",
+    9,
+    "0.07" },
+    { "EXPIRE",
+    "key seconds",
+    "Set a key's time to live in seconds",
+    0,
+    "0.09" },
+    { "EXPIREAT",
+    "key timestamp",
+    "Set the expiration for a key as a UNIX timestamp",
+    0,
+    "1.1" },
+    { "FLUSHALL",
+    "-",
+    "Remove all keys from all databases",
+    9,
+    "0.07" },
+    { "FLUSHDB",
+    "-",
+    "Remove all keys from the current database",
+    9,
+    "0.07" },
+    { "GET",
+    "key",
+    "Get the value of a key",
+    1,
+    "0.07" },
+    { "GETSET",
+    "key value",
+    "Set the string value of a key and return its old value",
+    1,
+    "0.091" },
+    { "HDEL",
+    "key field",
+    "Delete a hash field",
+    5,
+    "1.3.10" },
+    { "HEXISTS",
+    "key field",
+    "Determine if a hash field exists",
+    5,
+    "1.3.10" },
+    { "HGET",
+    "key field",
+    "Get the value of a hash field",
+    5,
+    "1.3.10" },
+    { "HGETALL",
+    "key",
+    "Get all the fields and values in a hash",
+    5,
+    "1.3.10" },
+    { "HINCRBY",
+    "key field increment",
+    "Increment the integer value of a hash field by the given number",
+    5,
+    "1.3.10" },
+    { "HKEYS",
+    "key",
+    "Get all the fields in a hash",
+    5,
+    "1.3.10" },
+    { "HLEN",
+    "key",
+    "Get the number of fields in a hash",
+    5,
+    "1.3.10" },
+    { "HMGET",
+    "key field [field ...]",
+    "Get the values of all the given hash fields",
+    5,
+    "1.3.10" },
+    { "HMSET",
+    "key field value [field value ...]",
+    "Set multiple hash fields to multiple values",
+    5,
+    "1.3.8" },
+    { "HSET",
+    "key field value",
+    "Set the string value of a hash field",
+    5,
+    "1.3.10" },
+    { "HSETNX",
+    "key field value",
+    "Set the value of a hash field, only if the field does not exist",
+    5,
+    "1.3.8" },
+    { "HVALS",
+    "key",
+    "Get all the values in a hash",
+    5,
+    "1.3.10" },
+    { "INCR",
+    "key",
+    "Increment the integer value of a key by one",
+    1,
+    "0.07" },
+    { "INCRBY",
+    "key increment",
+    "Increment the integer value of a key by the given number",
+    1,
+    "0.07" },
+    { "INFO",
+    "-",
+    "Get information and statistics about the server",
+    9,
+    "0.07" },
+    { "KEYS",
+    "pattern",
+    "Find all keys matching the given pattern",
+    0,
+    "0.07" },
+    { "LASTSAVE",
+    "-",
+    "Get the UNIX time stamp of the last successful save to disk",
+    9,
+    "0.07" },
+    { "LINDEX",
+    "key index",
+    "Get an element from a list by its index",
+    2,
+    "0.07" },
+    { "LINSERT",
+    "key BEFORE|AFTER pivot value",
+    "Insert an element before or after another element in a list",
+    2,
+    "2.1.1" },
+    { "LLEN",
+    "key",
+    "Get the length of a list",
+    2,
+    "0.07" },
+    { "LPOP",
+    "key",
+    "Remove and get the first element in a list",
+    2,
+    "0.07" },
+    { "LPUSH",
+    "key value",
+    "Prepend a value to a list",
+    2,
+    "0.07" },
+    { "LPUSHX",
+    "key value",
+    "Prepend a value to a list, only if the list exists",
+    2,
+    "2.1.1" },
+    { "LRANGE",
+    "key start stop",
+    "Get a range of elements from a list",
+    2,
+    "0.07" },
+    { "LREM",
+    "key count value",
+    "Remove elements from a list",
+    2,
+    "0.07" },
+    { "LSET",
+    "key index value",
+    "Set the value of an element in a list by its index",
+    2,
+    "0.07" },
+    { "LTRIM",
+    "key start stop",
+    "Trim a list to the specified range",
+    2,
+    "0.07" },
+    { "MGET",
+    "key [key ...]",
+    "Get the values of all the given keys",
+    1,
+    "0.07" },
+    { "MONITOR",
+    "-",
+    "Listen for all requests received by the server in real time",
+    9,
+    "0.07" },
+    { "MOVE",
+    "key db",
+    "Move a key to another database",
+    0,
+    "0.07" },
+    { "MSET",
+    "key value [key value ...]",
+    "Set multiple keys to multiple values",
+    1,
+    "1.001" },
+    { "MSETNX",
+    "key value [key value ...]",
+    "Set multiple keys to multiple values, only if none of the keys exist",
+    1,
+    "1.001" },
+    { "MULTI",
+    "-",
+    "Mark the start of a transaction block",
+    7,
+    "1.1.95" },
+    { "PERSIST",
+    "key",
+    "Remove the expiration from a key",
+    0,
+    "2.1.2" },
+    { "PING",
+    "-",
+    "Ping the server",
+    8,
+    "0.07" },
+    { "PSUBSCRIBE",
+    "pattern",
+    "Listen for messages published to channels matching the given patterns",
+    6,
+    "1.3.8" },
+    { "PUBLISH",
+    "channel message",
+    "Post a message to a channel",
+    6,
+    "1.3.8" },
+    { "PUNSUBSCRIBE",
+    "[pattern [pattern ...]]",
+    "Stop listening for messages posted to channels matching the given patterns",
+    6,
+    "1.3.8" },
+    { "QUIT",
+    "-",
+    "Close the connection",
+    8,
+    "0.07" },
+    { "RANDOMKEY",
+    "-",
+    "Return a random key from the keyspace",
+    0,
+    "0.07" },
+    { "RENAME",
+    "old new",
+    "Rename a key",
+    0,
+    "0.07" },
+    { "RENAMENX",
+    "old new",
+    "Rename a key, only if the new key does not exist",
+    0,
+    "0.07" },
+    { "RPOP",
+    "key",
+    "Remove and get the last element in a list",
+    2,
+    "0.07" },
+    { "RPOPLPUSH",
+    "source destination",
+    "Remove the last element in a list, append it to another list and return it",
+    2,
+    "1.1" },
+    { "RPUSH",
+    "key value",
+    "Append a value to a list",
+    2,
+    "0.07" },
+    { "RPUSHX",
+    "key value",
+    "Append a value to a list, only if the list exists",
+    2,
+    "2.1.1" },
+    { "SADD",
+    "key member",
+    "Add a member to a set",
+    3,
+    "0.07" },
+    { "SAVE",
+    "-",
+    "Synchronously save the dataset to disk",
+    9,
+    "0.07" },
+    { "SCARD",
+    "key",
+    "Get the number of members in a set",
+    3,
+    "0.07" },
+    { "SDIFF",
+    "key [key ...]",
+    "Subtract multiple sets",
+    3,
+    "0.100" },
+    { "SDIFFSTORE",
+    "destination key [key ...]",
+    "Subtract multiple sets and store the resulting set in a key",
+    3,
+    "0.100" },
+    { "SELECT",
+    "index",
+    "Change the selected database for the current connection",
+    8,
+    "0.07" },
+    { "SET",
+    "key value",
+    "Set the string value of a key",
+    1,
+    "0.07" },
+    { "SETEX",
+    "key timestamp value",
+    "Set the value and expiration of a key",
+    1,
+    "1.3.10" },
+    { "SETNX",
+    "key value",
+    "Set the value of a key, only if the key does not exist",
+    1,
+    "0.07" },
+    { "SHUTDOWN",
+    "-",
+    "Synchronously save the dataset to disk and then shut down the server",
+    9,
+    "0.07" },
+    { "SINTER",
+    "key [key ...]",
+    "Intersect multiple sets",
+    3,
+    "0.07" },
+    { "SINTERSTORE",
+    "destination key [key ...]",
+    "Intersect multiple sets and store the resulting set in a key",
+    3,
+    "0.07" },
+    { "SISMEMBER",
+    "key member",
+    "Determine if a given value is a member of a set",
+    3,
+    "0.07" },
+    { "SLAVEOF",
+    "host port",
+    "Make the server a slave of another instance, or promote it as master",
+    9,
+    "0.100" },
+    { "SMEMBERS",
+    "key",
+    "Get all the members in a set",
+    3,
+    "0.07" },
+    { "SMOVE",
+    "source destination member",
+    "Move a member from one set to another",
+    3,
+    "0.091" },
+    { "SORT",
+    "key [BY pattern] [LIMIT start count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]",
+    "Sort the elements in a list, set or sorted set",
+    0,
+    "0.07" },
+    { "SPOP",
+    "key",
+    "Remove and return a random member from a set",
+    3,
+    "0.101" },
+    { "SRANDMEMBER",
+    "key",
+    "Get a random member from a set",
+    3,
+    "1.001" },
+    { "SREM",
+    "key member",
+    "Remove a member from a set",
+    3,
+    "0.07" },
+    { "STRLEN",
+    "key",
+    "Get the length of the value stored in a key",
+    1,
+    "2.1.2" },
+    { "SUBSCRIBE",
+    "channel",
+    "Listen for messages published to the given channels",
+    6,
+    "1.3.8" },
+    { "SUBSTR",
+    "key start stop",
+    "Get a substring of the string stored at a key",
+    1,
+    "1.3.4" },
+    { "SUNION",
+    "key [key ...]",
+    "Add multiple sets",
+    3,
+    "0.091" },
+    { "SUNIONSTORE",
+    "destination key [key ...]",
+    "Add multiple sets and store the resulting set in a key",
+    3,
+    "0.091" },
+    { "SYNC",
+    "-",
+    "Internal command used for replication",
+    9,
+    "0.07" },
+    { "TTL",
+    "key",
+    "Get the time to live for a key",
+    0,
+    "0.100" },
+    { "TYPE",
+    "key",
+    "Determine the type stored at key",
+    0,
+    "0.07" },
+    { "UNSUBSCRIBE",
+    "[channel [channel ...]]",
+    "Stop listening for messages posted to the given channels",
+    6,
+    "1.3.8" },
+    { "UNWATCH",
+    "-",
+    "Forget about all watched keys",
+    7,
+    "2.1.0" },
+    { "WATCH",
+    "key [key ...]",
+    "Watch the given keys to determine execution of the MULTI/EXEC block",
+    7,
+    "2.1.0" },
+    { "ZADD",
+    "key score member",
+    "Add a member to a sorted set, or update its score if it already exists",
+    4,
+    "1.1" },
+    { "ZCARD",
+    "key",
+    "Get the number of members in a sorted set",
+    4,
+    "1.1" },
+    { "ZCOUNT",
+    "key min max",
+    "Count the members in a sorted set with scores within the given values",
+    4,
+    "1.3.3" },
+    { "ZINCRBY",
+    "key increment member",
+    "Increment the score of a member in a sorted set",
+    4,
+    "1.1" },
+    { "ZINTERSTORE",
+    "destination key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]",
+    "Intersect multiple sorted sets and store the resulting sorted set in a new key",
+    4,
+    "1.3.10" },
+    { "ZRANGE",
+    "key start stop",
+    "Return a range of members in a sorted set, by index",
+    4,
+    "1.1" },
+    { "ZRANGEBYSCORE",
+    "key min max",
+    "Return a range of members in a sorted set, by score",
+    4,
+    "1.050" },
+    { "ZRANK",
+    "key member",
+    "Determine the index of a member in a sorted set",
+    4,
+    "1.3.4" },
+    { "ZREM",
+    "key member",
+    "Remove a member from a sorted set",
+    4,
+    "1.1" },
+    { "ZREMRANGEBYRANK",
+    "key start stop",
+    "Remove all members in a sorted set within the given indexes",
+    4,
+    "1.3.4" },
+    { "ZREMRANGEBYSCORE",
+    "key min max",
+    "Remove all members in a sorted set within the given scores",
+    4,
+    "1.1" },
+    { "ZREVRANGE",
+    "key start stop",
+    "Return a range of members in a sorted set, by index, with scores ordered from high to low",
+    4,
+    "1.1" },
+    { "ZREVRANK",
+    "key member",
+    "Determine the index of a member in a sorted set, with scores ordered from high to low",
+    4,
+    "1.3.4" },
+    { "ZSCORE",
+    "key member",
+    "Get the score associated with the given member in a sorted set",
+    4,
+    "1.1" },
+    { "ZUNIONSTORE",
+    "destination key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]",
+    "Add multiple sorted sets and store the resulting sorted set in a new key",
+    4,
+    "1.3.10" }
+};
+
+#endif
index 09ab9189fa48974ea8c6b62cbfeb4bd17367c9a1..2aedcc4a225b6ef070ce734f1f869ebca43cf609 100644 (file)
 #include <errno.h>
 #include <sys/stat.h>
 #include <sys/time.h>
+#include <assert.h>
 
 #include "hiredis.h"
 #include "sds.h"
 #include "zmalloc.h"
 #include "linenoise.h"
+#include "help.h"
 
 #define REDIS_NOTUSED(V) ((void) V)
 
@@ -83,6 +85,149 @@ static long long mstime(void) {
     return mst;
 }
 
+/*------------------------------------------------------------------------------
+ * Help functions
+ *--------------------------------------------------------------------------- */
+
+#define CLI_HELP_COMMAND 1
+#define CLI_HELP_GROUP 2
+
+typedef struct {
+    int type;
+    int argc;
+    sds *argv;
+    sds full;
+
+    /* Only used for help on commands */
+    struct commandHelp *org;
+} helpEntry;
+
+static helpEntry *helpEntries;
+static int helpEntriesLen;
+
+static void cliInitHelp() {
+    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 = malloc(sizeof(helpEntry)*len);
+
+    for (i = 0; i < groupslen; i++) {
+        tmp.argc = 1;
+        tmp.argv = malloc(sizeof(sds));
+        tmp.argv[0] = sdscatprintf(sdsempty(),"@%s",commandGroups[i]);
+        tmp.full = tmp.argv[0];
+        tmp.type = CLI_HELP_GROUP;
+        tmp.org = 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 = &commandHelp[i];
+        helpEntries[pos++] = tmp;
+    }
+}
+
+/* Output command help to stdout. */
+static void cliOutputCommandHelp(struct commandHelp *help, int group) {
+    printf("\r\n  \x1b[1m%s\x1b[0m \x1b[90m%s\x1b[0m\r\n", help->name, help->params);
+    printf("  \x1b[33msummary:\x1b[0m %s\r\n", help->summary);
+    printf("  \x1b[33msince:\x1b[0m %s\r\n", help->since);
+    if (group) {
+        printf("  \x1b[33mgroup:\x1b[0m %s\r\n", commandGroups[help->group]);
+    }
+}
+
+/* Print generic help. */
+static void cliOutputGenericHelp() {
+    printf(
+        "redis-cli %s\r\n"
+        "Type: \"help @<group>\" to get a list of commands in <group>\r\n"
+        "      \"help <command>\" for help on <command>\r\n"
+        "      \"help <tab>\" to get a list of possible help topics\r\n"
+        "      \"quit\" to exit\r\n",
+        REDIS_VERSION
+    );
+}
+
+/* Output all command help, filtering by group or command name. */
+static void cliOutputHelp(int argc, char **argv) {
+    int i, j, len;
+    int group = -1;
+    helpEntry *entry;
+    struct commandHelp *help;
+
+    if (argc == 0) {
+        cliOutputGenericHelp();
+        return;
+    } else if (argc > 0 && argv[0][0] == '@') {
+        len = sizeof(commandGroups)/sizeof(char*);
+        for (i = 0; i < len; i++) {
+            if (strcasecmp(argv[0]+1,commandGroups[i]) == 0) {
+                group = i;
+                break;
+            }
+        }
+    }
+
+    assert(argc > 0);
+    for (i = 0; i < helpEntriesLen; i++) {
+        entry = &helpEntries[i];
+        if (entry->type != CLI_HELP_COMMAND) continue;
+
+        help = entry->org;
+        if (group == -1) {
+            /* Compare all arguments */
+            if (argc == entry->argc) {
+                for (j = 0; j < argc; j++) {
+                    if (strcasecmp(argv[j],entry->argv[j]) != 0) break;
+                }
+                if (j == argc) {
+                    cliOutputCommandHelp(help,1);
+                }
+            }
+        } else {
+            if (group == help->group) {
+                cliOutputCommandHelp(help,0);
+            }
+        }
+    }
+    printf("\r\n");
+}
+
+static void completionCallback(const char *buf, linenoiseCompletions *lc) {
+    size_t startpos = 0;
+    int mask;
+    int i;
+    size_t matchlen;
+    sds tmp;
+
+    if (strncasecmp(buf,"help ",5) == 0) {
+        startpos = 5;
+        while (isspace(buf[startpos])) startpos++;
+        mask = CLI_HELP_COMMAND | CLI_HELP_GROUP;
+    } else {
+        mask = CLI_HELP_COMMAND;
+    }
+
+    for (i = 0; i < helpEntriesLen; i++) {
+        if (!(helpEntries[i].type & mask)) continue;
+
+        matchlen = strlen(buf+startpos);
+        if (strncasecmp(buf+startpos,helpEntries[i].full,matchlen) == 0) {
+            tmp = sdsnewlen(buf,startpos);
+            tmp = sdscat(tmp,helpEntries[i].full);
+            linenoiseAddCompletion(lc,tmp);
+            sdsfree(tmp);
+        }
+    }
+}
+
 /*------------------------------------------------------------------------------
  * Networking / parsing
  *--------------------------------------------------------------------------- */
@@ -252,22 +397,6 @@ static int cliReadReply() {
     return REDIS_OK;
 }
 
-static void showInteractiveHelp(void) {
-    printf(
-    "\n"
-    "Welcome to redis-cli " REDIS_VERSION "!\n"
-    "Just type any valid Redis command to see a pretty printed output.\n"
-    "\n"
-    "It is possible to quote strings, like in:\n"
-    "  set \"my key\" \"some string \\xff\\n\"\n"
-    "\n"
-    "You can find a list of valid Redis commands at\n"
-    "  http://code.google.com/p/redis/wiki/CommandReference\n"
-    "\n"
-    "Note: redis-cli supports line editing, use up/down arrows for history."
-    "\n\n");
-}
-
 static int cliSendCommand(int argc, char **argv, int repeat) {
     char *command = argv[0];
     size_t *argvlen;
@@ -279,8 +408,8 @@ static int cliSendCommand(int argc, char **argv, int repeat) {
     }
 
     config.raw_output = !strcasecmp(command,"info");
-    if (!strcasecmp(command,"help")) {
-        showInteractiveHelp();
+    if (!strcasecmp(command,"help") || !strcasecmp(command,"?")) {
+        cliOutputHelp(--argc, ++argv);
         return REDIS_OK;
     }
     if (!strcasecmp(command,"shutdown")) config.shutdown = 1;
@@ -412,6 +541,8 @@ static void repl() {
     sds *argv;
 
     config.interactive = 1;
+    linenoiseSetCompletionCallback(completionCallback);
+
     while((line = linenoise(context ? "redis> " : "not connected> ")) != NULL) {
         if (line[0] != '\0') {
             argv = sdssplitargs(line,&argc);
@@ -430,6 +561,8 @@ static void repl() {
                     config.hostip = sdsnew(argv[1]);
                     config.hostport = atoi(argv[2]);
                     cliConnect(1);
+                } else if (argc == 1 && !strcasecmp(argv[0],"clear")) {
+                    linenoiseClearScreen();
                 } else {
                     long long start_time = mstime(), elapsed;
 
@@ -489,6 +622,7 @@ int main(int argc, char **argv) {
     config.historyfile = NULL;
     config.tty = isatty(fileno(stdout)) || (getenv("FAKETTY") != NULL);
     config.mb_sep = '\n';
+    cliInitHelp();
 
     if (getenv("HOME") != NULL) {
         config.historyfile = malloc(256);
diff --git a/utils/generate-command-help.rb b/utils/generate-command-help.rb
new file mode 100755 (executable)
index 0000000..f730eaf
--- /dev/null
@@ -0,0 +1,111 @@
+#!/usr/bin/env ruby
+
+GROUPS = [
+  "generic",
+  "string",
+  "list",
+  "set",
+  "sorted_set",
+  "hash",
+  "pubsub",
+  "transactions",
+  "connection",
+  "server"
+].freeze
+
+GROUPS_BY_NAME = Hash[*
+  GROUPS.each_with_index.map do |n,i|
+    [n,i]
+  end.flatten
+].freeze
+
+def argument arg
+  name = arg["name"].is_a?(Array) ? arg["name"].join(" ") : arg["name"]
+  name = arg["enum"].join "|" if "enum" == arg["type"]
+  name = arg["command"] + " " + name if arg["command"]
+  if arg["multiple"]
+    name = "#{name} [#{name} ...]"
+  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 "net/http"
+  require "net/https"
+  require "json"
+  require "uri"
+
+  url = URI.parse "https://github.com/antirez/redis-doc/raw/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)
+    @commands = JSON.parse(response.body)
+  else
+    response.error!
+  end
+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
+