Hedronite · Frontend Dev · Phase B · Mon 2026-10-12 · BACKFILL

clap derive subcommands, ValueEnum, and try_parse_from tests

Declare the command line as types; test the parse without a shell.

Lesson Class: Asr (T4 · phase B Product)
Cursor: B row 1: clap
Lab: hq · clap 4.6.7 · cargo test 7 passed
Next B-day: ratatui
Types Are the Interface
Parser, Subcommand, Args, ValueEnum.
Validate at the Edge
Ranges in the parser.
Test the Parse
try_parse_from and ErrorKind.
A bare command is an error kind too.

The CLI is an interface like a page. Declare it as types, let clap write the help and the errors, and test the parse without spawning a process.

§I. Frame

Asr B-days walk the interface ladder: clap, then ratatui, then GPUI. A terminal command is the first frontend most backend services get. TRPL ch12 parses std::env::args by hand; clap's derive API turns the same job into struct and enum definitions.

Today's CLI, hq, targets the Duha health service from Sunday. It only prints the URL it would call. HTTP is not the lesson.

Done bar: a derive CLI with global flags, two subcommands, a value enum, a range-checked number, and seven tests that never touch a shell.

§II. Three techniques

Types Are the Interface (named technique). Each piece of the command line is a type:

CLI pieceDerive
the program#[derive(Parser)] struct Cli
hq health, hq url#[derive(Subcommand)] enum Cmd
flags shared by one subcommand#[derive(Args)] struct HealthArgs
`--format textjson`#[derive(ValueEnum)] enum Format

Doc comments become help text. global = true lets --base and -v appear after the subcommand.

Validate at the Edge (named technique). Reject bad input in the parser so main never sees it:

#[arg(long, default_value_t = 2000,
      value_parser = clap::value_parser!(u64).range(1..=10_000))]
pub timeout_ms: u64,

Test the Parse, Not the Process (named technique). Cli::try_parse_from([...]) returns Result<Cli, clap::Error>. Assert on the struct or on err.kind(). Cli::command().debug_assert() catches definition mistakes (duplicate flags, bad defaults) in a unit test.

§III. Run it

cargo run -- --help (real):

hq: query the Duha health service

Usage: hq [OPTIONS] <COMMAND>

Commands:
  health  Probe GET /health once
  url     Print the URL that would be called, without calling it
  help    Print this message or the help of the given subcommand(s)

Options:
      --base <BASE>  Base URL of the service [env: HQ_BASE=] [default: http://127.0.0.1:8787]
  -v, --verbose...   Repeat for more detail (-v, -vv)
  -h, --help         Print help
  -V, --version      Print version

--base reads HQ_BASE from the environment and falls back to the default, all from one attribute. Out-of-range input (real, exit 2):

error: invalid value '0' for '--timeout-ms <TIMEOUT_MS>': 0 is not in 1..=10000

cargo run -- health -v --format json printed would GET http://127.0.0.1:8787/health (format=Json, timeout=2000ms, verbose=1).

cargo test: 7 passed, 0 warnings. The error-kind tests:

let err = Cli::try_parse_from(["hq", "health", "--timeout-ms", "0"]).unwrap_err();
assert_eq!(err.kind(), ErrorKind::ValueValidation);
let err = Cli::try_parse_from(["hq", "health", "--format", "yaml"]).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidValue);
let err = Cli::try_parse_from(["hq"]).unwrap_err();
assert_eq!(err.kind(), ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand);

Note the split: a range failure is ValueValidation; a value outside the enum is InvalidValue. A bare hq is an error kind too, and clap prints help for it.

§IV. Drills

  1. Drop global = true from --base. Which call now fails: hq --base http://x health or hq health --base http://x? (The second: a non-global flag must come before the subcommand.)
  2. Which test catches two flags that both claim -v? (Cli::command().debug_assert().)
  3. You want --format yaml to fail with a clear list of valid values. What do you derive? (ValueEnum on Format.)
  4. What exit code does clap use for a usage error? (2, as the run above shows.)

Close

Types Are the Interface, Validate at the Edge, Test the Parse. Declare the command as types, push checks into the parser, and test with try_parse_from. Next B-day draws the same service in ratatui.

Related