Standard.Arguments für alle, die clap kennen

Diese Seite stellt die Builder-API von clap neben Standard.Arguments. Sie ist für alle gedacht, Mensch oder KI-Assistent, die Kommandozeilen in Rust mit clap geschrieben haben und sie jetzt in Hilde schreiben. Sie sagt, was man aufruft, wo Hilde abweicht und warum, was fehlt und wie man ohne auskommt. Am Ende stehen drei vollständige Programme, jedes in beiden Sprachen.

Mit „clap“ ist hier clap 4 in seiner Builder-Form gemeint (Command, Arg, ArgAction), der Standard.Arguments folgt. Für die Derive-Form von clap gibt es keine Entsprechung (Abschnitt 2). Die Schnittstelle ist das Modul Standard.Arguments.

Was geprüft wird und was nicht: Die Hilde-Programme in Abschnitt 4 sind Testprogramme des Projekts. Sie werden übersetzt und ausgeführt, und ihre Ausgabe wird mit der hier gezeigten verglichen. Die Prüfläufe haben kein Rust, deshalb wurden die clap-Programme von Hand gebaut und ausgeführt, mit clap 4.6.7, rustc 1.85.1 und cargo 1.85.1. Was dabei herauskam, steht in Abschnitt 4.4.

1. Die Aufrufe

Ein Befehl wird durch Aufrufe auf einer Schreibreferenz, &!c, deklariert, und jede Deklaration einer Option gibt einen Schlüssel zurück; das Programm fragt die Treffer mit dem Schlüssel ab. Die Namen und die Hilfetexte einer Deklaration sind Literale, Span[Nat8, Static]. Deklarieren, parsen, abfragen, zerstören:

SchrittclapStandard.Arguments
deklarierenCommand::new("tool") und .arg(...)var c: Command := makeCommand("tool", "about") und addFlag(&!c, ...)
parsenget_matches()parse(&c, &words), mit den Wörtern des Prozesses aus processArguments()
abfragenmatches.get_one::<T>("id")textOf(&m, key) und die anderen Getter weiter unten
zerstörenvon Rust fallengelassendestroyMatches(m), destroyCommand(c), destroyArguments(words)

Die Deklarationen, ein clap-Aufruf je ein Hilde-Aufruf. Ein Kurzbuchstabe 0 und ein langer Name "" bedeuten: keiner. Die Wörter einer Auswahl werden durch einen senkrechten Strich verbunden geschrieben, wie in "fast|safe|small".

