From 4d682eb326c276dd3fd430af063ac2718643246b Mon Sep 17 00:00:00 2001 From: alkonosst Date: Fri, 26 Jun 2026 23:55:16 -0400 Subject: [PATCH 1/3] feat: Add native examples --- examples/Native/Native.cpp | 129 +++++++++++++++++ examples/NativeBatch/NativeBatch.cpp | 125 +++++++++++++++++ .../NativeValidation/NativeValidation.cpp | 130 ++++++++++++++++++ 3 files changed, 384 insertions(+) create mode 100644 examples/Native/Native.cpp create mode 100644 examples/NativeBatch/NativeBatch.cpp create mode 100644 examples/NativeValidation/NativeValidation.cpp 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; +} From e73c44865ccf2cd90a5a3fddca1c83e94fb35851 Mon Sep 17 00:00:00 2001 From: alkonosst Date: Sat, 27 Jun 2026 00:35:55 -0400 Subject: [PATCH 2/3] fix: Update library sources in CMakeLists.txt --- CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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) From 89f40b603302de8ecfac4cb61f009a868b51124a Mon Sep 17 00:00:00 2001 From: alkonosst Date: Sat, 27 Jun 2026 00:36:21 -0400 Subject: [PATCH 3/3] docs: Update logo and README for native support --- .img/logo.svg | 7 +-- README.md | 152 +++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 134 insertions(+), 25 deletions(-) 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/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 @@ PlatformIO Registry

+ + Coverage + License @@ -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