Skip to content
This repository
Browse code

Use linenoise for line editing on redis-cli.

  • Loading branch information...
commit cf87ebf22d0a75aada2486dea17117c1e8b7072b 1 parent aab055a
Michel Martens soveran authored

Showing 4 changed files with 461 additions and 29 deletions. Show diff stats Hide diff stats

  1. +5 2 Makefile
  2. +396 0 linenoise.c
  3. +41 0 linenoise.h
  4. +19 27 redis-cli.c
7 Makefile
@@ -16,7 +16,7 @@ DEBUG?= -g -rdynamic -ggdb
16 16
17 17 OBJ = adlist.o ae.o anet.o dict.o redis.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o
18 18 BENCHOBJ = ae.o anet.o redis-benchmark.o sds.o adlist.o zmalloc.o
19   -CLIOBJ = anet.o sds.o adlist.o redis-cli.o zmalloc.o
  19 +CLIOBJ = anet.o sds.o adlist.o redis-cli.o zmalloc.o linenoise.o
20 20 CHECKDUMPOBJ = redis-check-dump.o lzf_c.o lzf_d.o
21 21
22 22 PRGNAME = redis-server
@@ -34,12 +34,15 @@ ae_kqueue.o: ae_kqueue.c
34 34 ae_select.o: ae_select.c
35 35 anet.o: anet.c fmacros.h anet.h
36 36 dict.o: dict.c fmacros.h dict.h zmalloc.h
  37 +linenoise.o: linenoise.c
37 38 lzf_c.o: lzf_c.c lzfP.h
38 39 lzf_d.o: lzf_d.c lzfP.h
39 40 pqsort.o: pqsort.c
40 41 redis-benchmark.o: redis-benchmark.c fmacros.h ae.h anet.h sds.h adlist.h \
41 42 zmalloc.h
42   -redis-cli.o: redis-cli.c fmacros.h anet.h sds.h adlist.h zmalloc.h
  43 +redis-check-dump.o: redis-check-dump.c lzf.h
  44 +redis-cli.o: redis-cli.c fmacros.h anet.h sds.h adlist.h zmalloc.h \
  45 + linenoise.h
