diff --git a/.gitignore b/.gitignore index 3a2ff62..924c96d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Build artifacts -build/ +build*/ sdkconfig sdkconfig.old *.pyc diff --git a/CHANGELOG.md b/CHANGELOG.md index 92e1beb..3e7c656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ the merge PR. ## Unreleased +- **New: zshell login primitives** (#59) — `shell_obscure_set()`, + `shell_set_root_cmd()`, `shell_prompt_change()` and `shell_history_purge()`, + with `CONFIG_ZSHELL_START_OBSCURED`, `CONFIG_ZSHELL_CMD_ROOT`, + `CONFIG_ZSHELL_PROMPT_CHANGE` (default y) and + `CONFIG_ZSHELL_PROMPT_BUFF_SIZE` (default 20), mirroring upstream. The + shell supplies the mechanics; the application owns the credential check, as + in Zephyr's login sample (`examples/shell_demo` + `sdkconfig.login`). + `struct shell_ctx` grows by the prompt buffer when `PROMPT_CHANGE` is on. - **LOG_* output is colorized by default, using Zephyr's palette** (#57) — `CONFIG_ZSYS_LOG_BACKEND_SHOW_COLOR=y` (mirroring upstream's `CONFIG_LOG_BACKEND_SHOW_COLOR`) prints errors in bold red and warnings in diff --git a/components/zshell/Kconfig b/components/zshell/Kconfig index bcb2929..f625450 100644 --- a/components/zshell/Kconfig +++ b/components/zshell/Kconfig @@ -41,6 +41,43 @@ menu "Boreas Shell (zshell)" default "boreas> " depends on ZSHELL + config ZSHELL_PROMPT_CHANGE + bool "Allow prompt change at runtime" + default y + depends on ZSHELL + help + Enable shell_prompt_change(). Adds a ZSHELL_PROMPT_BUFF_SIZE + byte prompt buffer to each shell instance. + Mirrors CONFIG_SHELL_PROMPT_CHANGE. + + config ZSHELL_PROMPT_BUFF_SIZE + int "Prompt buffer size" + range 2 40 + default 20 + depends on ZSHELL_PROMPT_CHANGE + help + Mirrors CONFIG_SHELL_PROMPT_BUFF_SIZE. Prompts of this length or + longer (including NUL) are rejected with -EINVAL. + + config ZSHELL_START_OBSCURED + bool "Display asterisk when echoing" + depends on ZSHELL + help + Echo '*' instead of the typed character. Used for login prompts; + toggle at runtime with shell_obscure_set(). + Mirrors CONFIG_SHELL_START_OBSCURED. + + config ZSHELL_CMD_ROOT + string "Root command at init" + default "" + depends on ZSHELL + help + Restrict dispatch to this root command when the shell starts: + typed input becomes its arguments and no other root command is + reachable. Empty means the full command tree. Change at runtime + with shell_set_root_cmd(). Applied when the first shell instance + initializes. Mirrors CONFIG_SHELL_CMD_ROOT. + config ZSHELL_HISTORY bool "Enable command history" default y diff --git a/components/zshell/README.md b/components/zshell/README.md index 7ba8033..34deda0 100644 --- a/components/zshell/README.md +++ b/components/zshell/README.md @@ -94,6 +94,21 @@ shell_write(sh, raw_data, len); | Ctrl-C | Cancel line | | Ctrl-L | Clear screen | +## Login flows + +The shell does not own passwords or policy. It provides the same primitives +Zephyr's login sample composes: + +| API | Effect | +|-----|--------| +| `shell_obscure_set(sh, bool)` | Echo `*` instead of the typed character; returns the previous state | +| `shell_set_root_cmd("login")` | Only `login` is reachable; typed text becomes its arguments. `NULL` restores the tree | +| `shell_prompt_change(sh, "login: ")` | Runtime prompt (`CONFIG_ZSHELL_PROMPT_CHANGE`) | +| `shell_history_purge(sh)` | Drop the history entry holding the password | + +`examples/shell_demo` has a working `login`/`logout` pair; build it with +`idf.py -DLOGIN=y build` to start locked. + ## Transport abstraction The shell is transport-agnostic. `shell_transport_stdio` is the default (VFS stdio). Custom transports implement: @@ -158,6 +173,11 @@ Sanity checks if keys aren't reaching the shell: | `CONFIG_ZSHELL_PRINTF_BUFF_SIZE` | 256 | Output format buffer | | `CONFIG_ZSHELL_MAX_ROOT_CMDS` | 32 | Max root commands | | `CONFIG_ZSHELL_MAX_ARGC` | 12 | Max arguments per command | +| `CONFIG_ZSHELL_PROMPT` | `"boreas> "` | Default prompt | +| `CONFIG_ZSHELL_PROMPT_CHANGE` | y | `shell_prompt_change()` at runtime | +| `CONFIG_ZSHELL_PROMPT_BUFF_SIZE` | 20 | Runtime prompt buffer (2..40) | +| `CONFIG_ZSHELL_START_OBSCURED` | n | Echo `*` from startup; see `shell_obscure_set()` | +| `CONFIG_ZSHELL_CMD_ROOT` | `""` | Root command selected at startup; see `shell_set_root_cmd()` | | `CONFIG_ZSHELL_THREAD_STACK_SIZE` | 4096 | Shell thread stack | | `CONFIG_ZSHELL_THREAD_PRIORITY` | 5 | Shell thread priority | | `CONFIG_ZSHELL_HISTORY` | y | Command history | diff --git a/components/zshell/include/boreas/zshell/shell.h b/components/zshell/include/boreas/zshell/shell.h index 209a63b..cb08d4c 100644 --- a/components/zshell/include/boreas/zshell/shell.h +++ b/components/zshell/include/boreas/zshell/shell.h @@ -57,6 +57,10 @@ extern "C" { #define CONFIG_ZSHELL_PROMPT "boreas> " #endif +#ifndef CONFIG_ZSHELL_CMD_ROOT +#define CONFIG_ZSHELL_CMD_ROOT "" +#endif + /* ---------------------------------------------------------------- * History * ---------------------------------------------------------------- */ @@ -88,6 +92,10 @@ struct shell_ctx { struct k_mutex wr_mtx; /* thread-safe output */ int ret_val; /* last command return value */ bool echo; /* echo characters back */ + bool obscure; /* echo '*' instead of the typed character */ +#ifdef CONFIG_ZSHELL_PROMPT_CHANGE + char prompt[CONFIG_ZSHELL_PROMPT_BUFF_SIZE]; +#endif /* VT100 escape sequence decoder */ uint8_t esc_state; /* 0=normal, 1=got ESC, 2=got CSI */ @@ -205,18 +213,78 @@ void shell_info(const struct shell *sh, const char *fmt, ...) __attribute__((for */ void shell_write(const struct shell *sh, const void *data, size_t len); +/** + * Obscure typed characters (echo '*'), e.g. while reading a password. + * + * @return Previous obscure state (0 or 1), -EINVAL if @p sh is NULL. + * + * @note Upstream sets the flag atomically; Boreas writes a plain bool. Call + * it from the shell thread (a command handler), as the login sample does. + */ +int shell_obscure_set(const struct shell *sh, bool obscure); + +/** + * Change the prompt at runtime. Requires CONFIG_ZSHELL_PROMPT_CHANGE. + * + * @param sh Shell instance. + * @param prompt New prompt; an empty string is allowed, as upstream. + * @return 0 on success, -EINVAL if @p prompt is NULL or does not fit in + * CONFIG_ZSHELL_PROMPT_BUFF_SIZE, -EPERM if the option is disabled. + * + * @note Upstream trylocks the output mutex and returns -EBUSY; Boreas blocks + * on it instead, so -EBUSY is never returned. The prompt is read outside + * the lock when it is printed, so call this from the shell thread. + */ +int shell_prompt_change(const struct shell *sh, const char *prompt); + +/** + * Restrict dispatch to one root command. Typed input is passed as arguments + * beneath @p cmd, so sibling root commands (including help) are unreachable + * and tab completion offers only its subcommands. Applies to every shell + * instance, as upstream. NULL restores the full tree. + * + * @param cmd Root command name, or NULL. + * @return 0 on success, -EINVAL if @p cmd names no root command. + */ +int shell_set_root_cmd(const char *cmd); + +/** + * Drop all history entries, e.g. after a login command consumed a password. + * + * @param sh Shell instance. + * + * @note Upstream's sample calls z_shell_history_purge(sh->history); Boreas + * embeds the history in struct shell rather than pointing at it, so + * this takes the shell. Call it from a command handler (the shell + * thread); the history is not locked. No-op without + * CONFIG_ZSHELL_HISTORY. + */ +void shell_history_purge(const struct shell *sh); + /* ---------------------------------------------------------------- * Internal API (used by shell subsystem files) * ---------------------------------------------------------------- */ /* Command registry */ void shell_cmd_sort_root(void); +const struct shell_static_entry *shell_root_cmd_find(const char *name); +/* Selected root (shell_set_root_cmd), or NULL for the full tree */ +const struct shell_static_entry *shell_root_cmd_get(void); void shell_builtins_register(void); const struct shell_static_entry *shell_cmd_find(const struct shell_static_entry *cmds, size_t count, const char *name); +/* Line editing */ +void shell_process_char(struct shell *sh, char ch); +void shell_print_prompt(const struct shell *sh); +/* Echo len chars of cmd_buff starting at off (the one place the buffer reaches the terminal) */ +void shell_write_cmd(const struct shell *sh, uint16_t off, uint16_t len); + /* Parsing */ size_t shell_make_argv(char *line, char **argv, size_t max_argc); +/* shell_make_argv() with the selected root command, if any, placed at argv[0]. + * argv must hold CONFIG_ZSHELL_MAX_ARGC + 1 entries. */ +size_t shell_make_argv_root(char *line, char **argv); int shell_execute(struct shell *sh); /* History */ diff --git a/components/zshell/src/shell.c b/components/zshell/src/shell.c index 7427265..78234a5 100644 --- a/components/zshell/src/shell.c +++ b/components/zshell/src/shell.c @@ -9,6 +9,7 @@ #include "zshell/shell.h" +#include #include #include "zsys/log.h" @@ -19,13 +20,73 @@ LOG_MODULE_REGISTER(shell, LOG_LEVEL_INF); * Character Processing * ---------------------------------------------------------------- */ -static void shell_print_prompt(struct shell *sh) +void shell_print_prompt(const struct shell *sh) { +#ifdef CONFIG_ZSHELL_PROMPT_CHANGE + const char *prompt = sh->ctx.prompt; +#else const char *prompt = sh->default_prompt ? sh->default_prompt : CONFIG_ZSHELL_PROMPT; +#endif + shell_write(sh, prompt, strlen(prompt)); } -static void shell_process_char(struct shell *sh, char ch) +int shell_prompt_change(const struct shell *sh, const char *prompt) +{ +#ifdef CONFIG_ZSHELL_PROMPT_CHANGE + struct shell_ctx *ctx = (struct shell_ctx *)&sh->ctx; + + if (prompt == NULL || strlen(prompt) >= CONFIG_ZSHELL_PROMPT_BUFF_SIZE) { + return -EINVAL; + } + /* Never interleave with an in-progress write; see @note on the declaration */ + k_mutex_lock(&ctx->wr_mtx, K_FOREVER); + strcpy(ctx->prompt, prompt); + k_mutex_unlock(&ctx->wr_mtx); + return 0; +#else + (void)sh; + (void)prompt; + return -EPERM; +#endif +} + +void shell_write_cmd(const struct shell *sh, uint16_t off, uint16_t len) +{ + if (sh->ctx.obscure) { + for (uint16_t i = 0; i < len; i++) { + shell_write(sh, "*", 1); + } + return; + } + shell_write(sh, &sh->ctx.cmd_buff[off], len); +} + +void shell_history_purge(const struct shell *sh) +{ +#ifdef CONFIG_ZSHELL_HISTORY + /* history is embedded in the (logically const) shell; see shell.h */ + shell_history_init((struct shell_history *)&sh->history); +#else + (void)sh; +#endif +} + +int shell_obscure_set(const struct shell *sh, bool obscure) +{ + struct shell_ctx *ctx; + bool prev; + + if (sh == NULL) { + return -EINVAL; + } + ctx = (struct shell_ctx *)&sh->ctx; + prev = ctx->obscure; + ctx->obscure = obscure; + return prev; +} + +void shell_process_char(struct shell *sh, char ch) { /* VT100 escape sequence handling */ #ifdef CONFIG_ZSHELL_VT100 @@ -74,8 +135,8 @@ static void shell_process_char(struct shell *sh, char ch) /* Erase on terminal: backspace, rewrite rest, clear trailing */ shell_write(sh, "\b", 1); if (sh->ctx.cmd_buff_pos < sh->ctx.cmd_buff_len) { - shell_write(sh, &sh->ctx.cmd_buff[sh->ctx.cmd_buff_pos], - sh->ctx.cmd_buff_len - sh->ctx.cmd_buff_pos); + shell_write_cmd(sh, sh->ctx.cmd_buff_pos, + sh->ctx.cmd_buff_len - sh->ctx.cmd_buff_pos); shell_write(sh, " \b", 2); /* Move cursor back */ uint16_t move = sh->ctx.cmd_buff_len - sh->ctx.cmd_buff_pos; @@ -104,8 +165,8 @@ static void shell_process_char(struct shell *sh, char ch) case 0x05: /* Ctrl-E: move cursor to end */ if (sh->ctx.cmd_buff_pos < sh->ctx.cmd_buff_len) { - shell_write(sh, &sh->ctx.cmd_buff[sh->ctx.cmd_buff_pos], - sh->ctx.cmd_buff_len - sh->ctx.cmd_buff_pos); + shell_write_cmd(sh, sh->ctx.cmd_buff_pos, + sh->ctx.cmd_buff_len - sh->ctx.cmd_buff_pos); sh->ctx.cmd_buff_pos = sh->ctx.cmd_buff_len; } break; @@ -146,8 +207,8 @@ static void shell_process_char(struct shell *sh, char ch) for (uint16_t i = 0; i < removed; i++) { shell_write(sh, "\b", 1); } - shell_write(sh, &sh->ctx.cmd_buff[sh->ctx.cmd_buff_pos], - sh->ctx.cmd_buff_len - sh->ctx.cmd_buff_pos); + shell_write_cmd(sh, sh->ctx.cmd_buff_pos, + sh->ctx.cmd_buff_len - sh->ctx.cmd_buff_pos); for (uint16_t i = 0; i < removed; i++) { shell_write(sh, " ", 1); } @@ -163,7 +224,7 @@ static void shell_process_char(struct shell *sh, char ch) shell_print_prompt(sh); /* Reprint current line */ if (sh->ctx.cmd_buff_len > 0) { - shell_write(sh, sh->ctx.cmd_buff, sh->ctx.cmd_buff_len); + shell_write_cmd(sh, 0, sh->ctx.cmd_buff_len); } break; @@ -191,8 +252,8 @@ static void shell_process_char(struct shell *sh, char ch) if (sh->ctx.echo) { /* Write from cursor to end, then move cursor back */ - shell_write(sh, &sh->ctx.cmd_buff[sh->ctx.cmd_buff_pos - 1], - sh->ctx.cmd_buff_len - sh->ctx.cmd_buff_pos + 1); + shell_write_cmd(sh, sh->ctx.cmd_buff_pos - 1, + sh->ctx.cmd_buff_len - sh->ctx.cmd_buff_pos + 1); uint16_t move = sh->ctx.cmd_buff_len - sh->ctx.cmd_buff_pos; for (uint16_t i = 0; i < move; i++) { shell_write(sh, "\b", 1); @@ -247,6 +308,8 @@ static void shell_thread_entry(void *p1, void *p2, void *p3) int shell_init(struct shell *sh, const struct shell_transport *transport, const char *prompt) { + static bool root_applied; + memset(&sh->ctx, 0, sizeof(sh->ctx)); sh->iface = transport; @@ -260,6 +323,13 @@ int shell_init(struct shell *sh, const struct shell_transport *transport, const #else sh->ctx.echo = false; #endif +#ifdef CONFIG_ZSHELL_START_OBSCURED + sh->ctx.obscure = true; +#endif +#ifdef CONFIG_ZSHELL_PROMPT_CHANGE + /* Truncates a too-long default like upstream's strncpy into ctx->prompt */ + strncpy(sh->ctx.prompt, sh->default_prompt, CONFIG_ZSHELL_PROMPT_BUFF_SIZE - 1); +#endif sh->ctx.state = SHELL_STATE_INITIALIZED; @@ -280,6 +350,17 @@ int shell_init(struct shell *sh, const struct shell_transport *transport, const shell_builtins_register(); shell_cmd_sort_root(); + /* The selection is shared by all instances, so apply the Kconfig root + * once: a second shell_init() after a login must not relock the first. */ + if (CONFIG_ZSHELL_CMD_ROOT[0] != '\0' && !root_applied) { + root_applied = true; + if (shell_set_root_cmd(CONFIG_ZSHELL_CMD_ROOT) != 0) { + LOG_WRN("CONFIG_ZSHELL_CMD_ROOT '%s' is not a root command; using full " + "tree", + CONFIG_ZSHELL_CMD_ROOT); + } + } + #ifdef CONFIG_ZSHELL_HISTORY shell_history_init(&sh->history); #endif diff --git a/components/zshell/src/shell_builtins.c b/components/zshell/src/shell_builtins.c index 90ad78a..7b05abc 100644 --- a/components/zshell/src/shell_builtins.c +++ b/components/zshell/src/shell_builtins.c @@ -23,24 +23,22 @@ static int cmd_help(const struct shell *sh, size_t argc, char **argv) { if (argc > 1) { /* Help for a specific command */ - for (size_t i = 0; i < _shell_root_cmd_count; i++) { - if (strcmp(_shell_root_cmds[i]->syntax, argv[1]) == 0) { - const struct shell_static_entry *cmd = _shell_root_cmds[i]; - shell_print(sh, "%s - %s", cmd->syntax, cmd->help ? cmd->help : ""); - if (cmd->subcmd != NULL && cmd->subcmd->entry != NULL) { - shell_print(sh, "Subcommands:"); - const struct shell_static_entry *entries = - cmd->subcmd->entry; - for (size_t j = 0; entries[j].syntax != NULL; j++) { - shell_print(sh, " %-16s %s", entries[j].syntax, - entries[j].help ? entries[j].help : ""); - } - } - return 0; + const struct shell_static_entry *cmd = shell_root_cmd_find(argv[1]); + + if (cmd == NULL) { + shell_error(sh, "Unknown command: %s", argv[1]); + return -1; + } + shell_print(sh, "%s - %s", cmd->syntax, cmd->help ? cmd->help : ""); + if (cmd->subcmd != NULL && cmd->subcmd->entry != NULL) { + shell_print(sh, "Subcommands:"); + const struct shell_static_entry *entries = cmd->subcmd->entry; + for (size_t j = 0; entries[j].syntax != NULL; j++) { + shell_print(sh, " %-16s %s", entries[j].syntax, + entries[j].help ? entries[j].help : ""); } } - shell_error(sh, "Unknown command: %s", argv[1]); - return -1; + return 0; } /* List all root commands */ diff --git a/components/zshell/src/shell_cmd_reg.c b/components/zshell/src/shell_cmd_reg.c index bb37c40..b2557d5 100644 --- a/components/zshell/src/shell_cmd_reg.c +++ b/components/zshell/src/shell_cmd_reg.c @@ -10,11 +10,15 @@ #include "zshell/shell.h" +#include #include struct shell_static_entry *_shell_root_cmds[CONFIG_ZSHELL_MAX_ROOT_CMDS]; size_t _shell_root_cmd_count = 0; +/* One selection for every shell instance, matching shell_set_root_cmd() upstream */ +static const struct shell_static_entry *selected_root; + void shell_cmd_register(struct shell_static_entry *entry) { if (_shell_root_cmd_count < CONFIG_ZSHELL_MAX_ROOT_CMDS) { @@ -22,6 +26,35 @@ void shell_cmd_register(struct shell_static_entry *entry) } } +const struct shell_static_entry *shell_root_cmd_find(const char *name) +{ + for (size_t i = 0; i < _shell_root_cmd_count; i++) { + if (strcmp(_shell_root_cmds[i]->syntax, name) == 0) { + return _shell_root_cmds[i]; + } + } + return NULL; +} + +const struct shell_static_entry *shell_root_cmd_get(void) +{ + return selected_root; +} + +int shell_set_root_cmd(const char *cmd) +{ + const struct shell_static_entry *entry = NULL; + + if (cmd != NULL) { + entry = shell_root_cmd_find(cmd); + if (entry == NULL) { + return -EINVAL; + } + } + selected_root = entry; + return 0; +} + /* Insertion sort by syntax name (alphabetical) -- runs once at init */ void shell_cmd_sort_root(void) { diff --git a/components/zshell/src/shell_completion.c b/components/zshell/src/shell_completion.c index 21adfa6..9f44345 100644 --- a/components/zshell/src/shell_completion.c +++ b/components/zshell/src/shell_completion.c @@ -25,13 +25,15 @@ void shell_completion(struct shell *sh) { char line_copy[CONFIG_ZSHELL_CMD_BUFF_SIZE]; - char *argv[CONFIG_ZSHELL_MAX_ARGC]; + char *argv[CONFIG_ZSHELL_MAX_ARGC + 1]; + size_t argc; /* Copy buffer up to cursor position */ memcpy(line_copy, sh->ctx.cmd_buff, sh->ctx.cmd_buff_pos); line_copy[sh->ctx.cmd_buff_pos] = '\0'; - size_t argc = shell_make_argv(line_copy, argv, CONFIG_ZSHELL_MAX_ARGC); + /* A selected root sits at argv[0]; typed tokens only ever walk beneath it */ + argc = shell_make_argv_root(line_copy, argv); /* Determine if we're completing a partial token or starting a new one */ bool completing_partial = @@ -52,15 +54,7 @@ void shell_completion(struct shell *sh) candidate_count = _shell_root_cmd_count; } else { /* Walk tree to find parent */ - const struct shell_static_entry *cmd = NULL; - - /* Find root command */ - for (size_t i = 0; i < _shell_root_cmd_count; i++) { - if (strcmp(_shell_root_cmds[i]->syntax, argv[0]) == 0) { - cmd = _shell_root_cmds[i]; - break; - } - } + const struct shell_static_entry *cmd = shell_root_cmd_find(argv[0]); /* Walk subcommands */ for (size_t d = 1; d < walk_depth && cmd != NULL; d++) { @@ -134,11 +128,11 @@ void shell_completion(struct shell *sh) /* Insert remaining chars */ if (sh->ctx.cmd_buff_len + insert_len + 1 < CONFIG_ZSHELL_CMD_BUFF_SIZE) { memcpy(&sh->ctx.cmd_buff[sh->ctx.cmd_buff_pos], insert, insert_len); + shell_write_cmd(sh, sh->ctx.cmd_buff_pos, insert_len); sh->ctx.cmd_buff_pos += insert_len; sh->ctx.cmd_buff[sh->ctx.cmd_buff_pos] = ' '; sh->ctx.cmd_buff_pos++; sh->ctx.cmd_buff_len = sh->ctx.cmd_buff_pos; - shell_write(sh, insert, insert_len); shell_write(sh, " ", 1); } return; @@ -160,9 +154,9 @@ void shell_completion(struct shell *sh) const char *insert = matches[0]->syntax + partial_len; if (sh->ctx.cmd_buff_len + insert_len < CONFIG_ZSHELL_CMD_BUFF_SIZE) { memcpy(&sh->ctx.cmd_buff[sh->ctx.cmd_buff_pos], insert, insert_len); + shell_write_cmd(sh, sh->ctx.cmd_buff_pos, insert_len); sh->ctx.cmd_buff_pos += insert_len; sh->ctx.cmd_buff_len = sh->ctx.cmd_buff_pos; - shell_write(sh, insert, insert_len); } } @@ -173,7 +167,6 @@ void shell_completion(struct shell *sh) } /* Reprint prompt + current line */ - const char *prompt = sh->default_prompt ? sh->default_prompt : CONFIG_ZSHELL_PROMPT; - shell_write(sh, prompt, strlen(prompt)); - shell_write(sh, sh->ctx.cmd_buff, sh->ctx.cmd_buff_len); + shell_print_prompt(sh); + shell_write_cmd(sh, 0, sh->ctx.cmd_buff_len); } diff --git a/components/zshell/src/shell_parse.c b/components/zshell/src/shell_parse.c index 98d7443..d868cdf 100644 --- a/components/zshell/src/shell_parse.c +++ b/components/zshell/src/shell_parse.c @@ -55,17 +55,15 @@ size_t shell_make_argv(char *line, char **argv, size_t max_argc) return argc; } -/** - * Find a root command by name. - */ -static const struct shell_static_entry *find_root_cmd(const char *name) +size_t shell_make_argv_root(char *line, char **argv) { - for (size_t i = 0; i < _shell_root_cmd_count; i++) { - if (strcmp(_shell_root_cmds[i]->syntax, name) == 0) { - return _shell_root_cmds[i]; - } + const struct shell_static_entry *root = shell_root_cmd_get(); + + if (root == NULL) { + return shell_make_argv(line, argv, CONFIG_ZSHELL_MAX_ARGC); } - return NULL; + argv[0] = (char *)root->syntax; + return 1 + shell_make_argv(line, &argv[1], CONFIG_ZSHELL_MAX_ARGC); } /** @@ -93,23 +91,26 @@ static const struct shell_static_entry *find_subcmd(const union shell_cmd_entry */ int shell_execute(struct shell *sh) { - char *argv[CONFIG_ZSHELL_MAX_ARGC]; + /* +1: a selected root command occupies argv[0] ahead of the typed tokens */ + char *argv[CONFIG_ZSHELL_MAX_ARGC + 1]; char line_copy[CONFIG_ZSHELL_CMD_BUFF_SIZE]; + const struct shell_static_entry *cmd = shell_root_cmd_get(); + size_t argc; /* Copy buffer since make_argv modifies it */ memcpy(line_copy, sh->ctx.cmd_buff, sh->ctx.cmd_buff_len); line_copy[sh->ctx.cmd_buff_len] = '\0'; - size_t argc = shell_make_argv(line_copy, argv, CONFIG_ZSHELL_MAX_ARGC); - if (argc == 0) { + argc = shell_make_argv_root(line_copy, argv); + if (argc == (cmd != NULL ? 1 : 0)) { return 0; /* empty line */ } - - /* Walk the command tree */ - const struct shell_static_entry *cmd = find_root_cmd(argv[0]); if (cmd == NULL) { - shell_error(sh, "Unknown command: %s", argv[0]); - return -1; + cmd = shell_root_cmd_find(argv[0]); + if (cmd == NULL) { + shell_error(sh, "Unknown command: %s", argv[0]); + return -1; + } } /* Walk subcommands as deep as we can */ diff --git a/components/zshell/src/shell_vt100.c b/components/zshell/src/shell_vt100.c index 3b64e87..9b1eef8 100644 --- a/components/zshell/src/shell_vt100.c +++ b/components/zshell/src/shell_vt100.c @@ -36,16 +36,12 @@ void shell_vt100_process(struct shell *sh, char ch) if (shell_history_get(&sh->history, true, tmp, &len)) { /* Clear current line on terminal */ shell_write(sh, "\r\033[K", 4); - /* Reprint prompt */ - const char *prompt = sh->default_prompt - ? sh->default_prompt - : CONFIG_ZSHELL_PROMPT; - shell_write(sh, prompt, strlen(prompt)); + shell_print_prompt(sh); /* Load history entry */ memcpy(sh->ctx.cmd_buff, tmp, len); sh->ctx.cmd_buff_len = len; sh->ctx.cmd_buff_pos = len; - shell_write(sh, sh->ctx.cmd_buff, len); + shell_write_cmd(sh, 0, len); } } #endif @@ -58,21 +54,15 @@ void shell_vt100_process(struct shell *sh, char ch) char tmp[CONFIG_ZSHELL_CMD_BUFF_SIZE]; if (shell_history_get(&sh->history, false, tmp, &len)) { shell_write(sh, "\r\033[K", 4); - const char *prompt = sh->default_prompt - ? sh->default_prompt - : CONFIG_ZSHELL_PROMPT; - shell_write(sh, prompt, strlen(prompt)); + shell_print_prompt(sh); memcpy(sh->ctx.cmd_buff, tmp, len); sh->ctx.cmd_buff_len = len; sh->ctx.cmd_buff_pos = len; - shell_write(sh, sh->ctx.cmd_buff, len); + shell_write_cmd(sh, 0, len); } else { /* Past newest -- clear line */ shell_write(sh, "\r\033[K", 4); - const char *prompt = sh->default_prompt - ? sh->default_prompt - : CONFIG_ZSHELL_PROMPT; - shell_write(sh, prompt, strlen(prompt)); + shell_print_prompt(sh); sh->ctx.cmd_buff_len = 0; sh->ctx.cmd_buff_pos = 0; } @@ -118,10 +108,9 @@ void shell_vt100_process(struct shell *sh, char ch) sh->ctx.cmd_buff_pos - 1); sh->ctx.cmd_buff_len--; /* Rewrite from cursor, clear trailing char */ - shell_write(sh, - &sh->ctx.cmd_buff[sh->ctx.cmd_buff_pos], - sh->ctx.cmd_buff_len - - sh->ctx.cmd_buff_pos); + shell_write_cmd(sh, sh->ctx.cmd_buff_pos, + sh->ctx.cmd_buff_len - + sh->ctx.cmd_buff_pos); shell_write(sh, " ", 1); uint16_t move = sh->ctx.cmd_buff_len - sh->ctx.cmd_buff_pos + 1; diff --git a/examples/shell_demo/CMakeLists.txt b/examples/shell_demo/CMakeLists.txt index ceceb36..09a0b98 100644 --- a/examples/shell_demo/CMakeLists.txt +++ b/examples/shell_demo/CMakeLists.txt @@ -3,6 +3,10 @@ cmake_minimum_required(VERSION 3.16) set(EXTRA_COMPONENT_DIRS "../../components") # Boreas required configuration (notification-index reservation etc.) set(SDKCONFIG_DEFAULTS "sdkconfig.defaults;${CMAKE_CURRENT_LIST_DIR}/../../sdkconfig.boreas") +# Start locked (type the password alone at the `login: ` prompt): idf.py -DLOGIN=y build +if(LOGIN) + list(APPEND SDKCONFIG_DEFAULTS sdkconfig.login) +endif() include($ENV{IDF_PATH}/tools/cmake/project.cmake) diff --git a/examples/shell_demo/README.md b/examples/shell_demo/README.md index a297b5e..c9d9035 100644 --- a/examples/shell_demo/README.md +++ b/examples/shell_demo/README.md @@ -16,6 +16,8 @@ Demonstrates the Boreas shell (`zshell`) with hierarchical commands, tab complet | `thread` | Print thread stack and CPU usage statistics | | `app status` | Custom demo command: show app status | | `app version` | Custom demo command: show version | +| `login ` | Unlock the shell (see below) | +| `logout` | Lock the shell again | ## Key Bindings @@ -44,6 +46,24 @@ idf.py build idf.py -p /dev/cu.usbmodemXXXX flash monitor ``` +## Login / Logout + +Start the demo locked: `login` is the selected root command, so the prompt +reads `login: `, echo shows `*`, and the typed line is passed to `login` as its +argument. Enter just the password (`boreas`), not `login boreas`: + +```bash +idf.py -DLOGIN=y build # rm sdkconfig first if switching an existing build +``` + +The default password is `boreas`. `login` purges the history entry holding the +password, turns obscuring off, restores the full command tree and the normal +prompt. `logout` reverses that. The composition mirrors Zephyr's +`samples/subsys/shell/shell_module` login sample; the shell provides the +primitives (`shell_obscure_set()`, `shell_set_root_cmd()`, +`shell_prompt_change()`, `shell_history_purge()`) and the application owns +the credential check. + ## Adding Custom Commands ```c diff --git a/examples/shell_demo/main/main.c b/examples/shell_demo/main/main.c index 550b0c6..0be4056 100644 --- a/examples/shell_demo/main/main.c +++ b/examples/shell_demo/main/main.c @@ -2,10 +2,13 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright 2026 Intercreate * - * Shell demo: demonstrates the Boreas shell with built-in commands - * and a custom application command. + * Shell demo: demonstrates the Boreas shell with built-in commands, + * a custom application command, and an optional login/logout gate. */ +#include +#include + #include #include #include @@ -41,6 +44,55 @@ SHELL_STATIC_SUBCMD_SET_CREATE(app_subcmds, SHELL_CMD_REGISTER(app, &app_subcmds, "Application commands", NULL); +/* ---------------------------------------------------------------- + * login / logout + * + * Mirrors zephyr/samples/subsys/shell/shell_module/src/login_cmd.c. The + * shell only supplies the primitives (obscured echo, root command + * selection, prompt change, history purge); the password check is the + * application's. Build with -DLOGIN=y to start locked (sdkconfig.login). + * ---------------------------------------------------------------- */ + +#define DEFAULT_PASSWORD "boreas" +#define PROMPT_LOGIN "login: " + +static int cmd_login(const struct shell *sh, size_t argc, char **argv) +{ + static uint32_t attempts; + + /* example only -- not recommended for production use */ + if (argc < 2 || strcmp(argv[1], DEFAULT_PASSWORD) != 0) { + shell_error(sh, "Incorrect password!"); + attempts++; + if (attempts > 3) { + k_sleep(K_SECONDS(attempts)); + } + return -EINVAL; + } + + /* clear history so password not visible there */ + shell_history_purge(sh); + shell_obscure_set(sh, false); + shell_set_root_cmd(NULL); + shell_prompt_change(sh, CONFIG_ZSHELL_PROMPT); + shell_print(sh, "Logged in. Type 'help' for available commands."); + attempts = 0; + return 0; +} + +static int cmd_logout(const struct shell *sh, size_t argc, char **argv) +{ + (void)argc; + (void)argv; + shell_set_root_cmd("login"); + shell_obscure_set(sh, true); + shell_prompt_change(sh, PROMPT_LOGIN); + return 0; +} + +SHELL_CMD_REGISTER(login, NULL, "", cmd_login); +SHELL_CMD_REGISTER(logout, NULL, "Log out", cmd_logout); + /* ---------------------------------------------------------------- * Shell instance * ---------------------------------------------------------------- */ @@ -55,10 +107,15 @@ void app_main(void) /* Start the shell on the VFS console (UART by default, USB-Serial-JTAG * or USB-CDC if CONFIG_ESP_CONSOLE_* selects them). */ - shell_init(&demo_shell, &shell_transport_stdio, "boreas> "); + shell_init(&demo_shell, &shell_transport_stdio, + CONFIG_ZSHELL_CMD_ROOT[0] != '\0' ? PROMPT_LOGIN : CONFIG_ZSHELL_PROMPT); /* Main task can do other work or just idle */ - LOG_INF("Shell started. Type 'help' for available commands."); + if (CONFIG_ZSHELL_CMD_ROOT[0] != '\0') { + LOG_INF("Shell started locked. Hint: password = %s", DEFAULT_PASSWORD); + } else { + LOG_INF("Shell started. Type 'help' for available commands."); + } /* Keep main task alive */ while (1) { diff --git a/examples/shell_demo/sdkconfig.login b/examples/shell_demo/sdkconfig.login new file mode 100644 index 0000000..962225b --- /dev/null +++ b/examples/shell_demo/sdkconfig.login @@ -0,0 +1,4 @@ +# Overlay: start the shell demo locked; the password alone unlocks at `login: `. +# Appended to SDKCONFIG_DEFAULTS by CMakeLists.txt when -DLOGIN=y is given. +CONFIG_ZSHELL_START_OBSCURED=y +CONFIG_ZSHELL_CMD_ROOT="login" diff --git a/test/main/test_shell.c b/test/main/test_shell.c index 84310b7..05776be 100644 --- a/test/main/test_shell.c +++ b/test/main/test_shell.c @@ -3,9 +3,10 @@ * Copyright 2026 Intercreate * * Unit tests for shell parsing, command registration, history, - * and tab completion logic. + * line editing and dispatch (via a capture transport, no shell thread). */ +#include #include #include "unity.h" @@ -202,6 +203,397 @@ static void test_shell_history_empty(void) #endif /* CONFIG_ZSHELL_HISTORY */ +/* ---------------------------------------------------------------- + * Line-editing harness: capture transport, no shell thread + * ---------------------------------------------------------------- */ + +static char cap_buf[1024]; +static size_t cap_len; + +static int cap_write(const struct shell_transport *transport, const void *data, size_t length, + size_t *cnt) +{ + (void)transport; + size_t room = sizeof(cap_buf) - 1 - cap_len; + size_t n = length < room ? length : room; + + memcpy(&cap_buf[cap_len], data, n); + cap_len += n; + cap_buf[cap_len] = '\0'; + *cnt = length; + return 0; +} + +static const struct shell_transport_api cap_api = {.write = cap_write}; +static const struct shell_transport cap_transport = {.api = &cap_api}; + +static struct shell tsh; + +#ifdef CONFIG_ZSHELL_PROMPT_CHANGE +/* The harness prompt "t> " and the "login: " test both have to fit */ +BUILD_ASSERT(CONFIG_ZSHELL_PROMPT_BUFF_SIZE >= sizeof("login: "), + "shell tests assume CONFIG_ZSHELL_PROMPT_BUFF_SIZE >= 8"); +#endif + +/* Last dispatch seen by tcmd_handler */ +static size_t seen_argc; +static char seen_argv[CONFIG_ZSHELL_MAX_ARGC][32]; + +static int tcmd_handler(const struct shell *sh, size_t argc, char **argv) +{ + (void)sh; + seen_argc = argc; + for (size_t i = 0; i < argc && i < CONFIG_ZSHELL_MAX_ARGC; i++) { + strncpy(seen_argv[i], argv[i], sizeof(seen_argv[i]) - 1); + seen_argv[i][sizeof(seen_argv[i]) - 1] = '\0'; + } + return 0; +} + +SHELL_STATIC_SUBCMD_SET_CREATE(tcmd_subcmds, SHELL_CMD(get, NULL, "get", tcmd_handler) + SHELL_CMD(set, NULL, "set", tcmd_handler)); + +static struct shell_static_entry tcmd = { + .syntax = "tcmd", + .help = "test command", + .subcmd = &tcmd_subcmds, + .handler = tcmd_handler, +}; + +static int other_hits; + +static int other_handler(const struct shell *sh, size_t argc, char **argv) +{ + (void)sh; + (void)argc; + (void)argv; + other_hits++; + return 0; +} + +static struct shell_static_entry other = { + .syntax = "other", + .help = "other command", + .handler = other_handler, +}; + +static size_t saved_root_count; +static struct shell_static_entry *saved_root[CONFIG_ZSHELL_MAX_ROOT_CMDS]; + +static void cap_clear(void) +{ + cap_len = 0; + cap_buf[0] = '\0'; +} + +/* Fresh shell with a capture transport and a root tree of {other, tcmd} + builtins */ +static void harness_setup(void) +{ + saved_root_count = _shell_root_cmd_count; + memcpy(saved_root, _shell_root_cmds, saved_root_count * sizeof(saved_root[0])); + _shell_root_cmd_count = 0; + shell_cmd_register(&other); + shell_cmd_register(&tcmd); + shell_builtins_register(); /* help must be present for the gating tests */ + + memset(&tsh, 0, sizeof(tsh)); + tsh.iface = &cap_transport; + tsh.default_prompt = "t> "; +#ifdef CONFIG_ZSHELL_PROMPT_CHANGE + strcpy(tsh.ctx.prompt, "t> "); +#endif + k_mutex_init(&tsh.ctx.wr_mtx); + tsh.ctx.echo = true; + tsh.ctx.state = SHELL_STATE_ACTIVE; +#ifdef CONFIG_ZSHELL_HISTORY + shell_history_init(&tsh.history); +#endif + seen_argc = 0; + other_hits = 0; + cap_clear(); +} + +static void harness_teardown(void) +{ + shell_set_root_cmd(NULL); + _shell_root_cmd_count = saved_root_count; + memcpy(_shell_root_cmds, saved_root, saved_root_count * sizeof(saved_root[0])); +} + +static void feed(const char *s) +{ + while (*s != '\0') { + shell_process_char(&tsh, *s++); + } +} + +static void test_shell_insert_echo(void) +{ + harness_setup(); + feed("ab"); + TEST_ASSERT_EQUAL_STRING("ab", cap_buf); + TEST_ASSERT_EQUAL(2, tsh.ctx.cmd_buff_len); + TEST_ASSERT_EQUAL(2, tsh.ctx.cmd_buff_pos); + TEST_ASSERT_EQUAL_MEMORY("ab", tsh.ctx.cmd_buff, 2); + harness_teardown(); +} + +static void test_shell_backspace(void) +{ + harness_setup(); + feed("abc\b"); + TEST_ASSERT_EQUAL_STRING("abc\b \b", cap_buf); + TEST_ASSERT_EQUAL(2, tsh.ctx.cmd_buff_len); + TEST_ASSERT_EQUAL_MEMORY("ab", tsh.ctx.cmd_buff, 2); + harness_teardown(); +} + +static void test_shell_dispatch_subcmd(void) +{ + harness_setup(); + feed("tcmd set 5\r"); + TEST_ASSERT_EQUAL(2, seen_argc); + TEST_ASSERT_EQUAL_STRING("set", seen_argv[0]); + TEST_ASSERT_EQUAL_STRING("5", seen_argv[1]); + /* Prompt reprinted after execution */ + TEST_ASSERT_NOT_NULL(strstr(cap_buf, "\r\nt> ")); + harness_teardown(); +} + +static void test_shell_dispatch_unknown(void) +{ + harness_setup(); + feed("nope\r"); + TEST_ASSERT_EQUAL(0, seen_argc); + TEST_ASSERT_NOT_NULL(strstr(cap_buf, "Unknown command: nope")); + harness_teardown(); +} + +/* ---------------------------------------------------------------- + * Obscured input + * ---------------------------------------------------------------- */ + +static void test_shell_obscure_set_returns_previous(void) +{ + harness_setup(); + TEST_ASSERT_EQUAL(0, shell_obscure_set(&tsh, true)); + TEST_ASSERT_EQUAL(1, shell_obscure_set(&tsh, true)); + TEST_ASSERT_EQUAL(1, shell_obscure_set(&tsh, false)); + TEST_ASSERT_EQUAL(0, shell_obscure_set(&tsh, false)); + TEST_ASSERT_EQUAL(-EINVAL, shell_obscure_set(NULL, true)); + harness_teardown(); +} + +static void test_shell_obscure_insert_delete(void) +{ + harness_setup(); + shell_obscure_set(&tsh, true); + + feed("ab"); + TEST_ASSERT_EQUAL_STRING("**", cap_buf); + TEST_ASSERT_EQUAL_MEMORY("ab", tsh.ctx.cmd_buff, 2); + + cap_clear(); + feed("\b"); + TEST_ASSERT_EQUAL_STRING("\b \b", cap_buf); + TEST_ASSERT_EQUAL(1, tsh.ctx.cmd_buff_len); + +#ifdef CONFIG_ZSHELL_VT100 + /* Insert in the middle: the rewritten tail must stay obscured */ + cap_clear(); + feed("\033[D" + "z"); + TEST_ASSERT_EQUAL_MEMORY("za", tsh.ctx.cmd_buff, 2); + TEST_ASSERT_NULL(strchr(cap_buf, 'a')); + TEST_ASSERT_NULL(strchr(cap_buf, 'z')); +#endif + + /* Dispatch still sees the real text */ + feed("\r"); +#ifdef CONFIG_ZSHELL_VT100 + TEST_ASSERT_NOT_NULL(strstr(cap_buf, "Unknown command: za")); +#else + TEST_ASSERT_NOT_NULL(strstr(cap_buf, "Unknown command: a")); +#endif + harness_teardown(); +} + +/* Not obscured unless CONFIG_ZSHELL_START_OBSCURED (off in the test config) */ +static void test_shell_obscure_default_off(void) +{ + harness_setup(); + feed("a"); + TEST_ASSERT_EQUAL_STRING("a", cap_buf); + harness_teardown(); +} + +/* ---------------------------------------------------------------- + * Root command selection + * ---------------------------------------------------------------- */ + +static void test_shell_set_root_cmd_valid_invalid(void) +{ + harness_setup(); + TEST_ASSERT_EQUAL(-EINVAL, shell_set_root_cmd("bogus")); + TEST_ASSERT_NULL(shell_root_cmd_get()); + TEST_ASSERT_EQUAL(0, shell_set_root_cmd("tcmd")); + TEST_ASSERT_EQUAL_PTR(&tcmd, shell_root_cmd_get()); + /* A bad name leaves the current selection alone */ + TEST_ASSERT_EQUAL(-EINVAL, shell_set_root_cmd("bogus")); + TEST_ASSERT_EQUAL_PTR(&tcmd, shell_root_cmd_get()); + TEST_ASSERT_EQUAL(0, shell_set_root_cmd(NULL)); + TEST_ASSERT_NULL(shell_root_cmd_get()); + harness_teardown(); +} + +static void test_shell_root_dispatch_beneath(void) +{ + harness_setup(); + shell_set_root_cmd("tcmd"); + + /* Subcommand of the root */ + feed("set 7\r"); + TEST_ASSERT_EQUAL(2, seen_argc); + TEST_ASSERT_EQUAL_STRING("set", seen_argv[0]); + TEST_ASSERT_EQUAL_STRING("7", seen_argv[1]); + + /* Free text goes to the root's own handler as arguments */ + feed("secret\r"); + TEST_ASSERT_EQUAL(2, seen_argc); + TEST_ASSERT_EQUAL_STRING("tcmd", seen_argv[0]); + TEST_ASSERT_EQUAL_STRING("secret", seen_argv[1]); + + /* Blank line does nothing */ + seen_argc = 0; + feed(" \r"); + TEST_ASSERT_EQUAL(0, seen_argc); + harness_teardown(); +} + +static void test_shell_root_rejects_other_roots(void) +{ + harness_setup(); + shell_set_root_cmd("tcmd"); + + feed("other\r"); + TEST_ASSERT_EQUAL(0, other_hits); + TEST_ASSERT_EQUAL_STRING("other", seen_argv[1]); + + /* help is itself a root command, so it is unreachable too: it lands on + * the root handler as an argument instead of listing the tree */ + feed("help\r"); + TEST_ASSERT_EQUAL_STRING("help", seen_argv[1]); + TEST_ASSERT_NULL(strstr(cap_buf, "Available commands")); + TEST_ASSERT_NULL(strstr(cap_buf, "other command")); + + /* Positive control: with the tree restored, help does list it */ + shell_set_root_cmd(NULL); + cap_clear(); + feed("help\r"); + TEST_ASSERT_NOT_NULL(strstr(cap_buf, "other command")); + harness_teardown(); +} + +static void test_shell_root_clear_restores_tree(void) +{ + harness_setup(); + shell_set_root_cmd("tcmd"); + shell_set_root_cmd(NULL); + + feed("other\r"); + TEST_ASSERT_EQUAL(1, other_hits); + harness_teardown(); +} + +#ifdef CONFIG_ZSHELL_TAB_COMPLETION +static void test_shell_root_completion(void) +{ + harness_setup(); + shell_set_root_cmd("tcmd"); + + /* Completes beneath the root */ + feed("g\t"); + TEST_ASSERT_EQUAL(4, tsh.ctx.cmd_buff_len); + TEST_ASSERT_EQUAL_MEMORY("get ", tsh.ctx.cmd_buff, 4); + + /* Does not see other root commands */ + feed("\x03"); + cap_clear(); + feed("o\t"); + TEST_ASSERT_EQUAL(1, tsh.ctx.cmd_buff_len); + TEST_ASSERT_EQUAL_STRING("o\a", cap_buf); + + /* Empty line lists only the root's subcommands */ + feed("\x03"); + cap_clear(); + feed("\t"); + TEST_ASSERT_NOT_NULL(strstr(cap_buf, "get")); + TEST_ASSERT_NOT_NULL(strstr(cap_buf, "set")); + TEST_ASSERT_NULL(strstr(cap_buf, "other")); + + /* Completed text stays obscured */ + feed("\x03"); + shell_obscure_set(&tsh, true); + cap_clear(); + feed("g\t"); + TEST_ASSERT_EQUAL_MEMORY("get ", tsh.ctx.cmd_buff, 4); + TEST_ASSERT_EQUAL_STRING("*** ", cap_buf); + harness_teardown(); +} +#endif + +/* ---------------------------------------------------------------- + * Prompt change + * ---------------------------------------------------------------- */ + +#ifdef CONFIG_ZSHELL_PROMPT_CHANGE +static void test_shell_prompt_change(void) +{ + char too_long[CONFIG_ZSHELL_PROMPT_BUFF_SIZE + 1]; + + memset(too_long, 'x', sizeof(too_long) - 1); + too_long[sizeof(too_long) - 1] = '\0'; + + harness_setup(); + TEST_ASSERT_EQUAL(-EINVAL, shell_prompt_change(&tsh, NULL)); + TEST_ASSERT_EQUAL(-EINVAL, shell_prompt_change(&tsh, too_long)); + /* Exactly BUFF_SIZE - 1 chars fits */ + too_long[CONFIG_ZSHELL_PROMPT_BUFF_SIZE - 1] = '\0'; + TEST_ASSERT_EQUAL(0, shell_prompt_change(&tsh, too_long)); + + TEST_ASSERT_EQUAL(0, shell_prompt_change(&tsh, "login: ")); + feed("\r"); + TEST_ASSERT_EQUAL_STRING("\r\nlogin: ", cap_buf); + harness_teardown(); +} +#endif + +/* ---------------------------------------------------------------- + * History purge + * ---------------------------------------------------------------- */ + +#ifdef CONFIG_ZSHELL_HISTORY +static void test_shell_history_purge(void) +{ + char buf[CONFIG_ZSHELL_CMD_BUFF_SIZE]; + uint16_t len; + + harness_setup(); + feed("hunter2\r"); + TEST_ASSERT_TRUE(shell_history_get(&tsh.history, true, buf, &len)); + shell_history_reset(&tsh.history); + + shell_history_purge(&tsh); + TEST_ASSERT_FALSE(shell_history_get(&tsh.history, true, buf, &len)); + + /* Still usable afterwards */ + feed("later\r"); + TEST_ASSERT_TRUE(shell_history_get(&tsh.history, true, buf, &len)); + TEST_ASSERT_EQUAL_STRING("later", buf); + harness_teardown(); +} +#endif + /* ---------------------------------------------------------------- * Test group runner * ---------------------------------------------------------------- */ @@ -224,4 +616,28 @@ void test_shell_group(void) RUN_TEST(test_shell_history_skip_duplicate); RUN_TEST(test_shell_history_empty); #endif + + /* Line editing + dispatch */ + RUN_TEST(test_shell_insert_echo); + RUN_TEST(test_shell_backspace); + RUN_TEST(test_shell_dispatch_subcmd); + RUN_TEST(test_shell_dispatch_unknown); + + /* Login primitives (#59) */ + RUN_TEST(test_shell_obscure_set_returns_previous); + RUN_TEST(test_shell_obscure_insert_delete); + RUN_TEST(test_shell_obscure_default_off); + RUN_TEST(test_shell_set_root_cmd_valid_invalid); + RUN_TEST(test_shell_root_dispatch_beneath); + RUN_TEST(test_shell_root_rejects_other_roots); + RUN_TEST(test_shell_root_clear_restores_tree); +#ifdef CONFIG_ZSHELL_TAB_COMPLETION + RUN_TEST(test_shell_root_completion); +#endif +#ifdef CONFIG_ZSHELL_PROMPT_CHANGE + RUN_TEST(test_shell_prompt_change); +#endif +#ifdef CONFIG_ZSHELL_HISTORY + RUN_TEST(test_shell_history_purge); +#endif }