Positional Parameters und Getopts verwenden Bash-Scripting mit Optionen und Parametern

Von Mirco Lang

Skripte und Funktionen sind erst dann nützlich, wenn sie beim Aufruf manipuliert und mit Daten bestückt werden können. Je nach Ansatz gelingt das mit mehr oder weniger Aufwand und Komfort.

Beim Bash-Scripting gibt es die Möglichkeit, Parameter über benannte Optionen zu übergeben.
Beim Bash-Scripting gibt es die Möglichkeit, Parameter über benannte Optionen zu übergeben.
(Bild: Gerd Altmann (geralt) / Pixabay)

Auch Entwickler entwickeln sich und beim Scripting in der Bash geht der Weg oft von simplen Aliasen über deren komplexe Vertreter und simple Funktionen bis hin zu ausführlichen Skripten. Allen gemein: Ohne die Möglichkeit, beim Aufruf weitere Daten oder Anweisungen übergeben zu können, bleiben es im Grunde schlichte Shortcuts, die ein wenig Tippaufwand einsparen.

Das klingt für Sie nach dem eigentlichen Einsatzzweck eines Alias? Bingo – und darum sind Aliase auch nicht für die Aufnahme von Parametern ausgelegt. Freilich lässt sich das bewerkstelligen, aber selbst die Bash selbst empfiehlt generell, Funktionen anstelle von Aliasen zu nutzen. Auch wenn es Dinge gibt, die sich tatsächlich besser per Alias lösen lassen.

Funktionen und Skripte hingegen sind dafür gedacht, mehr oder weniger komplexe Aufgaben für unterschiedliche Daten und mit unterschiedlichen Ausprägungen zu erledigen. In der Bash lassen sich an beide Varianten gleichermaßen Argumente, Optionen, Parameter übergeben – was zunächst die Frage nach dem Unterschied aufwirft.

Im Grunde ist das Wording recht simpel, wie das Beispiel „echo -e foobar“ zeigt: Jedes der drei Elemente ist im Shell-Sinne ein Argument. Das Argument „echo“ ist das eigentliche Kommando, das Argument „-e“ ist eine benannte, dokumentierte Option zum Modifizieren des Kommandos und das Argument „foobar“ ist der dem Kommando übergebene Parameter. Parameter sind also in der Regel die eigentlichen Daten, die jeder Nutzer individuell übergibt. Optionen hingegen sind im Kommando festgelegte Schalter, die vom Nutzer mit Parametern versorgt werden.

Eigentlich ist das alles logisch, nahezu intuitiv, aber im Detail – nun, nicht ohne Grund gibt es auf Stack Overflow dazu Dutzende Fragen mit Millionen Aufrufen. Zumal sich der Einsatz von Parametern in der Praxis meist ebenfalls entwickelt: Von schlicht durchnummerierten Parametern (Positional Parameters) über deren manuelle Benennung bis hin zur Zuordnung zu benannten Optionen mit dem Bash- und sh-internen Tool „getopts“ (nicht zu verwechseln mit dem älteren Stand-Alone-Programm „getopt“). Aber der Reihe nach, wortwörtlich.

Positional Parameters

Parameter können in der Bash ohne jegliche Angaben an Skripte und Funktionen übergeben werden – sie werden schlicht durchnummeriert und landen in den Variablen $1, $2, $3 und so weiter. Zur Demonstration soll das denkbar einfachste Beispiel dienen, eine Funktion „printit“:

printit ()
{
   echo Parameter 1 ist $1
   echo Parameter 2 ist $2
}

Der Aufruf …

printit eins zwei

… produziert entsprechend:

Parameter 1 ist eins
Parameter 2 ist zwei

Zusätzlich zu den durchnummerierten Parametern gibt es noch ein paar spezielle Variablen: „$#“ steht beispielsweise für die Anzahl der übergebenen Parameter, „$?“ für den Exit-Code, „$*“ und „$@“ zeigen alle Parameter als einen String beziehungsweise als einzelne Strings und „$0“ beinhaltet den Namen des aufgerufenen Skripts oder im Falle von Funktionen der aufrufenden Shell.

Um sich damit etwas näher vertraut zu machen, ließe sich die obige Funktion zu einer Hilfsfunktion erweitern:

printit ()
{
   echo Parameter 1 ist $1
   echo Parameter 2 ist $2
   echo "@" steht für $@
   echo "*" steht für $*
   echo "#" steht für $#
   echo "?" steht für $?
   echo "0" steht für $0
}

Die Ausgabe ist entsprechend:

Parameter 1 ist eins
Parameter 2 ist zwei
@ steht für eins zwei
* steht für eins zwei
# steht für 2
? steht für 0
0 steht für /usr/bin/bash