43 46 redis.o: redis.c fmacros.h config.h redis.h ae.h sds.h anet.h dict.h \
44 47 adlist.h zmalloc.h lzf.h pqsort.h zipmap.h staticsymbols.h
45 48 sds.o: sds.c sds.h zmalloc.h
396 linenoise.c
... ... @@ -0,0 +1,396 @@
  1 +/* linenoise.c -- guerrilla line editing library against the idea that a
  2 + * line editing lib needs to be 20,000 lines of C code.
  3 + *
  4 + * You can find the latest source code at:
  5 + *
  6 + * http://github.com/antirez/linenoise
  7 + *
  8 + * Does a number of crazy assumptions that happen to be true in 99.9999% of
  9 + * the 2010 UNIX computers around.
  10 + *
  11 + * Copyright (c) 2010, Salvatore Sanfilippo <antirez at gmail dot com>
  12 + * All rights reserved.
  13 + *
  14 + * Redistribution and use in source and binary forms, with or without
  15 + * modification, are permitted provided that the following conditions are met:
  16 + *
  17 + * * Redistributions of source code must retain the above copyright notice,
  18 + * this list of conditions and the following disclaimer.
  19 + * * Redistributions in binary form must reproduce the above copyright
  20 + * notice, this list of conditions and the following disclaimer in the
  21 + * documentation and/or other materials provided with the distribution.
  22 + * * Neither the name of Redis nor the names of its contributors may be used
  23 + * to endorse or promote products derived from this software without
  24 + * specific prior written permission.
  25 + *
  26 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  27 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  28 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  29 + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  30 + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  31 + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  32 + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  33 + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  34 + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  35 + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  36 + * POSSIBILITY OF SUCH DAMAGE.
  37 + *
  38 + * References:
  39 + * - http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
  40 + * - http://www.3waylabs.com/nw/WWW/products/wizcon/vt220.html
  41 + *
  42 + * Todo list:
  43 + * - Switch to gets() if $TERM is something we can't support.
  44 + * - Filter bogus Ctrl+<char> combinations.
  45 + * - Win32 support
  46 + *
  47 + * Bloat:
  48 + * - Completion?
  49 + * - History search like Ctrl+r in readline?
  50 + *
  51 + * List of escape sequences used by this program, we do everything just
  52 + * with three sequences. In order to be so cheap we may have some
  53 + * flickering effect with some slow terminal, but the lesser sequences
  54 + * the more compatible.
  55 + *
  56 + * CHA (Cursor Horizontal Absolute)
  57 + * Sequence: ESC [ n G
  58 + * Effect: moves cursor to column n
  59 + *
  60 + * EL (Erase Line)
  61 + * Sequence: ESC [ n K
  62 + * Effect: if n is 0 or missing, clear from cursor to end of line
  63 + * Effect: if n is 1, clear from beginning of line to cursor
  64 + * Effect: if n is 2, clear entire line
  65 + *
  66 + * CUF (CUrsor Forward)
  67 + * Sequence: ESC [ n C
  68 + * Effect: moves cursor forward of n chars
  69 + *
  70 + */
  71 +
  72 +#include <termios.h>
  73 +#include <unistd.h>
  74 +#include <stdlib.h>
  75 +#include <stdio.h>
  76 +#include <errno.h>
  77 +#include <string.h>
  78 +#include <stdlib.h>
  79 +#include <sys/types.h>
  80 +#include <sys/ioctl.h>
  81 +#include <unistd.h>
  82 +
  83 +#define LINENOISE_MAX_LINE 4096
  84 +
  85 +static struct termios orig_termios; /* in order to restore at exit */
  86 +static int rawmode = 0; /* for atexit() function to check if restore is needed*/
  87 +static int atexit_registered = 0; /* register atexit just 1 time */
  88 +static int history_max_len = 100;
  89 +static int history_len = 0;
  90 +char **history = NULL;
  91 +
  92 +static void linenoiseAtExit(void);
  93 +int linenoiseHistoryAdd(char *line);
  94 +
  95 +static void freeHistory(void) {
  96 + if (history) {
  97 + int j;
  98 +
  99 + for (j = 0; j < history_len; j++)
  100 + free(history[j]);
  101 + free(history);
  102 + }
  103 +}
  104 +
  105 +static int enableRawMode(int fd) {
  106 + struct termios raw;
  107 +
  108 + if (!isatty(STDIN_FILENO)) goto fatal;
  109 + if (!atexit_registered) {
  110 + atexit(linenoiseAtExit);
  111 + atexit_registered = 1;
  112 + }
  113 + if (tcgetattr(fd,&orig_termios) == -1) goto fatal;
  114 +
  115 + raw = orig_termios; /* modify the original mode */
  116 + /* input modes: no break, no CR to NL, no parity check, no strip char,
  117 + * no start/stop output control. */
  118 + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
  119 + /* output modes - disable post processing */
  120 + raw.c_oflag &= ~(OPOST);
  121 + /* control modes - set 8 bit chars */
  122 + raw.c_cflag |= (CS8);
  123 + /* local modes - choing off, canonical off, no extended functions,
  124 + * no signal chars (^Z,^C) */
  125 + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
  126 + /* control chars - set return condition: min number of bytes and timer.
  127 + * We want read to return every single byte, without timeout. */
  128 + raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */
  129 +
  130 + /* put terminal in raw mode after flushing */
  131 + if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal;
  132 + rawmode = 1;
  133 + return 0;
  134 +
  135 +fatal:
  136 + errno = ENOTTY;
  137 + return -1;
  138 +}
  139 +
  140 +static void disableRawMode(int fd) {
  141 + /* Don't even check the return value as it's too late. */
  142 + if (rawmode && tcsetattr(fd,TCSAFLUSH,&orig_termios) != -1)
  143 + rawmode = 0;
  144 +}
  145 +
  146 +/* At exit we'll try to fix the terminal to the initial conditions. */
  147 +static void linenoiseAtExit(void) {
  148 + disableRawMode(STDIN_FILENO);
  149 + freeHistory();
  150 +}
  151 +
  152 +static int getColumns(void) {
  153 + struct winsize ws;
  154 +
  155 + if (ioctl(1, TIOCGWINSZ, &ws) == -1) return 80;
  156 + return ws.ws_col;
  157 +}
  158 +
  159 +static void refreshLine(int fd, const char *prompt, char *buf, size_t len, size_t pos, size_t cols) {
  160 + char seq[64];
  161 + size_t plen = strlen(prompt);
  162 +
  163 + while((plen+pos) >= cols) {
  164 + buf++;
  165 + len--;
  166 + pos--;
  167 + }
  168 + while (plen+len > cols) {
  169 + len--;
  170 + }
  171 +
  172 + /* Cursor to left edge */
  173 + snprintf(seq,64,"\x1b[0G");
  174 + if (write(fd,seq,strlen(seq)) == -1) return;
  175 + /* Write the prompt and the current buffer content */
  176 + if (write(fd,prompt,strlen(prompt)) == -1) return;
  177 + if (write(fd,buf,len) == -1) return;
  178 + /* Erase to right */
  179 + snprintf(seq,64,"\x1b[0K");
  180 + if (write(fd,seq,strlen(seq)) == -1) return;
  181 + /* Move cursor to original position. */
  182 + snprintf(seq,64,"\x1b[0G\x1b[%dC", (int)(pos+plen));
  183 + if (write(fd,seq,strlen(seq)) == -1) return;
  184 +}
  185 +
  186 +static int linenoisePrompt(int fd, char *buf, size_t buflen, const char *prompt) {
  187 + size_t plen = strlen(prompt);
  188 + size_t pos = 0;
  189 + size_t len = 0;
  190 + size_t cols = getColumns();
  191 + int history_index = 0;
  192 +
  193 + buf[0] = '\0';
  194 + buflen--; /* Make sure there is always space for the nulterm */
  195 +
  196 + /* The latest history entry is always our current buffer, that
  197 + * initially is just an empty string. */
  198 + linenoiseHistoryAdd("");
  199 +
  200 + if (write(fd,prompt,plen) == -1) return -1;
  201 + while(1) {
  202 + char c;
  203 + int nread;
  204 + char seq[2];
  205 +
  206 + nread = read(fd,&c,1);
  207 + if (nread <= 0) return len;
  208 + switch(c) {
  209 + case 13: /* enter */
  210 + history_len--;
  211 + return len;
  212 + case 4: /* ctrl-d */
  213 + history_len--;
  214 + return (len == 0) ? -1 : (int)len;
  215 + case 3: /* ctrl-c */
  216 + errno = EAGAIN;
  217 + return -1;
  218 + case 127: /* backspace */
  219 + case 8: /* ctrl-h */
  220 + if (pos > 0 && len > 0) {
  221 + memmove(buf+pos-1,buf+pos,len-pos);
  222 + pos--;
  223 + len--;
  224 + buf[len] = '\0';
  225 + refreshLine(fd,prompt,buf,len,pos,cols);
  226 + }
  227 + break;
  228 + case 20: /* ctrl-t */
  229 + if (pos > 0 && pos < len) {
  230 + int aux = buf[pos-1];
  231 + buf[pos-1] = buf[pos];
  232 + buf[pos] = aux;
  233 + if (pos != len-1) pos++;
  234 + refreshLine(fd,prompt,buf,len,pos,cols);
  235 + }
  236 + break;
  237 + case 2: /* ctrl-b */
  238 + goto left_arrow;
  239 + case 6: /* ctrl-f */
  240 + goto right_arrow;
  241 + case 16: /* ctrl-p */
  242 + seq[1] = 65;
  243 + goto up_down_arrow;
  244 + case 14: /* ctrl-n */
  245 + seq[1] = 66;
  246 + goto up_down_arrow;
  247 + break;
  248 + case 27: /* escape sequence */
  249 + if (read(fd,seq,2) == -1) break;
  250 + if (seq[0] == 91 && seq[1] == 68) {
  251 +left_arrow:
  252 + /* left arrow */
  253 + if (pos > 0) {
  254 + pos--;
  255 + refreshLine(fd,prompt,buf,len,pos,cols);
  256 + }
  257 + } else if (seq[0] == 91 && seq[1] == 67) {
  258 +right_arrow:
  259 + /* right arrow */
  260 + if (pos != len) {
  261 + pos++;
  262 + refreshLine(fd,prompt,buf,len,pos,cols);
  263 + }
  264 + } else if (seq[0] == 91 && (seq[1] == 65 || seq[1] == 66)) {
  265 +up_down_arrow:
  266 + /* up and down arrow: history */
  267 + if (history_len > 1) {
  268 + /* Update the current history entry before to
  269 + * overwrite it with tne next one. */
  270 + free(history[history_len-1-history_index]);
  271 + history[history_len-1-history_index] = strdup(buf);
  272 + /* Show the new entry */
  273 + history_index += (seq[1] == 65) ? 1 : -1;
  274 + if (history_index < 0) {
  275 + history_index = 0;
  276 + break;
  277 + } else if (history_index >= history_len) {
  278 + history_index = history_len-1;
  279 + break;
  280 + }
  281 + strncpy(buf,history[history_len-1-history_index],buflen);
  282 + buf[buflen] = '\0';
  283 + len = pos = strlen(buf);
  284 + refreshLine(fd,prompt,buf,len,pos,cols);
  285 + }
  286 + }
  287 + break;
  288 + default:
  289 + if (len < buflen) {
  290 + if (len == pos) {
  291 + buf[pos] = c;
  292 + pos++;
  293 + len++;
  294 + buf[len] = '\0';
  295 + if (plen+len < cols) {
  296 + /* Avoid a full update of the line in the
  297 + * trivial case. */
  298 + if (write(fd,&c,1) == -1) return -1;
  299 + } else {
  300 + refreshLine(fd,prompt,buf,len,pos,cols);
  301 + }
  302 + } else {
  303 + memmove(buf+pos+1,buf+pos,len-pos);
  304 + buf[pos] = c;
  305 + len++;
  306 + pos++;
  307 + buf[len] = '\0';
  308 + refreshLine(fd,prompt,buf,len,pos,cols);
  309 + }
  310 + }
  311 + break;
  312 + case 21: /* Ctrl+u, delete the whole line. */
  313 + buf[0] = '\0';
  314 + pos = len = 0;
  315 + refreshLine(fd,prompt,buf,len,pos,cols);
  316 + break;
  317 + case 11: /* Ctrl+k, delete from current to end of line. */
  318 + buf[pos] = '\0';
  319 + len = pos;
  320 + refreshLine(fd,prompt,buf,len,pos,cols);
  321 + break;
  322 + case 1: /* Ctrl+a, go to the start of the line */
  323 + pos = 0;
  324 + refreshLine(fd,prompt,buf,len,pos,cols);
  325 + break;
  326 + case 5: /* ctrl+e, go to the end of the line */
  327 + pos = len;
  328 + refreshLine(fd,prompt,buf,len,pos,cols);
  329 + break;
  330 + }
  331 + }
  332 + return len;
  333 +}
  334 +
  335 +static int linenoiseRaw(char *buf, size_t buflen, const char *prompt) {
  336 + int fd = STDIN_FILENO;
  337 + int count;
  338 +
  339 + if (buflen == 0) {
  340 + errno = EINVAL;
  341 + return -1;
  342 + }
  343 + if (enableRawMode(fd) == -1) return -1;
  344 + count = linenoisePrompt(fd, buf, buflen, prompt);
  345 + disableRawMode(fd);
  346 + printf("\n");
  347 + return count;
  348 +}
  349 +
  350 +char *linenoise(const char *prompt) {
  351 + char buf[LINENOISE_MAX_LINE];
  352 + int count;
  353 +
  354 + count = linenoiseRaw(buf,LINENOISE_MAX_LINE,prompt);
  355 + if (count == -1) return NULL;
  356 + return strdup(buf);
  357 +}
  358 +
  359 +/* Using a circular buffer is smarter, but a bit more complex to handle. */
  360 +int linenoiseHistoryAdd(char *line) {
  361 + if (history_max_len == 0) return 0;
  362 + if (history == 0) {
  363 + history = malloc(sizeof(char*)*history_max_len);
  364 + if (history == NULL) return 0;
  365 + memset(history,0,(sizeof(char*)*history_max_len));
  366 + }
  367 + line = strdup(line);
  368 + if (!line) return 0;
  369 + if (history_len == history_max_len) {
  370 + memmove(history,history+1,sizeof(char*)*(history_max_len-1));
  371 + history_len--;
  372 + }
  373 + history[history_len] = line;
  374 + history_len++;
  375 + return 1;
  376 +}
  377 +
  378 +int linenoiseHistorySetMaxLen(int len) {
  379 + char **new;
  380 +
  381 + if (len < 1) return 0;
  382 + if (history) {
  383 + int tocopy = history_len;
  384 +
  385 + new = malloc(sizeof(char*)*len);
  386 + if (new == NULL) return 0;
  387 + if (len < tocopy) tocopy = len;
  388 + memcpy(new,history+(history_max_len-tocopy), sizeof(char*)*tocopy);
  389 + free(history);
  390 + history = new;
  391 + }
  392 + history_max_len = len;
  393 + if (history_len > history_max_len)
  394 + history_len = history_max_len;
  395 + return 1;
  396 +}
