Standard.Arguments for those who know clap

This is a map from clap's builder API to Standard.Arguments, for an agent or a person who has written command lines in Rust with clap and now writes them in Hilde. It says what to call, where Hilde differs and why, what is not there and how to do without, and it ends with three complete programmes, each one written twice.

"clap" here is clap 4 and its builder form (Command, Arg, ArgAction), the form that Standard.Arguments follows; clap's derive form has no counterpart (section 2). The interface is the module Standard.Arguments.

What is checked and what is not. The Hilde programmes of section 4 are test programmes of the project: they are compiled and run, and what they print is compared with the output shown here. The project's checks have no Rust, so the clap programmes were built and run by hand, with clap 4.6.7, rustc 1.85.1 and cargo 1.85.1; section 4.4 says what came of it.

1. The calls

A command is declared by calls on a write reference, &!c, and every declaration of an option gives back a key; the programme asks the matches with the key. The names and the help texts of a declaration are literals, Span[Nat8, Static]. Declare, parse, ask, destroy:

StepclapStandard.Arguments
declareCommand::new("tool") and .arg(...)var c: Command := makeCommand("tool", "about") and addFlag(&!c, ...)
parseget_matches()parse(&c, &words), with the process's words from processArguments()
askmatches.get_one::<T>("id")textOf(&m, key) and the other getters below
destroydropped by RustdestroyMatches(m), destroyCommand(c), destroyArguments(words)

The declarations, one clap call to one Hilde call. A short letter of 0 and a long name of "" mean none. The words of a choice are written joined by a bar, as in "fast|safe|small".

clap builderStandard.ArgumentsNote
Command::new("tool") with .about("...")makeCommand("tool", "...")name and one line of help in one call; the command is linear
.version("1.0")setVersion(&!c, "1.0")-V and --version come with it
Arg::new("verbose").short('v').long("verbose").help("...")addVerbose(&!c, "..."), since -v and --verbose are reserved; option("name", 'n', "...") for othersa Spec, which the add... calls take
.action(ArgAction::SetTrue)addFlag(&!c, spec)ask with flagCount(&m, key) > 0
.action(ArgAction::Count)addFlag(&!c, repeatable(spec))flagCount(&m, key) is the count
.action(ArgAction::Set), a textaddText(&!c, spec, "")the last argument is the default, empty for none
.action(ArgAction::Append)addText(&!c, repeatable(spec), "")all values: textCount(&m, key), textAt(&m, key, i)
.value_parser(value_parser!(i64).range(1..=9))addWhole(&!c, spec, 1, 9, "")an Int64 in a range; wholeOf(&m, key)
.value_parser(BoolishValueParser::new())addBool(&!c, spec, "")true, yes, on, 1, false, no, off, 0; boolOf(&m, key); clap's value_parser!(bool) takes true and false only
.value_parser(["fast", "safe"])addChoice(&!c, spec, words, "")choiceOf(&m, key) gives the index of the word
.value_parser(value_parser!(PathBuf))addPath(&!c, spec)any bytes, not empty; pathOf(&m, key)
.required(true)required(spec)met by the environment too, when the option has one
.default_value("6")the last argument of addText, addWhole, addBool, addChoicethe default as the user would type it
.conflicts_with("b")addConflict(&!c, anyFlag(a), anyFlag(b))anyText, anyWhole, anyBool, anyChoice, anyPath for the other kinds
.requires("b")addDependency(&!c, anyPath(a), anyChoice(b))a may only be given with b
.env("TOOL_NAME")fromEnvironment(spec, "TOOL_NAME")checked as if typed; a flag takes a Bool word
.value_name("FILE")valueName(spec, "file")shown in capitals in the help
a positional Arg::new("file")addPositional(&!c, "file", "help", isRequired, isRepeatable)positionalOf, or positionalCount and positionalAt
.num_args(1..) on a positionalisRepeatable trueonly the last positional may be repeatable
a positional that is a pathaddPositionalPath(&!c, "file", "help", isRequired, isRepeatable)asked with pathOf
.subcommand(sub)addSubcommand(&!c, sub)sub is built apart, then moved in; the key is asked with chosen(&m, key)
.subcommand_required(true)requireSubcommand(&!c)
.alias("a") on a subcommandaddAlias(&!sub, "a")before the subcommand is attached
.global(true)global(spec)accepted by every subcommand below
get_one::<String>("id")textOf(&m, key)an Option of a reference
get_one::<i64>("id")wholeOf(&m, key)Option[Int64]
get_many::<String>("id")textCount(&m, key) and textAt(&m, key, i)for paths pathCount and pathAt
get_count("id")flagCount(&m, key)
get_flag("id")flagCount(&m, key) > 0
--help printed and exitHelp(text) of the outcome, or helpText(&c)the programme prints it and exits
the error printed and exitdescribeError(&c, &error)errorKind(&error), errorPosition(&error), then destroyError(error)
clap_completecompletionScript(&c, Bash())Zsh() and Fish() too
clap_mangenmanPage(&c, "October 7, 2026")a page in mdoc; the date is a parameter
the argfile crateexpandArgumentFiles(&words)words that begin with @ name files; call it before parse