Und hier lauert mal wieder eine kleine Anführungszeichen-Hölle: Man beachte, dass die Ausgaben von $@ und $* identisch sind – das gilt aber nur, solange zwei Bedingungen erfüllt sind: Die Variablen stehen nicht in Anführungszeichen und der interne Wort-Separator (Internal Field Seperator/IFS) wurde nicht verändert. Hier nun die Ausgaben mit Anführungszeichen und dem Doppelpunkt als IFS:

printit ()
{
   IFS=:
   echo "@" steht für $@
   echo "*" steht für $*
}

„printit eins zwei“ ergibt dann:

@ steht für eins zwei
* steht für eins:zwei

Man muss an dieser Stelle also umsichtig sein und prüfen, welche Variante tatsächlich benötigt wird. @ versteht alle Argumente separat, das * versteht alle Argumente als einen einzigen String.

Parameter nach Positionen sind einfach, bequem und manchmal sogar ausreichend. Aber natürlich sind Variablen wie „$Vorname“ und „$Nachname“ wesentlich verständlicher als schlicht ein paar Ziffern. Der logische Folgeschritt besteht im manuellen Umbenennen in der Art:

vorname="$1"
nachname="$2"

Innerhalb von Skripten und Funktionen sieht es damit schon deutlich aufgeräumter aus. Vor allem aber: Parameter via Ziffern versteht bestenfalls der jeweilige Autor einer Funktion ohne Probleme – und spätestens bei ein paar Dutzend Zeilen und einem halben Dutzend Parametern auch dieser nicht mehr.

Zudem handelt es sich hier bislang um frei definierbare Parameter. Was aber, wenn die Ausgabe von Namen in einem anderen Format erfolgen soll? Oder wahlweise rückwärts? Natürlich könnte man selbst dann noch positionale Parameter nutzen – mal als Dummy-Beispiel:

Jetzt Newsletter abonnieren

Täglich die wichtigsten Infos zu Softwareentwicklung und DevOps

Mit Klick auf „Newsletter abonnieren“ erkläre ich mich mit der Verarbeitung und Nutzung meiner Daten gemäß Einwilligungserklärung (bitte aufklappen für Details) einverstanden und akzeptiere die Nutzungsbedingungen. Weitere Informationen finde ich in unserer Datenschutzerklärung.

Aufklappen für Details zu Ihrer Einwilligung
printname Peter Schmidt r c

Wenn der gewünschte Output hier „Schmidt, Peter“ wäre, könnten r und c entsprechend für das Komma und die gedrehte Schreibweise stehen – wenn man zum Beispiel via if-Abfragen nach r und c an den Positionen 3 und 4 fahndet. Das ist aber nicht bloß kompliziert, es geht auch garantiert schief. Sei es, weil jemand Marie Luise Müller heißt oder Parameter versehentlich an der falschen Stelle stehen. Die Lösung besteht darin, Parameter über benannte Optionen zu übergeben.

Optionen mit getopts

Das Bash-interne getopts löst aber nicht bloß das Problem der Übersichtlichkeit, sondern spendiert der Parameterübergabe generell mehr Komfort. So spielt etwa die Reihenfolge der Optionen keine Rolle, Parameter können für Optionen verpflichtend sein, es gibt eine gewisse Fehlerbehandlung und so weiter.

Getopts liest nacheinander jede gegebene Option ein und speichert eventuell zugehörige Parameter in der Variablen „$OPTARG“. Diese werden dann üblicherweise über ein CASE-Statement innerhalb einer WHILE-Schleife verarbeitet. Statt eines abstrakten, generischen Beispiels, soll es hier ein Mini-Skript mit folgender Funktion veranschaulichen: Das Skript gibt Vorname, Mittelname und Nachname als Liste sowie – optional – eine kurze Variante in der Form „Nachname, Vorname“ aus.

Zunächst das kleine Skript „printname.sh“, der Übersicht halber mit nummerierten Zeilen:

 1 #!/bin/bash 2
 3 while getopts "f:m:l:r" option
 4 do
 5   case $option in
 6      f) firstname="$OPTARG";;
 7      m) middlename="$OPTARG";;
 8      l) lastname="$OPTARG";;
 9      r) reverse=1;;
14   esac
15 done
16
17 echo Vorname: "$firstname"
18 echo Zweitname: "$middlename"
19 echo Nachname: "$lastname"
20
21 if [[ reverse -eq 1 ]]; then echo Reverse: "$lastname", "$firstname"; fi

