#shell #cli #command-line

no-std bin+lib nut-shell

A lightweight command-line interface library for embedded systems

1 unstable release

Uses new Rust 2024

new 0.1.0 Dec 6, 2025

#267 in Embedded development

MIT/Apache

200KB
4K SLoC

nut-shell

Interactive CLI for microcontrollers. No heap, no bloat.

A lightweight command shell library for #![no_std] Rust environments with optional async and authentication support.

Platform License


Overview

nut-shell provides essential CLI primitives for embedded systems with strict memory constraints. Built specifically for microcontrollers, it offers an interactive command-line interface over serial connections (UART/USB), with optional features including async/await support, login authentication, tab completion and command history.

Design Philosophy: Essential primitives only. No shell scripting, no dynamic allocation, no bloat.


Key Features

Core Functionality (Always Present)

  • Path-based navigation - Unix-style hierarchical commands (system/info, network/status)
  • Command execution - Synchronous command support with structured argument parsing
  • Input parsing - Terminal I/O with line editing (backspace, double-ESC clear)
  • Global commands - ls, ?, clear

Optional Features

  • Async commands - Supports async/await (Embassy compatible). Zero overhead when disabled. (Default: disabled)
  • Authentication - SHA-256 password hashing, login flow, session management, and access control enforcement (Default: disabled)
  • Tab completion - Command and path prefix matching (Default: enabled)
  • Command history - Arrow key navigation with configurable buffer (Default: enabled)

What This Library Excludes

  • ❌ Shell scripting (piping, variables, conditionals, command substitution)
  • ❌ Command aliases
  • ❌ Job control (background jobs, fg/bg)
  • ❌ Output paging
  • ❌ Persistent history across reboots

See docs/PHILOSOPHY.md for rationale.


Quick Start

Bare-Metal Pattern:

// 1. Implement `CharIo` trait for your platform
impl CharIo for MyIo {
    type Error = MyError;
    fn get_char(&mut self) -> Result<Option<char>, Self::Error> { /* ... */ }
    fn put_char(&mut self, c: char) -> Result<(), Self::Error> { /* ... */ }
}

// 2. Define command tree with metadata
const STATUS: CommandMeta<Level> = CommandMeta {
    id: "status",
    name: "status",
    description: "Show system status",
    access_level: Level::User,
    kind: CommandKind::Sync,
    min_args: 0,
    max_args: 0,
};

const SYSTEM: Directory<Level> = Directory {
    name: "system",
    description: "System commands",
    access_level: Level::User,
    children: &[Node::Command(&STATUS)],
};

const ROOT: Directory<Level> = Directory {
    name: "",
    description: "Root",
    access_level: Level::Guest,
    children: &[Node::Directory(&SYSTEM)],
};

// 3. Implement `CommandHandler` trait
impl CommandHandler<MyConfig> for MyHandler {
    fn execute_sync(&self, id: &str, args: &[&str]) -> Result<Response<MyConfig>, CliError> {
        match id {
            "status" => status_fn::<MyConfig>(args),
            _ => Err(CliError::CommandNotFound)
        }
    }
}

// 4. Create shell and run main loop
let handler = MyHandler;    // Your `CommandHandler` implementation
let io = MyIo::new();       // Your `CharIo` implementation
let mut shell = Shell::new(&ROOT, handler, io);
shell.activate().ok();

loop {
    if let Some(c) = io.get_char()? {
        shell.process_char(c)?;
    }
}

Async Pattern (Embassy):

// 1. Define async command
const FETCH: CommandMeta<Level> = CommandMeta {
    id: "fetch",
    name: "fetch",
    description: "Fetch data from network",
    access_level: Level::User,
    kind: CommandKind::Async,  // Async command
    min_args: 0,
    max_args: 0,
};

// 2. Implement async handler
impl CommandHandler<MyConfig> for MyHandler {
    async fn execute_async(&self, id: &str, args: &[&str]) -> Result<Response<MyConfig>, CliError> {
        match id {
            "fetch" => fetch_fn::<MyConfig>(args).await,
            _ => Err(CliError::CommandNotFound)
        }
    }
}

