From 7f41fa4d496c4722481d4ec045cff75b4b911e09 Mon Sep 17 00:00:00 2001 From: swoiszwillo Date: Fri, 4 Sep 2026 21:49:20 -0600 Subject: [PATCH 1/9] refactor(zshell): single echo path and prompt accessor Route every cmd_buff-to-terminal write through shell_write_cmd() and every prompt print through shell_print_prompt(), replacing six copies of the default_prompt lookup. shell_process_char() becomes internal API so the host tests can drive line editing without the shell thread. No behavior change. Prep for obscured input and runtime prompt changes. Co-Authored-By: Claude Fable 5.1 --- .../zshell/include/boreas/zshell/shell.h | 6 ++++ components/zshell/src/shell.c | 28 +++++++++++-------- components/zshell/src/shell_completion.c | 5 ++-- components/zshell/src/shell_vt100.c | 27 ++++++------------ 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/components/zshell/include/boreas/zshell/shell.h b/components/zshell/include/boreas/zshell/shell.h index 209a63b..d0abd83 100644 --- a/components/zshell/include/boreas/zshell/shell.h +++ b/components/zshell/include/boreas/zshell/shell.h @@ -215,6 +215,12 @@ 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); int shell_execute(struct shell *sh); diff --git a/components/zshell/src/shell.c b/components/zshell/src/shell.c index 7427265..71babca 100644 --- a/components/zshell/src/shell.c +++ b/components/zshell/src/shell.c @@ -19,13 +19,19 @@ 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) { const char *prompt = sh->default_prompt ? sh->default_prompt : CONFIG_ZSHELL_PROMPT; + shell_write(sh, prompt, strlen(prompt)); } -static void shell_process_char(struct shell *sh, char ch) +void shell_write_cmd(const struct shell *sh, uint16_t off, uint16_t len) +{ + shell_write(sh, &sh->ctx.cmd_buff[off], len); +} + +void shell_process_char(struct shell *sh, char ch) { /* VT100 escape sequence handling */ #ifdef CONFIG_ZSHELL_VT100 @@ -74,8 +80,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 +110,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 +152,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 +169,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 +197,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); diff --git a/components/zshell/src/shell_completion.c b/components/zshell/src/shell_completion.c index 21adfa6..b3144df 100644 --- a/components/zshell/src/shell_completion.c +++ b/components/zshell/src/shell_completion.c @@ -173,7 +173,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_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; From 93afc0ae73b3f76b558d9f21347913dd4511f631 Mon Sep 17 00:00:00 2001 From: swoiszwillo Date: Fri, 4 Sep 2026 21:49:20 -0600 Subject: [PATCH 2/9] test(zshell): capture-transport harness for line editing and dispatch Hand-built struct shell with a write-capturing transport and no thread, plus a two-command root tree. Covers insert echo, backspace, subcommand dispatch and unknown-command handling. Co-Authored-By: Claude Fable 5.1 --- test/main/test_shell.c | 151 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 1 deletion(-) diff --git a/test/main/test_shell.c b/test/main/test_shell.c index 84310b7..7c817db 100644 --- a/test/main/test_shell.c +++ b/test/main/test_shell.c @@ -3,7 +3,7 @@ * 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 @@ -202,6 +202,149 @@ 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; + +/* 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 struct shell_static_entry other = { + .syntax = "other", + .help = "other command", + .handler = tcmd_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} */ +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); + + memset(&tsh, 0, sizeof(tsh)); + tsh.iface = &cap_transport; + tsh.default_prompt = "t> "; + 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; + cap_clear(); +} + +static void harness_teardown(void) +{ + _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(); +} + /* ---------------------------------------------------------------- * Test group runner * ---------------------------------------------------------------- */ @@ -224,4 +367,10 @@ 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); } From 9aa35f3b3dcdee407ca78ce1a0ab71c511e19c7c Mon Sep 17 00:00:00 2001 From: swoiszwillo Date: Fri, 4 Sep 2026 21:56:16 -0600 Subject: [PATCH 3/9] feat(zshell): obscured input, root command selection, runtime prompt Zephyr-compatible primitives for application login flows (#59): - shell_obscure_set(): echo '*' for every cmd_buff write; returns the previous state. CONFIG_ZSHELL_START_OBSCURED sets the initial state. - shell_set_root_cmd(): one selection shared by all instances, as upstream. Dispatch and tab completion place the root at argv[0] so typed input can only walk beneath it; help is itself a root command and therefore unreachable while a root is selected. NULL restores the tree, an unknown name returns -EINVAL. CONFIG_ZSHELL_CMD_ROOT (string) applies at init and warns if it names no command. - shell_prompt_change(): bounded by CONFIG_ZSHELL_PROMPT_BUFF_SIZE under CONFIG_ZSHELL_PROMPT_CHANGE (default y, 20); -EINVAL on NULL/oversize, -EPERM when disabled, as upstream. - shell_history_purge(): takes the shell because history is embedded here rather than pointed to; no-op without CONFIG_ZSHELL_HISTORY. shell_root_cmd_find() replaces the three private root lookups. Co-Authored-By: Claude Fable 5.1 --- components/zshell/Kconfig | 37 +++++++++++ .../zshell/include/boreas/zshell/shell.h | 48 ++++++++++++++ components/zshell/src/shell.c | 64 +++++++++++++++++++ components/zshell/src/shell_cmd_reg.c | 33 ++++++++++ components/zshell/src/shell_completion.c | 23 +++---- components/zshell/src/shell_parse.c | 45 ++++++------- 6 files changed, 215 insertions(+), 35 deletions(-) diff --git a/components/zshell/Kconfig b/components/zshell/Kconfig index bcb2929..64e58b5 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" + default n + 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(). Mirrors CONFIG_SHELL_CMD_ROOT. + config ZSHELL_HISTORY bool "Enable command history" default y diff --git a/components/zshell/include/boreas/zshell/shell.h b/components/zshell/include/boreas/zshell/shell.h index d0abd83..36d4c0e 100644 --- a/components/zshell/include/boreas/zshell/shell.h +++ b/components/zshell/include/boreas/zshell/shell.h @@ -57,6 +57,14 @@ extern "C" { #define CONFIG_ZSHELL_PROMPT "boreas> " #endif +#ifndef CONFIG_ZSHELL_PROMPT_BUFF_SIZE +#define CONFIG_ZSHELL_PROMPT_BUFF_SIZE 20 +#endif + +#ifndef CONFIG_ZSHELL_CMD_ROOT +#define CONFIG_ZSHELL_CMD_ROOT "" +#endif + /* ---------------------------------------------------------------- * History * ---------------------------------------------------------------- */ @@ -88,6 +96,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 */ @@ -124,6 +136,9 @@ extern struct shell_static_entry *_shell_root_cmds[]; extern size_t _shell_root_cmd_count; void shell_cmd_register(struct shell_static_entry *entry); +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); /** * Register a root-level command. @@ -205,6 +220,39 @@ 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. + */ +int shell_obscure_set(const struct shell *sh, bool obscure); + +/** + * Change the prompt at runtime. Requires CONFIG_ZSHELL_PROMPT_CHANGE. + * + * @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. + */ +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; help and tab completion see only its subcommands. Applies + * to every shell instance, as upstream. NULL restores the full tree. + * + * @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. + * + * 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. No-op without CONFIG_ZSHELL_HISTORY. + */ +void shell_history_purge(const struct shell *sh); + /* ---------------------------------------------------------------- * Internal API (used by shell subsystem files) * ---------------------------------------------------------------- */ diff --git a/components/zshell/src/shell.c b/components/zshell/src/shell.c index 71babca..ad2e52e 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" @@ -21,16 +22,67 @@ LOG_MODULE_REGISTER(shell, LOG_LEVEL_INF); 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)); } +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; + } + strcpy(ctx->prompt, prompt); + 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 */ @@ -266,6 +318,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; @@ -286,6 +345,11 @@ int shell_init(struct shell *sh, const struct shell_transport *transport, const shell_builtins_register(); shell_cmd_sort_root(); + if (CONFIG_ZSHELL_CMD_ROOT[0] != '\0' && 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_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 b3144df..6f8a2bb 100644 --- a/components/zshell/src/shell_completion.c +++ b/components/zshell/src/shell_completion.c @@ -25,13 +25,22 @@ 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]; + const struct shell_static_entry *root = shell_root_cmd_get(); + 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], as in shell_execute(); the typed + * tokens then only ever walk beneath it. */ + if (root != NULL) { + argv[0] = (char *)root->syntax; + argc = 1 + shell_make_argv(line_copy, &argv[1], CONFIG_ZSHELL_MAX_ARGC); + } else { + argc = shell_make_argv(line_copy, argv, CONFIG_ZSHELL_MAX_ARGC); + } /* Determine if we're completing a partial token or starting a new one */ bool completing_partial = @@ -52,15 +61,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++) { diff --git a/components/zshell/src/shell_parse.c b/components/zshell/src/shell_parse.c index 98d7443..3b8fd3a 100644 --- a/components/zshell/src/shell_parse.c +++ b/components/zshell/src/shell_parse.c @@ -55,19 +55,6 @@ 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) -{ - 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; -} - /** * Find a subcommand by name within a subcmd set. */ @@ -93,23 +80,33 @@ 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) { - 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; + if (cmd != NULL) { + /* Root selected: typed input is its argument list */ + argv[0] = (char *)cmd->syntax; + argc = 1 + shell_make_argv(line_copy, &argv[1], CONFIG_ZSHELL_MAX_ARGC); + if (argc == 1) { + return 0; /* empty line */ + } + } else { + argc = shell_make_argv(line_copy, argv, CONFIG_ZSHELL_MAX_ARGC); + if (argc == 0) { + return 0; /* empty line */ + } + 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 */ From 504895c471904491156e035a6954fb09aaba971d Mon Sep 17 00:00:00 2001 From: swoiszwillo Date: Fri, 4 Sep 2026 21:56:16 -0600 Subject: [PATCH 4/9] test(zshell): cover obscure, root selection, prompt change, history purge Ten cases from #59: obscure toggle return value, obscured insert/delete and mid-line rewrite, default-off state, valid/invalid root selection, dispatch beneath the root, rejection of sibling roots and help, clearing the root, root-aware completion, prompt bounds, and purge. Co-Authored-By: Claude Fable 5.1 --- test/main/test_shell.c | 241 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 240 insertions(+), 1 deletion(-) diff --git a/test/main/test_shell.c b/test/main/test_shell.c index 7c817db..54547dd 100644 --- a/test/main/test_shell.c +++ b/test/main/test_shell.c @@ -6,6 +6,7 @@ * line editing and dispatch (via a capture transport, no shell thread). */ +#include #include #include "unity.h" @@ -253,10 +254,21 @@ static struct shell_static_entry tcmd = { .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 = tcmd_handler, + .handler = other_handler, }; static size_t saved_root_count; @@ -280,6 +292,9 @@ static void harness_setup(void) 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; @@ -287,11 +302,13 @@ static void harness_setup(void) 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])); } @@ -345,6 +362,210 @@ static void test_shell_dispatch_unknown(void) 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"); + TEST_ASSERT_NOT_NULL(strstr(cap_buf, "Unknown command: za")); + 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 */ + feed("help\r"); + TEST_ASSERT_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")); + 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 * ---------------------------------------------------------------- */ @@ -373,4 +594,22 @@ void test_shell_group(void) 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 } From b2e54e3ac104b694c65cb2eda9bdf78e87c931dc Mon Sep 17 00:00:00 2001 From: swoiszwillo Date: Fri, 4 Sep 2026 21:57:43 -0600 Subject: [PATCH 5/9] docs(examples): login/logout in shell_demo, modeled on Zephyr's sample `login ` purges history, clears obscuring, restores the full tree and prompt; `logout` reverses it. `idf.py -DLOGIN=y build` appends sdkconfig.login (START_OBSCURED=y, CMD_ROOT="login") so the demo boots locked. The command-line SDKCONFIG_DEFAULTS override cannot be used here because the project CMake sets the list itself. Co-Authored-By: Claude Fable 5.1 --- examples/shell_demo/CMakeLists.txt | 4 ++ examples/shell_demo/README.md | 19 +++++++++ examples/shell_demo/main/main.c | 65 +++++++++++++++++++++++++++-- examples/shell_demo/sdkconfig.login | 4 ++ 4 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 examples/shell_demo/sdkconfig.login diff --git a/examples/shell_demo/CMakeLists.txt b/examples/shell_demo/CMakeLists.txt index ceceb36..74ba5dc 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 behind `login `: 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..49bc69c 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,23 @@ idf.py build idf.py -p /dev/cu.usbmodemXXXX flash monitor ``` +## Login / Logout + +Start the demo locked so that only `login ` is reachable, echo shows +`*`, and the prompt reads `login: `: + +```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..8dad4d8 --- /dev/null +++ b/examples/shell_demo/sdkconfig.login @@ -0,0 +1,4 @@ +# Overlay: start the shell demo locked behind `login `. +# Appended to SDKCONFIG_DEFAULTS by CMakeLists.txt when -DLOGIN=y is given. +CONFIG_ZSHELL_START_OBSCURED=y +CONFIG_ZSHELL_CMD_ROOT="login" From 8fcde1e99c7da7a3d6b4656bb511a4546436b2e4 Mon Sep 17 00:00:00 2001 From: swoiszwillo Date: Fri, 4 Sep 2026 21:57:44 -0600 Subject: [PATCH 6/9] docs(zshell): document login primitives and new Kconfig options Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 8 ++++++++ components/zshell/README.md | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) 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/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 | From 283c44013982940b4afbbba0bb5cb85f3370837f Mon Sep 17 00:00:00 2001 From: swoiszwillo Date: Fri, 4 Sep 2026 22:06:51 -0600 Subject: [PATCH 7/9] fix(zshell): address review of the login primitives - Tab completion echoed completed text in clear while obscured; route the two insert writes through shell_write_cmd() and test it. - shell_prompt_change() copies under wr_mtx so it cannot interleave with an in-progress write; @note the -EBUSY divergence from upstream. - Apply CONFIG_ZSHELL_CMD_ROOT once: the selection is shared, so a second shell_init() after a login must not relock the first instance. - shell_make_argv_root() replaces the duplicated root-at-argv[0] shape in dispatch and completion; cmd_help uses shell_root_cmd_find(). - Doxygen: @param on the new APIs, accurate help/completion wording, history purge threading note; root lookup helpers move to internal API. - Drop the dead CONFIG_ZSHELL_PROMPT_BUFF_SIZE fallback define. Co-Authored-By: Claude Fable 5.1 --- components/zshell/Kconfig | 3 +- .../zshell/include/boreas/zshell/shell.h | 34 ++++++++++++------- components/zshell/src/shell.c | 17 ++++++++-- components/zshell/src/shell_builtins.c | 30 ++++++++-------- components/zshell/src/shell_completion.c | 15 +++----- components/zshell/src/shell_parse.c | 28 ++++++++------- test/main/test_shell.c | 8 +++++ 7 files changed, 80 insertions(+), 55 deletions(-) diff --git a/components/zshell/Kconfig b/components/zshell/Kconfig index 64e58b5..0b180e1 100644 --- a/components/zshell/Kconfig +++ b/components/zshell/Kconfig @@ -76,7 +76,8 @@ menu "Boreas Shell (zshell)" 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(). Mirrors CONFIG_SHELL_CMD_ROOT. + with shell_set_root_cmd(). Applied when the first shell instance + initializes. Mirrors CONFIG_SHELL_CMD_ROOT. config ZSHELL_HISTORY bool "Enable command history" diff --git a/components/zshell/include/boreas/zshell/shell.h b/components/zshell/include/boreas/zshell/shell.h index 36d4c0e..d345bad 100644 --- a/components/zshell/include/boreas/zshell/shell.h +++ b/components/zshell/include/boreas/zshell/shell.h @@ -57,10 +57,6 @@ extern "C" { #define CONFIG_ZSHELL_PROMPT "boreas> " #endif -#ifndef CONFIG_ZSHELL_PROMPT_BUFF_SIZE -#define CONFIG_ZSHELL_PROMPT_BUFF_SIZE 20 -#endif - #ifndef CONFIG_ZSHELL_CMD_ROOT #define CONFIG_ZSHELL_CMD_ROOT "" #endif @@ -136,9 +132,6 @@ extern struct shell_static_entry *_shell_root_cmds[]; extern size_t _shell_root_cmd_count; void shell_cmd_register(struct shell_static_entry *entry); -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); /** * Register a root-level command. @@ -230,16 +223,23 @@ 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. */ 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; help and tab completion see only its subcommands. Applies - * to every shell instance, as upstream. NULL restores the full tree. + * 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); @@ -247,9 +247,13 @@ int shell_set_root_cmd(const char *cmd); /** * Drop all history entries, e.g. after a login command consumed a password. * - * 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. No-op without CONFIG_ZSHELL_HISTORY. + * @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); @@ -259,6 +263,9 @@ void shell_history_purge(const struct shell *sh); /* 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); @@ -271,6 +278,9 @@ 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 ad2e52e..78234a5 100644 --- a/components/zshell/src/shell.c +++ b/components/zshell/src/shell.c @@ -39,7 +39,10 @@ int shell_prompt_change(const struct shell *sh, const char *prompt) 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; @@ -305,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; @@ -345,9 +350,15 @@ int shell_init(struct shell *sh, const struct shell_transport *transport, const shell_builtins_register(); shell_cmd_sort_root(); - if (CONFIG_ZSHELL_CMD_ROOT[0] != '\0' && 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); + /* 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 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_completion.c b/components/zshell/src/shell_completion.c index 6f8a2bb..9f44345 100644 --- a/components/zshell/src/shell_completion.c +++ b/components/zshell/src/shell_completion.c @@ -26,21 +26,14 @@ void shell_completion(struct shell *sh) { char line_copy[CONFIG_ZSHELL_CMD_BUFF_SIZE]; char *argv[CONFIG_ZSHELL_MAX_ARGC + 1]; - const struct shell_static_entry *root = shell_root_cmd_get(); 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'; - /* A selected root sits at argv[0], as in shell_execute(); the typed - * tokens then only ever walk beneath it. */ - if (root != NULL) { - argv[0] = (char *)root->syntax; - argc = 1 + shell_make_argv(line_copy, &argv[1], CONFIG_ZSHELL_MAX_ARGC); - } else { - 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 = @@ -135,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; @@ -161,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); } } diff --git a/components/zshell/src/shell_parse.c b/components/zshell/src/shell_parse.c index 3b8fd3a..d868cdf 100644 --- a/components/zshell/src/shell_parse.c +++ b/components/zshell/src/shell_parse.c @@ -55,6 +55,17 @@ size_t shell_make_argv(char *line, char **argv, size_t max_argc) return argc; } +size_t shell_make_argv_root(char *line, char **argv) +{ + const struct shell_static_entry *root = shell_root_cmd_get(); + + if (root == NULL) { + return shell_make_argv(line, argv, CONFIG_ZSHELL_MAX_ARGC); + } + argv[0] = (char *)root->syntax; + return 1 + shell_make_argv(line, &argv[1], CONFIG_ZSHELL_MAX_ARGC); +} + /** * Find a subcommand by name within a subcmd set. */ @@ -90,18 +101,11 @@ int shell_execute(struct shell *sh) memcpy(line_copy, sh->ctx.cmd_buff, sh->ctx.cmd_buff_len); line_copy[sh->ctx.cmd_buff_len] = '\0'; - if (cmd != NULL) { - /* Root selected: typed input is its argument list */ - argv[0] = (char *)cmd->syntax; - argc = 1 + shell_make_argv(line_copy, &argv[1], CONFIG_ZSHELL_MAX_ARGC); - if (argc == 1) { - return 0; /* empty line */ - } - } else { - argc = shell_make_argv(line_copy, argv, CONFIG_ZSHELL_MAX_ARGC); - if (argc == 0) { - return 0; /* empty line */ - } + argc = shell_make_argv_root(line_copy, argv); + if (argc == (cmd != NULL ? 1 : 0)) { + return 0; /* empty line */ + } + if (cmd == NULL) { cmd = shell_root_cmd_find(argv[0]); if (cmd == NULL) { shell_error(sh, "Unknown command: %s", argv[0]); diff --git a/test/main/test_shell.c b/test/main/test_shell.c index 54547dd..e142aa5 100644 --- a/test/main/test_shell.c +++ b/test/main/test_shell.c @@ -510,6 +510,14 @@ static void test_shell_root_completion(void) 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 From 54797dcd95299538032ba3804e09951099ba338d Mon Sep 17 00:00:00 2001 From: swoiszwillo Date: Fri, 18 Sep 2026 09:25:55 -0600 Subject: [PATCH 8/9] fix(zshell): make the help-gating test real, note obscure/prompt threading - Register the builtins in the test harness so `help` exists; assert it lands on the root handler while a root is selected and lists the tree once cleared (previously passed vacuously because help was unregistered). - @note on shell_obscure_set(): plain bool, not upstream's atomic flag. - Extend the shell_prompt_change() note: the prompt is read outside the lock when printed, so call from the shell thread. - Drop the redundant `default n` on ZSHELL_START_OBSCURED. - .gitignore build*/ so variant build dirs stay untracked. Co-Authored-By: Claude Fable 5.1 --- .gitignore | 2 +- components/zshell/Kconfig | 1 - components/zshell/include/boreas/zshell/shell.h | 6 +++++- test/main/test_shell.c | 14 ++++++++++++-- 4 files changed, 18 insertions(+), 5 deletions(-) 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/components/zshell/Kconfig b/components/zshell/Kconfig index 0b180e1..f625450 100644 --- a/components/zshell/Kconfig +++ b/components/zshell/Kconfig @@ -61,7 +61,6 @@ menu "Boreas Shell (zshell)" config ZSHELL_START_OBSCURED bool "Display asterisk when echoing" - default n depends on ZSHELL help Echo '*' instead of the typed character. Used for login prompts; diff --git a/components/zshell/include/boreas/zshell/shell.h b/components/zshell/include/boreas/zshell/shell.h index d345bad..cb08d4c 100644 --- a/components/zshell/include/boreas/zshell/shell.h +++ b/components/zshell/include/boreas/zshell/shell.h @@ -217,6 +217,9 @@ 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); @@ -229,7 +232,8 @@ int shell_obscure_set(const struct shell *sh, bool obscure); * 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. + * 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); diff --git a/test/main/test_shell.c b/test/main/test_shell.c index e142aa5..e05686c 100644 --- a/test/main/test_shell.c +++ b/test/main/test_shell.c @@ -280,7 +280,7 @@ static void cap_clear(void) cap_buf[0] = '\0'; } -/* Fresh shell with a capture transport and a root tree of {other, tcmd} */ +/* 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; @@ -288,6 +288,7 @@ static void harness_setup(void) _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; @@ -468,9 +469,18 @@ static void test_shell_root_rejects_other_roots(void) 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 */ + /* 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(); } From 6cae7cea56672218b1ec787b4b35780d220c20ab Mon Sep 17 00:00:00 2001 From: swoiszwillo Date: Fri, 25 Sep 2026 12:01:22 -0600 Subject: [PATCH 9/9] fix(zshell): test config portability and locked-login docs - test: the obscure dispatch expectation now follows the VT100 guard (buffer is "a", not "za", when the mid-line insert is compiled out). - test: BUILD_ASSERT that CONFIG_ZSHELL_PROMPT_BUFF_SIZE holds the harness and "login: " prompts instead of strcpy'ing past a 2..7 byte buffer into neighbouring ctx fields. - docs(examples): with `login` as the root command the typed line is its argument, so the locked prompt takes the bare password, not `login `. Co-Authored-By: Claude Fable 5.1 --- examples/shell_demo/CMakeLists.txt | 2 +- examples/shell_demo/README.md | 5 +++-- examples/shell_demo/sdkconfig.login | 2 +- test/main/test_shell.c | 10 ++++++++++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/examples/shell_demo/CMakeLists.txt b/examples/shell_demo/CMakeLists.txt index 74ba5dc..09a0b98 100644 --- a/examples/shell_demo/CMakeLists.txt +++ b/examples/shell_demo/CMakeLists.txt @@ -3,7 +3,7 @@ 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 behind `login `: idf.py -DLOGIN=y build +# Start locked (type the password alone at the `login: ` prompt): idf.py -DLOGIN=y build if(LOGIN) list(APPEND SDKCONFIG_DEFAULTS sdkconfig.login) endif() diff --git a/examples/shell_demo/README.md b/examples/shell_demo/README.md index 49bc69c..c9d9035 100644 --- a/examples/shell_demo/README.md +++ b/examples/shell_demo/README.md @@ -48,8 +48,9 @@ idf.py -p /dev/cu.usbmodemXXXX flash monitor ## Login / Logout -Start the demo locked so that only `login ` is reachable, echo shows -`*`, and the prompt reads `login: `: +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 diff --git a/examples/shell_demo/sdkconfig.login b/examples/shell_demo/sdkconfig.login index 8dad4d8..962225b 100644 --- a/examples/shell_demo/sdkconfig.login +++ b/examples/shell_demo/sdkconfig.login @@ -1,4 +1,4 @@ -# Overlay: start the shell demo locked behind `login `. +# 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 e05686c..05776be 100644 --- a/test/main/test_shell.c +++ b/test/main/test_shell.c @@ -229,6 +229,12 @@ 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]; @@ -404,7 +410,11 @@ static void test_shell_obscure_insert_delete(void) /* 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(); }