Skip to content

fjulian79/libcli

Repository files navigation

libCli

A feature-rich yet lightweight and resource-conscious CLI implementation specifically designed for bare-metal embedded systems based on Arduino.

Features

Core Functionality

  • 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() and cli.loop() in your sketch

User Interface

  • 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: #>)

Argument Parsing

  • 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

Configuration

  • 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

Design Principles

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:

Zero Dynamic Memory Allocation

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

Efficiency First

  • 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

Simplicity and Ease of Use

  • Minimal API - Just cli.begin() and cli.loop() to get started
  • Automatic Registration - CLI_COMMAND(name) macro eliminates manual command table management
  • Clear Conventions - Consistent patterns and predictable behavior

Cross-Platform Portability

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.

Stream-Based Architecture

Works with any Arduino Stream implementation - Serial, Telnet, WebSockets, or custom streams. Write once, use with any transport layer.

Demo Project

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.

Documentation

Quick Start

Installation

PlatformIO

Add to your platformio.ini:

lib_deps = fjulian79/libCli@^4.4.0

Arduino IDE

Clone the repository into your Arduino libraries folder:

cd ~/Arduino/libraries
git clone https://github.com/fjulian79/libcli.git

Basic Example

#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
}

Command Signature

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 Unix main(argc, argv), the name is deliberately excluded - it's already known statically via CLI_COMMAND(name), so repeating it would waste a CLI_ARGVSIZ slot 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.

Configuration

libCli can be customized to fit your needs. All configuration options are optional.

Quick Config Example

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 wrapping

Add to platformio.ini:

build_flags = -Icfg  # Path to your cli_config.hpp

Available Options

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.

Example

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.

Advanced Usage

Using Different Streams

// Telnet stream
TelnetStream telnet;
cli.begin(&telnet);

// Switch streams at runtime
cli.setStream(&Serial);

Disabling Echo

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-enable

Programmatic Command Execution

The 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);

Checking Registered Commands

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.

Contributing

Contributions are welcome! Please feel free to:

  • Report issues on GitHub Issues
  • Submit pull requests
  • Suggest new features
  • Improve documentation

License

This library is licensed under GPL-3.0.

See LICENSE.txt for details.

Author

Julian Friedrich

About

A lightweight and versatile command-line interface for Arduino-based projects.

Topics

Resources

License

Stars

7 stars

Watchers

1 watching

Forks

Contributors