clap-BuilderStandard.ArgumentsAnmerkung
Command::new("tool") mit .about("...")makeCommand("tool", "...")Name und eine Hilfezeile in einem Aufruf; der Befehl ist linear
.version("1.0")setVersion(&!c, "1.0")-V und --version kommen damit
Arg::new("verbose").short('v').long("verbose").help("...")addVerbose(&!c, "..."), da -v und --verbose reserviert sind; option("name", 'n', "...") für andereeine Spec, die die add...-Aufrufe annehmen
.action(ArgAction::SetTrue)addFlag(&!c, spec)abfragen mit flagCount(&m, key) > 0
.action(ArgAction::Count)addFlag(&!c, repeatable(spec))flagCount(&m, key) ist die Zählung
.action(ArgAction::Set), ein TextaddText(&!c, spec, "")das letzte Argument ist der Vorgabewert, leer für keinen
.action(ArgAction::Append)addText(&!c, repeatable(spec), "")alle Werte: textCount(&m, key), textAt(&m, key, i)
.value_parser(value_parser!(i64).range(1..=9))addWhole(&!c, spec, 1, 9, "")ein Int64 in einem Bereich; 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) nimmt nur true und false
.value_parser(["fast", "safe"])addChoice(&!c, spec, words, "")choiceOf(&m, key) gibt den Index des Worts zurück
.value_parser(value_parser!(PathBuf))addPath(&!c, spec)beliebige Bytes, nicht leer; pathOf(&m, key)
.required(true)required(spec)auch durch die Umgebung erfüllt, wenn die Option eine hat
.default_value("6")das letzte Argument von addText, addWhole, addBool, addChoiceder Vorgabewert, wie der Benutzer ihn tippen würde
.conflicts_with("b")addConflict(&!c, anyFlag(a), anyFlag(b))anyText, anyWhole, anyBool, anyChoice, anyPath für die anderen Arten
.requires("b")addDependency(&!c, anyPath(a), anyChoice(b))a darf nur zusammen mit b angegeben werden
.env("TOOL_NAME")fromEnvironment(spec, "TOOL_NAME")geprüft, als wäre es getippt; ein Flag nimmt ein Bool-Wort
.value_name("FILE")valueName(spec, "file")in der Hilfe in Großbuchstaben angezeigt
ein positionales Arg::new("file")addPositional(&!c, "file", "help", isRequired, isRepeatable)positionalOf, oder positionalCount und positionalAt
.num_args(1..) bei einem PositionalenisRepeatable truenur das letzte Positionale darf wiederholbar sein
ein Positionales, das ein Pfad istaddPositionalPath(&!c, "file", "help", isRequired, isRepeatable)abgefragt mit pathOf
.subcommand(sub)addSubcommand(&!c, sub)sub wird getrennt gebaut, dann hineinverschoben; der Schlüssel wird mit chosen(&m, key) abgefragt
.subcommand_required(true)requireSubcommand(&!c)
.alias("a") bei einem UnterbefehladdAlias(&!sub, "a")bevor der Unterbefehl angehängt wird
.global(true)global(spec)von jedem darunterliegenden Unterbefehl akzeptiert
get_one::<String>("id")textOf(&m, key)ein Option einer Referenz
get_one::<i64>("id")wholeOf(&m, key)Option[Int64]
get_many::<String>("id")textCount(&m, key) und textAt(&m, key, i)für Pfade pathCount und pathAt
get_count("id")flagCount(&m, key)
get_flag("id")flagCount(&m, key) > 0
--help ausgegeben und BeendenHelp(text) des Ergebnisses, oder helpText(&c)das Programm gibt es aus und beendet sich
der Fehler ausgegeben und BeendendescribeError(&c, &error)errorKind(&error), errorPosition(&error), dann destroyError(error)
clap_completecompletionScript(&c, Bash())auch Zsh() und Fish()
clap_mangenmanPage(&c, "October 7, 2026")eine Seite in mdoc; das Datum ist ein Parameter
die Crate argfileexpandArgumentFiles(&words)Wörter, die mit @ beginnen, benennen Dateien; vor parse aufrufen

Was parse zurückgibt, ist Either[ArgumentError, Outcome]. Ein Outcome ist Run mit den Treffern, Help oder Version mit dem Text, den der Benutzer verlangt hat. Die Bibliothek gibt nichts aus und beendet den Prozess nie.

2. Wo Hilde abweicht, und warum

3. Was fehlt, und der Weg drumherum

clapIn HildeUmweg
#[derive(Parser)]nicht angebotenden Befehl von Hand deklarieren, wie in Abschnitt 4
ArgGroup, eines von mehreren verlangtnicht angebotenaddConflict für jedes Paar und eine Prüfung von flagCount oder textOf nach dem Parsen
num_args(2) bei einer Option, mehrere Werte nach einer Optioneine Option hat einen Wertdie Option wiederholen (repeatable), oder einen Text nehmen und ihn mit Standard.Text.split zerlegen
value_delimiter(',')nicht angeboteneine Textoption, mit Standard.Text.split zerlegt
ein eigener value_parserfünf Arten von Wertenden Text nehmen und ihn nach parse mit den Funktionen von Standard.Text parsen
Gleitkomma- und vorzeichenlose Zahlennur Int64einen Text nehmen und ihn parsen, oder addWhole mit einem Bereich verwenden
trailing_var_arg, last(true)ein Wort -- beendet die Optionendie positionalen Argumente nach -- bleiben der Reihe nach erhalten; das letzte Positionale darf wiederholbar sein
--no-flag-Negation, overrides_withnicht angebotendas zweite Flag deklarieren und addConflict zwischen beiden
infer_subcommands, infer_long_argsnieein Vorschlag für ein beinahe richtiges Wort
help_template, before_help, after_help, Farbendie Hilfe wird aus den Deklarationen geschriebenden zusätzlichen Text vor oder nach helpText(&c) ausgeben
value_hint, dynamische Vervollständigungdie Skripte nennen Optionen, Unterbefehle und die Wörter einer Auswahlkeiner
ArgMatches::value_sourcenicht angebotendie Option ohne Vorgabewert deklarieren und den Vorgabewert im Programm anwenden, dann ist der Fall bekannt
Command::print_help und render_usagehelpText(&c) und usageLine(&c) geben Zeichenketten zurücksie ausgeben
ein Wert, der kein UTF-8 ist, als Stringein Fehler für Textmit addPath deklarieren, das die Bytes nimmt

