/* * Copyright (C) 2009, 2020 Freie Universität Berlin * Copyright (C) 2013, INRIA. * Copyright (C) 2015 Kaspar Schleiser * * This file is subject to the terms and conditions of the GNU Lesser * General Public License v2.1. See the file LICENSE in the top level * directory for more details. */ /** * @ingroup sys_shell * @{ * * @file * @brief Implementation of a very simple command interpreter. * For each command (i.e. "echo"), a handler can be specified. * If the first word of a user-entered command line matches the * name of a handler, the handler will be called with the whole * command line as parameter. * * @author Kaspar Schleiser * @author René Kijewski * @author Juan Carrano * @author Hendrik van Essen * * @} */ #include #include #include #include #include #include #include #include "kernel_defines.h" #include "xfa.h" #include "shell.h" #include "shell_lock.h" /* define shell command cross file array */ XFA_INIT_CONST(shell_command_xfa_t*, shell_commands_xfa); #define ETX '\x03' /** ASCII "End-of-Text", or Ctrl-C */ #define EOT '\x04' /** ASCII "End-of-Transmission", or Ctrl-D */ #define BS '\x08' /** ASCII "Backspace" */ #define DEL '\x7f' /** ASCII "Delete" */ #if defined(MODULE_NEWLIB) || defined(MODULE_PICOLIBC) #define flush_if_needed() fflush(stdout) #else #define flush_if_needed() #endif /* MODULE_NEWLIB || MODULE_PICOLIBC */ #define SQUOTE '\'' #define DQUOTE '"' #define ESCAPECHAR '\\' #define SPACE ' ' #define TAB '\t' #define PARSE_ESCAPE_MASK 0x4; extern void shell_lock_checkpoint(char *line_buf, int len); extern bool shell_lock_is_locked(void); extern void shell_lock_auto_lock_refresh(void); enum parse_state { PARSE_BLANK = 0x0, PARSE_UNQUOTED = 0x1, PARSE_SINGLEQUOTE = 0x2, PARSE_DOUBLEQUOTE = 0x3, PARSE_UNQUOTED_ESC = 0x5, PARSE_SINGLEQUOTE_ESC = 0x6, PARSE_DOUBLEQUOTE_ESC = 0x7, }; static enum parse_state escape_toggle(enum parse_state s) { return s ^ PARSE_ESCAPE_MASK; } static shell_command_handler_t search_commands(const shell_command_t *entry, char *command) { for (; entry->name != NULL; entry++) { if (strcmp(entry->name, command) == 0) { return entry->handler; } } return NULL; } static shell_command_handler_t search_commands_xfa(char *command) { unsigned n = XFA_LEN(shell_command_t*, shell_commands_xfa); for (unsigned i = 0; i < n; i++) { const volatile shell_command_xfa_t *entry = shell_commands_xfa[i]; if (flash_strcmp(command, entry->name) == 0) { return entry->handler; } } return NULL; } static shell_command_handler_t find_handler( const shell_command_t *command_list, char *command) { shell_command_handler_t handler = NULL; if (command_list != NULL) { handler = search_commands(command_list, command); } if (handler == NULL) { handler = search_commands_xfa(command); } return handler; } static void print_commands(const shell_command_t *entry) { for (; entry->name != NULL; entry++) { printf("%-20s %s\n", entry->name, entry->desc); } } static void print_commands_xfa(void) { unsigned n = XFA_LEN(shell_command_xfa_t*, shell_commands_xfa); for (unsigned i = 0; i < n; i++) { const volatile shell_command_xfa_t *entry = shell_commands_xfa[i]; printf("%-20" PRIsflash " %" PRIsflash "\n", entry->name, entry->desc); } } static void print_help(const shell_command_t *command_list) { printf("Command Description\n" "---------------------------------------\n"); if (command_list != NULL) { print_commands(command_list); } print_commands_xfa(); } /** * Break input line into words, create argv and call the command handler. * * Words are broken up at spaces. A backslash escapes the character that comes * after (meaning if it is taken literally and if it is a space it does not break * the word). Spaces can also be protected by quoting with double or single * quotes. * * There are two unquoted states (PARSE_BLANK and PARSE_UNQUOTED) and two quoted * states (PARSE_SINGLEQUOTE and PARSE_DOUBLEQUOTE). In addition, every state * (except PARSE_BLANK) has an escaped pair state (e.g PARSE_SINGLEQUOTE and * PARSE_SINGLEQUOTE_ESC). * * For the following let's define some things * - Function transit(character, state) to change to 'state' after * 'character' was read. The order of a list of transit-functions matters. * - A BLANK is either SPACE or TAB * - '*' means any character * * PARSE_BLANK * transit(SQUOTE, PARSE_SINGLEQUOTE) * transit(DQUOTE, PARSE_DOUBLEQUOTE) * transit(ESCAPECHAR, PARSE_UNQUOTED_ESC) * transit(BLANK, PARSE_BLANK) * transit(*, PARSE_UNQUOTED) -> store character * * PARSE_UNQUOTED * transit(SQUOTE, PARSE_SINGLEQUOTE) * transit(DQUOTE, PARSE_DOUBLEQUOTE) * transit(BLANK, PARSE_BLANK) * transit(ESCAPECHAR, PARSE_UNQUOTED_ESC) * transit(*, PARSE_UNQUOTED) -> store character * * PARSE_UNQUOTED_ESC * transit(*, PARSE_UNQUOTED) -> store character * * PARSE_SINGLEQUOTE * transit(SQUOTE, PARSE_UNQUOTED) * transit(ESCAPECHAR, PARSE_SINGLEQUOTE_ESC) * transit(*, PARSE_SINGLEQUOTE) -> store character * * PARSE_SINGLEQUOTE_ESC * transit(*, PARSE_SINGLEQUOTE) -> store character * * PARSE_DOUBLEQUOTE * transit(DQUOTE, PARSE_UNQUOTED) * transit(ESCAPECHAR, PARSE_DOUBLEQUOTE_ESC) * transit(*, PARSE_DOUBLEQUOTE) -> store character * * PARSE_DOUBLEQUOTE_ESC * transit(*, PARSE_DOUBLEQUOTE) -> store character * * */ static void handle_input_line(const shell_command_t *command_list, char *line) { /* first we need to calculate the number of arguments */ int argc = 0; char *readpos = line; char *writepos = readpos; uint8_t pstate = PARSE_BLANK; if (IS_USED(MODULE_SHELL_HOOKS)) { shell_post_readline_hook(); } for (; *readpos != '\0'; readpos++) { char wordbreak = SPACE; bool is_wordbreak = false; switch (pstate) { case PARSE_BLANK: if (*readpos != SPACE && *readpos != TAB) { argc++; } if (*readpos == SQUOTE) { pstate = PARSE_SINGLEQUOTE; } else if (*readpos == DQUOTE) { pstate = PARSE_DOUBLEQUOTE; } else if (*readpos == ESCAPECHAR) { pstate = PARSE_UNQUOTED_ESC; } else if (*readpos != SPACE && *readpos != TAB) { pstate = PARSE_UNQUOTED; *writepos++ = *readpos; } break; case PARSE_UNQUOTED: if (*readpos == SQUOTE) { pstate = PARSE_SINGLEQUOTE; } else if (*readpos == DQUOTE) { pstate = PARSE_DOUBLEQUOTE; } else if (*readpos == ESCAPECHAR) { pstate = escape_toggle(pstate); } else if (*readpos == SPACE || *readpos == TAB) { pstate = PARSE_BLANK; *writepos++ = '\0'; } else { *writepos++ = *readpos; } break; case PARSE_SINGLEQUOTE: wordbreak = SQUOTE; is_wordbreak = true; break; case PARSE_DOUBLEQUOTE: wordbreak = DQUOTE; is_wordbreak = true; break; default: /* QUOTED state */ pstate = escape_toggle(pstate); *writepos++ = *readpos; break; } if (is_wordbreak) { if (*readpos == wordbreak) { if (wordbreak == SQUOTE || wordbreak == DQUOTE) { pstate = PARSE_UNQUOTED; } } else if (*readpos == ESCAPECHAR) { pstate = escape_toggle(pstate); } else { *writepos++ = *readpos; } } } *writepos = '\0'; if (pstate != PARSE_BLANK && pstate != PARSE_UNQUOTED) { printf("shell: incorrect quoting\n"); return; } if (argc == 0) { return; } /* then we fill the argv array */ int collected; char *argv[argc]; readpos = line; for (collected = 0; collected < argc; collected++) { argv[collected] = readpos; readpos += strlen(readpos) + 1; } /* then we call the appropriate handler */ shell_command_handler_t handler = find_handler(command_list, argv[0]); if (handler != NULL) { if (IS_USED(MODULE_SHELL_HOOKS)) { shell_pre_command_hook(argc, argv); int res = handler(argc, argv); shell_post_command_hook(res, argc, argv); } else { handler(argc, argv); } } else { if (strcmp("help", argv[0]) == 0) { print_help(command_list); } else { printf("shell: command not found: %s\n", argv[0]); } } } __attribute__((weak)) void shell_post_readline_hook(void) { } __attribute__((weak)) void shell_pre_command_hook(int argc, char **argv) { (void)argv; (void)argc; } __attribute__((weak)) void shell_post_command_hook(int ret, int argc, char **argv) { (void)ret; (void)argv; (void)argc; } static inline void print_prompt(void) { if (!IS_ACTIVE(CONFIG_SHELL_NO_PROMPT) && !IS_ACTIVE(SHELL_NO_PROMPT)) { putchar('>'); putchar(' '); } flush_if_needed(); } static inline void echo_char(char c) { if (!IS_ACTIVE(CONFIG_SHELL_NO_ECHO) && !IS_ACTIVE(SHELL_NO_ECHO)) { putchar(c); } } static inline void white_tape(void) { if (!IS_ACTIVE(CONFIG_SHELL_NO_ECHO) && !IS_ACTIVE(SHELL_NO_ECHO)) { putchar('\b'); putchar(' '); putchar('\b'); } } static inline void new_line(void) { if (!IS_ACTIVE(CONFIG_SHELL_NO_ECHO) && !IS_ACTIVE(SHELL_NO_ECHO)) { putchar('\r'); putchar('\n'); } } /** * @brief Read a single line from standard input into a buffer. * * In addition to copying characters, this routine echoes the line back to * stdout and also supports primitive line editing. * * If the input line is too long, the input will still be consumed until the end * to prevent the next line from containing garbage. * * We allow Unix (\n), DOS (\r\n), and Mac linebreaks (\r). * QEMU transmits only a single '\r' == 13 on hitting enter ("-serial stdio"). * DOS newlines are handled like hitting enter twice. * * @param buf Buffer where the input will be placed. * @param size Size of the buffer. The maximum line length will be one less * than size, to accommodate for the null terminator. * The minimum buffer size is 1. * * @return length of the read line, excluding the terminator, if reading was * successful. * @return EOF, if the end of the input stream was reached. * @return -ENOBUFS if the buffer size was exceeded. */ int readline(char *buf, size_t size) /* needed externally by module shell_lock */ { int curr_pos = 0; bool length_exceeded = false; assert((size_t) size > 0); while (1) { assert((size_t) curr_pos < size); int c = getchar(); switch (c) { case EOT: /* Ctrl-D terminates the current shell instance. */ /* fall-thru */ case EOF: return EOF; case ETX: /* Ctrl-C cancels the current line. */ curr_pos = 0; length_exceeded = false; /* fall-thru */ case '\r': /* fall-thru */ case '\n': buf[curr_pos] = '\0'; new_line(); return (length_exceeded) ? -ENOBUFS : curr_pos; /* check for backspace: */ case BS: /* 0x08 (BS) for most terminals */ /* fall-thru */ case DEL: /* 0x7f (DEL) when using QEMU */ if (curr_pos > 0) { curr_pos--; if ((size_t) curr_pos < size) { buf[curr_pos] = '\0'; length_exceeded = false; } white_tape(); } break; default: /* Always consume characters, but do not not always store them */ if ((size_t) curr_pos < size - 1) { buf[curr_pos++] = c; } else { length_exceeded = true; } echo_char(c); break; } flush_if_needed(); } } void shell_run_once(const shell_command_t *shell_commands, char *line_buf, int len) { if (IS_USED(MODULE_SHELL_LOCK)) { shell_lock_checkpoint(line_buf, len); } print_prompt(); while (1) { int res = readline(line_buf, len); if (IS_USED(MODULE_SHELL_LOCK)) { if (shell_lock_is_locked()) { break; } } if (IS_USED(MODULE_SHELL_LOCK_AUTO_LOCKING)) { /* reset lock countdown in case of new input */ shell_lock_auto_lock_refresh(); } switch (res) { case EOF: if (IS_USED(MODULE_SHELL_LOCK)) { shell_lock_do_lock(); } return; case -ENOBUFS: printf("shell: maximum line length exceeded\n"); break; default: handle_input_line(shell_commands, line_buf); break; } print_prompt(); } }