Parser für Befehlszeilenargumente
Inhaltsverzeichnis
Meine Intention mit diesem Artikel ist es, eine sprachunabhängige Vorlage für einen einfachen Befehlszeilenparser zu liefern. Nicht jedes Programm muss von einer dicken Bibliothek für das Parsen von Befehlszeilenargumenten abhängen. Das Publikum soll in die Lage versetzt werden, einen einfachen Parser in der Programmiersprache ihrer Wahl zu implementieren.
Dieser Artikel nimmt Bezug auf:
Einleitung #
Wenn man ein Befehlszeilenprogramm schreibt, muss man die Befehlszeilenargumente, mit denen das Programm aufgerufen wird, erkennen und prüfen. Der Programmteil der für das Erkennen und Prüfen der Argumente zuständig ist, wird Befehlszeilenparser oder in Englisch “command line parser” genannt. In jeder weit verbreiteten Programmiersprache mit der sich Befehlszeilenprogramme implementieren lassen, gibt es auch Bibliotheken mit einem Parser für die Verarbeitung von Befehlszeilenargumenten. Aber nicht in jeder Sprache ist der Parser Teil der Standardbibliothek.
Hier nur ein paar Beispiele:
Programmiersprache | Name | Standardbibliothek |
---|---|---|
C | getopt | nur für GCC |
C++ | CLI11 | nein |
C# | CommandLineParser | nein |
Java | Apache Commons CLI | nein |
JavaScript/NodeJS | util.parseArgs() | ja |
JavaScript/NodeJS | yargs | nein |
Python | argparser | ja |
Bash | getopt-Befehl | (ja) |
PowerShell | in Sprache integriert | ja |
Das bedeutet, dass man zum Parsen der Befehlszeile häufig eine Bibliothek als Abhängigkeit in das Projekt aufnehmen muss. Es kann sinnvoll sein, eine solche Abhängigkeit zu vermeiden. Besonders dann, wenn man nur wenige einfache Befehlszeilenargumente unterstützen möchte und sonst keine Abhängigkeiten benötigt. Ohne hier ausführlich das Für und Wider von Abhängigkeiten in Softwareprojekten diskutieren zu wollen, soll das Folgende kurz erwähnt werden: Abhängigkeiten haben neben dem offensichtlichen Vorteil, nämlich einen Teil des Programms nicht selbst implementieren zu müssen, auch einige Nachteile.
- Komplexität und Fehleranfälligkeit durch unnötig großen Funktionsumfang
- Unkontrollierbarer Umgang mit Fehlerbehebungen und Sicherheitslücken
- Schlechte Abwärtskompatibilität bei Weiterentwicklung
- Skripte benötigen zusätzlichen Installationsaufwand (kein Single-File-Deploy)
Wenn diese Nachteile überwiegen oder wenn man an Dependophobia1 leidet, ist es hilfreich zu wissen, wie ein einfacher Befehlszeilenparser implementiert werden kann.
Anforderungen #
Unter einem einfachen Befehlszeilenparser wird hier der folgende Funktionsumfang verstanden:
- Schalter
- Optionen mit einer festen Anzahl von Werten
- Syntax nach GNU/Linux (kurze und lange Schlüsselworte)
- Ein oder mehrere Positionsargumente
- Reihenfolge von Schaltern, Optionen und Positionsargumenten soll beliebig sein
- Hilfe
Unbekannte Schalter und Optionen werden als Positionsargument interpretiert. Fehlende Positionsargumente führen zu einer Fehlermeldung.
Programmablauf #
Der Parser arbeitet mit einem Index, der in die Liste der Befehlszeilenargumente zeigt, und einer Schleife. Das Ergebnis des Parsers wird in einer Reihe von Variablen festgehalten, die zunächst mit Standardwerten initialisiert und anschließend aus der Schleife heraus aktualisiert werden. Fehlermeldungen werden entweder in einer solchen Zustandsvariablen festgehalten oder sofort ausgegeben. Wird der Hilfeschalter erkannt, kann die Schleife sofort verlassen und der Hilfetext ausgegeben werden. Nach Verlassen der Schleife wird geprüft, ob alle notwendigen Positionsargumente angegeben wurden.
Der Ablauf soll hier mit Pseudo-Code verdeutlicht werden.
Im Beispiel heißt das Befehlszeilenprogramm tool
und unterstützt die folgende Syntax:
tool [-a|--alt] [-s|--speed <S>] [-o|--offset <X> <Y>] <Eingabedatei>
tool -h|--help
Es kann also optional in einem alternativen Modus arbeiten. Die Arbeitsgeschwindigkeit und ein Offset können mit Optionen angepasst werden und es benötigt den Pfad zu einer Eingabedatei.
Der Parser kann die folgenden Fehler vorfinden:
- Ein Wert für eine Option fehlt
- Das Format eines Optionswerts ist ungültig (keine Fließkommazahl für
--speed
oder--offset
) - Zu wenig Positionsargumente bzw. das einzige hier erwartete Positionsargument fehlt
- Zu viele Positionsargumente
Initialisierung #
Zu Beginn des Ablaufs werden die Variablen mit Standardwerten initialisiert, die das Ergebnis des Parsens festhalten.
Boolean error := false
String message := null
Boolean help := false
Boolean alt_mode := false
Float speed := 100.0
Float offset_x := 0.0
Float offset_y := 0.0
String input_file := null
Schleife #
Nun folgt die Schleife, welche über die Argumente läuft und die Variablen aktualisiert.
Zum Umwandeln der Zeichenketten in Fließkommazahlen werden die fiktiven Funktionen is_float()
und float()
genutzt. Die Funktion is_float()
gibt dann true
zurück, wenn die übergebene Zeichenkette als Fließkommazahl interpretiert werden kann.
Die Befehlszeilenargumente befinden sich in einer 0-basierten Liste mit dem Namen args
. Die Länge einer Liste kann mit der fiktiven Funktion length()
ermittelt werden.
Integer index := 0
while index < length(args) and error = null:
String arg := args[index]
if arg = "-h" or arg = "--help":
# Hilfeschalter
help := true
else if arg = "-a" or arg = "--alt":
# Schalter
alt_mode := true
else if arg = "-s" or arg = "--speed":
# Option mit einem Wert
index := index + 1
if index >= length(args):
error := true
message := "Fehlendes Argument für <S>"
else if not is_float(args[index]):
error := true
message := "Fließkommazahl für <S> erwartet"
else:
speed := float(args[index])
else if arg = "-o" or arg = "--ofset":
# Option mit zwei Werten
index := index + 1
if index >= length(args):
error := true
message := "Fehlendes Argument für <X>"
else if not is_float(args[index]):
error := true
message := "Fließkommazahl für <X> erwartet"
else:
offset_x := float(args[index])
index := index + 1
if index >= length(args):
error := true
message := "Fehlendes Argument für <Y>"
else if not is_float(args[index]):
error := true
message := "Fließkommazahl für <Y> erwartet"
else:
offset_y := float(args[index])
else:
# Positionsargument
if input_file = null:
input_file := arg
else:
error := true
message := "Es kann nur eine Eingabedatei verarbeitet werden"
index := index + 1
Hilfeausgabe #
Zu diesem Zeitpunkt steht bereits fest, ob der Benutzer die Hilfe aufrufen möchte oder nicht. Denn entweder er hat einen Hilfeschalter gesetzt oder nicht. Deshalb kann nun die Hilfe ausgegeben werden. Anschließend beendet sich das Programm mit dem Exit-Code 0 um zu signalisieren, dass es erfolgreich ausgeführt wurde.
if help:
print "Syntax: tool [-a] [-s <S>] [-o <X> <Y>] <Eingabedatei>"
print " tool -h|--help"
# Hier weiteren Hilfetext ausgeben
...
exit 0
Fehlererkennung #
Nach der Schleife muss geprüft werden, ob die notwendigen Argumente übergeben wurden, oder allgemeiner ob der in den Variablen festgehaltene Zustand für die weitere Programmausführung geeignet ist.
Für diese Beispiel muss nur geprüft werden, ob das Positionsargument für input_file
übergeben wurde.
if input_file = null:
error := true
message := "Eingabedatei muss angegeben werden"
An dieser Stelle könnten weitere Prüfungen für die Werte von Optionen und Positionsargumenten eingefügt werden. Im Beispiel könnte hier geprüft werden, ob die Eingabedatei vorhanden und lesbar ist.
Nun kann geprüft werden, ob der Parser einen Fehler erkannt hat oder nicht. Ist dies der Fall, wird der Fehler ausgegeben und das Programm mit einem Exit-Code ungleich 0 beendet.
if error:
print "Syntaxfehler beim Programmaufruf:"
print message
exit 1
Nun hat der Parser seine Arbeit getan und das Ergebnis ist in den Variablen festgehalten. Das Programm kann mit seiner eigentlichen Aufgabe fortfahren.
Wertumwandlungen #
Es ist häufig der Fall, dass man die Zeichenketten für die Optionswerte und Positionsargumente in andere Datentypen umwandeln muss. Im obigen Pseudo-Code wurde dies mit der damit verbundenen Fehlererkennung direkt “inline” implementiert.
Man könnte aber auch, um die Schleife übersichtlicher zu gestalten, die Argumente für die Werte zunächst in zusätzlichen Zeichenkettenvariablen zwischenspeichern und die Umwandlung und die dazugehörige Fehlererkennung erst nach der Schleife durchführen. Der Nachteil sind die zusätzlich erforderlichen Variablen.
Je nach dem welche Möglichkeiten die Programmiersprache bietet, kann man die Umwandlung der Optionswerte einschließlich der Fehlerbehandlung auch in eine Funktion auslagern. Wenn man hier auf das Werfen und Fangen von Ausnahmen verzichten möchte, muss die Funktion entweder die Möglichkeit haben mehrere Werte zurückzugeben (umgewandelter Wert, Fehlerzustand, Fehlernachricht) oder die Funktion muss den Fehlerzustand (error
und message
) anderweitig aktualisieren können.
Die folgende Pseudo-Funktion parse_float()
benötigt impliziten Lesezugriff auf args
und impliziten Schreibzugriff auf error
und message
.
function parse_float(Integer index, Float default_value, String name):
if index >= length(args):
error := true
message := "Fehlendes Argument für " + name
return default_value
String arg := args[index]
if not is_float(arg):
error := true
messgage := "Fließkommazahl für " + name + " erwartet"
return default_value
return float(arg)
Die Schleife ist damit übersichtlicher und zusätzliche Optionen lassen sich einfacher implementieren.
Integer index := 0
while index < length(args) and error = null:
String arg := args[index]
if arg = "-h" or arg = "--help":
# Hilfeschalter
help := true
else if arg = "-a" or arg = "--alt":
# Schalter
alt_mode := true
else if arg = "-s" or arg = "--speed":
# Option mit einem Wert
index := index + 1
speed := parse_float(index, speed, "<S>")
else if arg = "-o" or arg = "--ofset":
# Option mit zwei Werten
index := index + 1
offset_x := parse_float(index, offset_x, "<X>")
index := index + 1
offset_y := parse_float(index, offset_y, "<Y>")
else:
# Positionsargument
if input_file = null:
input_file := arg
else:
error := true
message := "Es kann nur eine Eingabedatei verarbeitet werden"
index := index + 1
Beliebig viele Positionsargumente #
Sollen mehrere Positionsargumente unterstützt werden, kann die Variable input_file
vom Typ Zeichenkette durch eine Variable input_files
vom Typ Liste von Zeichenketten ersetzt werden. Des weiteren wird eine fiktive Prozedur append()
genutzt, welche als Argumente eine Liste und ein Element erwartet, und das Element an die Liste anfügt. Nun wird die Schleife wie folgt angepasst.
Integer index := 0
while index < length(args) and error = null:
String arg := args[index]
if arg = "-h" or arg = "--help":
# Hilfeschalter
help := true
...
else:
# Positionsargument
append(input_files, arg)
Wenn das Programm sinnvoll damit umgehen kann, dass kein Positionsargument übergeben wird, entfällt die oben aufgeführte Fehlererkennung. Ist mindestens ein Positionsargument erforderlich wird die Fehlererkennung wie folgt angepasst.
if length(input_file) = 0:
error := true
message := "Es muss mindestens eine Eingabedatei angegeben werden"
Grenzen #
Der hier vorgestellte Algorithmus hat eine größere Schwäche:
Schlüsselworte unbekannter Schalter oder Optionen werden als Positionsargumente erkannt.
Der Aufruf tool --unknown Meine_Eingabedaten.txt
würde nicht melden, dass ein Schalter oder eine Option --unknown
nicht unterstützt wird, sondern würde die Fehlermeldung ausgeben: “Es kann nur eine Eingabedatei verarbeitet werden”. Diese Meldung spiegelt den eigentlichen Fehler nicht wieder und leitet den Benutzer in eine falsche Richtung.
Dieses Verhalten ließe sich abstellen, wenn man sicher davon ausgehen könnte, dass weder Optionswerte noch Positionsargumente mit einem Bindestrich -
beginnen. Für negative Zahlwerte ist diese Annahme aber bereits ungültig.
Man könnte die Annahme nun verfeinern und formulieren, dass Optionswerte und Positionsargumente nicht mit einem Bindestrich, gefolgt von einer Ziffer, beginnen dürfen und dass Schlüsselwörter von Schaltern und Optionen nicht mit einer Ziffer beginnen dürfen.
Nun hat man den Wertebereich für Optionen und Positionsargumente aber immer noch eingegrenzt. Außerdem wurde die Benennungsfreiheit für Schalter und Optionen geschwächt.
Die zusätzliche Komplexität lohnt sich für einfache Programme i. d. R. nicht.
Praktische Beispiele #
In diesem Abschnitt wird das Beispielprogramm in verschiedenen Programmiersprachen umgesetzt. In Sprachen, für die der Parser bereits in der Standardbibliothek enthalten ist, wird dieser auch genutzt. Für die anderen Sprachen wird der obige Algorithmus implementiert. Die Art und Weise wie Fehler gemeldet und Fehlertexte ausgegeben oder übermittelt werden, variiert zwischen den Beispielen. Die Beispiele sind absichtlich nicht so ähnlich wie möglich gestaltet. Es wurde vielmehr ein Stil angestrebt, der für kleine Programme in der jeweiligen Programmiersprache typisch ist.
Alle hier gezeigten Beispiele sind auch auf GitHub zu finden:
https://github.com/mastersign/commandline-parser-examples
C #
Die Implementierung in C folgt dem ANSI-C-Standard und ist portabel (Windows, Linux, macOS). In der folgenden Implementierung wurden lediglich stdio.h
und string.h
aus der Standardbibliothek genutzt.
Besonderheiten:
- Die Funktion
getopt()
aus der GNU-Standardbibliothek wird nicht verwendet. - Das Prüfen auf ein Schalter- oder Optionsschlüsselwort ist in der Funktion
matchKeyword()
implementiert. - Das Umwandeln eines Optionswerts in eine Fließkommazahl ist in der Funktion
parseDoubleValue()
implementiert. - Fehlermeldungen werden sofort bei Erkennung des Fehlers mit
fprintf()
ausgegeben, um Allokationen für konkatenierte Zeichenketten zu vermeiden. - Es wird keine Zustandsvariable für einen Fehler verwendet, statt dessen werden die Prozedur und das Programm sofort nach Ausgabe des Fehlertextes verlassen.
tool.c
:
#include <stdio.h>
#include <string.h>
// Hilfssymbole
#define TRUE 1
#define FALSE 0
#define OK 0
#define ERR 1
// Standardwerte
#define DEFAULT_SPEED 100.0
#define DEFAULT_OFFSET_X 0.0
#define DEFAULT_OFFSET_Y 0.0
// Variablen mit Standardwerten initialisieren
int help = FALSE;
int altMode = FALSE;
double speed = DEFAULT_SPEED;
double offsetX = DEFAULT_OFFSET_X;
double offsetY = DEFAULT_OFFSET_Y;
const char *inputFile = NULL;
void printHelp() {
printf("Syntax: tool [-a] [-s <S>] [-o <X>,<Y>] <Eingabedatei>\n");
printf(" tool -h | --help\n");
printf("\n");
printf("Ein Beispiel fuer ein Befehlszeilenprogramm\n");
printf("\n");
printf("Schalter:\n");
printf(" -h, --help\n");
printf(" Zeigt diese Hilfe an.\n");
printf(" -a, --alt\n");
printf(" Fuehrt das Programm im alternativen Modus aus.\n");
printf("\n");
printf("Optionen:\n");
printf(" -s, --speed <S>\n");
printf(" Gibt die Geschwindigkeit an.\n");
printf(" Standard: %.1f\n", DEFAULT_SPEED);
printf(" -o, --offset <X>,<Y>\n");
printf(" Gibt den Offset fuer die Ausfuehrung in X- und Y-Richtung an.\n");
printf(" Standard: %.1f,%.1f\n", DEFAULT_OFFSET_X, DEFAULT_OFFSET_Y);
}
int matchKeyword(
const char *arg,
const char *shortName, const char *longName
) {
return strcmp(arg, shortName) == 0
|| strcmp(arg, longName) == 0
? TRUE : FALSE;
}
int parseFloat(
const int argc, const char *argv[], const int index,
const char *metaVar, double *value
) {
if (index >= argc) {
fprintf(stderr,
"Syntaxfehler: Fehlendes Argument fuer %s\n",
metaVar);
return ERR;
}
int matches = sscanf(argv[index], "%lf", value);
if (matches != 1) {
fprintf(stderr,
"Syntaxfehler: Fliesskommazahl fuer %s erwartet\n",
metaVar);
return ERR;
}
return OK;
}
int parseCliArguments(const int argc, const char *argv[]) {
// Schleife beginnt mit Index 1, um Pfad der ausfuehrbaren Datei zu überspringen
for (int i = 1; i < argc; i++) {
if (matchKeyword(argv[i], "-h", "--help")) {
help = TRUE;
return OK; // Abkuerzung, wenn Hilfeschalter verwendet wird
} else if (matchKeyword(argv[i], "-a", "--alt")) {
// Schalter
altMode = TRUE;
} else if (matchKeyword(argv[i], "-s", "--speed")) {
// Option mit einem Wert
i++;
if (parseDoubleValue(argc, argv, i, "<S>", &speed) != OK) {
return ERR;
}
} else if (matchKeyword(argv[i], "-o", "--offset")) {
// Option mit zwei Werten
i++;
if (parseDoubleValue(argc, argv, i, "<X>", &offsetX) != OK) {
return ERR;
}
i++;
if (parseDoubleValue(argc, argv, i, "<Y>", &offsetY) != OK) {
return ERR;
}
} else {
// Positionsargument(e)
if (inputFile == NULL) {
inputFile = argv[i];
} else {
fprintf(stderr,
"Syntaxfehler: Es kann nur eine Eingabedatei verarbeitet werden\n");
return ERR;
}
}
}
// Fehlererkennung
if (inputFile == NULL) {
fprintf(stderr,
"Syntaxfehler: Es muss eine Eingabedatei angegeben werden\n");
return ERR;
}
return OK;
}
int main(int argc, char *argv[]) {
// Befehlszeilenparser ausführen
int parseResult = parseCliArguments(argc, argv);
if (help == TRUE) {
// Hilfetext ausgeben und beenden
printHelp();
return OK;
}
if (parseResult != OK) {
// Abbrechen wenn Parser Fehler erkannt hat
return ERR;
}
// Hauptprogramm
printf("Programm wird ausgefuehrt mit:\n");
if (altMode == TRUE) {
printf(" Modus: alternativ\n");
} else {
printf(" Modus: normal\n");
}
printf(" Speed: %.1f\n", speed);
printf(" Offset: (%.1f; %.1f)\n", offsetX, offsetY);
printf(" Input: %s\n", inputFile);
return OK;
}
C++ #
Der Befehlszeilenparser in C++ wurde in ein eigenes Modul cmdline
ausgelagert, weil davon ausgegangen wird, dass auch kleine Befehlszeilenprogramme in C++ i. d. R. mit einer Projektstruktur in mehreren Dateien aufgesetzt werden.
Die Implementierung nutzt Funktionen der Sprachversion C++20.
Besonderheiten:
- Die Header-Datei
cmdline.hpp
deklariert eine StrukturPosition
für den Wert der Option--offset
. - Ebenfalls in der Header-Datei wird die Struktur
CliArguments
deklariert. Sie fasst die Variablen, die das Ergebnis des Parsens festhalten, in eine Einheit zusammen. - Mit
std::locale::global(std::locale("en_US.UTF-8"))
wird die Ausgabeformatierung auf internationale Zahlenformate festgelegt. - Das Programm nutzt
std::string
stattstd::wstring
und UTF-8-Kodierung der Quellcode-Dateien2. So werden die Umlaute der Zeichenkettenliterale auch unter Windows korrekt im Terminal wiedergegeben.
Warnung: Ich bin nicht besonders erfahren in C++ und der Code kann sehr wahrscheinlich verbessert werden.
cmdline.hpp
(Header):
#include <string>
#include <iostream>
namespace cmdline
{
// Datenstruktur für 2-dimensionale Position
struct Position
{
double x;
double y;
};
std::ostream& operator<<(std::ostream& os, const Position& p);
// Datenstruktur für Parser-Ergebnis
struct CliArguments
{
std::string error;
bool help;
bool alt_mode;
double speed;
Position offset;
std::string input_file;
bool has_error();
};
// Standardwerte
const static double DEFAULT_SPEED = 100.0;
const static Position DEFAULT_OFFSET = { 0.0, 0.0 };
// Hilfeausgabe
void print_help();
// Befehlszeilenparser
void parse(const int argc, const char *argv[], CliArguments &target);
}
cmdline.cpp
(Implementierung):
#include <format>
#include "cmdline.hpp"
std::ostream& cmdline::operator<<(std::ostream& os, const Position& p)
{
os
<< "("
<< std::format("{:.1f}", p.x)
<< "; "
<< std::format("{:.1f}", p.y)
<< ")";
return os;
}
bool cmdline::CliArguments::has_error()
{
return !error.empty();
}
void cmdline::print_help()
{
std::cout
<< "Syntax: tool [-a] [-s <S>] [-o <X> <Y>] <Eingabedatei>" << std::endl
<< " tool -h | --help" << std::endl
<< std::endl
<< "Ein Beispiel für ein Befehlszeilenprogramm" << std::endl
<< std::endl
<< "Schalter:" << std::endl
<< " -h, --help" << std::endl
<< " Zeigt diese Hilfe an." << std::endl
<< " -a, --alt" << std::endl
<< " Führt das Programm im alternativen Modus aus." << std::endl
<< std::endl
<< "Optionen:" << std::endl
<< " -s, --speed <S>" << std::endl
<< " Gibt die Geschwindigkeit an." << std::endl
<< " Standard: "
<< std::format("{:.1f}", DEFAULT_SPEED)
<< std::endl
<< " -o, --offset <X> <Y>" << std::endl
<< " Gibt den Offset für die Ausführung in X- und Y-Richtung an." << std::endl
<< " Standard: "
<< std::format("{:.1f}", DEFAULT_OFFSET.x)
<< " "
<< std::format("{:.1f}", DEFAULT_OFFSET.y)
<< std::endl;
}
void cmdline::parse(const int argc, const char *argv[], CliArguments &target)
{
// Ergebnisfelder mit Standardwerten initialisieren
target.error.clear();
target.help = false;
target.alt_mode = false;
target.speed = DEFAULT_SPEED;
target.offset = DEFAULT_OFFSET;
target.input_file.clear();
int i;
std::string arg;
// Wertumwandlung in Lambda mit Closure über Argumente, Schleifenvariable und Ergebnis
auto parse_double = [&](const char* metaVar, double *value) -> bool
{
i++; // verschiebt die Position in der Schleife um eins
if (i >= argc) {
target.error = std::format("Fehlendes Argument für {0}", metaVar);
return false;
}
std::string s = argv[i];
try
{
*value = std::stod(s);
}
catch (std::invalid_argument)
{
target.error = std::format("Fließkommazahl für {0} erwartet", metaVar);
return false;
}
catch (std::out_of_range)
{
target.error = std::format("Wert für {0} ist zu groß oder zu klein", metaVar);
return false;
}
return true;
};
// Schleife über Argumente
for (i = 1; i < argc; i++)
{
arg = argv[i];
if (arg == "-h" || arg == "--help")
{
target.help = true;
return; // Abkürzung, wenn Hilfeschalter verwendet wird
}
else if (arg == "-a" || arg == "--alt")
{
// Schalter
target.alt_mode = true;
}
else if (arg == "-s" || arg == "--speed")
{
// Option mit einem Wert
if (!parse_double("<S>", &target.speed)) return;
}
else if (arg == "-o" || arg == "--offset")
{
// Option mit zwei Werten
if (!parse_double("<X>", &target.offset.x)) return;
if (!parse_double("<Y>", &target.offset.y)) return;
}
else
{
// Positionsargument(e)
if (target.input_file.empty())
{
target.input_file = arg;
}
else
{
target.error = "Es kann nur eine Eingabedatei verarbeitet werden";
return;
}
}
}
// Fehlererkennung
if (target.input_file.empty())
{
target.error = "Es muss eine Eingabedatei angegeben werden";
return;
}
}
tool.cpp
:
#include <string>
#include <iostream>
#include <format>
#include "cmdline.hpp"
cmdline::CliArguments cli_args;
int main(int argc, const char *argv[])
{
// https://utf8everywhere.org/
std::locale::global(std::locale("en_US.UTF-8"));
// Befehlszeilenparser ausführen
cmdline::parse(argc, argv, cli_args);
if (cli_args.help)
{
// Hilfetext ausgeben und beenden
cmdline::print_help();
return 0;
}
if (cli_args.has_error())
{
// Wenn Befehlszeilenparser Fehler erkannt hat,
// Fehler ausgeben und mit Exit-Code 1 beenden
std::cerr
<< "Syntaxfehler beim Programmaufruf:" << std::endl
<< cli_args.error << std::endl;
return 1;
}
// Hauptprogramm
std::cout << "Programm wird ausgeführt mit:" << std::endl;
if (cli_args.alt_mode)
{
std::cout << " Modus: alternativ" << std::endl;
}
else
{
std::cout << " Modus: normal" << std::endl;
}
std::cout
<< " Speed: " << std::format("{:.1f}", cli_args.speed) << std::endl
<< " Offset: " << cli_args.offset << std::endl
<< " Input: " << cli_args.input_file << std::endl;
return 0;
}
C# #
Die C#-Implementierung nutzt die Sprachversion 12 mit Top-Level-Statements.
Es wird eine Projektdatei benötigt, um den C#-Quellcode komfortable mit dotnet build
kompilieren zu können.
Ab dem .NET SDK 10 ist die Projektdatei für kleine Programme nicht mehr notwendig und *.cs
-Dateien können auch direkt mit dotnet run
ausgeführt werden.
Das Projekt wird hier für das .NET Framework 4.8 kompiliert, welches ab Windows 10 im Betriebssystem enthalten ist. Das bedeutet, dass die erzeugte EXE-Datei auf jedem aktuellen Windows ausführbar ist.
Wenn man die Projekt-Property TargetFramework
entfernt oder anpasst, kann das Programm auch unter Linux oder macOS kompiliert und ausgeführt werden.
Besonderheiten:
- Die Struktur
Position
kapselt die X- und Y-Koordinaten für die Option--offset
. - Die Klasse
CommandLine
implementiert den Befehlszeilenparser und besitzt auch die Properties für das Ergebnis. - Die Wertumwandlung und ein Teil der Fehlererkennung ist in der Methode
CommandLine.ParseDouble()
implementiert. - Für die Schlüsselworterkennung wird statt einer mehrfach verzweigten
if-else
-Kette dasswitch
-Statement mit Musterabgleich verwendet. - Fehler werden hier mit booleschen Rückgabewerten und der Zeichenkettenvariablen
ErrorMessage
kommuniziert. Alternativ könnte die Fehlerbehandlung auch mit Ausnahmen umgesetzt werden (siehe Java-Beispiel). - Die statische Methode
FormattableString.Invariant()
wird aufgerufen, um zu verhindern, dass ein lokalisiertes Nummernformat bei der Zeichenketteninterpolation genutzt wird.
tool.csproj
:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>.net48</TargetFramework>
<OutputType>Exe</OutputType>
<LangVersion>12</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
tool.cs
:
using System.Globalization;
using static System.FormattableString;
// Befehlszeilenparser ausführen
var cmdLine = new CommandLine();
if (cmdLine.Help)
{
// Hilfetext ausgeben und beenden
CommandLine.PrintHelp();
return 0;
}
if (cmdLine.HasError)
{
// Wenn Befehlszeilenparser Fehler erkannt hat,
// Fehler ausgeben und mit Exit-Code 1 beenden
Console.Error.WriteLine("Syntaxfehler beim Programmaufruf:");
Console.Error.WriteLine(cmdLine.ErrorMessage);
return 1;
}
// Hauptprogramm
Console.WriteLine("Programm wird ausgeführt mit:");
Console.WriteLine($" Modus: {(cmdLine.AltMode ? "alternativ" : "normal")}");
Console.WriteLine($" Speed: {cmdLine.Speed}");
Console.WriteLine($" Offset: {cmdLine.Offset}");
Console.WriteLine($" Input: {cmdLine.InputFile}");
return 0;
/// <summary>
/// Beschreibt eine 2-dimensionale Position.
/// </summary>
struct Position
{
public double X;
public double Y;
public override readonly string ToString() => Invariant($"({X:0.0}; {Y:0.0})");
}
/// <summary>
/// Der Befehlszeilenparser.
/// </summary>
class CommandLine
{
// Standardwerte
public const double DEFAULT_SPEED = 100.0;
public static Position DEFAULT_OFFSET = new() { X = 0.0, Y = 0.0 };
private readonly string[] argv;
public bool Help { get; private set; }
public bool AltMode { get; private set; }
private double speed = DEFAULT_SPEED;
private Position offset = DEFAULT_OFFSET;
public string? InputFile { get; private set; }
public string? ErrorMessage { get; private set; }
public double Speed => speed;
public Position Offset => offset;
public bool HasError => ErrorMessage is not null;
public CommandLine(string[]? argv = null)
{
this.argv = argv ?? Environment.GetCommandLineArgs();
Parse();
}
public static void PrintHelp() => Console.WriteLine(Invariant(
$"""
Syntax: tool [-a] [-s <S>] [-o <X> <Y>] <Eingabedatei>
tool -h | --help
Ein Beispiel für ein Befehlszeilenprogramm')
Schalter:
-h, --help
Zeigt diese Hilfe an.
-a, --alt
Führt das Programm im alternativen Modus aus.
Optionen:
-s, --speed <S>
Gibt die Geschwindigkeit an.
Standard: {CommandLine.DEFAULT_SPEED:0.0}
-o, --offset <X> <Y>
Gibt den Offset für die Ausführung in X- und Y-Richtung an.
Standard: {CommandLine.DEFAULT_OFFSET.X:0.0} {CommandLine.DEFAULT_OFFSET.Y:0.0}
"""));
private void Parse()
{
var i = 1; // Pfad zur ausführbaren Datei überspringen
while (i < argv.Length)
{
switch (argv[i])
{
case "-h" or "--help":
Help = true;
break;
case "-a" or "--alt":
// Schalter
AltMode = true;
break;
case "-s" or "--speed":
// Option mit einem Wert
if (!ParseDouble(++i, "<S>", out speed)) break;
break;
case "-o" or "--offset":
// Option mit zwei Werten
if (!ParseDouble(++i, "<X>", out offset.X)) break;
if (!ParseDouble(++i, "<Y>", out offset.Y)) break;
break;
default:
// Positionsargument
if (InputFile is null)
InputFile = argv[i];
else
ErrorMessage = "Kann nur eine Eingabedatei verarbeiten.";
break;
}
if (ErrorMessage is not null) return;
i++;
}
// Fehlererkennung
if (InputFile is null)
{
ErrorMessage = "Eine Eingabedatei muss angegeben werden.";
}
}
private bool ParseDouble(int i, string metavar, out double value)
{
if (i >= argv.Length)
{
value = 0.0;
ErrorMessage = $"Argument für {metavar} fehlt.";
return false;
}
if (!double.TryParse(argv[i],
NumberStyles.Float, CultureInfo.InvariantCulture,
out value))
{
ErrorMessage = $"Erwartet Fließkommazahl für {metavar}.";
return false;
}
return true;
}
}
Java #
Die Java-Implementierung nutzt das JDK 21. Es ist in Java schlecht möglich ein halbwegs sinnvoll strukturiertes Programm in einer Quellcode-Datei zu verfassen, da ein Java-Programm fast immer aus mehr als einer Klasse besteht und jede Klasse in einer eigenen Datei implementiert wird. Daher verteilt sich die Beispielimplementierung über mehrere Dateien.
Besonderheiten:
- Die Klasse
Position
kapselt die X- und Y-Koordinaten für die Option--offset
. - Die Fehlererkennung nutzt keine Rückgabewerte oder boolesche Fehlervariablen, sondern wirft eine eigene Ausnahme
CommandLineException
. - Die Klasse
CommandLine
implementiert den Befehlszeilenparser und besitzt auch die Properties für das Ergebnis. - Die Erkennung der Schlüsselworte wird mit
switch
für Zeichenketten durchgeführt. Für die Zusammenfassung von Kurz- und Langform der Schlüsselworte werdencase
-Statements ohnebreak
verwendet. - Durch den Aufruf
Locale.setDefault(Locale.ROOT)
wird das Nummernformat für die Ausgabe auf den internationalen Standard gesetzt.
Tool.java
:
import java.util.Locale;
public class Tool {
public static void main(String[] argv) {
Locale.setDefault(Locale.ROOT);
var cmdLine = new CommandLine();
// Befehlszeilenparser ausführen
cmdLine.parse(argv);
if (cmdLine.help()) {
// Hilfetext ausgeben und beenden
cmdLine.printHelp(System.out);
return;
}
if (cmdLine.hasError()) {
// Wenn Befehlszeilenparser Fehler erkannt hat,
// Fehler ausgeben und mit Exit-Code 1 beenden
System.err.println("Syntaxfehler beim Programmaufruf:");
System.err.println(cmdLine.error());
System.exit(1);
}
// Hauptprogramm
System.out.println("Programm wird ausgeführt mit:");
if (cmdLine.altMode()) {
System.out.println(" Modus: alternativ");
} else {
System.out.println(" Modus: normal");
}
System.out.printf(" Speed: %.1f\n", cmdLine.speed());
System.out.printf(" Offset: %s\n", cmdLine.offset().toString());
System.out.printf(" Input: %s\n", cmdLine.inputFile());
}
}
Position.java
:
/**
* Beschreibt eine 2-dimensionale Position.
*/
class Position {
public double x;
public double y;
public Position(double x, double y) {
this.x = x;
this.y = y;
}
public Position(Position p) {
this.x = p.x;
this.y = p.y;
}
public String toString() {
return String.format("(%.1f; %.1f)", x, y);
}
}
CommandLineException.java
:
class CommandLineException extends Exception {
public CommandLineException(String message) {
super(message);
}
public CommandLineException(String message, Throwable cause) {
super(message, cause);
}
}
CommandLine.java
:
import java.io.PrintStream;
/**
* Der Befehlszeilenparser.
*/
class CommandLine {
// Standardwerte
public static final double DEFAULT_SPEED = 100.0;
public static final Position DEFAULT_OFFSET = new Position(0.0, 0.0);
// Variablen
private String _error = null;
private boolean _help = false;
private boolean _altMode = false;
private double _speed = DEFAULT_SPEED;
private Position _offset = new Position(DEFAULT_OFFSET);
private String _inputFile = null;
public String error() { return _error; }
public boolean hasError() { return _error != null; }
public boolean help() { return _help; }
public boolean altMode() { return _altMode; }
public double speed() { return _speed; }
public Position offset() { return new Position(_offset); }
public String inputFile() { return _inputFile; }
public void printHelp(PrintStream s) {
s.println("Syntax: tool [-a] [-s <S>] [-o <X> <Y>] <Eingabedatei>");
s.println(" tool -h | --help");
s.println();
s.println("Ein Beispiel für ein Befehlszeilenprogramm");
s.println();
s.println("Schalter:");
s.println(" -h, --help");
s.println(" Zeigt diese Hilfe an.");
s.println(" -a, --alt");
s.println(" Führt das Programm im alternativen Modus aus.");
s.println();
s.println("Optionen:");
s.println(" -s, --speed <S>");
s.println(" Gibt die Geschwindigkeit an.");
s.printf(" Standard: %.1f\n", DEFAULT_SPEED);
s.println(" -o, --offset <X> <Y>");
s.println(" Gibt den Offset für die Ausführung in X- und Y-Richtung an.");
s.printf(" Standard: %s\n", DEFAULT_OFFSET);
}
public void parse(String[] argv) {
try {
parseInternal(argv);
} catch (CommandLineException e) {
_error = e.getMessage();
}
}
private void parseInternal(String[] argv) throws CommandLineException {
var i = 1; // Pfad zur ausführbaren Datei überspringen
while (i < argv.length)
{
switch (argv[i])
{
case "-h":
case "--help":
_help = true;
break;
case "-a":
case "--alt":
// Schalter
_altMode = true;
break;
case "-s":
case "--speed":
// Option mit einem Wert
_speed = parseDouble(argv, ++i, "<S>");
break;
case "-o":
case "--offset":
// Option mit zwei Werten
_offset.x = parseDouble(argv, ++i, "<X>");
_offset.y = parseDouble(argv, ++i, "<Y>");
break;
default:
// Positionsargument
if (_inputFile == null)
_inputFile = argv[i];
else
throw new CommandLineException(
"Kann nur eine Eingabedatei verarbeiten.");
break;
}
i++;
}
if (_help) return;
// Fehlererkennung
if (_inputFile == null)
{
throw new CommandLineException(
"Eine Eingabedatei muss angegeben werden.");
}
}
private double parseDouble(String[] argv, int i, String metavar)
throws CommandLineException {
if (i >= argv.length)
{
throw new CommandLineException(
"Argument für " + metavar + " fehlt.");
}
try {
return Double.parseDouble(argv[i]);
} catch (NumberFormatException e) {
throw new CommandLineException(
"Erwartet Fließkommazahl für " + metavar + ".");
}
}
}
Werden die Quelldateien kompiliert und in eine JAR-Datei mit der Hauptklasse Tool
als Einstiegspunkt verpackt, lässt sich das Programm wie folgt aufrufen:
java -jar tool.jar -- -a --offset 123.4 56.7 Eingabe.txt
Dabei trennt der doppelte Bindestrich --
die Argumente für den java
-Befehl von den Argumenten, die an Tool.main()
übergeben werden.
JavaScript / NodeJS #
NodeJS hat seit der Version 20 eine stabile Implementierung eines Befehlszeilenparsers in der Funktion util.parseArgs()
.
Sie ähnelt in ihrer Funktionalität in gewisser Weise dem getopt
-Befehl, den man in Bash-Skripten verwenden kann. Denn sie prüft und normalisiert die Befehlszeile mit einer einfachen Beschreibung der unterstützten Schalter und Optionen, bietet darüber hinaus aber keinerlei Komfortfunktionen.
Besonderheiten:
parseArgs()
unterstützt nur einen Wert pro Option. Daher werden mehrere Werte als kommaseparierte Liste übergeben.parseArgs()
führt keine Prüfung oder Umwandlung von Optionswerten oder Positionsargumenten durch.parseArgs()
generiert, anders als viele andere Befehlszeilenparser, keinen Hilfetext.- Die Variablendefinition erfolgt erst in der Phase der Umwandlung und Fehlererkennung. In diesem Schritt werden auch die Standardwerte als Fallback verwendet.
tool.js
:
const util = require('node:util')
const { exit } = require('node:process')
// Definition von Standardwerten
const DEFAULT_ARG_SPEED = '100.0'
const DEFAULT_ARG_OFFSET = '0.0,0.0'
function printHelp() {
console.log('Syntax: tool [-a] [-s <S>] [-o <X>,<Y>] <Eingabedatei>')
console.log(' tool -h | --help')
console.log('')
console.log('Ein Beispiel für ein Befehlszeilenprogramm')
console.log('')
console.log('Schalter:')
console.log(' -h, --help')
console.log(' Zeigt diese Hilfe an.')
console.log(' -a, --alt')
console.log(' Führt das Programm im alternativen Modus aus.')
console.log('')
console.log('Optionen:')
console.log(' -s, --speed <S>')
console.log(' Gibt die Geschwindigkeit an.')
console.log(` Standard: ${DEFAULT_ARG_SPEED}`)
console.log(' -o, --offset <X>,<Y>')
console.log(' Gibt den Offset für die Ausführung in X- und Y-Richtung an.')
console.log(` Standard: ${DEFAULT_ARG_OFFSET}`)
}
function parseCliArguments() {
// Definition der Schalter und Optionen
const cliArguments = {
options: {
'help': { short: 'h', type: 'boolean' },
'alt': { short: 'a', type: 'boolean' },
'speed': { short: 's', type: 'string' },
'offset': { short: 'o', type: 'string' },
},
allowPositionals: true,
}
// Argumente prüfen und normalisieren
const {
values: options,
positionals: inputFiles,
} = util.parseArgs(cliArguments)
// Ausgabe der Hilfe
if (options.help) {
printHelp()
exit(0)
}
// Umwandlungen und Fehlererkennung
const altMode = options.alt
const speed = Number.parseFloat(options.speed ?? DEFAULT_SPEED)
if (Number.isNaN(speed)) {
console.error('Syntaxfehler: Option -s/--speed <S> erwartet Fließkommazahl')
exit(1)
}
const offset = (options.offset ?? DEFAULT_OFFSET)
.split(',').map(Number.parseFloat)
if (offset.length != 2 ||
Number.isNaN(offset[0]) ||
Number.isNaN(offset[1])
) {
console.error('Syntaxfehler: Option -o/--offset <X>,<Y> erwartet zwei Fließkommazahlen')
exit(1)
}
const inputFile = inputFiles[0]
if (inputFiles.length != 1) {
console.error('Syntaxfehler: Eine Eingabedatei muss angegeben werden')
exit(1)
}
return {
altMode,
speed,
offsetX: offset[0],
offsetY: offset[1],
inputFile,
}
}
// Befehlszeilenparser ausführen
const cmdLine = parseCliArguments()
// Hauptprogramm
console.log('Programm wird ausgeführt mit:')
if (cmdLine.altMode) {
console.log(' Modus: alternativ')
} else {
console.log(' Modus: normal')
}
console.log(` Speed: ${cmdLine.speed}`)
console.log(` Offset: (${cmdLine.offsetX}; ${cmdLine.offsetY})`)
console.log(` Input: ${cmdLine.inputFile}`)
Python #
In Python führt eigentlich kein Weg am Package argparse
vorbei.
Es ist in der Standardbibliothek enthalten, einfach zu benutzen und deckt auch kompliziertere Fälle ab.
Python-Code der argparse
verwendet ist gut lesbar und kompakt.
tool.py
:
from argparse import ArgumentParser
# Definition der Befehlszeilenargumente
arg_parser = ArgumentParser(
prog="tool",
description="Ein Beispiel für ein Befehlszeilenprogramm")
arg_parser.add_argument("-a", "--alt", action="store_true", dest="alt_mode",
help="Führt das Programm im alternativen Modus aus")
arg_parser.add_argument("-s", "--speed", type=float,
metavar="S", default=100.0,
help="Gibt die Geschwindigkeit an")
arg_parser.add_argument("-o", "--offset", nargs=2, type=float,
metavar=("X", "Y"), default=[0.0, 0.0],
help="Gibt den Offset für die Ausführung in X- und Y-Richtung an.")
arg_parser.add_argument("input_file",
metavar="Eingabedatei",
help="Ein Pfad zur Eingabedatei.")
# Ausführung des Befehlszeilenparsers mit Abbruch bei Fehler
args = arg_parser.parse_args()
# Hauptprogramm
print("Programm wird ausgeführt mit:")
if args.alt_mode:
print(" Modus: alternativ")
else:
print(" Modus: normal")
print(f" Speed: {args.speed}")
print(f" Offset: ({args.offset[0]}; {args.offset[1]})")
print(f" Input: {args.input_file}")
Bash #
Obwohl man in Bash den getopt
-Befehl zur Prüfung und Normalisierung von Befehlszeilenargumenten nutzen kann, muss trotzdem eine Variante des im Pseudo-Code vorgestellten Parsers implementiert werden.
Besonderheiten:
getopt
unterstützt nur einen Wert pro Option. Daher werden mehrere Werte als kommaseparierte Liste übergeben.- Zeichenketten können nicht in Fließkommazahlen umgewandelt werden, denn Arithmetik funktioniert in Bash mit Zeichenketten.
- Es werden keine Variablen für den Fehlerzustand verwendet. Stattdessen wird das Skript im Fehlerfall sofort mit einem Exit-Code ungleich 0 beendet.
- Die Schleife betrachtet immer das erste Befehlszeilenargument
$1
und schiebt die Argumente mitshift
nach links. getopt
fügt nach den Schaltern und Optionen, aber vor den Positionsargumenten, einen doppelten Bindestrich--
ein. Die Schleife bricht beim Erreichen von--
vor den Positionsargumenten ab.
tool.sh
:
#!/bin/bash
# getopt nutzen, um Argumente zu prüfen und zu normalisieren
opts=$(getopt -o has:o: --long help,alt,speed:,offset: -n'tool' -- "$@")
if [ $? -ne 0 ]; then
>&2 echo "Ungültige Syntax"
exit 1
fi
# originale Befehlszeilenargumente durch normalisierte ersetzen
eval set -- "$opts"
# Variablen initialisieren
help=false
alt_mode=false
speed=100
offset=(0 0)
# Schleife für Schalter und Optionen
while ! [ $# -eq 0 ]; do
case "$1" in
-h | --help)
help=true
;;
-a | --alt)
alt_mode=true
;;
-s | --speed)
shift
speed="$1"
;;
-o | --offset)
# kommaseparierte Argumente in Array auftrennen
shift
IFS=',' read -r -a offset <<< "$1"
;;
--)
# Abbruchkriterium, angefügt von getopt
shift
break
;;
esac
shift
done
# Positionsargumente übertragen
if [ "$#" -eq 1 ]; then
input_file="$1"
fi
# Hilfeausgabe
if [ "$help" = true ]; then
echo "Syntax: tool [-a] [-s <S>] [-o <X>,<Y>] <Eingabedatei>"
echo " tool -h | --help"
echo ""
echo "Ein Beispiel für ein Befehlszeilenprogramm"
echo ""
echo "Schalter:"
echo " -h, --help"
echo " Zeigt diese Hilfe an."
echo " -a, --alt"
echo " Führt das Programm im alternativen Modus aus."
echo ""
echo "Optionen:"
echo " -s, --speed <S>"
echo " Gibt die Geschwindigkeit an."
echo " Standard: 100.0"
echo " -o, --offset <X>,<Y>"
echo " Gibt den Offset für die Ausführung in X- und Y-Richtung an."
echo " Standard: 0.0,0.0"
exit 0
fi
# Fehlererkennung
if ! [ "${#offset[@]}" -eq 2 ]; then
>&2 echo "Syntaxfehler: Zwei Argumente für --offset <X>,<Y> erwartet"
exit 1
fi
if [ -z "$input_file" ]; then
>&2 echo "Syntaxfehler: Eine Eingabedatei muss angegeben werden"
exit 1
fi
# Hauptprogramm
echo "Programm wird ausgeführt mit:"
if [ "$alt_mode" = true ]; then
echo " Modus: alternativ"
else
echo " Modus: normal"
fi
echo " Speed: $speed"
echo " Offset: (${offset[0]}; ${offset[1]})"
echo " Input: $input_file"
PowerShell #
Die PowerShell hat den Befehlszeilenparser in die Sprache bzw. die Laufzeitumgebung integriert. Skripte können mit dem param
-Schlüsselwort benannte Parameter definieren und diese mit Attributen3,4 genauer beschreiben.
Besonderheiten:
- PowerShell verwendet eigene Konvention für die Benennung von Schaltern und Optionen.
- Positionsargumente sind Optionen mit optionalem Schlüsselwort.
- Optionswerte können neben Zeichenketten auch typisierte .NET-Objekte sein.
- Optionen akzeptieren nur einen Wert, dieser kann aber auch ein Array (.NET-Objekt) sein.
- Direktzugriff auf alle Befehlszeilenargumente über die Variable
$args
ist möglich aber unüblich. - Skripte, Funktionen und für .NET kompilierte PowerShell-Module verwenden eine einheitliche “Commandlet”-Schnittstelle.
- Hilfetexte für Befehlszeilenargumente werden in einer Präambel formuliert.
- Der Aufruf der Hilfe für PowerShell-Befehle erfolgt nicht mit dem Befehlszeilenargument
-h
oder--help
sondern über das CommandletGet-Help
.
Z. B.:Get-Help .\tool.ps1 -Full
.
tool.ps1
:
<#
.SYNOPSIS
Ein Beispiel für ein Befehlszeilenprogramm
.DESCRIPTION
Hier könnte eine ausführliche Beschreibung für das Programm stehen.
.PARAMETER AlternativeMode
Führt das Programm im alternativen Modus aus.
.PARAMETER Speed
Gibt die Geschwindigkeit an.
.PARAMETER Offset
Gib den Offset für die Ausführung in X- und Y-Richtung an.
Erwartet ein Array mit zwei Fließkommzahlen.
.PARAMETER InputFile
Ein Pfad zur Datei mit Eingabedaten.
.NOTES
Version: 1.0
Author: Tobias Kiertscher
Creation Date: 2025-06-17
.LINK
https://honest-devhead.com/posts/commandline-3
#>
param (
[switch]$AlternativeMode,
[double]$Speed = 100.0,
[double[]]$Offset = @(0.0, 0.0),
[Parameter(Mandatory=$true, Position=0)]
[string]$InputFile
)
# Fehlererkennung
if ($Offset.Count -ne 2) {
Write-Error "Der Parameter -Offset erwartet ein Array mit genau zwei Werten."
exit 1
}
# Hauptprogramm
Write-Output "Programm wird ausgeführt mit:"
if ($AlternativeMode) {
Write-Output " Modus: alternativ"
} else {
Write-Output " Modus: normal"
}
Write-Output " Speed: $Speed"
Write-Output " Offset: ($($Offset[0]); $($Offset[1]))"
Write-Output " Input: $InputFile"
Ein kanonischer Aufruf des Skripts sieht wie folgt aus:
.\tool.ps1 -AlternativeMode -Offset @(123.4, 56.7) -InputFile "Eingabe.txt"
Der Parameter InputFile
ist ein Positionsargument, damit darf das Schlüsselwort weggelassen werden. Die Reihenfolge ist hier nicht wichtig, da es nur einen Positionsparameter gibt.
.\tool.ps1 "Eingabe.txt" -AlternativeMode -Offset @(123.4, 56.7)
Arrays werden von PowerShell in vielen Fällen auch ohne die Klammern am Komma erkannt. Und da der Pfad zur Eingabedatei keine Leerzeichen enthält, können für ihn die Anführungszeichen entfallen.
.\tool.ps1 -AlternativeMode -Offset 123.4, 56.7 Eingabe.txt
Wenn sich das Skript im PATH
befindet, kann der relative Pfad .\
und die Dateiendung weggelassen werden.
tool -AlternativeMode -Offset 123.4, 56.7 Eingabe.txt
Und zu guter Letzt können die Parameternamen beim Aufruf verkürzt werden. Denn PowerShell ignoriert nicht nur die Großschreibung der Schlüsselwörter, sondern erlaubt auch das Kürzen, so lange der Beginn noch eindeutig ist.
tool -a -o 123.4, 56.7 Eingabe.txt
Zusammenfassung #
Der hier dargestellte Algorithmus lässt sich gut verwenden, wenn man ein einfaches Befehlszeilenprogramm ohne zusätzliche Abhängigkeiten implementieren möchte und nur einfache Schalter und Optionen benötigt. Der größte Aufwand entfällt dabei oft auf die Umwandlung und Prüfung von Optionswerten und Positionsargumenten.
Bei Skriptsprachen (z. B. Python oder PowerShell) ist häufig bereits ein Befehlszeilenparser in der Standardbibliothek enthalten, so dass sich der Aufwand für eine eigene Implementierung selten rechtfertigen lässt. In manchen Umgebungen (z. B. Bash oder NodeJS) ist zwar ein Parser enthalten, dieser beschränkt sich aber auf eine syntaktische Prüfung und bietet nur wenige Funktionen. Daher müssen dort Typprüfung, Wertumwandlung und Hilfeausgabe im Detail implementiert werden, wenn man Abhängigkeiten vermeiden möchte. Bei kompilierten Sprachen wie C/C++, C# oder Java ist in der Standardbibliothek oft kein Befehlszeilenparser enthalten. Für einfache Fälle kann eine eigene Implementierung in diesen Sprachen Vorteile bieten.
-
Der medizinische Fachausdruck für die Angst vor zu vielen Abhängigkeiten 😉 ↩︎
-
UTF-8 Everywhere https://utf8everywhere.org/ ↩︎
-
Commandlet-Binding in PowerShell-Skripten https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_cmdletbindingattribute ↩︎
-
Fortgeschrittene Parameter in PowerShell https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters ↩︎