What parse gives back is Either[ArgumentError, Outcome]. An Outcome is Run with the matches, Help or Version with the text the user asked for. The library prints nothing and never ends the process.

2. Where Hilde differs, and why

3. What is not there, and the way round

clapIn HildeWay round
#[derive(Parser)]not offereddeclare the command by hand, as in section 4
ArgGroup, one of several requirednot offeredaddConflict for each pair, and a check of flagCount or textOf after the parse
num_args(2) on an option, several values after one optionan option has one valuerepeat the option (repeatable), or take one text and split it with Standard.Text.split
value_delimiter(',')not offeredone text option, split with Standard.Text.split
a custom value_parserfive kinds of valuetake the text and parse it after parse, with the functions of Standard.Text
floats and unsigned numbersInt64 onlytake a text and parse it, or use addWhole with a range
trailing_var_arg, last(true)a word -- ends the optionsthe positional arguments after -- are kept in order; the last positional may be repeatable
--no-flag negation, overrides_withnot offereddeclare the second flag and addConflict between the two
infer_subcommands, infer_long_argsnevera suggestion for a word nearly right
help_template, before_help, after_help, coloursthe help is written from the declarationsprint the extra text before or after helpText(&c)
value_hint, dynamic completionthe scripts name options, subcommands and the words of a choicenone
ArgMatches::value_sourcenot offereddeclare the option without a default and apply the default in the programme, then the case is known
Command::print_help and render_usagehelpText(&c) and usageLine(&c) give stringsprint them
a value that is not UTF-8 as a Stringan error for textdeclare it with addPath, which takes the bytes

4. Three programmes, each written twice

Each clap programme is the model; the Hilde programme under it is a test of the suite run with the command lines it builds itself instead of the process's, so that a run shows what every call does. A programme of your own takes processArguments() in place of the buffers of the example, and after parse and describeError it returns ExitSuccess() or an exit code of its own. What the Hilde programme prints is shown under it: the words of the errors and of the help are Hilde's, clap words them differently.

4.1 A command with a default, a range and a flag

The clap programme (Cargo.toml needs clap 4):

use clap::builder::BoolishValueParser;
use clap::{value_parser, Arg, ArgAction, Command};

fn main() {
    let matches = Command::new("greet")
        .about("Prints a greeting.")
        .version("1.0")
        .arg(
            Arg::new("name")
                .short('n')
                .long("name")
                .value_name("NAME")
                .help("Who to greet")
                .default_value("world"),
        )
        .arg(
            Arg::new("times")
                .short('t')
                .long("times")
                .help("How often")
                .value_parser(value_parser!(i64).range(1..=5))
                .default_value("1"),
        )
        .arg(
            Arg::new("polite")
                .long("polite")
                .help("Say please")
                .value_parser(BoolishValueParser::new())
                .default_value("false"),
        )
        .arg(
            Arg::new("loud")
                .short('l')
                .long("loud")
                .help("Shout")
                .action(ArgAction::SetTrue),
        )
        .get_matches();

    let name = matches.get_one::<String>("name").unwrap();
    let times = matches.get_one::<i64>("times").unwrap();
    let end = if matches.get_flag("loud") { "!" } else { "." };
    println!("Hello, {name}{end}");
    println!("times: {times}");
    println!("polite: {}", matches.get_one::<bool>("polite").unwrap());
}

