AxlArgs — Argument Parsing
Declarative command-line parsing. A static AxlArgsNode tree
describes the program — its flags, positionals, and any nested
subcommands — and axl_args_run parses argc / argv against
it and dispatches to the matching handler. The same AxlArgsNode
type describes the root program and every subcommand, so a
multi-level CLI (tool bios get …) is just a tree of nodes with no
bespoke dispatch code. Parsed values arrive typed (bool / int / string)
with validation driven by each option’s declared kind.
This is the current CLI parser. It replaces the deprecated
AxlSubcommand — Multi-command CLI dispatch dispatcher (axl_subcommand_*) — new tools should
use axl_args_run.
API Reference
Typedefs
-
typedef int (*AxlVerbHandler)(AxlArgs *args)
Verb handler signature. Return value becomes the program’s exit code.
The
argsobject exposes parsed flags and positional values via theaxl_args_get_*accessors; flags declared on parent nodes are visible (the accessors walk up the parent chain on miss).Lifetime contract — important when the handler enters an event loop (axl_loop_run) or otherwise blocks before returning:
The
AxlArgsstruct itself, allaxl_args_get_*accessor return values, and the variadic-positional view all live until the handler returns to axl_args_run (every level’sAxlArgslives on its own stack frame, so a deep handler still sees its parent’s flags through the recursion).String values returned by
axl_args_get_stringandaxl_args_get_multipoint into the program’sargv(which the runtime keeps alive for the program’s lifetime). They remain valid even after the handler returns — safe to stash in a global and reference from a later AxlLoop callback.Iteration views (
axl_args_get_pos_count,get_multi_count, and the underlying arrays) are freed when the handler returns. Loop callbacks that need to inspect repeatable flags or variadic positionals must capture them inside the handler before entering the loop.Numeric values are plain values — copy and use freely.
In short: extract everything you need from
argsbefore enteringaxl_loop_run; never callaxl_args_get_*from a loop callback.
-
typedef void (*AxlPreRunFunc)(AxlArgs *args)
Optional pre-handler hook. Called once at this node’s level after argument parsing succeeds, before recursing into a child verb (branch) or invoking the leaf handler. Useful for setting up shared resources whose lifetime spans every descendant (config files, opening sessions). When stacked across nested levels, parent’s
pre_runruns before child’s.
Enums
-
enum AxlArgType
Argument value type. Drives parsing and validation.
axl-args.h:
Declarative command-line parser for AXL tools. A tool declares a static AxlArgsNode tree (program name + flags + positionals + handler, OR a list of child verbs) and calls axl_args_run from main. The framework parses argv, validates types and bounds, generates a structured
--help, and dispatches to the matching leaf handler.The same
AxlArgsNodetype describes the root program AND every verb at every level. A node is either a leaf (setshandler, optionallypositionals) or a branch (setsverbs, an array of child nodes terminated by a zero entry). Mutually exclusive, validated at parse time.Single-leaf tool (one shape, no verbs):
static int do_run(AxlArgs *a) { const char *path = axl_args_get_string(a, "path"); return process(path); } static const AxlArgDesc positionals[] = { { .name = "path", .type = AXL_ARG_STRING, .required = true, .help = "Input file" }, {0} }; int main(int argc, char **argv) { return axl_args_run(argc, argv, &(AxlArgsNode){ .name = "mytool", .help = "Process a file", .positionals = positionals, .handler = do_run, }); }
Multi-verb tool (one level of verbs):
static const AxlArgsNode verbs[] = { { .name = "show", .handler = do_show, .positionals = slot_arg, .help = "Decoded fields for one slot" }, { .name = "list", .handler = do_list, .help = "List populated slots" }, {0} }; int main(int argc, char **argv) { return axl_args_run(argc, argv, &(AxlArgsNode){ .name = "memspd", .help = "Read JEDEC SPD content", .flags = flags, .verbs = verbs, }); }
Nested verbs (
mytool <category> <verb>shape):static const AxlArgsNode bios_verbs[] = { { .name = "test", .handler = bios_test, .help = "..." }, { .name = "pci", .handler = bios_pci, .help = "..." }, {0} }; static const AxlArgsNode top_verbs[] = { { .name = "bios", .verbs = bios_verbs, .help = "BIOS / SMBIOS subcommands" }, { .name = "load", .handler = do_load, .positionals = load_args, .help = "Load and run a UEFI image" }, {0} }; int main(int argc, char **argv) { return axl_args_run(argc, argv, &(AxlArgsNode){ .name = "mytool", .help = "Hardware diagnostic CLI", .verbs = top_verbs, }); }
Branch with a default handler — the
mytool bios→ “print
summary” pattern. Same node sets BOTH
verbsandhandler; the handler runs only when no sub-verb is supplied.static const AxlArgsNode bios_verbs[] = { { .name = "info", .handler = bios_info, .help = "Type 0 summary" }, { .name = "test", .handler = bios_test, .help = "Walk every record" }, {0} }; static const AxlArgsNode bios_node = { .name = "bios", .help = "BIOS / SMBIOS subcommands", .verbs = bios_verbs, .handler = bios_info, // fires when 'mytool bios' has no sub-verb };
--help,-h, and a lone?are always recognised and recurse naturally —mytool --helpshows the top tree,mytool bios ?shows just the bios subtree.?is a help alias for legacy-tool parity: any node that accepts a flag treats a bare?as--helpfor that node (after--it is an ordinary positional). Unknown flags / verbs / out-of-range typed args produce an error message qualified by the full breadcrumb (mytool bios: unknown verb ‘flarble`) and the auto-generated usage, exit non-zero, no handler invocation.Help format. The generated help is terse: a
Usage:line, then one aligned list of positionals, flags, and a single-h, --helprow — noArguments:/Flags:section headers and no(optional)suffix (the[<name>]brackets in the Usage line already mark optional positionals). The left column auto-sizes to the longest entry so it reads like a hand-written usage block.Negative-number positionals. A token of the form
-<digit>or-.<digit>(e.g.-1,-.5) is treated as a positional, not a flag — numeric short-flags can’t be registered, so there’s no ambiguity. This lets handlers receive negative numeric operands directly (mytool add .10 -1).**
--end-of-options.** A bare--token ends option parsing for the current node: it is consumed, and every following token is a positional unconditionally (flags,-h,?, and the barehelpword are no longer special). At a branch the next token still selects a verb; the chosen sub-command then parses its own argv slice fresh, so--does not propagate across levels (matches git/cargo scoping). Use it for positionals that start with-(mytool trust -- -dashfile.efi).Flag and visibility across levels. Flags declared on a parent node are visible to descendant handlers via the same accessors (
axl_args_get_*walks up the parent chain on miss).user_dataworks the same way — descendants inherit the nearest non-NULL parent value. Per-leaf positionals are leaf-local.Values:
-
enumerator AXL_ARG_BOOL
Presence flag (no value); —flag / -f.
-
enumerator AXL_ARG_STRING
String value.
-
enumerator AXL_ARG_U8
uint8_t with optional [min,max] bounds
-
enumerator AXL_ARG_U16
uint16_t
-
enumerator AXL_ARG_U32
uint32_t
-
enumerator AXL_ARG_U64
uint64_t
-
enumerator AXL_ARG_S64
int64_t (only signed type supported)
-
enumerator AXL_ARG_MULTI
Repeatable string (variadic positional, or repeatable flag)
-
enumerator AXL_ARG_CHOICE
String value restricted to a caller-supplied set.
-
enumerator AXL_ARG_BOOL
Functions
-
int axl_args_run(int argc, char **argv, const AxlArgsNode *root)
Parse
argvagainstrootand dispatch to the matching leaf handler.Behaviour:
--help/-hat any level prints the auto-generated help for that level to stdout and returns 0.Unknown flags / unknown verbs / missing required positionals / out-of-range typed args print a breadcrumb-prefixed error to stderr followed by the usage line and return 1.
Successful dispatch returns whatever the leaf handler returned.
Return value is POSIX exit-code shaped, not AxlStatus — tools doing
return axl_args_run(...)from main propagate it as the process exit code, so 0/1/2 follow shell convention rather than the negative-value AxlStatus scheme. Handlers should likewise return process-exit-shaped ints (0 success, non-zero failure).- Returns:
handler return value, 0 on
--help, 1 on parse error.
-
const char *axl_args_get_string(AxlArgs *args, const char *name)
Get a string-valued flag or positional by name.
Returns the parsed value, the descriptor’s
default_valueif the arg was unset, or NULL if no such name exists at this level OR any ancestor level (parent walk).
-
bool axl_args_get_bool(AxlArgs *args, const char *name)
Get a boolean flag value by name. Defaults to false when unset and no
default_valueis configured. Walks parents.
-
uint64_t axl_args_get_uint(AxlArgs *args, const char *name)
Get an unsigned-integer flag or positional by name.
Returns the parsed value (already validated against
min/maxby the framework), or 0 if unset / unknown name. Walks parents.
-
int64_t axl_args_get_int(AxlArgs *args, const char *name)
Get a signed-integer flag or positional by name. (Only
AXL_ARG_S64is supported.) Returns 0 if unset / unknown. Walks parents.
-
int axl_args_get_uint_offset(AxlArgs *args, const char *name, uint64_t *out_value)
Read a string-typed arg and parse it as
base[+offset].Convenience over
axl_args_get_string+ axl_strtou64_with_offset. Suited for tools that accept register/memory addresses in a single token like0x1000+0x50across many verbs — the descriptor is declared asAXL_ARG_STRINGand this accessor handles the parse so each handler doesn’t repeat it.Walks parents like the other accessors. The descriptor’s
default_value(if set) is used when the arg was unset.Note: unlike axl_args_get_uint (which returns the value directly with 0 on miss), this accessor returns
AXL_OK/AXL_ERR— there is no in-band sentinel for auint64_tparse failure, and 0 is a legitimate result.- Parameters:
out_value – [out] base + offset
- Returns:
AXL_OK on success (with out_value populated), AXL_ERR if the arg is missing, unset with no default, or fails to parse.
-
int axl_args_get_pos_count(AxlArgs *args)
Number of variadic positional arguments collected at this level (only meaningful when the leaf’s positional descriptor used
AXL_ARG_MULTIas its tail entry).
-
const char *axl_args_get_pos(AxlArgs *args, int index)
Get a variadic positional argument by index. NULL if out of range. Use after the named positionals have been consumed (the variadic tail starts at index 0 of this view).
-
int axl_args_get_multi_count(AxlArgs *args, const char *name)
Number of times a
AXL_ARG_MULTIflag was specified. Walks parents (so a leaf can inspect a--includerepeatable declared on the root).
-
const char *axl_args_get_multi(AxlArgs *args, const char *name, int index)
Get the n-th value of a repeatable (
AXL_ARG_MULTI) flag, or NULL ifindex>= count or the flag was never specified. Walks parents.
-
struct AxlArgDesc
- #include <axl-args.h>
Single-argument descriptor — used for both flags and positional arguments.
Designated initializers expected: only the fields you care about need to be set (zero defaults are sane for all fields).
For numeric types (
AXL_ARG_U8..AXL_ARG_S64):base:0 (auto-detect 0x prefix), 10, or 16 — passed to the axl_str_to_u64 family. Default 0 = auto.min,max:inclusive bounds. Each independently 0 = no bound (so a U8 with.min = 0, .max = 0xFFis the same as “no bounds at all”).For
AXL_ARG_S64the bounds are cast asint64_t. To express a negative lower bound, set.min = (uint64_t)(int64_t)-N— two’s complement round-trips through the cast.
For positionals:
short_nameanddefault_valueare ignored. Position in the array determines argument order. The LAST entry may havetype==AXL_ARG_MULTIto mean “collect all
remaining positionals” (variadic tail).
Public Members
-
const char *name
long name; positional arg name; lookup key
-
char short_name
short flag (single char), 0 if none
-
AxlArgType type
-
const char *default_value
default for flags when unset, NULL = unset
-
const char *help
one-line description (shown in —help)
-
bool required
(positionals only) error if missing
-
int base
(numeric types) 0 (auto) | 10 | 16
-
uint64_t min
(numeric types) inclusive min, 0 = none
-
uint64_t max
(numeric types) inclusive max, 0 = none
-
const char *const *choices
(
AXL_ARG_CHOICEonly) NULL-terminated array of allowed string values; the framework rejects any input not present here with the same breadcrumb-prefixed error it uses for out-of-range numerics, and lists the choices in--help. Comparison is case-sensitive by default; set choices_case_insensitive to relax. NULL or empty array makes a CHOICE behave like an unconstrained AXL_ARG_STRING.
-
bool choices_case_insensitive
(
AXL_ARG_CHOICEonly) when true, match against choices using ASCII case-folded comparison (axl_strcasecmp). Folds letters only — non-ASCII bytes compare as bytes, so localized or UTF-8 multi-byte choice strings still effectively match case-sensitively. The returned string reflects the user’s original casing; only the validation step is case-insensitive. Default false preserves the byte-equal contract earlier consumers rely on.
-
struct AxlArgsNode
- #include <axl-args.h>
A single node in the args tree. Same type at every level — the root program, an inner branch (“category”), and a leaf verb all use it.
A node is one of three shapes:
Leaf:
handlerset,verbsNULL. Positionals and per-leaf flags consumed at this level; handler invoked once parsing completes.Branch:
verbsset (NULL-terminated array of childAxlArgsNodes),handlerNULL. Positionals MUST be NULL on a branch — the first non-flag argument is the verb name. If no verb is supplied, the framework prints--help.Branch + default handler:
verbsANDhandlerboth set. Same dispatch as Branch when the user supplies a verb; when no verb is supplied (just branch-level flags or no args at all),handlerruns as the no-verb default. Useful for themytool bios→ “print summary” pattern (bioshas subverbs but defaults to a summary action). Positionals still MUST be NULL — the handler runs with parsed flags only. An unknown verb still errors; the handler is not a catch-all.
A node with neither
handlernorverbsis a configuration error and the parser exits non-zero before invoking anything.Designated initializers expected; zero defaults are sane.
Public Members
-
const char *name
program name at root, verb name at depth
-
const char *help
one-line description (shown in —help)
-
const AxlArgDesc *flags
per-node flags, NULL-terminated; may be NULL
-
const AxlArgDesc *positionals
leaf only (positional args, NULL-terminated)
-
const AxlArgsNode *verbs
branch only (child nodes, zero-entry-terminated)
-
AxlVerbHandler handler
leaf only
-
AxlPreRunFunc pre_run
optional, runs at this level top-down
-
void *user_data
per-node; descendants inherit nearest non-NULL
-
const char *help_prolog
Optional free-form text printed in
--helpBEFORE the auto-generatedUsage:line (and the per-section blocks that follow it). Use for a multi-paragraph description, usage examples, environment-variable list, or anything that doesn’t fit on the single-line help summary. Per-node: each level’s--helpuses its own prolog;mytool --helpshows the root node’s,mytool bios --helpshows the bios node’s. NULL = nothing printed (default).Whitespace policy: printed verbatim, then the framework adds a single trailing blank line as a separator before
Usage:. Don’t include leading or trailing newlines yourself — the framework handles separation.
-
const char *help_epilog
Optional free-form text printed in
--helpAFTER all auto-generated sections (Usage, Verbs / Flags, Arguments). Standard place for “see also” pointers, “report bugs to …” footers, links to external docs, version stamps, etc. Per-node like help_prolog; NULL = nothing printed.Whitespace policy: same as help_prolog — the framework prepends a single blank-line separator and appends a single trailing newline. Consumer should not include leading or trailing newlines.