// 3. Run shell in async task
#[embassy_executor::task]
async fn shell_task(usb: CdcAcmClass<'static, Driver<'static, USB>>) {
    let handler = MyHandler;
    let io = MyIo::new(usb);
    let mut shell = Shell::new(&ROOT, handler, io);
    shell.activate().ok();

    loop {
        let c = read_char().await;
        shell.process_char_async(c).await?;
        io.flush().await?;
    }
}

See docs/EXAMPLES.md for further implementation patterns.


Interaction session (authentication disabled)

Welcome to nut-shell! Type '?' for help.
@/> ?
  ?        - List global commands
  ls       - List directory contents
  clear    - Clear screen
  ESC ESC  - Clear input buffer
@/> ls
  system/  - Directory
  echo  - Echo arguments back
@/> echo hello world!
hello world!
@/> system
@/system> ls
  status  - Show system status
  version  - Show version information
@/system> status
System Status:
  CPU Usage: 23%
  Uptime: 42 hours
@/system> ..
@/>

Platform Support

Built for no_std embedded systems:

  • Tested on: ARM Cortex-M0 microcontrollers (RP2040, STM32F072)
  • Compatible with: Any ARMv6-M or higher microcontroller

Runtime compatibility:

  • Bare-metal (polling loop)
  • Embassy (async runtime) and other async runtimes

I/O abstraction: Platform-agnostic CharIo trait for UART, USB-CDC, or custom adapters.

See examples/ for complete working implementations on real hardware.


Memory Footprint

Measured on ARMv6-M (Cortex-M0/M0+) with opt-level = "z" and LTO enabled:

Feature Set Flash (.text + .rodata) RAM (.bss)
None (minimal) ~1.5KB 0B
All features ~1.2KB 0B

These measurements use zero-size stubs and minimal command tree to isolate nut-shell's code overhead. Your actual footprint will also include:

Flash costs:

  • Command implementations
  • Directory tree metadata (command names, descriptions)
  • CharIo/CredentialProvider/CommandHandler trait implementations

RAM costs (allocated in Shell instance):

  • Input buffer: MAX_INPUT bytes (default 128B)
  • History buffer: HISTORY_SIZE × MAX_INPUT bytes (default 10 × 128 = 1.3KB)
  • Path tracking and internal state: ~100B

Typical total footprint (with default config and features):

  • Flash: ~4-6KB (nut-shell + basic commands + trait implementations)
  • RAM: ~2KB (buffers on stack)

For detailed analysis: See size-analysis/README.md for methodology and complete breakdown across all feature combinations.


Authentication & Security

Optional authentication feature provides:

  • SHA-256 password hashing with per-user salts
  • Constant-time comparison (prevents timing attacks)
  • Password masking during input
  • Access control enforced at every path segment
  • Pluggable credential providers (build-time, flash storage, custom)

Interaction session (authentication enabled)

Welcome to nut-shell! Please login.
Login> admin:********
  Logged in. Type '?' for help.
admin@/> ls
  system/  - Directory
  echo  - Echo arguments back
admin@/> system/version
Firmware version 3.4.5
admin@/> logout
  Logged out.
Login> admin:*****
  Login failed. Try again.
Login> user:*******
  Logged in. Type '?' for help.
user@/> ls
  echo  - Echo arguments back
user@/>

Documentation

Document Description
README.md Quick start and overview (this file)
docs/EXAMPLES.md Implementation patterns, configuration, troubleshooting
docs/CHAR_IO.md CharIo trait design and platform adapters
docs/SECURITY.md Authentication patterns and security considerations
docs/PHILOSOPHY.md Design philosophy and feature criteria
docs/DESIGN.md Architecture decisions and design patterns
docs/DEVELOPMENT.md Build workflows, testing, and CI
cargo doc --open Complete API reference

Contributing

Contributions welcome! Review docs/PHILOSOPHY.md for feature criteria and docs/DESIGN.md for architectural patterns before implementing features.

Before submitting: Run ./scripts/ci-local to verify all CI checks pass.


License

Licensed under either of:

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.


Acknowledgments

Designed for the Rust embedded ecosystem, with inspiration from:

  • Unix shell navigation and commands
  • Embedded CLI best practices
  • no_std Rust patterns
  • Embassy async runtime architecture

Maintained by: Esben Dueholm Nørgaard (HybridChild)

Dependencies

~0.8–1.8MB
~37K SLoC