std/cli/argparse

Declarative argument parser for .harn CLI subcommand scripts dispatched via the harn-cli wedge (harn#2293 epic, harn#2295). Each ported subcommand declares a parser spec and calls parse(spec, argv) against the global argv the dispatch wedge installs.

The library is pure .harn — no new Rust builtins — and intentionally covers only the subset of flag parsing that today's ported commands need:

  • Positional arguments, required by default.
  • Long flags (--model, --model=val, --model val) and short flags (-m val, -mval).
  • Boolean switches (--json).
  • Repeated flags collected into a list (multi: true).
  • A -- separator routing everything after it into parsed.rest.
  • Structured error envelopes for the four kinds of parse failures callers can usefully react to.

Out of scope by design:

  • Nested subcommand parsers — each subcommand is its own embedded script and top-level dispatch has already picked it.
  • Shell completion generation — clap covers that for the top-level CLI; the per-subcommand scripts don't ship completions.
  • Validators beyond "value required" — the caller decides whether a parsed value is a valid integer / path / model id / etc.

Surface

import { parser, parse, render_help } from "std/cli/argparse"

let spec = parser({
  name:  "eval-prompt",
  about: "Render and run a .harn.prompt across a fleet of models.",
  args: [
    { name: "prompt",  kind: "positional", required: true,
      help: "Path to the .harn.prompt file." },
    { name: "model",   kind: "flag", short: "-m", long: "--model",
      multi: true, value_name: "ID",
      help: "Model id; repeat for multi-model fanout." },
    { name: "fixture", kind: "flag", long: "--fixture", value_name: "PATH",
      help: "Replay LLM responses from this JSONL fixture." },
    { name: "json",    kind: "switch", long: "--json",
      help: "Emit a JSON envelope instead of human output." },
  ],
  examples: [
    "eval-prompt prompts/summarize.harn.prompt",
    "eval-prompt -m claude-opus-4-7 -m claude-haiku-4-5 prompts/summarize.harn.prompt",
  ],
})

let result = parse(spec, argv)
let err = result.err
if err != nil {
  __io_eprintln(render_help(spec))
  __io_eprintln("error: " + err.hint)
  exit(2)
}
let parsed = result.ok
// parsed.prompt: string, parsed.model: list<string>, parsed.fixture: string?,
// parsed.json: bool, parsed.rest: list<string>

Argument kinds

KindRequired by defaultNotes
positionalYes (override with required: false)Set variadic: true to greedily collect the rest.
flagNoTakes a value. multi: true collects repeats into a list; default value becomes [] when unspecified.
switchNoBoolean; presence sets true. Default value becomes false when unspecified.

Error envelopes

parse returns either { ok: dict } or { err: ParseError }. Error kind is one of:

kindWhen
missing_requiredA required positional or flag was absent.
unknown_flagA --foo or -f not registered in the spec.
unknown_argAn extra positional beyond what's registered.
value_requiredA flag was the last argv entry with no value.
bad_valueA switch received a value (--json=true).

Each error carries the offending arg (the original argv string) and a short hint suitable for one-line stderr output.

--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 intentionally clap-flavored without trying to match byte-for-byte. Snapshot tests under conformance/tests/cli/ pin both the structure and the column alignment.