The Hilde programme:

import Standard.Buffer (
    Buffer,
    allocateEmpty,
    insertBack
);
import Standard.String (
    String,
    fromLiteral,
    destroyString,
    asSpan
);
import Standard.Arguments (
    Command,
    FlagKey,
    TextKey,
    WholeKey,
    BoolKey,
    ArgumentError,
    Outcome,
    Run,
    Help,
    Version,
    Matches,
    makeCommand,
    destroyCommand,
    setVersion,
    option,
    valueName,
    addFlag,
    addText,
    addWhole,
    addBool,
    destroyArguments,
    parse,
    destroyMatches,
    describeError,
    destroyError,
    flagCount,
    textOf,
    wholeOf,
    boolOf
);

module body Test is
    -- The keys are what clap's string ids are: what the programme asks the
    -- matches with.
    record Keys: Free is
        name: TextKey;
        times: WholeKey;
        polite: BoolKey;
        loud: FlagKey;
    end;

    -- What the programme does with the matches: the greeting, times over.
    generic [R: Region]
    function greet(m: &[Matches, R], keys: Keys): Unit is
        case textOf(m, keys.name) of
            when Some(value as given: &[String, R]) do
                print("Hello, ");
                print(asSpan(given));
                if flagCount(m, keys.loud) > 0 then
                    print("!");
                else
                    print(".");
                end if;
                print("\n");
            when None do
                printLn("Hello, nobody.");
        end case;
        case wholeOf(m, keys.times) of
            when Some(value as times: Int64) do
                print("times: ");
                printLn(times);
            when None do
                printLn("times: none");
        end case;
        case boolOf(m, keys.polite) of
            when Some(value as polite: Bool) do
                print("polite: ");
                printLn(polite);
            when None do
                printLn("polite: none");
        end case;
        return nil;
    end;

    -- One command line: parsed, then the greeting, or the help, the version
    -- or the error as the user is to read it.
    generic [R: Region]
    function run(c: &[Command, R], keys: Keys, words: Buffer[String]): Unit is
        case parse(c, &words) of
            when Right(right as outcome: Outcome) do
                case outcome of
                    when Run(matches as m: Matches) do
                        greet(&m, keys);
                        destroyMatches(m);
                    when Help(text as t: String) do
                        print(asSpan(&t));
                        destroyString(t);
                    when Version(text as t: String) do
                        print(asSpan(&t));
                        destroyString(t);
                end case;
            when Left(left as e: ArgumentError) do
                let message: String := describeError(c, &e);
                print(asSpan(&message));
                destroyString(message);
                destroyError(e);
        end case;
        destroyArguments(words);
        return nil;
    end;

    -- A command line is a buffer of words, built a word at a time.
    function noWords(): Buffer[String] is
        return allocateEmpty();
    end;

    function plus(line: Buffer[String], word: Span[Nat8, Static]): Buffer[String] is
        var out: Buffer[String] := line;
        insertBack(&!out, fromLiteral(word));
        return out;
    end;

    function main(): ExitCode is
        -- Command::new("greet").about(..).version(..)
        var c: Command := makeCommand("greet", "Prints a greeting.");
        setVersion(&!c, "1.0");
        -- Arg::new with the name name, short n, a value_name and a default_value
        let name: TextKey := addText(&!c, valueName(option("name", 'n', "Who to greet"), "NAME"), "world");
        -- Arg::new("times").value_parser(value_parser!(i64).range(1..=5)).default_value("1")
        let times: WholeKey := addWhole(&!c, option("times", 't', "How often"), 1, 5, "1");
        -- Arg::new("polite").value_parser(BoolishValueParser::new()).default_value("false")
        let polite: BoolKey := addBool(&!c, option("polite", 0, "Say please"), "false");
        -- Arg::new("loud").action(ArgAction::SetTrue)
        let loud: FlagKey := addFlag(&!c, option("loud", 'l', "Shout"));
        let keys: Keys := Keys(name => name, times => times, polite => polite, loud => loud);
        printLn("the defaults:");
        run(&c, keys, noWords());
        printLn("a name, a count and a flag:");
        run(&c, keys, plus(plus(plus(plus(plus(noWords(), "-n"), "Ann"), "--times=2"), "--polite=yes"), "--loud"));
        printLn("a count out of its range:");
        run(&c, keys, plus(plus(noWords(), "--times"), "9"));
        printLn("a Bool that is not one:");
        run(&c, keys, plus(noWords(), "--polite=maybe"));
        printLn("a name nearly right:");
        run(&c, keys, plus(plus(noWords(), "--nmae"), "Ann"));
        printLn("the version:");
        run(&c, keys, plus(noWords(), "--version"));
        printLn("the help:");
        run(&c, keys, plus(noWords(), "-h"));
        destroyCommand(c);
        return ExitSuccess();
    end;
