std/cli/argparse
Declarative, schema-aware argument parsing for .harn CLI subcommand
scripts dispatched via the harn-cli wedge (harn#2293 epic,
harn#2295). Each subcommand declares a parser spec and parses the
global argv installed by the dispatch wedge.
The library is pure .harn and covers:
- Required and optional positional arguments, including one variadic positional.
- Long flags (
--model,--model=val,--model val) and short flags (-m val,-mval). - Boolean switches (
--json). - Repeated flags collected with
multi: true. - Primitive
string,int,float,bool, andlistdecoding. - A bare
--terminator that routes following tokens to a separaterestlist. - Optional schema validation and narrowing through
parse_typed<T>.
Nested subcommand parsers and shell completion generation remain out of scope. Top-level clap dispatch already owns those concerns.
API
pub fn parser(spec: ParserSpec) -> ParserSpec
pub fn parse(
spec: ParserSpec,
argv: list<string>,
) -> Result<CliInvocation<dict>, CliParseFailure>
pub fn parse_typed<T>(
spec: ParserSpec,
argv: list<string>,
schema: Schema<T>,
apply_defaults: bool = false,
) -> Result<CliInvocation<T>, CliParseFailure>
pub fn help_requested(argv: list<string>) -> bool
pub fn render_help(spec: ParserSpec) -> string
CliInvocation<T> has two fields:
{options: T, rest: list<string>}
Declared arguments are under .options. When no flag value is pending, a
bare -- stops argument parsing; later tokens are placed under .rest and
are never mixed into the option bag or passed to the schema. Required
argument checks still run.
Handle canonical help before parsing, then use native Result helpers rather
than inspecting an object envelope:
import { help_requested, parse, parser, render_help } from "std/cli/argparse"
const spec = parser({
name: "render",
args: [{name: "input", kind: "positional"}],
})
if help_requested(argv) {
__io_println(render_help(spec))
exit(0)
}
const result = parse(spec, argv)
if is_err(result) {
const failure = unwrap_err(result)
__io_eprintln(render_help(spec))
__io_eprintln("error: " + failure.message)
exit(2)
}
const invocation = unwrap(result)
const input = invocation.options.input
const forwarded = invocation.rest
Parser spec
ParserSpec is:
{
name: string,
about?: string,
args: list<ArgSpec>,
examples?: list<string>,
}
Each ArgSpec supports:
| Field | Meaning |
|---|---|
name | Non-empty, unique key written to .options. |
kind | "positional", "flag", or "switch". |
short, long | Flag/switch aliases such as "-m" and "--model". At least one is required for flags and switches. |
required | Positionals default to true; flags and switches default to false. |
multi | For flags, collect repeated occurrences into one list. |
variadic | For a positional, greedily collect remaining positional tokens. |
value_name | Placeholder used by help output; defaults to the uppercased name. |
help | One-line help text. |
parse | "string" (default), "int", "float", "bool", or "list". |
separator | Delimiter for parse: "list"; defaults to ",". |
default | Already-typed, non-nil value inserted unchanged when an optional argument is absent. It does not satisfy required: true. |
A switch never consumes a value: its presence produces true, and its
implicit default is false. A multi flag implicitly defaults to [].
Other omitted optional values have no implicit entry unless default is
declared.
Defaults are not strings waiting to be decoded. For example, an integer
flag uses default: 4, not default: "4", and a list flag uses a list
value. parser() rejects defaults that cannot satisfy the declared decoder,
kind, and multiplicity, so an invalid static declaration fails before argv is
read. Required presence is checked first: a required flag or switch must
appear in argv even if its spec declares a default. Explicit defaults, the
switch false default, and the multi [] default are then applied to
omitted optional arguments.
Positionals bind from left to right. A required positional cannot follow an
optional positional because that declaration has no unambiguous binding rule;
parser() rejects it. A variadic positional must remain last.
Primitive decoding
parse decodes argv strings at the argument boundary:
| Decoder | Accepted value and output |
|---|---|
string | The original argv string. |
int | A base-10 integer with an optional leading sign. |
float | A signed or unsigned decimal, optionally with an exponent. |
bool | Case-insensitive true, yes, y, 1, on, or false, no, n, 0, off, after trimming. |
list | list<string> split on separator, or on , when omitted. |
Invalid int, float, or bool text returns an argv-stage
invalid_value failure. Switches may omit parse, declare
parse: "string", or declare parse: "bool"; because they do not consume
values, all three forms still produce a boolean.
A signed numeric token such as -7 or -0.25 binds to the next int or
float positional when that decoder accepts it. Registered short aliases are
still resolved as flags, and other dash-prefixed tokens remain
unknown_flag failures.
With both multi: true and parse: "list", each occurrence is split and
the results are flattened into one list. For example:
{name: "tag", kind: "flag", long: "--tag", multi: true,
parse: "list", separator: ":"}
["--tag", "core:cli", "--tag=docs:tests"] produces
["core", "cli", "docs", "tests"], not a nested list.
Typed parsing
parse_typed<T> first performs the same argv parsing and primitive
decoding as parse, then validates only the resulting .options against
the supplied Schema<T>. On success, .options is narrowed to T while
.rest remains list<string>.
import { parse_typed, parser } from "std/cli/argparse"
type RunOptions = {
input: string,
jobs: int,
tags: list<string>,
verbose: bool,
}
const spec = parser({
name: "run",
args: [
{name: "input", kind: "positional"},
{name: "jobs", kind: "flag", long: "--jobs", parse: "int", default: 1},
{name: "tags", kind: "flag", long: "--tags", parse: "list", multi: true},
{name: "verbose", kind: "switch", long: "--verbose", parse: "bool"},
],
})
const result = parse_typed(spec, argv, schema_of(RunOptions))
if is_err(result) {
__io_eprintln(unwrap_err(result).message)
exit(2)
}
const invocation = unwrap(result)
const jobs: int = invocation.options.jobs
The fourth argument controls schema-owned defaults. It defaults to
false, which checks without applying schema defaults. Pass true to
apply defaults declared by the schema. ArgSpec.default values are always
applied by argv parsing first and are already typed; this flag does not
control them.
Failures
parse and parse_typed return CliParseFailure in Result.Err:
{
kind: "cli_parse_failure",
stage: "argv" | "schema",
code: string,
message: string,
issues: list<{
stage: "argv" | "schema",
code: string,
message: string,
arg?: string,
path?: string,
}>,
}
The failure and every issue contain only JSON-safe data.
Argv stage
An argv-stage failure has the same code at the top level and in its one
issue. The issue's arg identifies the argument name, flag, or token
associated with the failure.
code | When |
|---|---|
missing_required | A required positional or flag is absent after defaults are applied. |
unknown_flag | A long or short flag is not registered in the spec. |
unknown_arg | A positional appears after all declared positionals are filled. |
value_required | A value-taking flag is the last argv entry. |
bad_value | A switch is supplied with an inline value. |
invalid_value | Primitive int, float, or bool decoding fails. |
Schema stage
When argv parsing succeeds but schema validation fails, the top-level
failure has stage: "schema" and code: "schema_mismatch". Each schema
report issue is projected into issues with stage: "schema", its schema
validation code and message, and its path (for example, jobs). If the
schema report omits issues or paths, argparse supplies one issue and uses
root as the fallback path.
This stage distinction lets command code render one stable failure shape
without re-checking or re-validating .options at the call site.
Static parser errors
parser(spec) checks static programmer-owned declarations and throws
immediately when a spec is invalid. These exceptions are not
CliParseFailure values. It rejects:
- Empty argument names, duplicate names, invalid kinds, and duplicate flag aliases.
- Positionals with
short,long, ormulti, and any positional after a variadic positional. - Flags or switches without a
shortorlongalias, or withvariadic: true. - Switches with
multi: true, or with a decoder other than the defaultstringorbool. - Unknown primitive decoders.
separatorwithoutparse: "list", or an empty list separator.
--help rendering
render_help(spec) returns a stable, snapshot-tested layout:
{about, if present}
USAGE:
{name} [OPTIONS] <positional>...
ARGS:
<positional> help text
OPTIONS:
-s, --long <VALUE> help text
--switch help text
-h, --help Print help
EXAMPLES:
{example 1}
{example 2}
The layout is clap-flavored without trying to match it byte-for-byte.
Snapshot tests under conformance/tests/cli/ pin its structure and column
alignment.