Werkzeuge
CLI-Argumente fuer Skripte
Dieses Tutorial zeigt, wie du in Antra Shell-Argumente verarbeitest, Optionen definierst und daraus benutzerfreundliche CLI-Skripte baust.
Der Fokus ist bewusst praktisch:
- zuerst ein kleiner Parser mit expliziter Spec
- dann dieselbe Idee mit
@option(...)und@arg(...) - danach Help-Texte und
parse_or_help(...) - zum Schluss
@mainfuer echte Skripte ohne Boilerplate
Wenn du nur schnell ein Beispiel starten willst, schau auch in:
Wann Antra::Arg die richtige Wahl ist#
Antra::Arg ist die kleine Stdlib-Surface fuer Kommandozeilenargumente.
Sie ist passend, wenn dein Skript Dinge wie diese akzeptieren soll:
- Flags wie
--verboseoder-v - Optionen mit Wert wie
--port 8080 - positionale Argumente wie Dateinamen
- kleine Help-Texte fuer
--help
Wenn dein Skript nur rohe Shell-Argumente als Array braucht, reicht oft schon
Antra::System.argv(). Sobald du aber boolesche Flags, Defaults, int-Werte
oder klare Fehlermeldungen willst, ist Antra::Arg der bessere Einstieg.
1. Der einfachste Start mit einer expliziten Spec#
Die direkteste Form ist ein Spec-Array.
import Antra::Arg as Arg
import Antra::System as System
my spec = [
[name: "verbose", short: "v", type: :bool, doc: "Enable verbose output"],
[name: "port", short: "p", type: :int, default: 8080, doc: "Port to bind"],
[name: "config", type: :string, doc: "Config file"],
[name: "files", positional: true, many: true, doc: "Input files"]
]
my parsed = Arg.parse(spec, System.argv()).!
println(parsed.options.verbose)
println(parsed.options.port)
println(parsed.options.config)
println(parsed.positionals.files)Wichtig ist die Rueckgabeform von Arg.parse(...).!:
parsed.optionsenthaelt benannte Optionenparsed.positionalsenthaelt positionale Argumenteparsed.argventhaelt das rohe Original-argv
Das heisst: du definierst zuerst die Form, und danach arbeitest du mit einem normalisierten Ergebnis-Map weiter.
2. Wie einzelne Spec-Eintraege gelesen werden#
Ein Eintrag wie dieser:
[name: "port", short: "p", type: :int, default: 8080, doc: "Port to bind"]bedeutet:
name: "port"ist der semantische Name im Ergebnisshort: "p"erlaubt zusaetzlich-ptype: :intcoerct den Wert zu einer Ganzzahldefault: 8080setzt einen Fallbackdoc: ...erscheint spaeter im Help-Text
Optionen koennen ausserdem als verpflichtend markiert werden:
[name: "config", type: :string, required: true, doc: "Config file"]Dann muss der Aufruf --config ... liefern, solange kein default gesetzt ist.
Ein positionaler Eintrag sieht so aus:
[name: "files", positional: true, many: true, doc: "Input files"]Das bedeutet:
- positional statt
--option many: truesammelt mehrere Restwerte in einem Array
Auch positionale Argumente koennen required: true setzen. Generell gilt bei
Antra::Arg: required funktioniert fuer Optionen und Positionals; ein
gesetzter default macht den Eintrag dabei effektiv optional.
3. Typische Eingaben und wie sie gemappt werden#
Zu dieser Spec:
my spec = [
[name: "verbose", short: "v", type: :bool],
[name: "port", short: "p", type: :int, default: 8080],
[name: "files", positional: true, many: true]
]passen zum Beispiel diese Aufrufe:
my-tool --verbose --port 3000 a.ant b.ant
my-tool -v -p 3000 a.ant b.ant
my-tool --port=3000 a.ant b.antDas Ergebnis sieht dann grob so aus:
%{
options: %{ verbose: true, port: 3000 },
positionals: %{ files: ["a.ant", "b.ant"] },
argv: ["--verbose", "--port", "3000", "a.ant", "b.ant"],
rest: []
}4. Werte validieren mit Antra::Validator#
Typen wie :int sorgen dafuer, dass --port abc scheitert. Oft willst du aber
mehr: einen Port im Bereich 1..65535, eine E-Mail-Form, oder nur bestimmte
erlaubte Werte. Dafuer gibt es das optionale Feld validator:, das Validatoren
aus Antra::Validator nutzt (kompakte Referenz in
stdlib/Arg.md).
import Antra::Arg as Arg
import Antra::Validator as V
my spec = [
[name: "port", type: :int, validator: V.value_range(1, 65535)],
[name: "mode", type: :string, validator: V.in_list(["dev", "prod"])],
[name: "name", type: :string, validator: [V.required(), V.length_range(2, 40)]]
]
println(Arg.parse(spec, ["--port", "8080", "--mode", "dev", "--name", "Ada"]).!.options.port)
# 8080Schlaegt eine Regel fehl, kommt ein klarer Parse-Fehler mit der Meldung des Validators:
println(Arg.parse(spec, ["--port", "70000"]))
# %Err["Arg.parse: invalid value for '--port': Please enter a number between 1 and 65535."]
println(Arg.parse(spec, ["--mode", "stage"]))
# %Err["Arg.parse: invalid value for '--mode': Please choose one of: dev, prod."]Ein paar Dinge, die den Alltag leichter machen:
validator:nimmt eine Validator-Funktion oder eine Liste. Eine Liste wird wieValidator.all(...)behandelt: der Reihe nach, der erste Fehler gewinnt.- Die Validierung laeuft auf dem rohen String vor der Typ-Coercion. Dieselben
Validatoren funktionieren deshalb auch interaktiv (
Console.ask) oder fuer Config-Werte — eine Regel, ueberall. - Ein
default:-Wert wird nicht validiert; waehle ihn also gueltig. validator:gilt fuer Optionen und Positionals gleichermassen; beimany: truewird jeder Einzelwert geprueft.
Die verfuegbaren Bausteine (integer, email, url, ip, length_range,
value_range, in_list, regexp, …) und die Kombinatoren all/any stehen in
der Validator-Referenz.
5. Der praktische Alltag: Help statt harter Fehler#
Fuer kleine Skripte willst du oft nicht sofort mit einem Parse-Fehler abbrechen, sondern stattdessen einen Help-Text zeigen.
Dafuer gibt es zwei Stufen.
Nur Help rendern#
import Antra::Arg as Arg
println(Arg.help(spec).!)
println(Arg.help(spec, [program: "deploy"]).!)Mit program: "deploy" bekommt der Text automatisch eine Usage:-Zeile.
Parse oder Help/Fehler als strukturierter Outcome#
import Antra::Arg as Arg
import Antra::System as System
match Arg.parse_or_help(spec, System.argv(), [program: "deploy"]).! {
%Parsed[parsed] -> run(parsed)
%Help[_, text] -> println(text)
%ParseError[_, err, text] -> {
println(err)
println("")
println(text)
}
}Das ist fuer Enduser-Skripte oft die angenehmste Form, weil der Parser nicht selbst druckt oder beendet, aber trotzdem alles liefert, was du fuer eine gute CLI brauchst.
6. Dieselbe CLI direkt an einer Funktion beschreiben#
Wenn du die Spec lieber direkt an der Funktion haengen haben willst, nutze
@option(...) und @arg(...).
import Antra::Arg as Arg
@option(name: "verbose", short: "v", type: :bool, doc: "Enable verbose output")
@option(name: "target", type: :string, default: "dev", doc: "Deployment target")
@arg(name: "files", many: true, doc: "Input files")
fn cli(argv) -> Arg.parse_attrs(main.cli, argv).!Der Vorteil ist hier weniger Tipparbeit an der Aufrufstelle:
my parsed = cli(["--verbose", "--target", "prod", "a.ant", "b.ant"])Fuer Help und den Outcome-Flow gibt es die parallelen Metadaten-Varianten:
println(Arg.help_attrs(main.cli, [program: "deploy"]).!)
match Arg.parse_or_help_attrs(main.cli, argv, [program: "deploy"]).! {
%Parsed[parsed] -> run(parsed)
%Help[_, text] -> println(text)
%ParseError[_, err, text] -> {
println(err)
println(text)
}
}7. Wann explizite Spec und wann Metadaten?#
Beides ist gueltig, aber die Einsatzfaelle sind etwas verschieden.
Explizite Spec ist meist besser, wenn:
- du die CLI-Datenform getrennt von der Funktion halten willst
- du dieselbe Spec an mehreren Stellen wiederverwenden willst
- du zuerst rein datengetrieben denken willst
Metadaten direkt an der Funktion sind meist besser, wenn:
- das Skript einen klaren Einstiegspunkt hat
- der Parser nur fuer genau diese eine Funktion existiert
- Help, Parse und Ausfuehrung nah zusammenliegen sollen
8. Ein alltagstauglicher manueller CLI-Flow#
Ohne @main sieht ein kleines Skript oft so aus:
import Antra::Arg as Arg
import Antra::System as System
my spec = [
[name: "verbose", short: "v", type: :bool, doc: "Enable verbose output"],
[name: "files", positional: true, many: true, doc: "Input files"]
]
fn run(parsed) -> {
println(parsed.options.verbose)
println(parsed.positionals.files)
}
match Arg.parse_or_help(spec, System.argv(), [program: "sync-files"]).! {
%Parsed[parsed] -> run(parsed)
%Help[_, text] -> println(text)
%ParseError[_, err, text] -> {
println(err)
println("")
println(text)
}
}Das ist eine gute Default-Form, wenn du den Einstieg explizit im Skript sehen willst.
9. Noch kuerzer mit @main#
Fuer echte Enduser-Skripte ist @main oft die bequemste Surface.
import Antra::Arg as Arg
@main(help: true, exit_codes: [error: 2])
@option(name: "verbose", short: "v", type: :bool, doc: "Enable verbose output")
@option(name: "port", short: "p", type: :int, default: 8080, doc: "Port to bind")
@arg(name: "files", many: true, doc: "Input files")
fn main(parsed) -> {
println("verbose: " <> str(parsed.options.verbose))
println("port: " <> str(parsed.options.port))
println("files: " <> str(parsed.positionals.files))
}Das bedeutet im Alltag:
- die gestartete Datei sucht ihren
@main-Einstiegspunkt automatisch help: truezeigt bei--help, leeremargvoder Parse-Fehlern den Help-Textexit_codes: [error: 2]setzt fuer Parse-Fehler einen expliziten Exit-Code- bei erfolgreichem Parse wird
main(parsed)direkt aufgerufen
Fuer viele kleine Tools ist das die angenehmste Kombination aus kurzer Syntax, brauchbarer Hilfe und sauberem Fehlermodus.
10. Ein realistisches Mini-Beispiel#
Dieses Skript akzeptiert eine Zielumgebung und eine Liste von Dateien:
import Antra::Arg as Arg
@main(help: true, exit_codes: [error: 2])
@option(name: "target", type: :string, default: "dev", doc: "Deployment target")
@option(name: "verbose", short: "v", type: :bool, doc: "Enable verbose output")
@arg(name: "files", many: true, doc: "Files to upload")
fn main(parsed) -> {
println("target: " <> parsed.options.target)
println("verbose: " <> str(parsed.options.verbose))
println("files: " <> str(parsed.positionals.files))
}Typische Aufrufe:
antra deploy.ant --help
antra deploy.ant --target prod release.ant smoke.ant
antra deploy.ant -v --target stage notes.ant11. Typische Stolperfallen#
Ein paar Dinge sind in der Praxis besonders haeufig.
argv muss ein Array sein#
Arg.parse(...), Arg.parse_or_help(...) und ihre Varianten erwarten immer ein
Array von Strings.
Wenn du von der Shell kommst, ist System.argv() oder @main fast immer die
richtige Quelle.
:bool-Optionen brauchen keinen expliziten Wert#
Eine boolesche Option wie:
[name: "verbose", short: "v", type: :bool]wird als Flag gedacht. --verbose setzt dann auf true, und im Help-Text wird
effektiv default: false gezeigt.
Parse-Fehler und Help sind absichtlich getrennt vom Druck#
Arg.parse(...).! wirft bei Fehlern.
Arg.parse_or_help(...).! wirft dagegen nicht fuer normale Help- oder
Parse-Outcomes, sondern gibt einen Outcome-Map zurueck. Das ist Absicht, damit
du als Skriptautor selbst kontrollierst, was gedruckt wird.
@main mit CLI-Metadaten braucht import Antra::Arg as Arg#
Wenn @main zusammen mit @option(...) oder @arg(...) verwendet wird, muss
das Skript Antra::Arg unter dem Alias Arg importieren.
12. Welche Form sollte ich als Default nehmen?#
Wenn du schnell produktiv sein willst, ist diese Faustregel brauchbar:
- fuer kleine Tools mit einem klaren Einstiegspunkt:
@main(help: true) - fuer etwas explizitere Skripte oder wiederverwendbare Parser:
parse_or_help! - fuer interne oder programmatische Nutzung:
parse!beziehungsweiseparse_attrs!
Wenn du heute nur eine Form lernen willst, lerne zuerst
Arg.parse_or_help(..., [program: "..."]).! und danach @main(help: true).
Beide decken den normalen Enduser-Alltag sehr gut ab.