end module body.

What it prints:

the defaults:
Hello, world.
times: 1
polite: false
a name, a count and a flag:
Hello, Ann!
times: 2
polite: true
a count out of its range:
greet: 9 is out of range for --times, which takes 1 to 5
Usage: greet [OPTIONS]
Try greet --help for more information.
a Bool that is not one:
greet: maybe is not allowed for --polite, which takes true, false, yes, no, on, off, 1 or 0
Usage: greet [OPTIONS]
Try greet --help for more information.
a name nearly right:
greet: unknown option --nmae, did you mean --name?
Usage: greet [OPTIONS]
Try greet --help for more information.
the version:
greet 1.0
the help:
Prints a greeting.

Usage: greet [OPTIONS]

Options:
  -n, --name <NAME>      Who to greet [default: world]
  -t, --times <TIMES>    How often [range: 1 to 5] [default: 1]
      --polite <POLITE>  Say please [default: false]
  -l, --loud             Shout
  -h, --help             Print help
  -V, --version          Print version

4.2 Subcommands, an alias, a global counter

The clap programme:

use clap::{Arg, ArgAction, Command};

fn main() {
    let matches = Command::new("pack")
        .about("A small archive tool.")
        .version("0.2")
        .subcommand_required(true)
        .arg(
            Arg::new("verbose")
                .short('v')
                .long("verbose")
                .help("More output")
                .action(ArgAction::Count)
                .global(true),
        )
        .subcommand(
            Command::new("add")
                .about("Add files.")
                .alias("a")
                .arg(
                    Arg::new("force")
                        .short('f')
                        .long("force")
                        .help("Overwrite what is there")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("file")
                        .help("Files to add")
                        .required(true)
                        .num_args(1..),
                ),
        )
        .subcommand(
            Command::new("list").about("List the archive.").arg(
                Arg::new("long")
                    .short('l')
                    .long("long")
                    .help("One line each")
                    .action(ArgAction::SetTrue),
            ),
        )
        .get_matches();

    match matches.subcommand() {
        Some(("add", sub)) => {
            println!("verbose: {}", sub.get_count("verbose"));
            println!("add, force: {}", sub.get_flag("force") as u8);
            for file in sub.get_many::<String>("file").unwrap() {
                println!("  file: {file}");
            }
        }
        Some(("list", sub)) => {
            println!("verbose: {}", sub.get_count("verbose"));
            println!("list, long: {}", sub.get_flag("long") as u8);
        }
        _ => unreachable!(),
    }
}

The Hilde programme. The counter is declared on the command and asked there: it counts wherever on the line it was given.

import Standard.Buffer (
    Buffer,
    allocateEmpty,
    insertBack
);
import Standard.String (
    String,
    fromLiteral,
    destroyString,
    asSpan
);
import Standard.Arguments (
    Command,
    FlagKey,
    PositionalKey,
    SubcommandKey,
    ArgumentError,
    Outcome,
    Run,
    Help,
    Version,
    Matches,
    makeCommand,
    destroyCommand,
    setVersion,
    addAlias,
    option,
    repeatable,
    global,
    addFlag,
    addPositional,
    addSubcommand,
    requireSubcommand,
    destroyArguments,
    parse,
    destroyMatches,
    describeError,
    destroyError,
    flagCount,
    positionalCount,
    positionalAt,
    chosen,
    addVerbose
);

