clap derive subcommands, ValueEnum, and try_parse_from tests
Declare the command line as types; test the parse without a shell.
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 piece | Derive | |
|---|---|---|
| the program | #[derive(Parser)] struct Cli | |
hq health, hq url | #[derive(Subcommand)] enum Cmd | |
| flags shared by one subcommand | #[derive(Args)] struct HealthArgs | |
| `--format text | json` | #[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
- Drop
global = truefrom--base. Which call now fails:hq --base http://x healthorhq health --base http://x? (The second: a non-global flag must come before the subcommand.) - Which test catches two flags that both claim
-v? (Cli::command().debug_assert().) - You want
--format yamlto fail with a clear list of valid values. What do you derive? (ValueEnumonFormat.) - 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
- Tome: TRPL ch12.1 (grounded-in), ch12.6 (referenced)
- Web: clap derive reference · tutorial · ErrorKind
- Paired Duha 2026-10-11: health binary on 127.0.0.1:8787