41 linenoise.h
... ... @@ -0,0 +1,41 @@
  1 +/* linenoise.h -- guerrilla line editing library against the idea that a
  2 + * line editing lib needs to be 20,000 lines of C code.
  3 + *
  4 + * See linenoise.c for more information.
  5 + *
  6 + * Copyright (c) 2010, Salvatore Sanfilippo <antirez at gmail dot com>
  7 + * All rights reserved.
  8 + *
  9 + * Redistribution and use in source and binary forms, with or without
  10 + * modification, are permitted provided that the following conditions are met:
  11 + *
  12 + * * Redistributions of source code must retain the above copyright notice,
  13 + * this list of conditions and the following disclaimer.
  14 + * * Redistributions in binary form must reproduce the above copyright
  15 + * notice, this list of conditions and the following disclaimer in the
  16 + * documentation and/or other materials provided with the distribution.
  17 + * * Neither the name of Redis nor the names of its contributors may be used
  18 + * to endorse or promote products derived from this software without
  19 + * specific prior written permission.
  20 + *
  21 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  22 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  23 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  24 + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  25 + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  26 + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  27 + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  28 + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  29 + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  30 + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  31 + * POSSIBILITY OF SUCH DAMAGE.
  32 + */
  33 +
  34 +#ifndef __LINENOISE_H
  35 +#define __LINENOISE_H
  36 +
  37 +char *linenoise(const char *prompt);
  38 +int linenoiseHistoryAdd(char *line);
  39 +int linenoiseHistorySetMaxLen(int len);
  40 +
  41 +#endif /* __LINENOISE_H */