module body Test is
    record Keys: Free is
        verbose: FlagKey;
        add: SubcommandKey;
        force: FlagKey;
        files: PositionalKey;
        list: SubcommandKey;
        oneLine: FlagKey;
    end;

    -- What the programme does with the matches: match on the subcommand, as
    -- clap's matches.subcommand() is matched.
    generic [R: Region]
    function act(m: &[Matches, R], keys: Keys): Unit is
        print("verbose: ");
        printLn(flagCount(m, keys.verbose));
        if chosen(m, keys.add) then
            print("add, force: ");
            printLn(flagCount(m, keys.force));
            let count: Index := positionalCount(m, keys.files);
            var i: Index := 0;
            while i < count do
                case positionalAt(m, keys.files, i) of
                    when Some(value as file: &[String, R]) do
                        print("  file: ");
                        printLn(asSpan(file));
                    when None do
                        skip;
                end case;
                i := i + 1;
            end while;
        else if chosen(m, keys.list) then
            print("list, long: ");
            printLn(flagCount(m, keys.oneLine));
        end if;
        return nil;
    end;

    generic [R: Region]
    function run(c: &[Command, R], keys: Keys, words: Buffer[String]): Unit is
        case parse(c, &words) of
            when Right(right as outcome: Outcome) do
                case outcome of
                    when Run(matches as m: Matches) do
                        act(&m, keys);
                        destroyMatches(m);
                    when Help(text as t: String) do
                        print(asSpan(&t));
                        destroyString(t);
                    when Version(text as t: String) do
                        print(asSpan(&t));
                        destroyString(t);
                end case;
            when Left(left as e: ArgumentError) do
                let message: String := describeError(c, &e);
                print(asSpan(&message));
                destroyString(message);
                destroyError(e);
        end case;
        destroyArguments(words);
        return nil;
    end;

    -- A command line is a buffer of words, built a word at a time.
    function noWords(): Buffer[String] is
        return allocateEmpty();
    end;

    function plus(line: Buffer[String], word: Span[Nat8, Static]): Buffer[String] is
        var out: Buffer[String] := line;
        insertBack(&!out, fromLiteral(word));
        return out;
    end;

    function main(): ExitCode is
        -- Command::new("pack").subcommand_required(true)
        var c: Command := makeCommand("pack", "A small archive tool.");
        setVersion(&!c, "0.2");
        requireSubcommand(&!c);
        -- Arg::new with the name verbose, short v, the action Count and global
        let verbose: FlagKey := addVerbose(&!c, "More output");
        -- Command::new("add").alias("a"), built apart and attached
        var add: Command := makeCommand("add", "Add files.");
        addAlias(&!add, "a");
        let force: FlagKey := addFlag(&!add, option("force", 'f', "Overwrite what is there"));
        -- Arg::new("files").required(true).num_args(1..)
        let files: PositionalKey := addPositional(&!add, "file", "Files to add", true, true);
        let addKey: SubcommandKey := addSubcommand(&!c, add);
        var list: Command := makeCommand("list", "List the archive.");
        let oneLine: FlagKey := addFlag(&!list, option("long", 'l', "One line each"));
        let listKey: SubcommandKey := addSubcommand(&!c, list);
        let keys: Keys := Keys(verbose => verbose, add => addKey, force => force, files => files, list => listKey, oneLine => oneLine);
        printLn("add, with a flag and two files:");
        run(&c, keys, plus(plus(plus(plus(noWords(), "add"), "-f"), "a.txt"), "b.txt"));
        printLn("the alias, and a global flag before the subcommand:");
        run(&c, keys, plus(plus(plus(noWords(), "-vv"), "a"), "x.txt"));
        printLn("a global flag after the subcommand:");
        run(&c, keys, plus(plus(plus(noWords(), "list"), "-v"), "--long"));
        printLn("no subcommand:");
        run(&c, keys, noWords());
        printLn("add without a file:");
        run(&c, keys, plus(plus(noWords(), "add"), "-f"));
        printLn("a subcommand nearly right:");
        run(&c, keys, plus(noWords(), "lst"));
        printLn("the help of a subcommand:");
        run(&c, keys, plus(plus(noWords(), "help"), "add"));
        destroyCommand(c);
        return ExitSuccess();
    end;