4. Drei Programme, jedes zweimal geschrieben

Jedes clap-Programm ist das Vorbild; das Hilde-Programm darunter ist ein Test der Suite ausgeführt mit den Kommandozeilen, die er selbst baut, statt mit denen des Prozesses, damit ein Lauf zeigt, was jeder Aufruf tut. Ein eigenes Programm nimmt processArguments() anstelle der Puffer des Beispiels, und nach parse und describeError gibt es ExitSuccess() oder einen eigenen Exit-Code zurück. Was das Hilde-Programm ausgibt, steht darunter: Die Wörter der Fehler und der Hilfe sind Hildes, clap formuliert sie anders.

4.1 Ein Befehl mit Vorgabewert, Bereich und Flag

Das clap-Programm (Cargo.toml braucht 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());
}

Das Hilde-Programm:

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.

Was es ausgibt:

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 Unterbefehle, ein Alias, ein globaler Zähler

Das clap-Programm:

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!(),
    }
}

Das Hilde-Programm. Der Zähler wird am Befehl deklariert und dort abgefragt: Er zählt, wo immer in der Zeile er angegeben wurde.

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.

Was es ausgibt:

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 Die Umgebung, ein Konflikt, eine Abhängigkeit

Das clap-Programm (für .env wird das Feature env von clap gebraucht):

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
    );
}

Das Hilde-Programm, ausgeführt mit CONVERT_JOBS=4 in seiner Umgebung (die Datei env.txt des Tests). Die Abhängigkeit ist die zweite Zeile, die die Deklaration nach den Optionen braucht: Ein Konflikt und eine Abhängigkeit werden am Befehl deklariert, zwischen Schlüsseln.

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.

Was es ausgibt:

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 Was clap für dieselben Kommandozeilen ausgab

python3 scripts/clap-compare/compare.py <Verzeichnis der Binärdateien> führt jede Kommandozeile, die die drei Hilde-Programme ausführen, gegen das clap-Programm aus, das sie nachbilden, mit CONVERT_JOBS=4 in der Umgebung für convert, wie env.txt es hat, und vergleicht mit der oben gezeigten program-stdout.txt. Ausgeführt auf einem aarch64-Linux-Rechner mit rustc 1.85.1, cargo 1.85.1 und clap 4.6.7, das rust-version 1.85 deklariert, mit den Paketen der Cargo.lock, aufgelöst für diesen Compiler:

clap-compare scenarios=21 same=8 both-refuse=11 differs=2 bad=0
In der HilfeHildeclap 4.6.7
der Platzhalter eines Wertsder Name in Großbuchstaben, <TIMES>, <FILE>...der Name wie geschrieben, <times>, <file>..., sofern nicht value_name("NAME") angegeben ist
ein Bereich[range: 1 to 5] nach dem Textnichts
ein Bool, das von BoolishValueParser geparst wirdnichts[possible values: true, false]
ein Zähler-v, --verbose More output [repeatable]-v, --verbose... More output
die Reihenfolge der Optionen eines Unterbefehlsder globale Zähler zuerstdie eigenen Optionen des Unterbefehls zuerst
die Hilfe eines Unterbefehlshat -V, --versionhat keines (das Programm ruft propagate_version nicht auf)

Das Werkzeug prüft sich selbst: --self-test setzt in einem Lauf eine falsche Erwartung ein und macht aus einer abgewiesenen Zeile eine gültige und endet mit 1, sofern nicht beides gesehen wird (self-test-cases=2 self-test-wrong=0).

5. Wo es weitergeht