46 redis-cli.c
@@ -39,6 +39,7 @@
39 39 #include "sds.h"
40 40 #include "adlist.h"
41 41 #include "zmalloc.h"
  42 +#include "linenoise.h"
42 43
43 44 #define REDIS_CMD_INLINE 1
44 45 #define REDIS_CMD_BULK 2
@@ -451,39 +452,30 @@ static char **convertToSds(int count, char** args) {
451 452 return sds;
452 453 }
453 454
454   -static char *prompt(char *line, int size) {
455   - char *retval;
456   -
457   - do {
458   - printf(">> ");
459   - retval = fgets(line, size, stdin);
460   - } while (retval && *line == '\n');
461   - line[strlen(line) - 1] = '\0';
462   -
463   - return retval;
464   -}
465   -
466 455 static void repl() {
467 456 int size = 4096, max = size >> 1, argc;
468   - char buffer[size];
469   - char *line = buffer;
  457 + char *line;
470 458 char **ap, *args[max];
471 459
472   - while (prompt(line, size)) {
473   - argc = 0;
474   -
475   - for (ap = args; (*ap = strsep(&line, " \t")) != NULL;) {
476   - if (**ap != '\0') {
477   - if (argc >= max) break;
478   - if (strcasecmp(*ap,"quit") == 0 || strcasecmp(*ap,"exit") == 0)
479   - exit(0);
480   - ap++;
481   - argc++;
482   - }
  460 + while((line = linenoise(">> ")) != NULL) {
  461 + if (line[0] != '\0') {
  462 + linenoiseHistoryAdd(line);
  463 + argc = 0;
  464 +
  465 + for (ap = args; (*ap = strsep(&line, " \t")) != NULL;) {
  466 + if (**ap != '\0') {
  467 + if (argc >= max) break;
  468 + if (strcasecmp(*ap,"quit") == 0 || strcasecmp(*ap,"exit") == 0)
  469 + exit(0);
  470 + ap++;
  471 + argc++;
  472 + }
  473 + }
  474 +
  475 + cliSendCommand(argc, convertToSds(argc, args), 1);
483 476 }
484 477
485   - cliSendCommand(argc, convertToSds(argc, args), 1);
486   - line = buffer;
  478 + free(line);
487 479 }
488 480
489 481 exit(0);

0 comments on commit cf87ebf

Please sign in to comment.
Something went wrong with that request. Please try again.