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:
| Schritt | clap | Standard.Arguments |
|---|---|---|
| deklarieren | Command::new("tool") und .arg(...) | var c: Command := makeCommand("tool", "about") und addFlag(&!c, ...) |
| parsen | get_matches() | parse(&c, &words), mit den Wörtern des Prozesses aus processArguments() |
| abfragen | matches.get_one::<T>("id") | textOf(&m, key) und die anderen Getter weiter unten |
| zerstören | von Rust fallengelassen | destroyMatches(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-Builder | Standard.Arguments | Anmerkung |
|---|---|---|
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 andere | eine 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 Text | addText(&!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, addChoice | der 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 Positionalen | isRepeatable true | nur das letzte Positionale darf wiederholbar sein |
| ein Positionales, das ein Pfad ist | addPositionalPath(&!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 Unterbefehl | addAlias(&!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 Beenden | Help(text) des Ergebnisses, oder helpText(&c) | das Programm gibt es aus und beendet sich |
| der Fehler ausgegeben und Beenden | describeError(&c, &error) | errorKind(&error), errorPosition(&error), dann destroyError(error) |
clap_complete | completionScript(&c, Bash()) | auch Zsh() und Fish() |
clap_mangen | manPage(&c, "October 7, 2026") | eine Seite in mdoc; das Datum ist ein Parameter |
die Crate argfile | expandArgumentFiles(&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
- Ein Schlüssel, keine Zeichenketten-ID.
addFlaggibt einenFlagKeyzurück,addTexteinenTextKeyund so weiter; das Programm fragt mit dem Schlüssel ab. Ein im Programm falsch geschriebener Name ist eine Variable, die der Compiler nicht kennt (H6039), und ein Schlüssel, der als falsche Art abgefragt wird, wird abgewiesen (H3060): Der Fehler, den clap erst finden kann, wenn das Programm läuft, wird beim Übersetzen gefunden. Der Preis ist, dass die Schlüssel dort aufbewahrt werden müssen, wo das Programm abfragt: einrecorddavon, wie in den Beispielen. - Ein Wert, kein Beenden.
get_matches()gibt bei einem Fehler aus und beendet den Prozess;parsegibt einenArgumentErrorzurück, und das Programm gibt ihn aus und liefert seinen eigenen Exit-Code zurück.try_get_matches()ist in clap das Naheliegendste. Nichts, was ein Benutzer tippt, hält das Programm an. - Kein Derive. clap's Derive-Form braucht aus einem Typ erzeugten Code, den Hilde nicht hat. Der Builder ist die gesamte Schnittstelle.
- Keine Präfix-Abkürzung.
--verbbedeutet nicht--verbose; ein Präfix, der heute eindeutig ist, ist mehrdeutig, sobald die nächste Option hinzukommt. Ein beinahe richtiges Wort bekommt stattdessen einen Vorschlag:unknown option --nmae, did you mean --name?clap hat die Abkürzung nur auf Anforderung (infer_long_args). - Vorgabewerte sind Text.
addWhole(..., 1, 5, "1"),addBool(..., "false"),addChoice(..., "json|text", "text"): Jede Art nimmt ihren Vorgabewert so, wie der Benutzer ihn tippen würde, und er wird von dem Code geprüft, der einen getippten Wert prüft. Es gibt kein typisiertesdefault_value_t. - Zweimal angegeben ist ein Fehler, sofern die Deklaration es nicht erlaubt. Ein Flag oder eine Option, die ein zweites Mal angegeben wird, ist
Repeated, wieSetundSetTruees in clap 4 sind;repeatable(spec)ist die eine Markierung, die ein Flag zu einem Zähler und eine Textoption zu einer Liste macht, wo clap die AktionenCountundAppendhat. - Eine Option nimmt immer das nächste Wort.
--output -vgibt-vals Wert, wie getopt es tut; clap liest ein Wort mit führendem Bindestrich als Option, sofern nichtallow_hyphen_valuesgesetzt ist. - Ein Bool nimmt die längere Liste.
addBoolnimmt true, yes, on, 1 und false, no, off, 0, das ist clap'sBoolishValueParserohne seine einzelnen Buchstaben; clap'svalue_parser!(bool)nimmt nurtrueundfalse. Ein Vorgabewert für ein Bool ist eines dieser Wörter als Text. - Lineare Werte. Ein
Command, dieMatches, die Wörter und einArgumentErrorwerden mit dendestroy...-Aufrufen zurückgegeben; Rust lässt sie fallen. Der Compiler weist ein Programm ab, das eines vergisst. - Namen sind ASCII, Text ist UTF-8. Optionsnamen, Unterbefehlsnamen und die Wörter einer Auswahl sind ASCII; ein Textwert, der kein UTF-8 ist, ist ein Fehler (
InvalidUtf8), und ein Pfad wird als die Bytes genommen, die er ist. - Die Wörter sind ein Argument, nicht der Prozess.
parseliest nichts außer dem Puffer, der ihm gegeben wird:processArguments()sind die Wörter des Prozesses ohne den Namen des Programms, ein Test baut seinen eigenen. - Ein Ganzzahltyp.
addWholeist einInt64mit einem Bereich; es gibt keinu8, keinen Gleitkommatyp. Ein Bereich wie0..=255ist der Weg, es auszudrücken.
3. Was fehlt, und der Weg drumherum
| clap | In Hilde | Umweg |
|---|---|---|
#[derive(Parser)] | nicht angeboten | den Befehl von Hand deklarieren, wie in Abschnitt 4 |
ArgGroup, eines von mehreren verlangt | nicht angeboten | addConflict 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 Option | eine Option hat einen Wert | die Option wiederholen (repeatable), oder einen Text nehmen und ihn mit Standard.Text.split zerlegen |
value_delimiter(',') | nicht angeboten | eine Textoption, mit Standard.Text.split zerlegt |
ein eigener value_parser | fünf Arten von Werten | den Text nehmen und ihn nach parse mit den Funktionen von Standard.Text parsen |
| Gleitkomma- und vorzeichenlose Zahlen | nur Int64 | einen Text nehmen und ihn parsen, oder addWhole mit einem Bereich verwenden |
trailing_var_arg, last(true) | ein Wort -- beendet die Optionen | die positionalen Argumente nach -- bleiben der Reihe nach erhalten; das letzte Positionale darf wiederholbar sein |
--no-flag-Negation, overrides_with | nicht angeboten | das zweite Flag deklarieren und addConflict zwischen beiden |
infer_subcommands, infer_long_args | nie | ein Vorschlag für ein beinahe richtiges Wort |
help_template, before_help, after_help, Farben | die Hilfe wird aus den Deklarationen geschrieben | den zusätzlichen Text vor oder nach helpText(&c) ausgeben |
value_hint, dynamische Vervollständigung | die Skripte nennen Optionen, Unterbefehle und die Wörter einer Auswahl | keiner |
ArgMatches::value_source | nicht angeboten | die Option ohne Vorgabewert deklarieren und den Vorgabewert im Programm anwenden, dann ist der Fall bekannt |
Command::print_help und render_usage | helpText(&c) und usageLine(&c) geben Zeichenketten zurück | sie ausgeben |
ein Wert, der kein UTF-8 ist, als String | ein Fehler für Text | mit 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
- same, 8: Jede Kommandozeile, die angenommen wird und ein Ergebnis ausgibt, liefert in clap und in Hilde dieselben Bytes: bei
greetdie Vorgabewerte, ein Name mit einer Zahl und einem Flag sowie die Version; beipackaddmit einem Flag und zwei Dateien, der Alias mit einem globalen Zähler vor dem Unterbefehl und der Zähler danach; beiconvertdie Umgebung allein und die Zeile, die über die Umgebung gewinnt. - both refuse, 11: Jede Kommandozeile, die Hilde abweist, weist clap mit dem Exit-Code 2 ab: bei
greeteine Zahl außerhalb ihres Bereichs, ein Bool, das keines ist, und eine falsch geschriebene Option; beipackkein Unterbefehl,addohne Datei und ein falsch geschriebener Unterbefehl; beiconverteine Ausgabe ohne Format, zwei Flags, die sich ausschließen, Jobs außerhalb des Bereichs, ein Format, das keines ist, und keine Eingabe. Die Wörter unterscheiden sich, wie oben gesagt; clap nennt eine ähnliche Option oder einen ähnlichen Unterbefehl (tip: a similar argument exists: '--name'), wo Hilde „did you mean“ sagt. - differs, 2: die Hilfe von
greetund die Hilfe vonpack add. Die Unterschiede, alle in der Hilfe und keiner in dem, was eine Zeile ergibt:
| In der Hilfe | Hilde | clap 4.6.7 |
|---|---|---|
| der Platzhalter eines Werts | der 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 Text | nichts |
ein Bool, das von BoolishValueParser geparst wird | nichts | [possible values: true, false] |
| ein Zähler | -v, --verbose More output [repeatable] | -v, --verbose... More output |
| die Reihenfolge der Optionen eines Unterbefehls | der globale Zähler zuerst | die eigenen Optionen des Unterbefehls zuerst |
| die Hilfe eines Unterbefehls | hat -V, --version | hat 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
Standard.Arguments: jeder Aufruf oben, mit dem, was er tut.examples/learn-arguments: ein längeres Programm mit Unterbefehlen, einem globalen Zähler, einem Bereich und einer Auswahl.