freeX: Programmparameter verwalten, Teil 9
|
Perl hier, Perl da, Perl ist immer da!
| |
Sobald ein Programm mehr als nur eine einzige kleine Aufgabe erledigen
soll, werden Parameter erforderlich, die das Programm steuern. Man kann
die Kommandozeilenparameter manuell parsen oder dieses einem Perl-Modul
überlassen. Dieser Artikel stellt eine bequeme Möglichkeit vor, Parameter
in Perl zu verarbeiten.
|
|
1. Das Modul Getopt::Long |
Parameter in Perl zu verarbeiten bedeutet meistens, daß manuell mit der Systemvariable »@ARGV« gearbeitet wird. In diesem Array ist die gesamte Kommandozeile gespeichert, damit sie im Programm zur Verfügung steht.
Parameter in der Kommandozeile anzugeben, ist wichtig, wenn das Programm mehr als nur eine kleine Aufgabe übernehmen soll. Wenn das Programm mehrere Probleme bearbeiten soll, müssen diese irgendwie spezifiziert werden. Das erfolgt üblicherweise über Parameter oder Konfigurationen. Selbst bei mehreren Konfigurationsdateien muß dem Programm jeweils mitgeteilt werden, welche Konfigurationsdatei verwendet werden soll. Selbst dann kommt man kaum daran vorbei, die Kommandozeile zu parsen.
Das kann jedoch beliebig komplex werden, abhängig davon, wie flexibel das Programm reagieren soll. Häufig werden sowohl kurze Parameter wie »-v« als auch lange Parameter wie »--verbose« verwendet. Letztere wurden wohl vom GNU-Projekt eingeführt, um die vielen Parameter von GNU-Werkzeugen in der Kommandozeile unterbringen zu können (siehe »man ls«).
Daneben gibt es Parameter, die Argumente erfordern, wie z.B. »--user joey«, die auf unterschiedliche Arten angegeben werden können, z.B. auch als »--user=joey«. Dann gibt es Parameter, die Aktionen wie das Erhöhen von Variablen zur Folge haben. Wenn ein Programm mehrere Log-Stufen hat, macht es Sinn, eine Variable z.B. mit dem Parameter »-v«, zu erhöhen, um mehr Ausgaben zu erzeugen.
Darüberhinaus gibt es auch noch Parameter, mit denen direkte Aktionen ausgelöst werden. Häufig bewirkt der Parameter »-h« neben der Ausgabe von Hilfetexten auch das sofortige Beenden des Programms. Abhängig davon, welche Arten von Parametern das Programm unterstützen soll, wird die Routine komplexer, mit der die Kommandozeilenparameter geparst werden.
Eine einfache Routine, die an das Programm übergebene Parameter untersucht und entsprechend reagiert könnte wie folgt aussehen:
$argc = 0;
while ($argc <= $#ARGV) {
if ($ARGV[$argc] eq "-h" || $ARGV[$argc] eq "--help") {
help ();
exit;
} elsif ($ARGV[$argc] eq "-a"
|| $ARGV[$argc] eq "--assign"
|| $ARGV[$argc] eq "--assignments") {
latex_assignments ();
} elsif ($ARGV[$argc] eq "-c" || $ARGV[$argc] eq "--cols") {
if (($argc < $#ARGV) && ($ARGV[$argc+1] !~ /^-/)) {
$opt_cols = $ARGV[++$argc];
}
[..]
} else {
printf "Unknown argument: %s\n", $ARGV[$argc];
}
$argc++;
}
Bei größerer Anzahl Argumenten wächst diese Schleife leicht über mehrere Bildschirmseiten. Der Übersicht ist das selten förderlich.
Das Modul Getopt::Long
So muß es jedoch nicht aussehen, denn für diese Aufgaben hat Johan Vromans das Modul Getopt::Long entwickelt. Dieses Modul ist in aktuellen Perl-Distributionen enthalten und muß daher nicht nachinstalliert werden. Es liest die auf der Kommandozeile angegebenen Parameter und untersucht sie anhand der übergebenen Regeln. Dabei wird, soweit es möglich ist, auch eine Syntaxüberprüfung der Parameter durchgeführt.
use Getopt::Long;
Das Modul wird zwar in der Perl-Distribution mitgeliefert, jedoch nicht automatisch in jedes Programm eingebunden. Daher wird es mit der »use«-Anweisung zu Anfang des Programms zum Programm hinzugefügt. Anschließend steht die Funktion »GetOptions« zur Verfügung, mit der die Kommandozeile untersucht wird.
Als einziger Parameter wird ein assoziatives Array (Hash) mit Beschreibungen der erlaubten Parameter erwartet. Dieses Array bestimmt, wie der Parser die Parameter interpretieren soll. Das assoziative Array kann separat aufgebaut und übergeben oder direkt als Parameter angegeben werden.
Konfiguration als Hash
Im folgenden Artikel wird für die Konfiguration bzw. Parametrisierung des Programms ein assoziatives Array »%config« verwendet. Dieses macht andauerndes Deklarieren von neuen globalen Variablen überflüssig und die gesamte Konfiguration befindet sich in einem einzigen Objekt. Das folgende Konstrukt setzt ein paar Konfigurationsvariablen, die später verwendet werden:
my %config = ('verbose' => 0,
'debug' => 0);
Mit diesem Array werden gleich Werte für die einzelnen Variablen voreingestellt, damit das Programm ohne Parameter in der Kommandozeile mit vernünftigen Werten arbeiten kann. Anstelle des Wertes 0 kann man auch den Wert »undef« verwenden.
Parameter erkennen
Zu Anfang soll der Parameter »--verbose« erkannt werden, der die Variable »$config{'verbose'}« setzt. Dazu wird »GetOptions« mitgeteilt, welcher Wert verändert werden soll. Wenn einfach »$variable« bzw. in diesem Fall »$config{'verbose'}« an die Funktion übergeben würde, sähe die Funktion jedoch nur den Wert der Variablen, jedoch nicht, wo der Wert gespeichert ist. Der Wert könnte daher nur in der Funktion selbst geändert werden und würde nicht außerhalb sichtbar sein.
Daher wird eine Referenz auf die Variable übergeben. Die Referenz ist das Pendant zum Pointer in C. Perl legt dabei keine Kopie der Variablen an, sondern speichert einen Zeiger auf den von der Variablen belegten Speicherplatz. Wenn die Funktion diesen Speicherplatz kennt, kann sie den dort gespeicherten Wert auch verändern.
Ein Aufruf von »GetOptions«, der auf den Parameter »--verbose« reagieren soll und den Wert der Variablen »$variable« verändern soll, sieht dann wie folgt aus:
GetOptions ('verbose' => \$variable);
Für das oben vorgestellte Array wird stattdessen auf den Wert eines einzelnen Elements in Perl mit »$config{'verbose'}« verwiesen. Auf den Speicherplatz verweist somit die Referenz »\$config{'verbose'}«, wozu der Funktionsaufruf leicht abgewandelt wird:
GetOptions ('verbose' => \$config{'verbose'});
Mit dieser Einstellung reagiert die Funktion nicht nur auf »--verbose« sondern ebenfalls auf »-v« und »-verbose« sowie alle eindeutigen Abkürzungen von letzterem und »--verbose«. Dieses ist das voreingestellte Verhalten von »GetOptions« und kann auf Wunsch abgeschaltet werden.
GetOptions ('verbose' => \$config{'verbose'},
'value' => \$config{'debug'});
Bei diesem Aufruf könnten die Parameter nur bis »-va« und »-ve« abgekürzt werden. »-v« selbst ist nicht mehr eindeutig, was »GetOptions« auch ausgibt, wenn dieser Parameter versucht wird aufzurufen. Wie man dieses ändern kann, wird am Ende des Artikels beschrieben.
In vielen Fällen unterstützt ein Programm mehrere Stufen von Ausgaben. Keine zusätzlichen Ausgaben, ein paar Informationen, mehr Informationen und viele Informationen. Das entspricht Werten von 0 bis 3. Um so etwas zu steuern, ist es sinnvoll, wenn man »-v« einfach häufiger in der Kommandozeile angeben kann, um mehr Ausgaben hervorzubringen.
Das ist mit diesem Modul überhaupt kein Problem. Wenn ein Plus-Zeichen hinter dem Namen des Parameters geschrieben ist, erhöht jeder solche Parameter den Zähler. Ohne das Plus-Zeichen würde die Variable immer nur mit einem neuen Wert überschrieben werden. Mit dem folgenden Aufruf wird die Variable bei jedem Parameter »-v« erhöht, mit der die Ausgaben gesteuert werden.
GetOptions ('verbose+' => \$config{'verbose'});
Parameter mit Argumenten
Wie eingangs gesehen sind nicht alle Parameter Schalter, die Optionen aus- bzw. einschalten, sondern sie können auch selbst Argumente erhalten. Auch ein Zähler reicht kaum aus, wenn man als Parameter einen TCP-Port angeben möchte, z.B. 1536. Da wäre es schon sinnvoller, wenn man »--port 1536« oder »--port=1536« schreiben könnte.
Nichts leichter als das. In einem solchen Fall verlangt dieses Modul lediglich einen kleinen Tip, um welchen Typ von Parametern es sich handelt. Unterstützt werden Ganzzahlen, Fließkommazahlen, beliebige Zeichenketten sowie Ganzzahlen in unterschiedlichen Kodierungen. Wenn der falsche Typ in der Kommandozeile verwendet wird, gibt es eine Fehlermeldung in der aufrufenden Shell. Das Argument darf ein eigener Parameter sein, wie z.B. im Fall von »-d 10« oder auch direkt an den Parameter geschrieben werden, wie z.B. im Fall »-d=10« oder »--debug=10«.
| f | Fließkommazahl, z.B. 30.10 |
| i | Ganzzahl, z.B. 1035 |
| o | Ganzzahl unterschiedlicher Kodierungen, z.B. 010 (8) oder 0x10 (16) |
| s | Beliebige Zeichenkette |
| Tabelle 1: Parameter-Typen | |
Der gewünschte Typ wird zusammen mit dem Namen der Option angegeben, durch ein Gleichheitszeichen getrennt. Anstatt »debug« wird daher »debug=i« geschrieben, wenn der Parameter »--debug« ein ganzzahliges Argument erfordert. Fehlt dieses in der Kommandozeile, gibt es eine Fehlermeldung.
GetOptions ('debug=i' => \$config{'debug'});
Wird der Parameter »--debug« oder »-d« mehrfach angegeben, so überschreibt der jeweils letzte Parameter alle vorherigen. Wird als Typ »o« angegeben, dann erwartet Perl den Typ "extended integer" im Perl-Stil. In diesem Fall werden nicht nur dezimale Ganzzahlen (wie z.B. 1, 10, 1002) erkannt, sondern genauso auch Oktal-, Hexadezimal- und Binärzahlen.
Die Zahlen werden auf die gleiche Weise erkannt wie auch innerhalb von Perl-Programmen. Ganzzahlen werden dabei wie bisher geschrieben als Folge von Ziffern. Oktalzahlen sind Ganzzahlen mit Basis 8 (statt 10 bei Dezimalzahlen). Sie beginnen mit einer 0 und verwenden nur die Ziffern von 0 bis 7. 010 ist somit 8, 017 15. Hexadezimalzahlen verwenden die Basis 16 und Ziffern von 0 bis 9 sowie A bis F. Sie beginnen immer mit dem Präfix »0x«. 0x10 ist daher 16 und 0x17 ist 23.
Perl unterstützt gleichermaßen auch die Binärdarstellung von Zahlen. In diesem Fall sind nur die Ziffern 0 und 1 zugelassen. Solche Zahlen beginnen zur Unterscheidung immer mit dem Präfix »0b«. Somit stellt die Zahl 0b101 die 5 dar und 0b1010 die 10. Die nebenstehende Abbildung zeigt ein paar Anwendungsbeispiele dazu.
Ein Sonderfall wird behandelt, wenn als Name des Parameters »debug:
Mehrfache Parameter
Es gibt Momente, da ist es nicht sinnvoll, wenn der letzte Parameter alle vorherigen mit gleichem Namen überschreibt. Wer in C programmiert und seinen Quellcode compiliert, wird Argumente wie »-I . -I include -I ../include« oder ähnlich kennen. Es wäre hier fatal, wenn der letzte Parameter die vorherigen überschreiben würde.
Das Modul Getopt::Long unterstützt ohne weiteres auch mehrfache Nennungen von Parametern. Im obigen Beispiel wird als Argument eine beliebige Zeichenkette (hier ein relativer oder absoluter Pfadname) erwartet. Damit »GetOptions« die angegebenen Argumente zuweisen kann, wird jedoch keine Referenz auf eine einfache Variable beim Aufruf erwartet, sondern eine Referenz auf ein Array.
Wenn »GetOptions« erkennt, daß die Referenz auf ein Array verweist, wird es die erkannten Argumente sukzessive zum Array hinzufügen, wie das folgende Programmfragment zeigt:
my @include = ();
GetOptions ('include=s' => \@include);
printf " include: %s\n", join (":", @include) if (@include);
Die Funktion »GetOptions« unterstützt sogar eigenständige Konfigurationen. Wer nicht für jede Option vorher eine Variable setzen bzw. nicht für jede Option einen eigenen Parameter vergeben möchte, übergibt als Referenz einfach die Referenz auf das gesamte Konfigurations-Hash. »GetOptions« prüft dann, ob als Argument für den Parameter eine Zeichenkette der Form »foo=bar« angegeben wurde. Wenn das der Fall ist, wird das Element »foo« im assoziativen Array »%config« auf den Wert »bar« gesetzt. Wenn das Argument kein Gleichheitszeichen enthält, wird der Wert auf 1 gesetzt.
GetOptions ('config=s' => \%config);
Mit dieser Einstellung werden die Parameter »-c name=Joey« und »-c domain=infodrom.org« erkannt und »$config{'name'}« sowie »$config{'domain'}« dementsprechend gesetzt. Auf diese Weise können beliebige zusätzliche Möglichkeiten zur Konfiguration eröffnet werden ohne daß für alle eigene Parameter festgelegt werden müssen.
Funktionsaufrufe
Wie eingangs erwähnt, lösen einige Parameter üblicherweise direkte Aktionen aus, die z.B. mit dem Beenden des Programms verbunden sind. Auch das läßt sich mit Getopt::Long auf einfache Art implementieren. Wenn ein Parameter eine Funktion auslösen soll, wird anstelle einer Referenz auf einen Speicherplatz eine Referenz auf eine Funktion oder eine anonyme Funktion übergeben.
Die Funktion kann beliebigen Code enthalten. Um die Übersichtlichkeit nicht zu verlieren, sollte eine anonyme Funktion jedoch nur wenig Code enthalten und gegebenenfalls eine herkömmliche Funktion aufrufen, in der die eigentliche Arbeit geschieht.
Das folgende Beispiel verdeutlicht die Verwendung von anonymen Funktionen. Jedesmal, wenn »--info« oder »-i« in der Kommandozeile gefunden wird, ruft »GetOptions« die angegebene Funktion auf, die den aktuellen Wert von »$config{verbose}« ausgibt. Die nebenstehende Abbildung verdeutlicht die Funktion mit ein paar Aufrufen.
GetOptions ('verbose+' => \$config{verbose},
'info' => sub { printf "verbosity: %d\n",
$config{verbose}; });
Wenn der Parameter sowieso nur eine eigene Funktion aufrufen würde, z.B. wenn »--help« die Funktion »help()« aufrufen soll, die den gesamten Hilfetext auf dem Bildschirm ausgibt, macht es nur wenig Sinn, dafür eine anonyme Funktion zu verwenden. In diesem Fall wird die eigentliche Funktion am besten direkt referenziert. Das geschieht über das Perl-Konstrukt »\&funktionsname«.
Für das Beispiel in der nebenstehenden Abbildung wurde das Programm um die Funktion »foo« ergänzt, die nur das Wort »foo« ausgibt und anschließend das Programm beendet.
sub foo
{
printf "foo\n";
exit (0);
}
Darüberhinaus wurde der Aufruf von »GetOptions« dahingehend erweitert, daß der Parameter »--foo« diese Funktion triggert. Dazu wurde der Aufruf lediglich um
'foo' => \&foo,
ergänzt. Der Effekt ist der gewünschte. Kurz nachdem dieser Parameter gelesen wird, beendet sich das Programm.
Mehrere Namen für Parameter
Wie zu erwarten, bietet Perl auch mit Getopt::Long eine flexible Schnittstelle. Hin und wieder ist es erforderlich, daß Parameter mit mehreren Namen bezeichnet werden können. Wenn man aus Gründen der Kompatibilität alte Parameter weiterhin unterstützen möchte, weil sich deren Verwendung weitläufig eingebürgert hat, ist diese Möglichkeit sehr praktisch.
Dazu wird das Pipe-Zeichen bzw. der Alternativen-Strich »|« im Namen für die Unterscheidung verwendet. So bedeutet »verbose|loud«, daß sowohl »--verbose« als auch »--loud« erkannt werden soll. »GetOptions« erkennt dann auch automatisch »-v« und »-l« sowie alle eindeutigen Abkürzungen der Wörter.
Auf diese Weise kann zudem festgelegt werden, wie »-v« interpretiert werden soll, wenn als Parameter sowohl »--verbose« als auch »--value« möglich sind. In einem solchen Fall wird der Aufruf von oben einfach ergänzt.
GetOptions ('verbose' => \$config{'verbose'},
'value|v' => \$config{'debug'});
Anschließend bewirkt der Parameter »-v« genau das gleiche wie »--value«.
Argumente ohne Parameter
Alle Kommandozeilenparameter, die nicht von »GetOptions« erkannt werden, befinden sich nach dem Aufruf der Funktion weiterhin im Array »@ARGV«. Parameter, die nicht erkannt wurden, werden jedoch aus diesem Array gelöscht und erzeugen eine Fehlermeldung. Wenn solche Parameter an das Programm weitergegeben werden sollen, müssen sie durch »--« von den zu parsenden Parametern abgegrenzt werden.
programm -v -f -a -- -z
Bei diesem Programmaufruf steht der Parameter »-z« nach dem Aufruf von »GetOptions« im Array »@ARGV« und erzeugt auch keine Fehlermeldung.
Resources
Getopt::Long - Extended processing of command line options perldoc Getopt::Long