end module body.

What it prints:

add, with a flag and two files:
verbose: 0
add, force: 1
  file: a.txt
  file: b.txt
the alias, and a global flag before the subcommand:
verbose: 2
add, force: 0
  file: x.txt
a global flag after the subcommand:
verbose: 1
list, long: 1
no subcommand:
pack: a subcommand is required
Usage: pack [OPTIONS] <COMMAND>
Try pack --help for more information.
add without a file:
pack: <FILE> is required
Usage: pack add [OPTIONS] <FILE>...
Try pack add --help for more information.
a subcommand nearly right:
pack: unknown subcommand lst, did you mean list?
Usage: pack [OPTIONS] <COMMAND>
Try pack --help for more information.
the help of a subcommand:
Add files.

Usage: pack add [OPTIONS] <FILE>...

Arguments:
  <FILE>...  Files to add

Options:
  -v, --verbose  More output [repeatable]
  -f, --force    Overwrite what is there
  -h, --help     Print help
  -V, --version  Print version

4.3 The environment, a conflict, a dependency

The clap programme (the env feature of clap is needed for .env):

use clap::{value_parser, Arg, ArgAction, Command};
use std::path::PathBuf;

fn main() {
    let matches = Command::new("convert")
        .about("Converts a file.")
        .arg(
            Arg::new("format")
                .short('f')
                .long("format")
                .help("Output format")
                .value_parser(["json", "text"])
                .env("CONVERT_FORMAT"),
        )
        .arg(
            Arg::new("jobs")
                .short('j')
                .long("jobs")
                .help("Parallel jobs")
                .value_parser(value_parser!(i64).range(1..=64))
                .env("CONVERT_JOBS")
                .default_value("1"),
        )
        .arg(
            Arg::new("quiet")
                .short('q')
                .long("quiet")
                .help("Print nothing")
                .action(ArgAction::SetTrue)
                .conflicts_with("verbose"),
        )
        .arg(
            Arg::new("verbose")
                .short('v')
                .long("verbose")
                .help("Print more")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("output")
                .short('o')
                .long("output")
                .help("Where to write")
                .value_parser(value_parser!(PathBuf))
                .requires("format"),
        )
        .arg(
            Arg::new("exclude")
                .short('x')
                .long("exclude")
                .help("Skip a name")
                .action(ArgAction::Append),
        )
        .arg(
            Arg::new("input")
                .help("The file to convert")
                .required(true)
                .value_parser(value_parser!(PathBuf)),
        )
        .get_matches();

    println!("input: {}", matches.get_one::<PathBuf>("input").unwrap().display());
    match matches.get_one::<String>("format") {
        Some(format) => println!("format: {format}"),
        None => println!("format: none"),
    }
    println!("jobs: {}", matches.get_one::<i64>("jobs").unwrap());
    match matches.get_one::<PathBuf>("output") {
        Some(output) => println!("output: {}", output.display()),
        None => println!("output: none"),
    }
    let skipped: Vec<&String> = matches
        .get_many::<String>("exclude")
        .map(|names| names.collect())
        .unwrap_or_default();
    println!("exclude: {}", skipped.len());
    for name in skipped {
        println!("  skip: {name}");
    }
    println!(
        "quiet: {}, verbose: {}",
        matches.get_flag("quiet") as u8,
        matches.get_flag("verbose") as u8
    );
}

The Hilde programme, run with CONVERT_JOBS=4 in its environment (the file env.txt of the test). The dependency is the second line the declaration needs after the options: a conflict and a dependency are declared on the command, between keys.

import Standard.Buffer (
    Buffer,
    allocateEmpty,
    insertBack
);
import Standard.String (
    String,
    fromLiteral,
    destroyString,
    asSpan
);
import Standard.Arguments (
    Command,
    FlagKey,
    TextKey,
    WholeKey,
    ChoiceKey,
    PathKey,
    ArgumentError,
    Outcome,
    Run,
    Help,
    Version,
    Matches,
    makeCommand,
    destroyCommand,
    option,
    repeatable,
    fromEnvironment,
    addFlag,
    addText,
    addWhole,
    addChoice,
    addPath,
    addPositionalPath,
    anyFlag,
    anyPath,
    anyChoice,
    addConflict,
    addDependency,
    destroyArguments,
    parse,
    destroyMatches,
    describeError,
    destroyError,
    flagCount,
    textCount,
    textAt,
    wholeOf,
    choiceOf,
    pathOf,
    addVerbose
);

