BOROSOFTWAREAll work →
scantool/libcli.c 634 lines
1/*
2 * Copyright (C) 2001 Richard Almeida & Ibex Ltd ([email protected])
3 *
4 * Copyright (C) 2015 Tomasz Kaźmierczak ([email protected])
5 * - added command completion
6 * (c) fenugrec 2014-2022
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 */
13
14
15#include <stdbool.h>
16
17#include <time.h>
18#include <ctype.h>
19
20#include <stdio.h>
21#include <stdlib.h>
22#include <string.h>
23
24#include "libcli.h"
25
26#ifdef HAVE_LIBREADLINE
27#include <readline/readline.h>
28#include <readline/history.h>
29#endif
30
31
32/************** misc defs */
33#define PROMPTBUFSIZE 80 //Length of prompt before the '>' character.
34#define CLI_MAXARGS 300
35#define INPUT_MAX 1400 //big enough to fit long "diag sendreq..." commands
36
37#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
38
39
40/* currently (2022/10), cppcheck has trouble with assert() and flow control:
41 * https://trac.cppcheck.net/ticket/10333
42 * So, we define an alternate assert() that is sure to be analyzed properly.
43 */
44#ifdef __cppcheck__
45 #undef ASSERT_FAIL
46 #define ASSERT_FAIL() exit(1)
47#endif // __cppcheck__
48
49#ifndef ASSERT_FAIL
50 #include <assert.h>
51 #define ASSERT_FAIL(x) assert(0)
52#endif // ASSERT_FAIL
53
54
55/************** assert handling */
56/** we don't necessarily use default assert() implementation.
57 * (by defaut it invokes fprintf __FILE__ __LINE__ etc which may pull in undesired stuff)
58 * See https://barrgroup.com/embedded-systems/how-to/define-assert-macro
59 *
60 */
61#define CLI_ASSERT(expr) \
62 if (expr) { \
63 } else \
64 ASSERT_FAIL(expr)
65
66
67/****** compiler-specific tweaks ******/
68#ifdef __GNUC__
69 #define UNUSED(X) X __attribute__((unused)) //magic !
70#else
71 #define UNUSED(X) X //how can we suppress "unused parameter" warnings on other compilers?
72#endif // __GNUC__
73
74//hacks for MS Visual studio / visual C
75#if defined(_MSC_VER)
76 #if _MSC_VER < 1910 /* anything older than Visual Studio 2017 */
77 #define snprintf _snprintf //danger : _snprintf doesn't guarantee zero-termination !?
78//as of Visual Studio 2015 the snprintf function is c99 compatible.
79//https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/snprintf-snprintf-snprintf-l-snwprintf-snwprintf-l?view=msvc-160
80 #pragma message("Warning: MSVC _sprintf() may be dangerous ! Please ask your compiler vendor to supply a C99-compliant snprintf()...")
81 #endif /* _MSC_VER < 1910 Visual Studio 2017 */
82#endif /* _MSC_VER Visual Studio */
83
84
85
86
87/************ private data **********/
88static const struct cmd_tbl_entry *root_cmd_table; /* point to current root table */
89static struct cli_callbacks callbacks = {0};
90
91#ifdef HAVE_LIBREADLINE
92//current global command level for command completion
93static const struct cmd_tbl_entry *current_cmd_level;
94//command level in the command line, also needed for command completion
95static const struct cmd_tbl_entry *completion_cmd_level;
96#endif
97
98
99
100/************* fwd decls */
101static enum cli_retval do_cli(const struct cmd_tbl_entry *cmd_tbl, const char *prompt, FILE *instream, int argc, char **argv);
102
103
104char *cli_basic_get_input(const char *prompt, FILE *instream) {
105 char *input;
106 bool do_prompt;
107
108 CLI_ASSERT(instream);
109
110 input = malloc(INPUT_MAX);
111 if (!input) {
112 return NULL;
113 }
114
115 do_prompt = 1;
116 while (1) {
117 if (do_prompt && prompt) {
118 printf("%s", prompt);
119 fflush(stdout);
120 }
121
122 if (fgets(input, INPUT_MAX, instream)) {
123 break;
124 }
125 if (feof(instream)) {
126 free(input);
127 return NULL;
128 }
129 /* Ignore error and try again, but don't prompt. */
130 clearerr(instream);
131 do_prompt = 0;
132 }
133 input[strcspn(input, "\r\n")] = '\0'; /* Remove trailing CR/LF */
134 return input;
135}
136
137
138#ifdef HAVE_LIBREADLINE
139
140/* Caller must free returned buffer */
141static char *get_input(const char *prompt) {
142 char *input;
143 /* XXX Does readline change the prompt? */
144 char localprompt[128];
145 strncpy(localprompt, prompt, sizeof(localprompt));
146
147 input = readline(localprompt);
148 if (input && *input) {
149 add_history(input);
150 }
151 return input;
152}
153
154char *command_generator(const char *text, int state) {
155 static int list_index, length;
156 const struct cmd_tbl_entry *cmd_entry;
157
158 CLI_ASSERT(text);
159
160 //a new word to complete
161 if (state == 0) {
162 list_index = 0;
163 length = strlen(text);
164 }
165
166 //find the command
167 while (completion_cmd_level[list_index].command != NULL) {
168 cmd_entry = &completion_cmd_level[list_index];
169 list_index++;
170 if (strncmp(cmd_entry->command, text, length) == 0 && !(cmd_entry->flags & CLI_CMD_HIDDEN)) {
171 char *ret_name;
172 //we must return a copy of the string; libreadline frees it for us
173 ret_name = malloc(strlen(cmd_entry->command) + 1);
174 if (!ret_name) {
175 return NULL;
176 }
177 strcpy(ret_name, cmd_entry->command);
178 return ret_name;
179 }
180 }
181 return (char *)NULL;
182}
183
184char **scantool_completion(const char *text, int start, UNUSED(int end)) {
185 char **matches;
186
187 CLI_ASSERT(text);
188
189 //start == 0 is when the command line is either empty or contains only whitespaces
190 if (start == 0) {
191 //we are at the beginning of the command line, so the completion command level is equal to the current command level
192 completion_cmd_level = current_cmd_level;
193 rl_attempted_completion_over = 0;
194 }
195 //(start != end) means that we are trying to complete a command;
196 //(start > 0 and start == end) means that all commands are completed and we need to check for their sub-commands;
197 //we handle here both cases so that the completion_cmd_level is always up-to-date
198 else {
199 //parse the command line so that we know on what command level we should do the completion
200 struct cmd_tbl_entry const *parsed_level = current_cmd_level;
201
202 //we need to omit leading whitespaces
203 size_t begin_at = strspn(rl_line_buffer, " ");
204 const char *cmd = &rl_line_buffer[begin_at];
205 //now get the length of the first command
206 size_t cmd_length = strcspn(cmd, " ");
207
208 //check all completed commands
209 while (cmd_length > 0 && cmd[cmd_length] == ' ') {
210 //find out what it might be...
211 bool found = 0;
212 for (int i = 0; parsed_level[i].command != NULL; i++) {
213 //if found the command on the current level
214 if (!(parsed_level[i].flags & CLI_CMD_HIDDEN) &&
215 strlen(parsed_level[i].command) == cmd_length &&
216 strncmp(parsed_level[i].command, cmd, cmd_length) == 0) {
217 //does it have sub-commands?
218 if (parsed_level[i].sub_cmd_tbl != NULL) {
219 //go deeper
220 parsed_level = parsed_level[i].sub_cmd_tbl;
221 rl_attempted_completion_over = 0;
222 found = 1;
223 } else if (parsed_level[i].flags & CLI_CMD_FILEARG) {
224 //the command accepts a filename as an argument, so use the libreadline's filename completion
225 rl_attempted_completion_over = 0;
226 return NULL;
227 } else {
228 //if no sub-commands, then no more completion
229 rl_attempted_completion_over = 1;
230 return NULL;
231 }
232 //stop searching on this level and go to another command in the command line (if any)
233 break;
234 }
235 }
236 //went through all commands and didn't find anything? then it is an unknown command
237 if (!found) {
238 rl_attempted_completion_over = 1;
239 return NULL;
240 }
241
242 //move past the just-parsed command
243 cmd = &cmd[cmd_length];
244 //again, omit whitespaces
245 begin_at = strspn(cmd, " ");
246 cmd = &cmd[begin_at];
247 //length of the next command
248 cmd_length = strcspn(cmd, " ");
249 }
250
251 //update the completion command level for the command_generator() function
252 completion_cmd_level = parsed_level;
253 }
254
255 matches = rl_completion_matches(text, command_generator);
256 if (matches == NULL) {
257 //this will disable the default (filename and username) completion in case no command matches are found
258 rl_attempted_completion_over = 1;
259 }
260 return matches;
261}
262
263static void readline_init(const struct cmd_tbl_entry *curtable) {
264 CLI_ASSERT(curtable);
265
266 //preset levels for current table
267 current_cmd_level = curtable;
268 completion_cmd_level = curtable;
269
270 //our custom completion function
271 rl_attempted_completion_function = scantool_completion;
272}
273
274#else // so no libreadline
275
276static char *get_input(const char *prompt) {
277 return cli_basic_get_input(prompt, stdin);
278}
279
280static void readline_init(UNUSED(const struct cmd_tbl_entry *cmd_table)) {
281}
282
283#endif //HAVE_LIBREADLINE
284
285
286/** get input from user or file
287 *
288 * @instream either stdin or file
289 * @return NULL if no more input
290 */
291static char *command_line_input(const char *prompt, FILE *instream) {
292 CLI_ASSERT(instream);
293
294 if (instream == stdin) {
295 return get_input(prompt);
296 }
297
298 /* Reading from init or command file; no prompting or history */
299 return cli_basic_get_input(NULL, instream);
300}
301
302
303// just a thin wrapper
304static void libcli_logcmd(int argc, char **argv) {
305 if (callbacks.cli_logcmd) {
306 callbacks.cli_logcmd(argc, argv);
307 }
308 return;
309}
310
311
312void cli_set_callbacks(const struct cli_callbacks* new_callbacks) {
313 if (!new_callbacks) {
314 return;
315 }
316 callbacks = *new_callbacks;
317 return;
318}
319
320
321/** execute commands read from filename;
322 *
323 * ret CMD_OK if file was readable (command/parsing problems are OK)
324 * ret CMD_FAILED if file was unreadable
325 * forward CMD_EXIT if applicable */
326static enum cli_retval command_file(const char *filename) {
327 int rv;
328 FILE *fstream;
329
330 CLI_ASSERT(filename);
331
332 if ( (fstream=fopen(filename, "r"))) {
333 printf("running commands from file %s...\n", filename);
334 rv=do_cli(root_cmd_table, NULL, fstream, 0, NULL);
335 fclose(fstream);
336 return (rv==CMD_EXIT)? CMD_EXIT:CMD_OK;
337 }
338 return CMD_FAILED;
339}
340
341
342/* Find matching cmd_tbl_entry for command *cmd in table *cmdt.
343 * Returns the command or the custom handler if
344 * no match was found and the custom handler exists.
345 */
346static const struct cmd_tbl_entry *find_cmd(const struct cmd_tbl_entry *cmdt, const char *cmd) {
347 const struct cmd_tbl_entry *ctp;
348 const struct cmd_tbl_entry *custom_cmd;
349
350 CLI_ASSERT(cmdt && cmd);
351
352 ctp = cmdt;
353 custom_cmd = NULL;
354
355 while (ctp->command) {
356 if (ctp->flags & CLI_CMD_CUSTOM) {
357 // found a custom handler; save it
358 custom_cmd = ctp;
359 }
360
361 if (strcasecmp(ctp->command, cmd) == 0) {
362 return ctp;
363 }
364 ctp++;
365 }
366
367 return custom_cmd;
368}
369
370
371
372
373static char nullstr[2] = {0,0}; //can't be const char because it goes into argv
374
375/** CLI processor
376 *
377 * @prompt Optional. Will be prompted only if argc==0
378 * @argc If supplied, then this is one shot cli, ie run the command
379 * @instream stdin or file to source commands
380 *
381 * prints *prompt,
382 */
383static enum cli_retval do_cli(const struct cmd_tbl_entry *cmd_tbl, const char *prompt, FILE *instream, int argc, char **argv) {
384 /* Build up argc/argv */
385 const struct cmd_tbl_entry *ctp;
386 int cmd_argc;
387 char *cmd_argv[CLI_MAXARGS];
388 char *input = NULL;
389 int rv;
390 bool done; //when set, sub-command processing is ended and returns to upper level
391 int i;
392
393 CLI_ASSERT(cmd_tbl && instream);
394
395 if (!prompt) {
396 prompt = "";
397 }
398
399 char promptbuf[PROMPTBUFSIZE]; /* Was 1024, who needs that long a prompt? (the part before user input up to '>') */
400
401#ifdef HAVE_LIBREADLINE
402 //set the current command level for command completion
403 current_cmd_level = cmd_tbl;
404#endif
405
406 rv = CMD_FAILED;
407 done = 0;
408 snprintf(promptbuf, PROMPTBUFSIZE, "%s> ", prompt);
409 while (!done) {
410 char *inptr, *s;
411
412 if (argc == 0) {
413 /* Get Input */
414 if (input) {
415 free(input);
416 }
417 input = command_line_input(promptbuf, instream);
418 if (!input) {
419 break;
420 }
421
422 /* Parse it */
423 inptr = input;
424 if (*inptr == '@') { //printing comment
425 printf("%s\n", inptr);
426 continue;
427 }
428 if (*inptr == '#') { //non-printing comment
429 continue;
430 }
431 cmd_argc = 0;
432 while ( (s = strtok(inptr, " \t")) != NULL ) {
433 cmd_argv[cmd_argc] = s;
434 inptr = NULL;
435 if (cmd_argc == (ARRAY_SIZE(cmd_argv)-1)) {
436 fprintf(stderr, "Warning : excessive # of arguments\n");
437 break;
438 }
439 cmd_argc++;
440 }
441 cmd_argv[cmd_argc] = nullstr;
442 } else {
443 /* Use supplied argc */
444 cmd_argc = argc;
445 for (i = 0; i < argc; i++) {
446 cmd_argv[i] = argv[i];
447 }
448 }
449
450 if (cmd_argc == 0) {
451 continue;
452 }
453 ctp = find_cmd(cmd_tbl, cmd_argv[0]);
454
455 if (ctp == NULL) {
456 printf("Unrecognized command. Try \"help\"\n");
457 if ((instream != stdin) || (argc > 0)) {
458 //processing a file, or running a subcommand : abort
459 break;
460 }
461 //else : continue getting input
462 continue;
463 }
464
465 if (ctp->sub_cmd_tbl) {
466 /* has sub-commands */
467 libcli_logcmd(1, cmd_argv);
468 snprintf(promptbuf, PROMPTBUFSIZE,"%s/%s",
469 prompt, ctp->command);
470 /* Sub menu */
471 rv = do_cli(ctp->sub_cmd_tbl,
472 promptbuf,
473 instream,
474 cmd_argc-1,
475 &cmd_argv[1]);
476#ifdef HAVE_LIBREADLINE
477 //went out of the sub-menu, so update the command level for command completion
478 current_cmd_level = cmd_tbl;
479#endif
480 if (rv == CMD_EXIT) { // allow exiting prog. from a
481 // submenu
482 done = 1;
483 }
484 snprintf(promptbuf, PROMPTBUFSIZE, "%s> ", prompt);
485 } else {
486 // Regular command
487 libcli_logcmd(cmd_argc, cmd_argv);
488 rv = ctp->routine(cmd_argc, cmd_argv);
489 switch (rv) {
490 case CMD_USAGE:
491 printf("Usage: %s\n%s\n", ctp->usage, ctp->help);
492 break;
493 case CMD_UP:
494 if (cmd_tbl == root_cmd_table) {
495 //ignore cmd_up here
496 break;
497 }
498 // fallthrough
499 case CMD_EXIT:
500 done = 1;
501 break;
502 }
503 }
504
505 if (argc) {
506 /* Single command */
507 break;
508 }
509 } //while !done
510
511 if (input) {
512 free(input);
513 }
514 if (rv == CMD_UP) {
515 return CMD_OK;
516 }
517 return rv;
518}
519
520
521/* start a cli with <name> as a prompt, and optionally run the <initscript> file */
522void cli_enter(const char *name, const char *initscript, const struct cmd_tbl_entry *cmdtable) {
523 CLI_ASSERT(name && cmdtable);
524
525 root_cmd_table = cmdtable;
526
527 readline_init(cmdtable);
528
529 if (initscript != NULL) {
530 int rv=command_file(initscript);
531 switch (rv) {
532 case CMD_OK:
533 /* script was succesful, start a normal CLI afterwards */
534 break;
535 case CMD_FAILED:
536 printf("Problem with file %s\n", initscript);
537 // fallthrough, yes
538 default:
539 case CMD_EXIT:
540 goto exit_cleanup;
541 }
542 }
543
544 printf("\n");
545 /* And go start CLI */
546 (void)do_cli(root_cmd_table, name, stdin, 0, NULL);
547
548exit_cleanup:
549 if (callbacks.cli_atexit) {
550 callbacks.cli_atexit();
551 }
552
553 root_cmd_table = NULL;
554 return;
555
556}
557
558
559/********* generic commands */
560
561enum cli_retval cmd_source(int argc, char **argv) {
562 char *file;
563 int rv;
564
565 if (argc < 2) {
566 printf("No filename\n");
567 return CMD_USAGE;
568 }
569
570 file = argv[1];
571 rv=command_file(file);
572 if (rv == CMD_FAILED) {
573 printf("Couldn't read %s\n", file);
574 }
575
576 return rv;
577}
578
579
580enum cli_retval cli_help_basic(int argc, char **argv, const struct cmd_tbl_entry *cmd_table) {
581 const struct cmd_tbl_entry *ctp;
582
583 CLI_ASSERT(cmd_table);
584
585 if (argc > 1) {
586 /* Single command help */
587 int found = 0;
588 ctp = cmd_table;
589 while (ctp->command) {
590 if (strcasecmp(ctp->command, argv[1]) == 0) {
591 printf("%s: %s\n", ctp->command, ctp->help);
592 printf("Usage: %s\n", ctp->usage);
593 found++;
594 break;
595 }
596 ctp++;
597 }
598 if (!found) {
599 printf("help: %s: no such command\n", argv[1]);
600 }
601 return CMD_OK;
602 }
603
604 /* Print help */
605 printf("Available commands are :\n");
606 ctp = cmd_table;
607 while (ctp->command) {
608 if ((ctp->flags & CLI_CMD_HIDDEN) == 0) {
609 printf("\t%s\n", ctp->usage);
610 }
611 if (ctp->flags & CLI_CMD_CUSTOM) {
612 /* list custom subcommands too */
613 printf("Custom commands for the current level:\n");
614 char *cust_special[]= { "?", NULL };
615 char **temp_argv = &cust_special[0];
616 ctp->routine(1, temp_argv);
617 }
618 ctp++;
619 }
620 printf("\nTry \"help <command>\" for further help\n");
621
622
623 return CMD_OK;
624}
625
626enum cli_retval cmd_up(UNUSED(int argc), UNUSED(char **argv)) {
627 return CMD_UP;
628}
629
630
631enum cli_retval cmd_exit(UNUSED(int argc), UNUSED(char **argv)) {
632 return CMD_EXIT;
633}
634