A tiny, portable, readline-style command shell for embedded systems, written in pure C.
The core is two files (shell.c / shell.h) with no OS or hardware
dependency: all I/O goes through three functions you implement for your
platform. A reference port running the shell over USB CDC ACM with
ThreadX/USBX on an STM32 is included (shell_app.c / shell_app.h).
Cyclone5:~$ help
Available commands:
clear : Clear screen
device : Device commands
help : Display the list of commands
history : Show command history
quit : Quit the shell
version : Show the shell version
Cyclone5:~$ device info
Device : (DEVID 0x483, rev 0x1003)
UID : 0043002E-31385119-33383438
- Readline-style line editing — cursor movement, insert/overwrite
mode, word-wise motion and deletion, kill-to-end, clear-screen; both
the cursor keys and the classic
Ctrlshortcuts work. - Tab completion — completes commands and subcommands at any nesting depth; on multiple matches it lists the candidates and extends the word to their longest common prefix (bash-like).
- Command history — browse with Up/Down (or
Ctrl+P/Ctrl+N); duplicate entries are moved to the front instead of repeated. - Nested subcommands — command tables may nest to any depth
(
date set 12:00); entering a bare group name lists its subcommands. Unique prefixes are accepted (dev infrunsdevice info). - Quoting — arguments may be quoted in whole or in part
(
"ab cd",t0.txt="a b"), with\"\'\\escapes; unterminated quotes are reported. A command can also opt out of parsing and receive the raw line tail (raw_args). - Optional login — username/password prompt with masked input before the shell starts.
- ANSI colors — colored prompt, help listing and error messages.
- Small and self-contained — no external dependencies; the heap is used only for history entries. Line length, history depth and argument count are compile-time constants.
| File | Purpose |
|---|---|
shell.c/.h |
Portable shell core: line editing, completion, history, parsing and dispatch. No platform dependency. |
shell_app.c/.h |
Reference port: shell I/O over USB CDC ACM (USBX), running in its own ThreadX thread on an STM32, plus an example command table. |
Implement the three I/O functions declared in shell.h on top of
your transport (UART, USB CDC, RTT, telnet, ...):
/* Formatted output to the terminal. */
void shell_printf(const char *fmt, ...);
/* Blocking read of one received byte. */
int shell_getchar(void);
/* 1 when a received byte is already buffered (a read would not
block). Used to tell a lone <Esc> press from an escape sequence. */
int shell_char_pending(void);Then define a command table and run the shell:
#include "shell.h"
static int32_t version_cmd(int32_t argc, char **argv) {
shell_printf("v1.0\n");
return 0;
}
static const cmd_t cmdlist[] = {
{"clear", "Clear screen", clear_cmd, NULL, 0},
{"help", "Display the list of commands", help_cmd, NULL, 0},
{"history", "Show command history", history_cmd, NULL, 0},
{"quit", "Quit the shell", quit_cmd, NULL, 0},
{"version", "Show the version", version_cmd, NULL, 0},
{NULL, NULL, NULL, NULL, 0}};
/* Blocks until the "quit" command. Pass a username and password to
require a login first, or NULL/NULL for none. */
shell("board:~$ ", cmdlist, NULL, NULL);help_cmd, clear_cmd, history_cmd and quit_cmd are
provided by the core, ready to be registered.
Each entry of the NULL-terminated cmd_t table describes one command:
typedef struct cmd_s {
const char *name; /* command word */
const char *comment; /* one-line help text */
int32_t (*func)(int32_t argc, char *argv[]);
const struct cmd_s *sub; /* optional NULL-terminated subcommand table */
uint8_t raw_args; /* 1: pass the rest of the line unparsed */
} cmd_t;subturns the entry into a group: dispatch and Tab completion descend into the subtable, to any depth.funcmay be NULL for a group; entering the bare group name then lists its subcommands. Iffuncis set as well, it runs when the group name is entered alone:static const cmd_t device_cmdlist[] = { {"info", "Show MCU identity", device_info_cmd, NULL, 0}, {"reboot", "Restart the device", device_reboot_cmd, NULL, 0}, {NULL, NULL, NULL, NULL, 0}}; /* in the top-level table: */ {"device", "Device commands", device_info_cmd, device_cmdlist, 0},
raw_args = 1skips argument parsing: the handler receives the rest of the line verbatim asargv[1]— no blank splitting, quotes kept as typed. Useful for free-text commands (echo,eval, ...).Handlers get the classic
argc/argv;argv[0]is the (canonical) command name.
The shell decodes standard VT100/ANSI escape sequences and echoes all
input itself — use any serial terminal (PuTTY, Tera Term, minicom,
screen) with local echo off. CR, LF and CRLF line endings are all
accepted.
Pass a username and password to shell() to require a login before
the prompt appears; password input is masked. In the reference port
this is switched with a compile-time flag:
#define SHELL_LOGIN 1 /* prompts for user/password from shell_app.c */Compile-time constants at the top of shell.c:
| Constant | Default | Meaning |
|---|---|---|
MAX_INPUT_LINE |
80 | Maximum line length |
MAX_ARGS |
10 | Maximum argv entries per command |
HISTORY_SIZE |
5 | History depth (entries are heap-allocated) |
MAX_MATCHED_CMD |
20 | Completion candidates listed at most |
LOGIN_INPUT_MAX |
32 | Username/password buffer size |
shell_app.c shows a complete integration:
shell_printf()formats into a static buffer, converts\nto\r\nand writes throughux_device_class_cdc_acm_write().shell_getchar()reads USB packets chunk-wise and hands out one byte at a time; while the COM port is closed it sleeps instead of busy-spinning.shell_char_pending()reports whether the current USB packet still holds unread bytes — escape sequences arrive within one packet, which is how a loneEscpress is told apart from a sequence.shellTaskHandler()is the ThreadX thread entry: it waits for USB enumeration, runs the shell, and afterquitwaits for the cable to be replugged before offering a new session.