module body Test is
    record Keys: Free is
        format: ChoiceKey;
        jobs: WholeKey;
        quiet: FlagKey;
        verbose: FlagKey;
        output: PathKey;
        exclude: TextKey;
        input: PathKey;
    end;

    -- What the programme does with the matches.
    generic [R: Region]
    function convert(m: &[Matches, R], keys: Keys): Unit is
        case pathOf(m, keys.input) of
            when Some(value as file: &[String, R]) do
                print("input: ");
                printLn(asSpan(file));
            when None do
                printLn("input: none");
        end case;
        case choiceOf(m, keys.format) of
            when Some(value as which: Index) do
                if which = 0 then
                    printLn("format: json");
                else
                    printLn("format: text");
                end if;
            when None do
                printLn("format: none");
        end case;
        case wholeOf(m, keys.jobs) of
            when Some(value as jobs: Int64) do
                print("jobs: ");
                printLn(jobs);
            when None do
                printLn("jobs: none");
        end case;
        case pathOf(m, keys.output) of
            when Some(value as file: &[String, R]) do
                print("output: ");
                printLn(asSpan(file));
            when None do
                printLn("output: none");
        end case;
        let skipped: Index := textCount(m, keys.exclude);
        print("exclude: ");
        printLn(skipped);
        var i: Index := 0;
        while i < skipped do
            case textAt(m, keys.exclude, i) of
                when Some(value as name: &[String, R]) do
                    print("  skip: ");
                    printLn(asSpan(name));
                when None do
                    skip;
            end case;
            i := i + 1;
        end while;
        print("quiet: ");
        print(flagCount(m, keys.quiet));
        print(", verbose: ");
        printLn(flagCount(m, keys.verbose));
        return nil;
    end;

    generic [R: Region]
    function run(c: &[Command, R], keys: Keys, words: Buffer[String]): Unit is
        case parse(c, &words) of
            when Right(right as outcome: Outcome) do
                case outcome of
                    when Run(matches as m: Matches) do
                        convert(&m, keys);
                        destroyMatches(m);
                    when Help(text as t: String) do
                        print(asSpan(&t));
                        destroyString(t);
                    when Version(text as t: String) do
                        print(asSpan(&t));
                        destroyString(t);
                end case;
            when Left(left as e: ArgumentError) do
                let message: String := describeError(c, &e);
                print(asSpan(&message));
                destroyString(message);
                destroyError(e);
        end case;
        destroyArguments(words);
        return nil;
    end;

    -- A command line is a buffer of words, built a word at a time.
    function noWords(): Buffer[String] is
        return allocateEmpty();
    end;

    function plus(line: Buffer[String], word: Span[Nat8, Static]): Buffer[String] is
        var out: Buffer[String] := line;
        insertBack(&!out, fromLiteral(word));
        return out;
    end;

    function main(): ExitCode is
        var c: Command := makeCommand("convert", "Converts a file.");
        -- Arg::new("format").value_parser(["json", "text"]).env("CONVERT_FORMAT")
        let format: ChoiceKey := addChoice(&!c, fromEnvironment(option("format", 'f', "Output format"), "CONVERT_FORMAT"), "json|text", "");
        -- Arg::new("jobs").value_parser(value_parser!(i64).range(1..=64)).env("CONVERT_JOBS").default_value("1")
        let jobs: WholeKey := addWhole(&!c, fromEnvironment(option("jobs", 'j', "Parallel jobs"), "CONVERT_JOBS"), 1, 64, "1");
        let quiet: FlagKey := addFlag(&!c, option("quiet", 'q', "Print nothing"));
        let verbose: FlagKey := addVerbose(&!c, "Print more");
        -- Arg::new("output").value_parser(value_parser!(PathBuf)).requires("format")
        let output: PathKey := addPath(&!c, option("output", 'o', "Where to write"));
        -- Arg::new("exclude").action(ArgAction::Append)
        let exclude: TextKey := addText(&!c, repeatable(option("exclude", 'x', "Skip a name")), "");
        -- Arg::new("input").required(true), a positional argument that is a path
        let input: PathKey := addPositionalPath(&!c, "input", "The file to convert", true, false);
        -- Arg::new("quiet").conflicts_with("verbose")
        addConflict(&!c, anyFlag(quiet), anyFlag(verbose));
        addDependency(&!c, anyPath(output), anyChoice(format));
        let keys: Keys := Keys(format => format, jobs => jobs, quiet => quiet, verbose => verbose, output => output, exclude => exclude, input => input);
        printLn("the file alone, the jobs from the environment:");
        run(&c, keys, plus(noWords(), "in.txt"));
        printLn("everything on the line, which wins over the environment:");
        run(&c, keys, plus(plus(plus(plus(plus(plus(plus(plus(plus(noWords(), "-f"), "json"), "-j8"), "-o"), "out.json"), "-x"), "a.tmp"), "--exclude=b.tmp"), "in.txt"));
        printLn("an output without a format:");
        run(&c, keys, plus(plus(plus(noWords(), "-o"), "out.json"), "in.txt"));
        printLn("two flags that exclude each other:");
        run(&c, keys, plus(plus(plus(noWords(), "-q"), "-v"), "in.txt"));
        printLn("jobs out of the range:");
        run(&c, keys, plus(plus(plus(noWords(), "-j"), "99"), "in.txt"));
        printLn("a format that is not one:");
        run(&c, keys, plus(plus(plus(noWords(), "-f"), "xml"), "in.txt"));
        printLn("no input:");
        run(&c, keys, plus(noWords(), "-v"));
        destroyCommand(c);
        return ExitSuccess();
    end;
