Antra

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 @main fuer 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 --verbose oder -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.options enthaelt benannte Optionen
  • parsed.positionals enthaelt positionale Argumente
  • parsed.argv enthaelt 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 Ergebnis
  • short: "p" erlaubt zusaetzlich -p
  • type: :int coerct den Wert zu einer Ganzzahl
  • default: 8080 setzt einen Fallback
  • doc: ... 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: true sammelt 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.ant

Das 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)
# 8080

Schlaegt 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 wie Validator.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; bei many: true wird 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: true zeigt bei --help, leerem argv oder Parse-Fehlern den Help-Text
  • exit_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.ant

11. 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! beziehungsweise parse_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.

Quelle dieser Seite