Command Line Interface for embedded systems
Demo of CLI running on Arduino Nano. Memory usage: 16KiB of ROM and 0.6KiB of static RAM. Most of static RAM is used by help strings.
Dual-licensed under Apache 2.0 or MIT.
This library is not stable yet, meaning it's API is likely to change. Some of the API might be a bit ugly, but I don't see a better solution for now. If you have suggestions - open an Issue or a Pull Request.
- Static allocation
- UTF-8 support
- No dynamic dispatch
- Configurable memory usage
- Declaration of commands with enums
- Options and flags support
- Subcommand support
- Left/right support (move inside current input)
- Parsing of arguments to common types
- Autocompletion of command names (with tab)
- History (navigate with up and down keypress)
- Help (generated from doc comments)
- Formatted write with ufmt
- No panicking branches in generated code, when optimized
- Any byte-stream interface is supported (
embedded_io::Write
as output stream, input bytes are given one-by-one) - Colors through ANSI escape sequences
- Navigation through history with search of current input
- Support wrapping of generated str slices in user macro (useful for arduino progmem)
Add embedded-cli
and necessary crates to your app:
[dependencies]
embedded-cli = "0.2.1"
embedded-io = "0.6.1"
ufmt = "0.2.0"
Define a writer that will be used to output bytes:
struct Writer {
// necessary fields (for example, uart tx handle)
};
impl embedded_io::ErrorType for Writer {
// your error type
}
impl embedded_io::Write for Writer {
fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
todo!()
}
fn flush(&mut self) -> Result<(), Self::Error> {
todo!()
}
}
Build a CLI, specifying how much memory to use for command buffer (where bytes are stored until user presses enter) and history buffer (so user can navigate with up/down keypress):
let (command_buffer, history_buffer) = unsafe {
static mut COMMAND_BUFFER: [u8; 32] = [0; 32];
static mut HISTORY_BUFFER: [u8; 32] = [0; 32];
(COMMAND_BUFFER.as_mut(), HISTORY_BUFFER.as_mut())
};
let mut cli = CliBuilder::default()
.writer(writer)
.command_buffer(command_buffer)
.history_buffer(history_buffer)
.build()
.ok()?;
In this example static mut buffers were used, so we don't use stack memory.
Note that we didn't call unwrap()
. It's quite important to keep embedded code
without panics since every panic adds quite a lot to RAM and ROM usage. And
most embedded systems don't have a lot of it.
Define you command structure with enums and derive macro:
use embedded_cli::Command;
#[derive(Command)]
enum Base<'a> {
/// Say hello to World or someone else
Hello {
/// To whom to say hello (World by default)
name: Option<&'a str>,
},
/// Stop CLI and exit
Exit,
}
Doc-comments will be used in generated help.
And you're ready to provide all incoming bytes to cli and handle commands:
use ufmt::uwrite;
// read byte from somewhere (for example, uart)
// let byte = nb::block!(rx.read()).void_unwrap();
let _ = cli.process_byte::<Base, _>(
byte,
&mut Base::processor(|cli, command| {
match command {
Base::Hello { name } => {
// last write in command callback may or may not
// end with newline. so both uwrite!() and uwriteln!()
// will give identical results
uwrite!(cli.writer(), "Hello, {}", name.unwrap_or("World"))?;
}
Base::Exit => {
// We can write via normal function if formatting not needed
cli.writer().write_str("Cli can't shutdown now")?;
}
}
Ok(())
}),
);
If you have a lot of commands it may be useful to split them into multiple enums and place their logic into multiple modules. This is also supported via command groups.
Create extra command enum:
#[derive(Command)]
#[command(help_title = "Manage Hardware")]
enum GetCommand {
/// Get current LED value
GetLed {
/// ID of requested LED
led: u8,
},
/// Get current ADC value
GetAdc {
/// ID of requested ADC
adc: u8,
},
}
Group commands into new enum:
#[derive(CommandGroup)]
enum Group<'a> {
Base(Base<'a>),
Get(GetCommand),
/// This variant will capture everything, that
/// other commands didn't parse. You don't need
/// to add it, just for example
Other(RawCommand<'a>),
}
And then process it in similar way:
let _ = cli.process_byte::<Group, _>(
byte,
&mut Group::processor(|cli, command| {
match command {
Group::Base(cmd) => todo!("process base command"),
Group::Get(cmd) => todo!("process get command"),
Group::Other(cmd) => todo!("process all other, not parsed commands"),
}
Ok(())
}),
);
You can check full arduino example here. There is also a desktop example that runs in normal terminal. So you can play with CLI without flashing a real device.
Command can have any number of arguments. Types of argument must implement FromArgument
trait:
struct CustomArg<'a> {
// fields
}
impl<'a> embedded_cli::arguments::FromArgument<'a> for CustomArg<'a> {
fn from_arg(arg: &'a str) -> Result<Self, &'static str>
where
Self: Sized {
todo!()
}
}
Library provides implementation for following types:
- All numbers (u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, usize, isize, f32, f64)
- Boolean (bool)
- Character (char)
- String slices (&str)
Open an issue if you need some other type.
CLI uses whitespace (normal ASCII whitespace with code 0x20
) to split input into command
and its arguments. If you want to provide argument, that contains spaces, just wrap it
with quotes.
Input | Argument 1 | Argument 2 | Notes |
---|---|---|---|
cmd abc def | abc | def | Space is treated as argument separator |
cmd "abc def" | abc def | To use space inside argument, surround it with quotes | |
cmd "abc\" d\\ef" | abc" d\ef | To use quotes or slashes, escape them with \ | |
cmd "abc def" test | abc def | test | You can mix quoted arguments and non-quoted |
cmd "abc def"test | abc def | test | Space between quoted args is optional |
cmd "abc def""test 2" | abc def | test 2 | Space between quoted args is optional |
When using Command
derive macro, it automatically generates help from doc comments:
#[derive(Command)]
enum Base<'a> {
/// Say hello to World or someone else
Hello {
/// To whom to say hello (World by default)
name: Option<&'a str>,
},
/// Stop CLI and exit
Exit,
}
List all commands with help
:
$ help
Commands:
hello Say hello to World or someone else
exit Stop CLI and exit
Get help for specific command with help <COMMAND>
:
$ help hello
Say hello to World or someone else
Usage: hello [NAME]
Arguments:
[NAME] To whom to say hello (World by default)
Options:
-h, --help Print help
Or with <COMMAND> --help
or <COMMAND> -h
:
$ exit --help
Stop CLI and exit
Usage: exit
Options:
-h, --help Print help
You'll need to begin communication (usually through a UART) with a device running a CLI. Terminal is required for correct experience. Following control sequences are supported:
- \r or \n sends a command (\r\n is also supported)
- \b removes last typed character
- \t tries to autocomplete current input
- Esc[A (key up) and Esc[B (key down) navigates through history
- Esc[C (key right) and Esc[D (key left) moves cursor inside current input
If you run CLI through a serial port (like on Arduino with its UART-USB converter), you can use for example PuTTY or tio.
Memory usage depends on version of crate, enabled features and complexity of your commands. Below is memory usage of arduino example when different features are enabled. Memory usage might change in future versions, but I'll try to keep this table up to date.
Features | ROM, bytes | Static RAM, bytes |
---|---|---|
10120 | 274 | |
autocomplete |
12116 | 290 |
history |
12406 | 315 |
autocomplete history |
13704 | 331 |
help |
14216 | 544 |
autocomplete help |
15290 | 556 |
history help |
16488 | 585 |
autocomplete history help |
16594 | 597 |
This table is generated using this script. As table shows, enabling help adds quite a lot to memory usage since help usually requires a lot of text to be stored. Also enabling all features almost doubles ROM usage comparing to all features disabled.