end module body.

What it prints:

the file alone, the jobs from the environment:
input: in.txt
format: none
jobs: 4
output: none
exclude: 0
quiet: 0, verbose: 0
everything on the line, which wins over the environment:
input: in.txt
format: json
jobs: 8
output: out.json
exclude: 2
  skip: a.tmp
  skip: b.tmp
quiet: 0, verbose: 0
an output without a format:
convert: --output needs --format
Usage: convert [OPTIONS] <INPUT>
Try convert --help for more information.
two flags that exclude each other:
convert: --quiet cannot be given together with --verbose
Usage: convert [OPTIONS] <INPUT>
Try convert --help for more information.
jobs out of the range:
convert: 99 is out of range for --jobs, which takes 1 to 64
Usage: convert [OPTIONS] <INPUT>
Try convert --help for more information.
a format that is not one:
convert: xml is not allowed for --format, which takes json, text
Usage: convert [OPTIONS] <INPUT>
Try convert --help for more information.
no input:
convert: <INPUT> is required
Usage: convert [OPTIONS] <INPUT>
Try convert --help for more information.

4.4 What clap printed for the same command lines

python3 scripts/clap-compare/compare.py <directory of the binaries> runs every command line that the three Hilde programmes run against the clap programme it models, with CONVERT_JOBS=4 in the environment for convert as env.txt has it, and compares with the program-stdout.txt shown above. Run on an aarch64 Linux host with rustc 1.85.1, cargo 1.85.1 and clap 4.6.7, which declares rust-version 1.85, the packages of Cargo.lock resolved for that compiler:

clap-compare scenarios=21 same=8 both-refuse=11 differs=2 bad=0
In the helpHildeclap 4.6.7
the placeholder of a valuethe name in capitals, <TIMES>, <FILE>...the name as written, <times>, <file>..., unless value_name("NAME") is given
a range[range: 1 to 5] after the textnothing
a Bool that is parsed by BoolishValueParsernothing[possible values: true, false]
a counter-v, --verbose More output [repeatable]-v, --verbose... More output
the order of the options of a subcommandthe global counter firstthe subcommand's own options first
the help of a subcommandhas -V, --versionhas none (the programme does not call propagate_version)

The tool checks itself: --self-test puts a wrong expectation into a run scenario and makes a refused line a valid one, and ends 1 unless both are seen (self-test-cases=2 self-test-wrong=0).

5. Where to look next