diff --git a/.img/logo.svg b/.img/logo.svg
index 12b42c9..15ba9f2 100644
--- a/.img/logo.svg
+++ b/.img/logo.svg
@@ -33,15 +33,12 @@
font-size="9" fill="#6e7681">Serial Monitor - 115200 baud
- Advanced
- CLI
- for Arduino
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6ffeae5..877a1ad 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,7 +2,11 @@ cmake_minimum_required(VERSION 3.14)
project(AdvancedCLI VERSION 0.6.0 LANGUAGES CXX)
# Add sources to the library target.
-add_library(AdvancedCLI STATIC src/AdvancedCLI.cpp)
+add_library(AdvancedCLI STATIC
+ src/internal/AdvancedCLI.cpp
+ src/internal/acli-argument.cpp
+ src/internal/acli-command.cpp
+)
# Alias with author prefix. Consumers link "alkonosst::AdvancedCLI".
add_library(alkonosst::AdvancedCLI ALIAS AdvancedCLI)
diff --git a/README.md b/README.md
index f6a551d..1cb8e0e 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
- A modern command-line parsing library for Arduino with zero dynamic memory allocation.
+ A modern command-line parsing C++ library for embedded and native.
@@ -16,6 +16,9 @@
+
+
+
@@ -32,9 +35,12 @@
- [Description](#description)
- [Key Features](#key-features)
- [Quick Example](#quick-example)
+ - [Arduino](#arduino)
+ - [Native (Desktop)](#native-desktop)
- [Installation](#installation)
- [PlatformIO](#platformio)
- [Arduino IDE](#arduino-ide)
+ - [CMake](#cmake)
- [Usage](#usage)
- [Including the library](#including-the-library)
- [Namespace](#namespace)
@@ -62,27 +68,35 @@
# Description
-**AdvancedCLI** is an Arduino library for defining commands, registering typed arguments, and dispatching parsed callbacks from any serial or stream input. Commands are registered once in `setup()` and then parsed on each incoming line in `loop()` - no manual token splitting required.
+**AdvancedCLI** is a command-line parsing library for embedded and native C++. It defines commands,
+registers typed arguments, and dispatches parsed callbacks from any text input: a serial line on a
+microcontroller, or `argv` / `stdin` in a desktop program. Commands are registered once at startup and
+then parsed on each input line - no manual token splitting required.
-The library is designed for all architectures, from AVR (_some new boards with more RAM like Nano
-Every_) to 32-bit (_ESP32, ESP8266, ARM Cortex-M, RP2040, etc._). All storage uses fixed-size,
+The library targets everything from AVR (_newer boards with more RAM, like the Nano Every_) to
+32-bit MCUs (_ESP32, ESP8266, ARM Cortex-M, RP2040, etc._), and also builds natively on desktop
+(Linux, macOS, Windows) for command-line tools and unit testing. All storage uses fixed-size,
statically allocated buffers; there is no dynamic memory allocation.
# Key Features
-- **Zero dynamic allocation** - Fixed-size buffers throughout; no use of `new`, `malloc`, or `String`.
+- **Zero dynamic allocation** - Fixed-size buffers throughout; no use of `new`, `malloc`, or `std::string`/`String`.
- **Typed arguments** - Named, positional, flag, integer, and float arguments with automatic type checking.
- **Custom output sink** - Attach any print function to route all CLI output (help, errors, etc.) to the desired destination.
- **Sub-commands** - Two-level hierarchical command structures (e.g. `wifi scan`, `wifi connect -ssid ...`).
- **Persistent arguments** - Parent-level args supplied before the sub-command name (e.g. `joy -n 2 cal`); readable from all sub-command callbacks.
- **Aliases** - Short names for any argument (e.g. `-v` as an alias for `-verbose`).
- **Validation callbacks** - Per-argument validators that accept or reject values before the command executes.
-- **Help system** - `printHelp()` lists all registered commands with their arguments and descriptions. An optional `depth` parameter controls the detail level: commands only (`1`), commands and sub-commands (`2`), or full output (`3`, default).
+- **Help system** - `printHelp()` lists all registered commands with their arguments and
+ descriptions. An optional `depth` parameter controls the detail level: commands only (`1`),
+ commands and sub-commands (`2`), or full output (`3`, default).
- **Error routing** - Per-command `onError()` callbacks and per-argument `onInvalid()` callbacks.
- **Case-insensitive by default** - Command and argument matching is case-insensitive unless changed with `setCaseSensitive(true)`.
# Quick Example
+## Arduino
+
```cpp
#include
@@ -131,6 +145,54 @@ Sending `hello -name Arduino` over serial prints:
Hello, Arduino!
```
+## Native (Desktop)
+
+```cpp
+#include
+
+#include
+#include
+
+using namespace ACLI;
+
+static AdvancedCLI cli; // Global instance of the CLI parser
+static ArgStr name_arg; // Global handle for the "name" argument
+
+int main(int argc, char** argv) {
+ // Route all library output (help and error messages) to stdout.
+ cli.setOutput([](const char* msg) { std::puts(msg); });
+
+ // Register a "hello" command with a named "name" argument and an execution callback.
+ Command& hello = cli.addCommand("hello").setDescription("Greets the provided name.");
+ name_arg = hello.addArg("name", "World").setDescription("Name to greet.");
+ hello.onExecute([](Command& cmd) {
+ std::printf("Hello, %s!\n", cmd.getArg(name_arg).getValue());
+ });
+
+ // Join argv[1..] into a single line (bounded by MAX_INPUT_LEN), then parse it once.
+ char line[Config::MAX_INPUT_LEN] = {};
+ size_t pos = 0;
+ for (int i = 1; i < argc; ++i) {
+ if (i > 1 && pos < sizeof(line) - 1) line[pos++] = ' ';
+ for (const char* p = argv[i]; *p && pos < sizeof(line) - 1; ++p) line[pos++] = *p;
+ }
+ line[pos] = '\0';
+
+ cli.parse(line);
+ return cli.lastParseOk() ? 0 : 1; // 0 on success, 1 on a parse/execution error
+}
+```
+
+Running the program with `hello -name World` prints:
+
+```
+Hello, World!
+```
+
+> [!TIP]
+> See the runnable [`examples/Native`](examples/Native), [`examples/NativeBatch`](examples/NativeBatch),
+> and [`examples/NativeValidation`](examples/NativeValidation) programs for complete native usage.
+
# Installation
## PlatformIO
@@ -155,8 +217,30 @@ lib_deps =
3. Search for **"AdvancedCLI"**.
4. Click **Install**.
+## CMake
+
+For desktop C++ projects, pull the library with `FetchContent` and link the `alkonosst::AdvancedCLI`
+target:
+
+```cmake
+include(FetchContent)
+FetchContent_Declare(
+ AdvancedCLI
+ GIT_REPOSITORY https://github.com/alkonosst/AdvancedCLI.git
+ GIT_TAG vx.y.z # pin a release tag (recommended), or a branch/commit
+)
+FetchContent_MakeAvailable(AdvancedCLI)
+
+target_link_libraries(your_app PRIVATE alkonosst::AdvancedCLI)
+```
+
# Usage
+> [!NOTE]
+> The snippets below use Arduino's `Serial` as the output sink for brevity, but the API is identical
+> on native: pass any sink to `setOutput()` (e.g. `std::puts`) and call `parse()` wherever your input
+> arrives - in `loop()` on Arduino, or from `argv` / a read loop on desktop.
+
## Including the library
A single header includes all public types:
@@ -180,7 +264,8 @@ static ArgFlag verbose_flag;
## Registering Commands
-Call `addCommand()` during `setup()` and chain builder methods to configure the command. The resulting `Command&` reference is used to attach arguments and a callback:
+Call `addCommand()` at startup and chain builder methods to configure the command. The resulting
+`Command&` reference is used to attach arguments and a callback:
```cpp
Command& cmd = cli.addCommand("ping");
@@ -198,7 +283,8 @@ cli.addCommand("ping")
## Argument Types
-Each `add*()` method returns a typed **handle** (`ArgStr`, `ArgInt`, etc.). Store it as a global variable and pass it to `cmd.getArg(handle)` inside the callback to retrieve the parsed value.
+Each `add*()` method returns a typed **handle** (`ArgStr`, `ArgInt`, etc.). Store it as a global
+variable and pass it to `cmd.getArg(handle)` inside the callback to retrieve the parsed value.
| Type | Registration method | Input syntax | Handle / Reader |
| ------------------ | ------------------------ | ------------- | -------------------------- |
@@ -312,7 +398,9 @@ Both quote styles support the same escape sequences inside:
| `\t` | tab |
> [!NOTE]
-> A quoted token that contains double quotes **cannot** use double quotes as the outer delimiter without escaping them. The equivalent of `'{"key":"value"}'` using double quotes is `"{\"key\":\"value\"}"`. Single quotes are simpler in that case.
+> A quoted token that contains double quotes **cannot** use double quotes as the outer delimiter
+> without escaping them. The equivalent of `'{"key":"value"}'` using double quotes is
+> `"{\"key\":\"value\"}"`. Single quotes are simpler in that case.
## Reading Parsed Values
@@ -337,7 +425,9 @@ if (field.isSet()) Serial.println(field.getValue());
### getParsedArgCount()
-`cmd.getParsedArgCount()` returns the number of arguments that were explicitly provided or carried a default value during the last parse. Call it inside the execution callback to branch on how many arguments were supplied without testing each one individually:
+`cmd.getParsedArgCount()` returns the number of arguments that were explicitly provided or carried a
+default value during the last parse. Call it inside the execution callback to branch on how many
+arguments were supplied without testing each one individually:
```cpp
wifi_cmd.onExecute([](Command& cmd) {
@@ -413,7 +503,10 @@ joy.addSubCommand("cal").onExecute([](Command& cmd) {
```
> [!IMPORTANT]
-> Persistent args must be registered on the parent command **before** any `addSubCommand()` call. Calling `addSubCommand()` seals the parent's argument list; any `addPersistent*Arg()` (or `addArg()`) attempted afterwards returns an invalid handle and sets `isValid()` to `false`. The correct order is:
+> Persistent args must be registered on the parent command **before** any `addSubCommand()` call.
+> Calling `addSubCommand()` seals the parent's argument list; any `addPersistent*Arg()` (or
+> `addArg()`) attempted afterwards returns an invalid handle and sets `isValid()` to `false`. The
+> correct order is:
>
> ```cpp
> Command& joy = cli.addCommand("joy");
@@ -421,9 +514,13 @@ joy.addSubCommand("cal").onExecute([](Command& cmd) {
> joy.addSubCommand("cal"); // 2. then register sub-commands
> ```
-**Persistent arg types**: `addPersistentArg`, `addPersistentFlag`, `addPersistentIntArg`, `addPersistentFloatArg`; each with the same optional-default and builder-method support as their regular counterparts.
+**Persistent arg types**: `addPersistentArg`, `addPersistentFlag`, `addPersistentIntArg`,
+`addPersistentFloatArg`; each with the same optional-default and builder-method support as their
+regular counterparts.
-**Standalone parent**: calling the parent command directly (e.g. `joy -n 5` with no sub-command) works exactly as before - persistent args behave like ordinary named args when no sub-command is present.
+**Standalone parent**: calling the parent command directly (e.g. `joy -n 5` with no sub-command)
+works exactly as before - persistent args behave like ordinary named args when no sub-command is
+present.
## Aliases
@@ -528,7 +625,9 @@ Available commands:
## Error Handling
-**Command-level error handler (`onError`):** replaces the default CLI error output for a specific command. It is called for both parse errors (missing required argument, wrong type) and explicit `fail()` calls:
+**Command-level error handler (`onError`):** replaces the default CLI error output for a specific
+command. It is called for both parse errors (missing required argument, wrong type) and explicit
+`fail()` calls:
```cpp
reboot_cmd.onError([](Command&, const char* err) {
@@ -559,7 +658,8 @@ cli.onUnknownCommand([](const char* name) {
});
```
-**`parse()` return value:** `cli.parse()` returns `false` if any error occurred during parsing or execution. The same value is accessible afterwards via `cli.lastParseOk()`:
+**`parse()` return value:** `cli.parse()` returns `false` if any error occurred during parsing or
+execution. The same value is accessible afterwards via `cli.lastParseOk()`:
```cpp
bool ok = cli.parse(buf);
@@ -569,9 +669,12 @@ if (!ok) Serial.println("Parse failed.");
## Validation And Invalid Callbacks
> [!IMPORTANT]
-> Validation callbacks require `ACLI_ENABLE_VALIDATION_FN=1` in your build flags. This is enabled by default on 32-bit platforms (ESP32, ARM, RP2040). It is disabled by default on AVR to conserve RAM.
+> Validation callbacks require `ACLI_ENABLE_VALIDATION_FN=1` in your build flags. This is enabled by
+> default on 32-bit platforms (ESP32, ARM, RP2040). It is disabled by default on AVR to conserve
+> RAM.
-Call `setValidator()` on any typed argument to supply a predicate. The parser rejects the value and fires an error if the predicate returns `false`:
+Call `setValidator()` on any typed argument to supply a predicate. The parser rejects the value and
+fires an error if the predicate returns `false`:
```cpp
static ArgInt servo_angle;
@@ -625,7 +728,7 @@ build_flags =
## Capacity Diagnostics
-Call these utility methods at the end of `setup()` to verify that all registrations fit within the configured limits:
+Call these utility methods after registering all commands to verify that all registrations fit within the configured limits:
| Method | Returns |
| ---------------------------- | ------------------------------------------------------------------ |
@@ -661,7 +764,13 @@ When no overflow occurs, `getAttemptedCommandCount()` equals `getCommandCount()`
| Capacity | Conservative (less RAM) | Generous |
> [!NOTE]
-> On AVR, lambdas **with captures** (e.g. `[&]`, `[=]`) cannot be used as callbacks because `std::function` is unavailable. Use plain non-capturing lambdas, which decay to function pointers, or named free functions.
+> Native desktop builds (Linux, macOS, Windows) follow the same configuration as the 32-bit column:
+> `std::function` callbacks, capturing lambdas, and validation are all available.
+
+> [!NOTE]
+> On AVR, lambdas **with captures** (e.g. `[&]`, `[=]`) cannot be used as callbacks because
+> `std::function` is unavailable. Use plain non-capturing lambdas, which decay to function pointers,
+> or named free functions.
> [!WARNING]
> On AVR, `ACLI_ENABLE_VALIDATION_FN` and `ACLI_ENABLE_INVALID_FN` default to `0`. Enabling them on
@@ -670,7 +779,10 @@ When no overflow occurs, `getAttemptedCommandCount()` equals `getCommandCount()`
# Release Status
-This project is in active development. Until reaching version **v1.0.0**, consider it **beta software**. APIs may change in future releases, and some features may be incomplete or unstable. Please report any issues on the [GitHub Issues](https://github.com/alkonosst/AdvancedCLI/issues) page.
+This project is in active development. Until reaching version **v1.0.0**, consider it **beta
+software**. APIs may change in future releases, and some features may be incomplete or unstable.
+Please report any issues on the [GitHub Issues](https://github.com/alkonosst/AdvancedCLI/issues)
+page.
# License
diff --git a/examples/Native/Native.cpp b/examples/Native/Native.cpp
new file mode 100644
index 0000000..0ad4e27
--- /dev/null
+++ b/examples/Native/Native.cpp
@@ -0,0 +1,129 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Maximiliano Ramirez
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+/**
+ * Native - A run-to-completion command-line program driven by argv.
+ *
+ * Demonstrates: setOutput, addCommand, addFlag, addIntArg, addArg (with defaults), setRequired,
+ * onExecute, onUnknownCommand, parse, printHelp, and using lastParseOk() as the process exit code.
+ *
+ * Unlike the Arduino examples (setup/loop + Serial), a native program receives its command on the
+ * command line and runs once. The tokens after the program name are joined into a single line and
+ * handed to the parser, exactly as if the user had typed them into a serial console.
+ *
+ * Build:
+ * - PowerShell: $env:EXAMPLE="examples/Native"; pio run -e native-example
+ * - bash/WSL : export EXAMPLE="examples/Native"; pio run -e native-example
+ *
+ * Run the resulting binary directly so you can pass arguments:
+ * .pio/build/native-example/program led -pin 13 -on -label kitchen
+ * .pio/build/native-example/program add -a 20 -b 22
+ * .pio/build/native-example/program help
+ * .pio/build/native-example/program (no args: prints the help listing)
+ *
+ * The process exit code is 0 on success and 1 when parsing or execution fails.
+ */
+
+#include
+
+#include
+#include
+
+using namespace ACLI;
+
+// CLI instance and argument handles at file scope, so the (non-capturing) callbacks can reach them.
+static AdvancedCLI cli;
+
+static ArgInt led_pin;
+static ArgFlag led_on;
+static ArgStr led_label;
+
+static ArgInt add_a;
+static ArgInt add_b;
+
+static ArgStr help_target;
+
+static void setupCli() {
+ // Route all library output (help and error messages) to stdout.
+ cli.setOutput([](const char* msg) { std::puts(msg); });
+
+ // Replace the default "[CLI] Unknown command" output with our own message.
+ cli.onUnknownCommand(
+ [](const char* name) { std::printf("Unknown command: \"%s\". Try \"help\".\n", name); });
+
+ // led -pin -on -label
+ Command& led = cli.addCommand("led");
+ led.setDescription("Turn an LED on or off.");
+ led_pin = led.addIntArg("pin", 13).setDescription("GPIO pin number (default 13).");
+ led_on = led.addFlag("on").setDescription("Turn the LED on (absent means off).");
+ led_label = led.addArg("label", "led").setDescription("Friendly name for the LED.");
+ led.onExecute([](Command& cmd) {
+ std::printf("[led] \"%s\" on pin %d -> %s\n",
+ cmd.getArg(led_label).getValue(),
+ cmd.getArg(led_pin).getValue(),
+ cmd.getArg(led_on).isSet() ? "ON" : "OFF");
+ });
+
+ // add -a -b
+ Command& add = cli.addCommand("add");
+ add.setDescription("Add two integers.");
+ add_a = add.addIntArg("a").setRequired().setDescription("First addend (required).");
+ add_b = add.addIntArg("b", 0).setDescription("Second addend (default 0).");
+ add.onExecute([](Command& cmd) {
+ const int32_t a = cmd.getArg(add_a).getValue();
+ const int32_t b = cmd.getArg(add_b).getValue();
+ std::printf("[add] %d + %d = %d\n", a, b, a + b);
+ });
+
+ // help [command]
+ Command& help = cli.addCommand("help");
+ help.setDescription("List all commands, or show help for one command.");
+ help_target = help.addPosArg("command").setDescription("Command name (optional).");
+ help.onExecute([](Command& cmd) {
+ ParsedStr target = cmd.getArg(help_target);
+ if (target.isSet()) {
+ cli.printHelp(target.getValue());
+ } else {
+ cli.printHelp();
+ }
+ });
+}
+
+int main(int argc, char** argv) {
+ std::puts("----------------------------");
+ std::puts("AdvancedCLI - Native Example");
+ std::puts("----------------------------");
+
+ setupCli();
+
+ if (!cli.isValid()) {
+ std::fprintf(stderr,
+ "[WARN] CLI registration overflowed: check MAX_COMMANDS/MAX_ARGS_TOTAL.\n");
+ }
+
+ // No command given: show the help listing and exit successfully.
+ if (argc < 2) {
+ std::puts("AdvancedCLI - Native example. Available commands:");
+ cli.printHelp();
+ return 0;
+ }
+
+ // Join argv[1..] into a single line (bounded by MAX_INPUT_LEN), then parse it once.
+ char line[Config::MAX_INPUT_LEN] = {};
+ size_t pos = 0;
+ for (int i = 1; i < argc; ++i) {
+ if (i > 1 && pos < sizeof(line) - 1) line[pos++] = ' ';
+ for (const char* p = argv[i]; *p && pos < sizeof(line) - 1; ++p) {
+ line[pos++] = *p;
+ }
+ }
+ line[pos] = '\0';
+
+ cli.parse(line);
+
+ // Run-to-completion: report success or failure through the process exit code.
+ return cli.lastParseOk() ? 0 : 1;
+}
diff --git a/examples/NativeBatch/NativeBatch.cpp b/examples/NativeBatch/NativeBatch.cpp
new file mode 100644
index 0000000..186bd91
--- /dev/null
+++ b/examples/NativeBatch/NativeBatch.cpp
@@ -0,0 +1,125 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Maximiliano Ramirez
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+/**
+ * NativeBatch - Runs a built-in sequence of command lines to show many features at once.
+ *
+ * Demonstrates: positional arguments, sub-commands, persistent arguments (parsed before the
+ * sub-command name), aliases, getArgByName() with parent-persistent fallback, printHelp(depth), and
+ * the output-capturing inject(input, buf, size) overload.
+ *
+ * Run-to-completion: there is no interactive input. main() feeds a fixed script through inject()
+ * and prints the result of each line, so a single run shows the whole feature set.
+ *
+ * Build & run:
+ * - PowerShell: $env:EXAMPLE="examples/NativeBatch"; pio run -e native-example -t exec
+ * - bash/WSL : export EXAMPLE="examples/NativeBatch"; pio run -e native-example -t exec
+ */
+
+#include
+
+#include
+#include
+
+using namespace ACLI;
+
+static AdvancedCLI cli;
+
+static ArgStr copy_src;
+static ArgStr copy_dst;
+
+static ArgInt read_addr;
+
+static void setupCli() {
+ // Indent library output so it stands out from the "$ " echo lines.
+ cli.setOutput([](const char* msg) { std::printf(" %s\n", msg); });
+
+ // copy - two positional arguments matched by order, no leading dashes.
+ Command& copy = cli.addCommand("copy");
+ copy.setDescription("Copy a file.");
+ copy_src = copy.addPosArg("src").setDescription("Source path.");
+ copy_dst = copy.addPosArg("dst").setDescription("Destination path.");
+ copy.onExecute([](Command& cmd) {
+ std::printf(" copy \"%s\" -> \"%s\"\n",
+ cmd.getArg(copy_src).getValue(),
+ cmd.getArg(copy_dst).getValue());
+ });
+
+ // dev - a parent command with persistent arguments shared by all of its sub-commands.
+ // IMPORTANT: register every persistent argument BEFORE adding sub-commands. Adding a sub-command
+ // seals the parent, so it can no longer accept new argument registrations.
+ Command& dev = cli.addCommand("dev");
+ dev.setDescription("Talk to a device bus.");
+ dev.addPersistentArg("bus", "i2c").setDescription("Bus name (default i2c).");
+ dev.addPersistentFlag("verbose").setAlias("v").setDescription("Verbose output (alias: -v).");
+
+ // dev scan - reads the parent's persistent args by name (getArgByName falls back to the parent).
+ Command& scan = dev.addSubCommand("scan");
+ scan.setDescription("Scan the bus for devices.");
+ scan.onExecute([](Command& cmd) {
+ const bool verbose = cmd.getArgByName("verbose").isSet();
+ std::printf(" scan bus=%s%s\n",
+ cmd.getArgByName("bus").getValue(),
+ verbose ? " (verbose)" : "");
+ });
+
+ // dev read -addr - has its own argument plus access to the persistent ones.
+ Command& read = dev.addSubCommand("read");
+ read.setDescription("Read one register.");
+ read_addr = read.addIntArg("addr", 0).setDescription("Register address (accepts 0x.. hex).");
+ read.onExecute([](Command& cmd) {
+ const bool verbose = cmd.getArgByName("verbose").isSet();
+ std::printf(" read bus=%s addr=%d%s\n",
+ cmd.getArgByName("bus").getValue(),
+ cmd.getArg(read_addr).getValue(),
+ verbose ? " (verbose)" : "");
+ });
+}
+
+// Echo the line, then dispatch it through the parser.
+static void run(const char* line) {
+ std::printf("$ %s\n", line);
+ cli.inject(line);
+ std::printf("\n");
+}
+
+int main() {
+ std::puts("----------------------------------");
+ std::puts("AdvancedCLI - Native Batch Example");
+ std::puts("----------------------------------");
+
+ setupCli();
+
+ std::puts("=== Positional arguments ===");
+ run("copy notes.txt backup/notes.txt");
+
+ std::puts("=== Sub-commands + persistent args (given before the sub-command name) ===");
+ run("dev scan");
+ run("dev -bus spi scan");
+ run("dev -bus spi -verbose read -addr 0x1F");
+
+ std::puts("=== Alias: -v is an alias for -verbose ===");
+ run("dev -v read -addr 42");
+
+ std::puts("=== printHelp at each depth ===");
+ std::puts("-- depth 1 (commands only) --");
+ cli.printHelp(1);
+ std::puts("-- depth 2 (+ sub-commands) --");
+ cli.printHelp(2);
+ std::puts("-- depth 3 (+ argument lines) --");
+ cli.printHelp(3);
+
+ // The inject(input, buf, size) overload redirects the output sink into a buffer for the duration
+ // of the call. It captures what the LIBRARY writes to the sink (help and error messages), not a
+ // command callback's own printf(), which goes straight to stdout. Here we capture the error from
+ // an invalid address so nothing reaches the console.
+ std::puts("\n=== Capturing library output with inject(input, buf, size) ===");
+ char captured[256];
+ cli.inject("dev read -addr xyz", captured, sizeof(captured));
+ std::printf("captured %zu byte(s):\n%s\n", std::strlen(captured), captured);
+
+ return 0;
+}
diff --git a/examples/NativeValidation/NativeValidation.cpp b/examples/NativeValidation/NativeValidation.cpp
new file mode 100644
index 0000000..90f3952
--- /dev/null
+++ b/examples/NativeValidation/NativeValidation.cpp
@@ -0,0 +1,130 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Maximiliano Ramirez
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+/**
+ * NativeValidation - Validation and error handling.
+ *
+ * Demonstrates: setRequired, setValidator (int / float / string), onInvalid (per-argument
+ * override), onError (command-level handler), Command::fail(), the built-in type check, and
+ * onUnknownCommand. The script runs both valid and invalid inputs so every error path is visible.
+ *
+ * Error routing priority, highest first:
+ * 1. Per-argument onInvalid() (when a specific argument fails validation/type check)
+ * 2. Command-level onError() (everything else: required-arg, type errors, fail())
+ * 3. Default output to the sink (when neither is registered)
+ *
+ * Run-to-completion: main() feeds a fixed script through inject(); no interactive input.
+ *
+ * Build & run:
+ * - PowerShell: $env:EXAMPLE="examples/NativeValidation"; pio run -e native-example -t exec
+ * - bash/WSL : export EXAMPLE="examples/NativeValidation"; pio run -e native-example -t exec
+ */
+
+#include
+
+#include
+
+using namespace ACLI;
+
+static AdvancedCLI cli;
+
+static ArgInt set_port;
+static ArgFloat set_ratio;
+static ArgStr set_name;
+
+static void setupCli() {
+ cli.setOutput([](const char* msg) { std::printf(" %s\n", msg); });
+
+ cli.onUnknownCommand(
+ [](const char* name) { std::printf(" unknown command: \"%s\"\n", name); });
+
+ // set -port <1..65535> -ratio <0..1> -name
+ Command& set = cli.addCommand("set");
+ set.setDescription("Set configuration values (with validation).");
+
+ // Required integer with a range validator. It has no onInvalid(), so a rejected value (or a
+ // type error, or it being missing) is routed to the command-level onError() below.
+ set_port = set.addIntArg("port")
+ .setRequired()
+ .setDescription("TCP port, 1-65535 (required).")
+ .setValidator([](int32_t value) { return value >= 1 && value <= 65535; });
+
+ // Float with a range validator AND a per-argument onInvalid(): its failures are handled here
+ // instead of by onError().
+ set_ratio = set.addFloatArg("ratio", 1.0f)
+ .setDescription("Load ratio, 0.0-1.0 (default 1.0).")
+ .setValidator([](float value) { return value >= 0.0f && value <= 1.0f; })
+ .onInvalid([](const char* name, const char* value, const char* reason) {
+ std::printf(" onInvalid: -%s rejected \"%s\" (%s)\n",
+ name,
+ value,
+ reason[0] ? reason : "failed validator");
+ });
+
+ // String validator: reject an empty name.
+ set_name =
+ set.addArg("name", "default")
+ .setDescription("Non-empty name (default \"default\").")
+ .setValidator([](const char* value) { return value != nullptr && value[0] != '\0'; });
+
+ // Command-level handler for any error without a per-argument onInvalid().
+ set.onError([](Command&, const char* message) { std::printf(" [set error] %s\n", message); });
+
+ set.onExecute([](Command& cmd) {
+ std::printf(" OK: port=%d ratio=%g name=\"%s\"\n",
+ cmd.getArg(set_port).getValue(),
+ cmd.getArg(set_ratio).getValue(),
+ cmd.getArg(set_name).getValue());
+ });
+
+ // commit - shows Command::fail(), used to signal a runtime failure from inside the callback.
+ Command& commit = cli.addCommand("commit");
+ commit.setDescription("Commit staged changes.");
+ commit.onError(
+ [](Command&, const char* message) { std::printf(" [commit error] %s\n", message); });
+ commit.onExecute([](Command& cmd) { cmd.fail("nothing staged to commit"); });
+}
+
+// Echo the line, dispatch it, and report success/failure.
+static void run(const char* line) {
+ std::printf("$ %s\n", line);
+ cli.inject(line);
+ std::printf(" -> %s\n\n", cli.lastParseOk() ? "success" : "failure");
+}
+
+int main() {
+ std::puts("---------------------------------------");
+ std::puts("AdvancedCLI - Native Validation Example");
+ std::puts("---------------------------------------");
+
+ setupCli();
+
+ std::puts("=== Valid input ===");
+ run("set -port 8080 -ratio 0.5 -name web");
+
+ std::puts("=== Required argument missing -> onError ===");
+ run("set -ratio 0.5");
+
+ std::puts("=== Integer validator rejects out-of-range port -> onError ===");
+ run("set -port 70000 -ratio 0.5");
+
+ std::puts("=== Built-in type check rejects non-numeric port -> onError ===");
+ run("set -port xyz -ratio 0.5");
+
+ std::puts("=== Float validator rejects ratio -> per-argument onInvalid ===");
+ run("set -port 8080 -ratio 2.0");
+
+ std::puts("=== String validator rejects empty name -> onError ===");
+ run("set -port 8080 -name \"\"");
+
+ std::puts("=== Command::fail() from inside a callback ===");
+ run("commit");
+
+ std::puts("=== Unknown command -> onUnknownCommand ===");
+ run("frobnicate --now");
+
+ return 0;
+}