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;