]> git.saurik.com Git - redis.git/blob - src/redis-cli.c
Use helper function for string object length
[redis.git] / src / redis-cli.c
1 /* Redis CLI (command line interface)
2 *
3 * Copyright (c) 2009-2010, Salvatore Sanfilippo <antirez at gmail dot com>
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions are met:
8 *
9 * * Redistributions of source code must retain the above copyright notice,
10 * this list of conditions and the following disclaimer.
11 * * Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 * * Neither the name of Redis nor the names of its contributors may be used
15 * to endorse or promote products derived from this software without
16 * specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
22 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28 * POSSIBILITY OF SUCH DAMAGE.
29 */
30
31 #include "fmacros.h"
32 #include "version.h"
33
34 #include <stdio.h>
35 #include <string.h>
36 #include <stdlib.h>
37 #include <unistd.h>
38 #include <ctype.h>
39 #include <errno.h>
40 #include <sys/stat.h>
41 #include <sys/time.h>
42 #include <assert.h>
43
44 #include "hiredis.h"
45 #include "sds.h"
46 #include "zmalloc.h"
47 #include "linenoise.h"
48 #include "help.h"
49
50 #define REDIS_NOTUSED(V) ((void) V)
51
52 static redisContext *context;
53 static struct config {
54 char *hostip;
55 int hostport;
56 char *hostsocket;
57 long repeat;
58 int dbnum;
59 int interactive;
60 int shutdown;
61 int monitor_mode;
62 int pubsub_mode;
63 int raw_output; /* output mode per command */
64 int tty; /* flag for default output format */
65 int stdinarg; /* get last arg from stdin. (-x option) */
66 char mb_sep;
67 char *auth;
68 char *historyfile;
69 } config;
70
71 static void usage();
72 char *redisGitSHA1(void);
73
74 /*------------------------------------------------------------------------------
75 * Utility functions
76 *--------------------------------------------------------------------------- */
77
78 static long long mstime(void) {
79 struct timeval tv;
80 long long mst;
81
82 gettimeofday(&tv, NULL);
83 mst = ((long)tv.tv_sec)*1000;
84 mst += tv.tv_usec/1000;
85 return mst;
86 }
87
88 /*------------------------------------------------------------------------------
89 * Help functions
90 *--------------------------------------------------------------------------- */
91
92 #define CLI_HELP_COMMAND 1
93 #define CLI_HELP_GROUP 2
94
95 typedef struct {
96 int type;
97 int argc;
98 sds *argv;
99 sds full;
100
101 /* Only used for help on commands */
102 struct commandHelp *org;
103 } helpEntry;
104
105 static helpEntry *helpEntries;
106 static int helpEntriesLen;
107
108 static void cliInitHelp() {
109 int commandslen = sizeof(commandHelp)/sizeof(struct commandHelp);
110 int groupslen = sizeof(commandGroups)/sizeof(char*);
111 int i, len, pos = 0;
112 helpEntry tmp;
113
114 helpEntriesLen = len = commandslen+groupslen;
115 helpEntries = malloc(sizeof(helpEntry)*len);
116
117 for (i = 0; i < groupslen; i++) {
118 tmp.argc = 1;
119 tmp.argv = malloc(sizeof(sds));
120 tmp.argv[0] = sdscatprintf(sdsempty(),"@%s",commandGroups[i]);
121 tmp.full = tmp.argv[0];
122 tmp.type = CLI_HELP_GROUP;
123 tmp.org = NULL;
124 helpEntries[pos++] = tmp;
125 }
126
127 for (i = 0; i < commandslen; i++) {
128 tmp.argv = sdssplitargs(commandHelp[i].name,&tmp.argc);
129 tmp.full = sdsnew(commandHelp[i].name);
130 tmp.type = CLI_HELP_COMMAND;
131 tmp.org = &commandHelp[i];
132 helpEntries[pos++] = tmp;
133 }
134 }
135
136 /* Output command help to stdout. */
137 static void cliOutputCommandHelp(struct commandHelp *help, int group) {
138 printf("\r\n \x1b[1m%s\x1b[0m \x1b[90m%s\x1b[0m\r\n", help->name, help->params);
139 printf(" \x1b[33msummary:\x1b[0m %s\r\n", help->summary);
140 printf(" \x1b[33msince:\x1b[0m %s\r\n", help->since);
141 if (group) {
142 printf(" \x1b[33mgroup:\x1b[0m %s\r\n", commandGroups[help->group]);
143 }
144 }
145
146 /* Print generic help. */
147 static void cliOutputGenericHelp() {
148 printf(
149 "redis-cli %s\r\n"
150 "Type: \"help @<group>\" to get a list of commands in <group>\r\n"
151 " \"help <command>\" for help on <command>\r\n"
152 " \"help <tab>\" to get a list of possible help topics\r\n"
153 " \"quit\" to exit\r\n",
154 REDIS_VERSION
155 );
156 }
157
158 /* Output all command help, filtering by group or command name. */
159 static void cliOutputHelp(int argc, char **argv) {
160 int i, j, len;
161 int group = -1;
162 helpEntry *entry;
163 struct commandHelp *help;
164
165 if (argc == 0) {
166 cliOutputGenericHelp();
167 return;
168 } else if (argc > 0 && argv[0][0] == '@') {
169 len = sizeof(commandGroups)/sizeof(char*);
170 for (i = 0; i < len; i++) {
171 if (strcasecmp(argv[0]+1,commandGroups[i]) == 0) {
172 group = i;
173 break;
174 }
175 }
176 }
177
178 assert(argc > 0);
179 for (i = 0; i < helpEntriesLen; i++) {
180 entry = &helpEntries[i];
181 if (entry->type != CLI_HELP_COMMAND) continue;
182
183 help = entry->org;
184 if (group == -1) {
185 /* Compare all arguments */
186 if (argc == entry->argc) {
187 for (j = 0; j < argc; j++) {
188 if (strcasecmp(argv[j],entry->argv[j]) != 0) break;
189 }
190 if (j == argc) {
191 cliOutputCommandHelp(help,1);
192 }
193 }
194 } else {
195 if (group == help->group) {
196 cliOutputCommandHelp(help,0);
197 }
198 }
199 }
200 printf("\r\n");
201 }
202
203 static void completionCallback(const char *buf, linenoiseCompletions *lc) {
204 size_t startpos = 0;
205 int mask;
206 int i;
207 size_t matchlen;
208 sds tmp;
209
210 if (strncasecmp(buf,"help ",5) == 0) {
211 startpos = 5;
212 while (isspace(buf[startpos])) startpos++;
213 mask = CLI_HELP_COMMAND | CLI_HELP_GROUP;
214 } else {
215 mask = CLI_HELP_COMMAND;
216 }
217
218 for (i = 0; i < helpEntriesLen; i++) {
219 if (!(helpEntries[i].type & mask)) continue;
220
221 matchlen = strlen(buf+startpos);
222 if (strncasecmp(buf+startpos,helpEntries[i].full,matchlen) == 0) {
223 tmp = sdsnewlen(buf,startpos);
224 tmp = sdscat(tmp,helpEntries[i].full);
225 linenoiseAddCompletion(lc,tmp);
226 sdsfree(tmp);
227 }
228 }
229 }
230
231 /*------------------------------------------------------------------------------
232 * Networking / parsing
233 *--------------------------------------------------------------------------- */
234
235 /* Send AUTH command to the server */
236 static int cliAuth() {
237 redisReply *reply;
238 if (config.auth == NULL) return REDIS_OK;
239
240 reply = redisCommand(context,"AUTH %s",config.auth);
241 if (reply != NULL) {
242 freeReplyObject(reply);
243 return REDIS_OK;
244 }
245 return REDIS_ERR;
246 }
247
248 /* Send SELECT dbnum to the server */
249 static int cliSelect() {
250 redisReply *reply;
251 char dbnum[16];
252 if (config.dbnum == 0) return REDIS_OK;
253
254 snprintf(dbnum,sizeof(dbnum),"%d",config.dbnum);
255 reply = redisCommand(context,"SELECT %s",dbnum);
256 if (reply != NULL) {
257 freeReplyObject(reply);
258 return REDIS_OK;
259 }
260 return REDIS_ERR;
261 }
262
263 /* Connect to the client. If force is not zero the connection is performed
264 * even if there is already a connected socket. */
265 static int cliConnect(int force) {
266 if (context == NULL || force) {
267 if (context != NULL)
268 redisFree(context);
269
270 if (config.hostsocket == NULL) {
271 context = redisConnect(config.hostip,config.hostport);
272 } else {
273 context = redisConnectUnix(config.hostsocket);
274 }
275
276 if (context->err) {
277 fprintf(stderr,"Could not connect to Redis at ");
278 if (config.hostsocket == NULL)
279 fprintf(stderr,"%s:%d: %s\n",config.hostip,config.hostport,context->errstr);
280 else
281 fprintf(stderr,"%s: %s\n",config.hostsocket,context->errstr);
282 redisFree(context);
283 context = NULL;
284 return REDIS_ERR;
285 }
286
287 /* Do AUTH and select the right DB. */
288 if (cliAuth() != REDIS_OK)
289 return REDIS_ERR;
290 if (cliSelect() != REDIS_OK)
291 return REDIS_ERR;
292 }
293 return REDIS_OK;
294 }
295
296 static void cliPrintContextErrorAndExit() {
297 if (context == NULL) return;
298 fprintf(stderr,"Error: %s\n",context->errstr);
299 exit(1);
300 }
301
302 static sds cliFormatReply(redisReply *r, char *prefix) {
303 sds out = sdsempty();
304 switch (r->type) {
305 case REDIS_REPLY_ERROR:
306 if (config.tty) out = sdscat(out,"(error) ");
307 out = sdscatprintf(out,"%s\n", r->str);
308 break;
309 case REDIS_REPLY_STATUS:
310 out = sdscat(out,r->str);
311 out = sdscat(out,"\n");
312 break;
313 case REDIS_REPLY_INTEGER:
314 if (config.tty) out = sdscat(out,"(integer) ");
315 out = sdscatprintf(out,"%lld\n",r->integer);
316 break;
317 case REDIS_REPLY_STRING:
318 if (config.raw_output || !config.tty) {
319 out = sdscatlen(out,r->str,r->len);
320 } else {
321 /* If you are producing output for the standard output we want
322 * a more interesting output with quoted characters and so forth */
323 out = sdscatrepr(out,r->str,r->len);
324 out = sdscat(out,"\n");
325 }
326 break;
327 case REDIS_REPLY_NIL:
328 out = sdscat(out,"(nil)\n");
329 break;
330 case REDIS_REPLY_ARRAY:
331 if (r->elements == 0) {
332 out = sdscat(out,"(empty list or set)\n");
333 } else {
334 unsigned int i, idxlen = 0;
335 char _prefixlen[16];
336 char _prefixfmt[16];
337 sds _prefix;
338 sds tmp;
339
340 /* Calculate chars needed to represent the largest index */
341 i = r->elements;
342 do {
343 idxlen++;
344 i /= 10;
345 } while(i);
346
347 /* Prefix for nested multi bulks should grow with idxlen+2 spaces */
348 memset(_prefixlen,' ',idxlen+2);
349 _prefixlen[idxlen+2] = '\0';
350 _prefix = sdscat(sdsnew(prefix),_prefixlen);
351
352 /* Setup prefix format for every entry */
353 snprintf(_prefixfmt,sizeof(_prefixfmt),"%%s%%%dd) ",idxlen);
354
355 for (i = 0; i < r->elements; i++) {
356 /* Don't use the prefix for the first element, as the parent
357 * caller already prepended the index number. */
358 out = sdscatprintf(out,_prefixfmt,i == 0 ? "" : prefix,i+1);
359
360 /* Format the multi bulk entry */
361 tmp = cliFormatReply(r->element[i],_prefix);
362 out = sdscatlen(out,tmp,sdslen(tmp));
363 sdsfree(tmp);
364 }
365 sdsfree(_prefix);
366 }
367 break;
368 default:
369 fprintf(stderr,"Unknown reply type: %d\n", r->type);
370 exit(1);
371 }
372 return out;
373 }
374
375 static int cliReadReply() {
376 redisReply *reply;
377 sds out;
378
379 if (redisGetReply(context,(void**)&reply) != REDIS_OK) {
380 if (config.shutdown)
381 return REDIS_OK;
382 if (config.interactive) {
383 /* Filter cases where we should reconnect */
384 if (context->err == REDIS_ERR_IO && errno == ECONNRESET)
385 return REDIS_ERR;
386 if (context->err == REDIS_ERR_EOF)
387 return REDIS_ERR;
388 }
389 cliPrintContextErrorAndExit();
390 return REDIS_ERR; /* avoid compiler warning */
391 }
392
393 out = cliFormatReply(reply,"");
394 freeReplyObject(reply);
395 fwrite(out,sdslen(out),1,stdout);
396 sdsfree(out);
397 return REDIS_OK;
398 }
399
400 static int cliSendCommand(int argc, char **argv, int repeat) {
401 char *command = argv[0];
402 size_t *argvlen;
403 int j;
404
405 if (context == NULL) {
406 printf("Not connected, please use: connect <host> <port>\n");
407 return REDIS_OK;
408 }
409
410 config.raw_output = !strcasecmp(command,"info");
411 if (!strcasecmp(command,"help") || !strcasecmp(command,"?")) {
412 cliOutputHelp(--argc, ++argv);
413 return REDIS_OK;
414 }
415 if (!strcasecmp(command,"shutdown")) config.shutdown = 1;
416 if (!strcasecmp(command,"monitor")) config.monitor_mode = 1;
417 if (!strcasecmp(command,"subscribe") ||
418 !strcasecmp(command,"psubscribe")) config.pubsub_mode = 1;
419
420 /* Setup argument length */
421 argvlen = malloc(argc*sizeof(size_t));
422 for (j = 0; j < argc; j++)
423 argvlen[j] = sdslen(argv[j]);
424
425 while(repeat--) {
426 redisAppendCommandArgv(context,argc,(const char**)argv,argvlen);
427 while (config.monitor_mode) {
428 if (cliReadReply() != REDIS_OK) exit(1);
429 fflush(stdout);
430 }
431
432 if (config.pubsub_mode) {
433 printf("Reading messages... (press Ctrl-C to quit)\n");
434 while (1) {
435 if (cliReadReply() != REDIS_OK) exit(1);
436 }
437 }
438
439 if (cliReadReply() != REDIS_OK)
440 return REDIS_ERR;
441 }
442 return REDIS_OK;
443 }
444
445 /*------------------------------------------------------------------------------
446 * User interface
447 *--------------------------------------------------------------------------- */
448
449 static int parseOptions(int argc, char **argv) {
450 int i;
451
452 for (i = 1; i < argc; i++) {
453 int lastarg = i==argc-1;
454
455 if (!strcmp(argv[i],"-h") && !lastarg) {
456 sdsfree(config.hostip);
457 config.hostip = sdsnew(argv[i+1]);
458 i++;
459 } else if (!strcmp(argv[i],"-h") && lastarg) {
460 usage();
461 } else if (!strcmp(argv[i],"-x")) {
462 config.stdinarg = 1;
463 } else if (!strcmp(argv[i],"-p") && !lastarg) {
464 config.hostport = atoi(argv[i+1]);
465 i++;
466 } else if (!strcmp(argv[i],"-s") && !lastarg) {
467 config.hostsocket = argv[i+1];
468 i++;
469 } else if (!strcmp(argv[i],"-r") && !lastarg) {
470 config.repeat = strtoll(argv[i+1],NULL,10);
471 i++;
472 } else if (!strcmp(argv[i],"-n") && !lastarg) {
473 config.dbnum = atoi(argv[i+1]);
474 i++;
475 } else if (!strcmp(argv[i],"-a") && !lastarg) {
476 config.auth = argv[i+1];
477 i++;
478 } else if (!strcmp(argv[i],"-i")) {
479 fprintf(stderr,
480 "Starting interactive mode using -i is deprecated. Interactive mode is started\n"
481 "by default when redis-cli is executed without a command to execute.\n"
482 );
483 } else if (!strcmp(argv[i],"-c")) {
484 fprintf(stderr,
485 "Reading last argument from standard input using -c is deprecated.\n"
486 "When standard input is connected to a pipe or regular file, it is\n"
487 "automatically used as last argument.\n"
488 );
489 } else if (!strcmp(argv[i],"-v")) {
490 printf("redis-cli shipped with Redis version %s (%s)\n", REDIS_VERSION, redisGitSHA1());
491 exit(0);
492 } else {
493 break;
494 }
495 }
496 return i;
497 }
498
499 static sds readArgFromStdin(void) {
500 char buf[1024];
501 sds arg = sdsempty();
502
503 while(1) {
504 int nread = read(fileno(stdin),buf,1024);
505
506 if (nread == 0) break;
507 else if (nread == -1) {
508 perror("Reading from standard input");
509 exit(1);
510 }
511 arg = sdscatlen(arg,buf,nread);
512 }
513 return arg;
514 }
515
516 static void usage() {
517 fprintf(stderr, "usage: redis-cli [-iv] [-h host] [-p port] [-s /path/to/socket] [-a authpw] [-r repeat_times] [-n db_num] cmd arg1 arg2 arg3 ... argN\n");
518 fprintf(stderr, "usage: echo \"argN\" | redis-cli -x [options] cmd arg1 arg2 ... arg(N-1)\n\n");
519 fprintf(stderr, "example: cat /etc/passwd | redis-cli -x set my_passwd\n");
520 fprintf(stderr, "example: redis-cli get my_passwd\n");
521 fprintf(stderr, "example: redis-cli -r 100 lpush mylist x\n");
522 fprintf(stderr, "\nRun in interactive mode: redis-cli -i or just don't pass any command\n");
523 exit(1);
524 }
525
526 /* Turn the plain C strings into Sds strings */
527 static char **convertToSds(int count, char** args) {
528 int j;
529 char **sds = zmalloc(sizeof(char*)*count);
530
531 for(j = 0; j < count; j++)
532 sds[j] = sdsnew(args[j]);
533
534 return sds;
535 }
536
537 #define LINE_BUFLEN 4096
538 static void repl() {
539 int argc, j;
540 char *line;
541 sds *argv;
542
543 config.interactive = 1;
544 linenoiseSetCompletionCallback(completionCallback);
545
546 while((line = linenoise(context ? "redis> " : "not connected> ")) != NULL) {
547 if (line[0] != '\0') {
548 argv = sdssplitargs(line,&argc);
549 linenoiseHistoryAdd(line);
550 if (config.historyfile) linenoiseHistorySave(config.historyfile);
551 if (argv == NULL) {
552 printf("Invalid argument(s)\n");
553 continue;
554 } else if (argc > 0) {
555 if (strcasecmp(argv[0],"quit") == 0 ||
556 strcasecmp(argv[0],"exit") == 0)
557 {
558 exit(0);
559 } else if (argc == 3 && !strcasecmp(argv[0],"connect")) {
560 sdsfree(config.hostip);
561 config.hostip = sdsnew(argv[1]);
562 config.hostport = atoi(argv[2]);
563 cliConnect(1);
564 } else if (argc == 1 && !strcasecmp(argv[0],"clear")) {
565 linenoiseClearScreen();
566 } else {
567 long long start_time = mstime(), elapsed;
568
569 if (cliSendCommand(argc,argv,1) != REDIS_OK) {
570 cliConnect(1);
571
572 /* If we still cannot send the command,
573 * print error and abort. */
574 if (cliSendCommand(argc,argv,1) != REDIS_OK)
575 cliPrintContextErrorAndExit();
576 }
577 elapsed = mstime()-start_time;
578 if (elapsed >= 500) {
579 printf("(%.2fs)\n",(double)elapsed/1000);
580 }
581 }
582 }
583 /* Free the argument vector */
584 for (j = 0; j < argc; j++)
585 sdsfree(argv[j]);
586 zfree(argv);
587 }
588 /* linenoise() returns malloc-ed lines like readline() */
589 free(line);
590 }
591 exit(0);
592 }
593
594 static int noninteractive(int argc, char **argv) {
595 int retval = 0;
596 if (config.stdinarg) {
597 argv = zrealloc(argv, (argc+1)*sizeof(char*));
598 argv[argc] = readArgFromStdin();
599 retval = cliSendCommand(argc+1, argv, config.repeat);
600 } else {
601 /* stdin is probably a tty, can be tested with S_ISCHR(s.st_mode) */
602 retval = cliSendCommand(argc, argv, config.repeat);
603 }
604 return retval;
605 }
606
607 int main(int argc, char **argv) {
608 int firstarg;
609
610 config.hostip = sdsnew("127.0.0.1");
611 config.hostport = 6379;
612 config.hostsocket = NULL;
613 config.repeat = 1;
614 config.dbnum = 0;
615 config.interactive = 0;
616 config.shutdown = 0;
617 config.monitor_mode = 0;
618 config.pubsub_mode = 0;
619 config.raw_output = 0;
620 config.stdinarg = 0;
621 config.auth = NULL;
622 config.historyfile = NULL;
623 config.tty = isatty(fileno(stdout)) || (getenv("FAKETTY") != NULL);
624 config.mb_sep = '\n';
625 cliInitHelp();
626
627 if (getenv("HOME") != NULL) {
628 config.historyfile = malloc(256);
629 snprintf(config.historyfile,256,"%s/.rediscli_history",getenv("HOME"));
630 linenoiseHistoryLoad(config.historyfile);
631 }
632
633 firstarg = parseOptions(argc,argv);
634 argc -= firstarg;
635 argv += firstarg;
636
637 /* Try to connect */
638 if (cliConnect(0) != REDIS_OK) exit(1);
639
640 /* Start interactive mode when no command is provided */
641 if (argc == 0) repl();
642 /* Otherwise, we have some arguments to execute */
643 return noninteractive(argc,convertToSds(argc,argv));
644 }