Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build artifacts
build/
build*/
sdkconfig
sdkconfig.old
*.pyc
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions components/zshell/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions components/zshell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 |
Expand Down
68 changes: 68 additions & 0 deletions components/zshell/include/boreas/zshell/shell.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ extern "C" {
#define CONFIG_ZSHELL_PROMPT "boreas> "
#endif

#ifndef CONFIG_ZSHELL_CMD_ROOT
#define CONFIG_ZSHELL_CMD_ROOT ""
#endif

/* ----------------------------------------------------------------
* History
* ---------------------------------------------------------------- */
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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 */
Expand Down
103 changes: 92 additions & 11 deletions components/zshell/src/shell.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include "zshell/shell.h"

#include <errno.h>
#include <string.h>

#include "zsys/log.h"
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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
Expand Down
Loading
Loading