In Zeile 3 werden die verfügbaren Optionen aufgelistet: „fml“ stehen hier für First-, middle und Lastname, „r“ steht für die Rückwärtsschreibweise „Nachname, Vorname“. Wichtig sind die Doppelpunkte: Ein Doppelpunkt nach einem Optionsbuchstaben besagt, dass diese Option einen Parameter benötigt – in diesem Fall also den jeweilen Namenspart. Gespeichert werden die Optionen/Parameter hier jeweils in der Variablen „option“, die Sie aber beliebig nennen können.

Die while-Schleife läuft also für jede gegebene Option einmal durch; das case-Statement ordnet die zur Option angegebenen Parameter/Werte wiederum sinnvoll benannten Variablen zu, hier eben „firstname“, „middlename“ und „lastname“.

Für die Option „r“ sieht es minimal anders aus: Hier wird kein Parameter übergeben, sondern stattdessen einfach eine Variable „reverse“ auf 1 gesetzt – da es sich hier um eine Option nach dem üblichen Verständnis handelt; einen Schalter, der den Programmablauf verändert, hier also eine zusätzliche Aktion durchführt.

Nach der while-Schleife folgt dann das eigentliche Skript, hier die simple, formatierte Ausgabe der übergebenen Daten in Listen- und Rückwärtsform.

Der Aufruf des Skripts …

printname.sh -l Müller -f Thomas -m J. -r

… erzeugt die Ausgabe:

Vorname: Thomas
Zweitname: J.
Nachname Müller
Reverse: Müller, Thomas

Sie sehen hier schon: Die Reihenfolge der Optionen spielt keine Rolle.

Es gibt aber noch einen relevanten Doppelpunkt, der hier im Skript nicht vorkommt: Wenn im getopts-Statement ein solcher am Anfang der Optionen (vor dem f) steht, also …

getopts ":f:m:l:r"

…, unterdrückt getopts etwaige Fehlermeldungen – also zum Beispiel fehlende Parameter für Optionen oder ungültige Optionen. In der Praxis wird dieser führende Doppelpunkt häufig gesetzt, um eine eigene Fehlerbehandlung umzusetzen. Im case-Statement könnte das etwa so aussehen:


\?) echo "Invalid option" & exit;;
:) echo "Missing Argument" & exit;;

Das Fragezeichen greift nicht definierte Optionen ab, der Doppelpunkt fehlende Parameter für Optionen, die im getopts-Statement einen Doppelpunkt haben.

Und noch eine Zeile wird Ihnen im case-Statement häufig begegnen:


h|*) echo $usage

Das Sternchen fängt beide Fehlerarten (? und :) von oben ab und „h“ steht freilich wie üblich für einen Hilferuf – sprich: Werden falsche Optionen übergeben oder erforderliche Parameter für Optionen nicht übergeben, wird die Hilfe ($usage) angezeigt. Oder eben, wenn „-h“ als Option direkt aufgerufen wird.

Mit diesen Snippets können Sie sich durchaus ein brauchbares Template für Ihre Skripte und Funktionen zusammenbauen. Eine nicht ganz unerhebliche „Kleinigkeit“ sollte aber noch erwähnt werden. Mal angenommen, der Aufruf erfolgt so:

printname.sh -f -l

Man könnte nun annehmen, getopts würde eine Fehlermeldung ausgeben, weil die Optionen f und l ohne weitere Parameter gesetzt wurden. Stimmt aber natürlich nicht, hier würde „-l“ als Parameter für die f-Option interpretiert! Eine weitere Verfeinerung und Überprüfung, wie das Auschließen von Parametern, die mit „-“ beginnen oder bestimmte Format-Erfordernisse, müssen Sie manuell erledigen.

Eine letzte Frage dürfte fast immer aufkommen: Wie lassen sich Optionen als „zwingend nötig“ (mandatory) festlegen? Ganz klar: Gar nicht – Optionen sind naturgemäß optional … Auch an dieser Stelle müssten Sie manuell tätig werden und schlicht prüfen, ob die gewünschten Variablen nach Durchlauf der while-Schleife gesetzt sind oder nicht.

Müssen hier im Beispiel also Vor- und Nachname vorhanden sein, würde sich folgende Erweiterung des Skripts anbieten:

if [ ! "$firstname" ] || [ ! "$lastname" ]; then
   echo "Die Optionen -f und -l müssen gesetzt werden!" & exit 1
fi

Es lohnt sich durchaus, getopts auch bei kleineren Skripten von Beginn an einzusetzen, schließlich neigen Skripte dazu, im Laufe der Zeit zu wachsen. Mit einer persönlichen getopts-Vorlage ist der Aufwand minimal, eine nachträgliche Migration allerdings garantiert nicht unproblematisch.

(ID:48539424)