Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

embshell

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

Features

  • 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 Ctrl shortcuts 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 inf runs device 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.

Repository layout

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.

Porting

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.

Command table

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;
  • sub turns the entry into a group: dispatch and Tab completion descend into the subtable, to any depth. func may be NULL for a group; entering the bare group name then lists its subcommands. If func is 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 = 1 skips argument parsing: the handler receives the rest of the line verbatim as argv[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.

Key bindings

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.

Login

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 */

Configuration

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

Reference port (STM32 + ThreadX/USBX)

shell_app.c shows a complete integration:

  • shell_printf() formats into a static buffer, converts \n to \r\n and writes through ux_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 lone Esc press is told apart from a sequence.
  • shellTaskHandler() is the ThreadX thread entry: it waits for USB enumeration, runs the shell, and after quit waits for the cable to be replugged before offering a new session.

About

A tiny, portable command-line shell for embedded C projects readline-style editing, tab completion, nested commands.

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages