Skip to main content

embassy_shell/
lib.rs

1//! A tiny `no_std` interactive shell in the spirit of bash, designed to run
2//! on top of [embassy](https://www.embassy.dev) (or any async executor) over
3//! any transport that implements [`embedded_io_async::Read`] /
4//! [`embedded_io_async::Write`] — UART, USB CDC, RTT-with-serial-gwakeup, ...
5//!
6//! # Features
7//!
8//! * Bash-like line editing: cursor keys, Home/End, Delete, Backspace,
9//!   Ctrl-U (kill line), Ctrl-W (delete word), Ctrl-L (clear screen).
10//! * `Tab` completion of command names and per-command argument completion
11//!   (fixed option lists or custom completer callbacks), with bash-style
12//!   common-prefix insertion and candidate listing.
13//! * Command history (Up/Down) with configurable entry count
14//!   ([`Shell::max_history`]) and per-entry length
15//!   ([`Shell::max_history_len`]).
16//! * Ctrl-C interrupts a *running* command (its handler future is dropped).
17//! * Simple command registration with closures; commands get an output handle
18//!   ([`Io`]) so they can `await` slow writes (e.g. to USB CDC).
19//! * Built-in `help`, `history`, `clear` (overridable by user commands).
20//! * No dependency on `futures`/`tokio`; only `embedded-io`/
21//!   `embedded-io-async`. Uses `alloc` (a global allocator is required).
22//!
23//! # Example
24//!
25//! ```
26//! use embassy_shell::Shell;
27//!
28//! let mut shell = Shell::new();
29//!
30//! shell.add_command("hello", "greet someone", |args, mut io| {
31//!     Box::pin(async move {
32//!         let name = args.get(0).unwrap_or("world");
33//!         io.println(&format!("Hello, {name}!")).await
34//!     })
35//! });
36//!
37//! shell.add_command_with_options(
38//!     "led",
39//!     "switch the led on or off",
40//!     &["on", "off"],
41//!     |args, mut io| Box::pin(async move {
42//!         io.println(match args.get(0) {
43//!             Some("on") => "led on",
44//!             Some("off") => "led off",
45//!             _ => "usage: led on|off",
46//!         })
47//!         .await
48//!     }),
49//! );
50//!
51//! let mut input: &[u8] = b"hello bob\nled on\n";
52//! let mut output: Vec<u8> = Vec::new();
53//! futures::executor::block_on(shell.run(&mut input, &mut output)).unwrap();
54//!
55//! let text = String::from_utf8_lossy(&output);
56//! assert!(text.contains("Hello, bob!"));
57//! assert!(text.contains("led on"));
58//! ```
59//!
60//! # Using it with embassy
61//!
62//! With embassy, run the shell as a task over a UART or a USB CDC serial
63//! port. Both implement the required traits:
64//!
65//! ```ignore
66//! // `uart` is an embassy-stm32/esp/nrf UART handle, or a USB CDC
67//! // `SerialPort` from embassy-usb — anything implementing
68//! // embedded-io-async Read + Write.
69//! #[embassy_executor::task]
70//! async fn shell_task(mut uart: Uart<'static, PERIPHERALS>) {
71//!     let mut shell = Shell::new();
72//!     shell.add_command("reboot", "reset the board", |_args, mut io| {
73//!         Box::pin(async move {
74//!             io.println("bye!").await?;
75//!             io.flush().await?;
76//!             cortex_m::peripheral::SCB::sys_reset();
77//!         })
78//!     });
79//!     shell.run(&mut uart, &mut uart).await.ok();
80//! }
81//! ```
82//!
83//! # Cargo features
84//!
85//! * `unicode` *(enabled by default)* — decode multi-byte UTF-8 sequences
86//!   typed at the prompt. Disabling it removes the UTF-8 continuation
87//!   reader from the input path and saves flash; bytes `>= 0x80` are then
88//!   treated as individual Latin-1 characters, so real UTF-8 input (e.g.
89//!   pasted non-ASCII text) will be garbled on display. Command handling
90//!   itself is byte-oriented and unaffected.
91//! * `defmt` *(disabled by default)* — emit trace-level logs (received
92//!   keys, dispatched commands, completion counts) via
93//!   [`defmt`](https://docs.rs/defmt). Enable with
94//!   `embassy-shell = { version = "...", features = ["defmt"] }`.
95//!
96//! # Notes and limitations
97//!
98//! * Memory use is bounded: a typed line is capped at [`Shell::max_line_len`]
99//!   bytes (further characters are rejected with a `BEL`), and each history
100//!   entry is truncated to [`Shell::max_history_len`] bytes, so long pasted
101//!   input cannot exhaust the heap.
102//! * Command handlers return [`BoxFuture`] (wrap an `async move` block in
103//!   `Box::pin`). This type-erases per-command future types so they can live
104//!   in one table. The boxed futures are not `Send`; this matches embassy's
105//!   executor model (tasks are pinned to one core).
106//! * Ctrl-C cancels the handler future; handlers should be cancel-safe (do
107//!   not hold locks across await points, or clean up in a guard).
108//! * Bytes typed while a command is running are not echoed; only the last
109//!   byte typed before/after the interrupt is retained for the next line.
110//! * Line editing assumes an ANSI/VT100-compatible terminal (putty, minicom,
111//!   `picocom`, modern Windows Terminal, ...).
112
113#![no_std]
114#![forbid(unsafe_code)]
115
116extern crate alloc;
117
118/// Internal trace logging: compiles to `defmt::trace!` with the `defmt`
119/// feature, to nothing otherwise.
120#[cfg(feature = "defmt")]
121macro_rules! log {
122    ($s:literal $(, $arg:expr)*) => {
123        defmt::trace!($s $(, $arg)*)
124    };
125}
126
127#[cfg(not(feature = "defmt"))]
128macro_rules! log {
129    ($($tt:tt)*) => {};
130}
131
132mod command;
133mod error;
134mod io;
135mod keys;
136mod parse;
137mod shell;
138mod util;
139
140pub use command::Args;
141pub use error::{Error, Result};
142pub use io::{BoxFuture, Io};
143pub use shell::Shell;