A feature-rich yet lightweight and resource-conscious CLI implementation specifically designed for bare-metal embedded systems based on Arduino.
- Automatic Command Registration - Define commands with
CLI_COMMAND(name)macro, no manual table management- Preprocessor-aware: only compiled code gets registered
- Cross-platform compatible: works on all Arduino platforms
- See Command Registration for how it works
- Stream-Based I/O - Works with any Arduino Stream (Serial, Telnet, WebSocket, etc.)
- Low Memory Footprint - Configurable buffer sizes, ~400 bytes RAM with defaults
- Simple Integration - Just
cli.begin()andcli.loop()in your sketch
- Command History - Navigate through multiple previous commands using arrow keys
- Ring buffer stores multiple commands based on size
- Preserves escaped characters
- Stores invalid commands for easy correction
- Automatic duplicate filtering (consecutive identical commands)
- Optional feature: Can be disabled to save RAM and flash memory
- Tab Completion - Bash-like command completion
- Press Tab to auto-complete commands
- Single match: Completes and adds space for argument input
- Line wrapping respects terminal width
- VT100 cursor control for seamless editing
- Optional feature: Can be disabled to save flash memory
- VT100 Terminal Support - Standard terminal sequences for better usability
- Arrow Up/Down: Navigate command history
- Tab: Auto-complete commands
- Backspace/DEL: Edit commands
- Ctrl+L: Clear screen
- Ctrl+K: Clear line
- Bell signal support
- Configurable Prompt - Customize the command prompt (default:
#>)
- Flexible Arguments - Up to 4 arguments by default (configurable)
- Quoted Strings - Support for arguments with spaces:
cmd "arg with spaces" - Escape Sequences - Handle special characters:
\",\\,\n,\t,\r - Configurable Separator - Space by default, but can be changed
- Header-Based Config - Override defaults via
cli_config.hpp - Build Flag Config - Or configure directly in
platformio.ini - Fully Documented - See CONFIGURATION.md for all options
Modern dual-core MCUs like the RP2040 or ESP32 running at 133MHz+ enable more sophisticated CLI features than was possible when this project started. libCli embraces these capabilities to provide better usability while maintaining its focus on efficiency and lightweight operation. There must be a clear difference between a microcontroller CLI and embedded Linux running on a Raspberry Pi - libCli shall not become bash!
At this point you may ask: And why does the library then provide features like command history and tab completion? The answer is simple: Because we can with reasonable effort! With careful design and optimization, these features can be implemented in a way that is both efficient and user-friendly, without compromising the core principles of the library. The goal is to provide a powerful yet lightweight CLI solution that enhances the user experience while respecting the constraints of embedded systems. What's not planned is add things like argument auto-completion, scripting capabilities, or a full-fledged shell environment.
The library is built on these core principles:
Uses exclusively static memory allocation - no malloc(), free(), or dynamic containers under the hood.
- Deterministic Memory Usage - Know exact RAM consumption at compile time
- No Heap Fragmentation - Essential for long-running embedded systems that may run for months or years
- Predictable Behavior - No risk of malloc failures or memory leaks
- Real-Time Safe - Constant-time operations, no unpredictable heap management overhead
- Easy Debugging - Static analysis tools can verify memory usage
All buffers (command buffer, history buffer, command table) are sized at compile time through configuration defines.
// Memory usage is completely predictable:
// - Command table: CLI_COMMANDS_MAX * sizeof(cliCmd_t)
// - Command buffer: CLI_COMMANDSIZ bytes
// - History buffer: CLI_HISTORYSIZ bytes
// - Argument array: CLI_ARGVSIZ * sizeof(char*)
// Total: ~400 bytes with defaults- Small Footprint - ~400 bytes RAM with default configuration
- No Bloat - Only essential features, no unnecessary overhead
- Optimized for Microcontrollers - Every byte and CPU cycle counts
- Minimal API - Just
cli.begin()andcli.loop()to get started - Automatic Registration -
CLI_COMMAND(name)macro eliminates manual command table management - Clear Conventions - Consistent patterns and predictable behavior
libCli uses constructor-based command registration instead of linker tricks or build scripts to ensure reliable operation across all Arduino platforms (STM32, ESP32, ESP8266, RP2040, AVR, SAMD, etc.). This approach is preprocessor-aware, meaning commands inside #ifdef blocks are only registered when actually compiled.
Unlike Python script-based solutions that scan source code, libCli's registration respects the preprocessor. Disabled code is never registered, and the system works identically across different toolchains without requiring linker script modifications.
For a detailed explanation of why this design was chosen and how it works, see Command Registration.
Works with any Arduino Stream implementation - Serial, Telnet, WebSockets, or custom streams. Write once, use with any transport layer.
For a complete, real-world example of libCli in action, check out clidemo.
This full-featured demo project showcases:
- Multiple command implementations - Various command types and patterns
- Telnet integration - Remote CLI access over network
- Custom transport streams - Beyond simple Serial usage
- Production patterns - Best practices for real-world applications
- Unit tests - Testing strategies for CLI commands
The demo runs on multiple platforms (see platformio.ini in the demo repository for the complete list) and serves as the primary development testbed for libCli itself. This means it's always up-to-date with the latest features and API changes, making it an excellent reference for real-world usage patterns.
- API Reference - Complete API documentation for all classes and methods
- Configuration Guide - Detailed configuration options and memory considerations
- Command Registration - How automatic command registration works and why
- Changelog - Version history and release notes
Add to your platformio.ini:
lib_deps = fjulian79/libCli@^4.4.0Clone the repository into your Arduino libraries folder:
cd ~/Arduino/libraries
git clone https://github.com/fjulian79/libcli.git#include <Arduino.h>
#include <cli/cli.hpp>
Cli cli;
// Define a command - automatically registered!
CLI_COMMAND(ver) {
ioStream.printf("MyProject v1.0.0\n");
ioStream.printf("Build: %s %s\n", __DATE__, __TIME__);
return 0; // Return 0 for success
}
void setup() {
Serial.begin(115200);
while (!Serial); // Wait for Serial on boards that need it
cli.begin(); // Initialize CLI (uses Serial by default)
Serial.println("Ready! Type 'help' for commands.");
}
void loop() {
cli.loop(); // Process CLI input
}All commands follow this signature:
int8_t cmd_<name>(Stream& ioStream, const char *argv[], uint8_t argc)- ioStream - Use for all I/O operations (
printf,println, etc.) - argv - Array of arguments,
argv[0]is the first argument after the command name, not the command name itself. Unlike Unixmain(argc, argv), the name is deliberately excluded - it's already known statically viaCLI_COMMAND(name), so repeating it would waste aCLI_ARGVSIZslot for no benefit. See API.md for details. - argc - Number of arguments, excluding the command name
- return - 0 for success, negative for errors
See API.md for complete details.
libCli can be customized to fit your needs. All configuration options are optional.
Create cli_config.hpp in your project:
#pragma once
#define CLI_COMMANDS_MAX 20 // Support more commands
#define CLI_COMMANDSIZ 200 // Longer command lines
#define CLI_HISTORYSIZ 500 // More history
#define CLI_ARGVSIZ 8 // More arguments
#define CLI_PROMPT "$ " // Custom prompt
#define CLI_TAB_COMPLETION 1 // Enable tab completion (default)
#define CLI_TERMINAL_WIDTH 80 // Terminal width for wrappingAdd to platformio.ini:
build_flags = -Icfg # Path to your cli_config.hpp| Option | Default | Description |
|---|---|---|
CLI_COMMANDS_MAX |
10 | Maximum number of commands |
CLI_COMMANDSIZ |
100 | Max command length (bytes) |
CLI_HISTORYSIZ |
200 | History buffer size (bytes) |
CLI_ARGVSIZ |
4 | Max number of arguments |
CLI_PROMPT |
"#>" |
Command prompt string |
CLI_TAB_COMPLETION |
1 | Enable tab completion (0=off) |
CLI_TERMINAL_WIDTH |
80 | Terminal width for wrapping |
For complete configuration documentation, see CONFIGURATION.md.
A basic example demonstrating command definition and usage is included in the repository:
examples/basic/basic.ino
This example shows the fundamentals of defining commands, handling arguments, and integrating libCli into your sketch.
// Telnet stream
TelnetStream telnet;
cli.begin(&telnet);
// Switch streams at runtime
cli.setStream(&Serial);Suppresses the per-character echo of user input, useful when a hostapplication drives the CLI programmatically and handles its own echo. Note this only affects the echo of typed characters - prompts, the bell signal, error messages and the tab-completion match list are always written regardless of this setting. It is therefore not suitable for masking password input, since e.g. history navigation or Ctrl+L still reprint the current buffer unconditionally.
cli.setEcho(false); // No character echo
// Process commands
cli.setEcho(true); // Re-enableThe command name is looked up separately, so it is not part of argv, just
like an interactively typed command never sees its own name in argv[0]:
const char* args[] = {"verbose"};
int8_t result = CliCommand::exec(Serial, "status", args, 1);void setup() {
cli.begin();
size_t dropped = CliCommand::getDropCnt();
if (dropped > 0) {
Serial.printf("WARNING: %d commands not registered!\n", dropped);
Serial.printf("Increase CLI_COMMANDS_MAX\n");
}
}For more examples and detailed API usage, see API.md.
Contributions are welcome! Please feel free to:
- Report issues on GitHub Issues
- Submit pull requests
- Suggest new features
- Improve documentation
This library is licensed under GPL-3.0.
See LICENSE.txt for details.
Julian Friedrich
- GitHub: @fjulian79
- Project: github.com/fjulian79/libcli