Bash-Skripte
(GNU Bash 5.2.21 unter Linux Mint 22)
Bash ist nicht nur ein Kommandozeileninterpreter, sondern auch eine Programmiersprache. Somit lassen sich nicht nur einzelne Kommandos mit Bash ausführen, sondern auch Programme, die in Bash geschrieben sind. Bash eignet sich besonders zur Analyse und Administration von Rechnern und Netzwerken, da Systemprogramme sehr einfach aufgerufen werden können. Mit Bash können leicht kurze Skripte beispielsweise für die Automatisierung von Prozessen oder die Stapelverarbeitung von Daten geschrieben werden. Aufgrund seiner beschränkten Einsatzmöglichkeiten und der stellenweise nicht besonders intuitiven Syntax ist Bash für Einsteiger in die Programmierung eher weniger geeignet. Die Syntax von Bash stammt noch aus einer Zeit, als Speicherplatz knapp war und man bemüht war, Kommandos mit möglichst wenig Zeichen zu notieren.
Bash unterstützt keine Gleitkomma-Arithmetik, keine mehrdimensionalen Arrays und keine mehrzeiligen Kommentare.
Zur Demonstration wird das folgende kurze Skript, das lediglich eine Zählschleife enthält, zunächst als Datei foobar gespeichert:
clear # leert das Terminalfenster
for ((i=1; i<=10; i++)); do
echo $i
done
Mit dem Kommando bash foobar
wird es nun ausgeführt. Da Skripte, die ohne explizite Angabe eines Interpreters direkt von der Standard-Shell ausgeführt werden können, lässt sich das Programm auch mit dem Kommando ./foobar
starten. Dies funktioniert allerdings nur, wenn die Datei zuvor ausführbar gemacht wurde. Dies geschieht mit dem Kommando chmod +x foobar
.
Offenbar ist die Standard-Shell unter Linux Mint ohnehin bash (was das Kommando echo $SHELL
bestätigt), denn wenn man das Skript mit dem Kommando dash foobar
aufruft, kommt es zu einem Fehler, da dash und bash nicht kompatibel sind. Auf anderen Rechnersystemen kann die Standard-Shell aber eine andere als bash sein, weshalb es aus Gründen der Portabilität sinnvoll ist, im Skript selbst zu vermerken, mit welcher Shell es ausgeführt werden soll. Dies geschieht mit dem sogenannten Shebang in der ersten Zeile eines Skriptes, bei dem nach den Zeichen #! der Pfad zu dem gewünschten Interpreter angegeben wird (dies gilt auch für andere Programmiersprachen):
#!/usr/bin/bash
clear # leert das Terminalfenster
for ((i=1; i<=10; i++)); do
echo $i
done
Wird der Shebang jetzt probehalber auf #!/usr/bin/dash geändert, kommt es bei der Ausführung des Skriptes mit ./foobar
wieder zu einer Fehlermeldung. Wird das Skript so nun aber mit bash foobar
ausgeführt, kommt es zu keinem Fehler, da der Interpreter explizit angegeben und damit der Shebang bei der Ausführung ignoriert wurde.
Außerdem ist es häufig sinnvoll, dem Dateinamen eine Erweiterung zu geben, über die der Datentyp der Datei sichtbar wird. Im Fall von Shell-Skripten ist das die Endung .sh.
Variablen und Kommentare
Die Wertzuweisung zu einer Variablen erfolgt mit dem =-Operator, wobei keine Leerzeichen vor und nach dem Operator zulässig sind.
Zulässige Zeichen für den Bezeichner der Variablen sind die Buchstaben a-z, A-Z, Zahlen und der Unterstrich _. Der Bezeichner darf nicht mit einer Zahl beginnen. Groß- und Kleinschreibung von Bezeichnern wird unterschieden.
#!/usr/bin/bash
# Definition von Variablen
a=42 # zulässig
A=50 # zulässig
_b_=255 # zulässig
: ' Diese Bezeichner sind unzulässig:
dö=18
50cents=100
a-100=200
'
echo $a; echo $aa
a=Osterhase
echo $a
unset a
for ((i=1; i<=10; i++)); do echo $i; echo "-"; done
exit
echo "Diese Zeile wird nach exit nicht mehr ausgegeben."
Aufgerufen („expandiert“) wird eine Variable durch vorangestelltes $-Zeichen (Zeile 12). Der Aufruf unbekannter Variablen wird kommentarlos ignoriert (echo $aa erzeugt hier nur einen Zeilenvorschub).
In Zeile 13 wird der Variable a, die bislang eine Zahl enthielt, ein String zugewiesen, was durch implizite Typumwandlung der Bash eine Typverletzung umgeht. Mit anderen Worten, in Bash lässt sich der Datentyp einer Variable durch Zuweisung eines entsprechenden Wertes problemlos ändern.
Mit unset kann eine Variable wieder gelöscht werden (Zeile 15).
Das Semikolon am Ende einer Anweisung ist nur dann nötig, wenn mehrere Anweisungen in einer Zeile stehend voneinander abgegrenzt werden müssen (Zeile 17).
Das Kommando exit beendet den Programmablauf. Alle folgenden Zeilen werden nicht mehr ausgeführt (Zeile 19).
Im obigen Beispiel wurden einzeilige Kommentare mit # eingeleitet. Mehrzeilige Kommentare existieren in Bash nicht, können aber mit obigem Workaround erreicht werden (Zeilen 7 bis 11: der Doppelpunkt repräsentiert ein Shell-Kommando, das nichts tut, und der folgende Text ist dessen Parameter).
Einrückungen sind im Bash-Quelltext vorteilhaft für die Übersichtlichkeit, aber nicht vorgeschrieben.
Anmerkung: Bash bringt im Gegensatz zu vielen anderen Programmiersprachen nur wenige eigene Sprachelemente mit (sog. builtins, s. hier). Dies wird durch die Tatsache kompensiert, dass Bash auf vorinstallierte Kommandos aus /usr/bin
zurückgreifen kann.
Eingabe und Ausgabe
Die zu verarbeitenden Daten können aber nicht nur aus Variablen gelesen, sondern auch vom Benutzer direkt eingeben werden. Dazu dient das Kommando read. In diesem Fall wird es mit der Option -p (prompt) aufgerufen, womit der Eingabe ein kurzer Text vorangestellt werden kann.
Die Ausgabe erfolgt dann mit dem Kommando echo.
Mit der Option -n wird der Zeilenumbruch am Ende des übergebenen Textes unterdrückt.
read -p "Eingabe: " eingabe
echo Die Eingabe war: $eingabe
echo -n "Foo"; echo -n "Bar"
Normalerweise erfolgt die Ausgabe über den Kanal stdout (standard output), also das Terminal. Die Eingabe erfolgt über den Kanal stdin (standard input), also die Tastatur (siehe auch UbuntuUsers-Wiki). Es ist über eine Umleitung mit den Operatoren > und < aber auch möglich, in eine Datei zu schreiben oder aus einer Datei zu lesen, was folgende Beispiele verdeutlichen:
echo "Dies ist die erste Zeile." > ./foobar.txt # löscht die Datei und schreibt den String in die Datei
echo "Dies ist noch eine Zeile." >> ./foobar.txt # fügt den String ans Ende der Datei an
read a < ./foobar.txt # liest eine Zeile aus der Datei
echo $a
echo
contents=$(< ./foobar.txt) # liest die gesamte Datei ein
echo "$contents"
Pseudokommando
Das Zeichen : repräsentiert ein Pseudokommando, das nichts tut und damit z. B. als Platzhalter verwendet werden kann, um ein Skript valide zu schreiben (und damit ausführbar zu halten), auch wenn der Code, für den der Platzhalter steht, noch nicht geschrieben ist.
if [[ $str == foo ]];
then : # der eigentliche Code an dieser Stelle kommt später
else echo "not foo"
fi
Benutzerdefinierte Kommandos erzeugen
Bash-Skripte können selbst zu Shell-Kommandos werden, indem man sie mit Root-Rechten unter /usr/local/bin abspeichert (ohne Dateinamenserweiterung .sh). Theoretisch können alle Orte dazu verwendet werden, die in der Umgebungsvariable PATH aufgeführt sind, allerdings ist /usr/local/bin explizit für eigene, systemunabhängige Skripte vorgesehen. (Eine Übersicht aller Umgebungsvariablen wird mit dem Kommando printenv
angezeigt.)
Bei der Namenswahl ist darauf zu achten, dass es kein Kommando mit dem gewünschten Namen bereits gibt. Dies lässt sich mit type NAME
prüfen, das den Pfad zum Kommando NAME ausgibt, falls dieses existiert.
Beispiel:
type helloworld
(nicht vergeben)
sudo xed /usr/local/bin/helloworld
#!/usr/bin/bash
echo "Hallo, Welt!"
Anschließend wird das Skript ausführbar gemacht:
sudo chmod +x /usr/local/bin/helloworld
Nun kann es als Kommando aufgerufen werden:
helloworld
Argumente an das Skript übergeben und auslesen
Wie bei jedem anderen Shell-Kommando auch können an die Datei nun Argumente übergeben werden, die das Skript dann verarbeiten kann. Hierbei wird zunächst nicht zwischen den Optionen des Kommandos und anderen Argumenten unterschieden.
Zunächst wird das Skript helloworld folgendermaßen ergänzt:
#!/usr/bin/bash
echo "Hallo, Welt!"
echo "${0}" # gibt den Pfad zum Skript aus
echo "${@}" # gibt alle Argumente aus
echo "$2" # gibt das zweite Argument aus
echo "${#@}" # gibt die Anzahl der Argumente aus
echo "$#" # gibt ebenfalls die Anzahl der Argumente aus
for arg in "${@}"; do # gibt die Argumente einzeln aus
echo $arg
done
for arg; do # gibt die Argumente ebenfalls einzeln aus
echo $arg
done
Nun kann man das Skript mit ein paar beliebigen Argumenten aufrufen und erhält dann die entsprechende Ausgabe:
benutzer@rechner:~$ helloworld -a -b -cd Foobar
Hallo, Welt!
/usr/local/bin/helloworld
-a -b -cd Foobar
-b
4
4
-a
-b
-cd
Foobar
-a
-b
-cd
Foobar
Zwar lassen sich die Argumente jetzt mit gewöhnlichen Prüfungen von Bedingungen parsen, dieser Ansatz ist jedoch relativ umständlich. Einfacher hingegen ist die Verwendung des Kommandos getopts, bei dem eine Unterscheidung zwischen den Optionen des Kommando und weiteren Argumenten stattfindet, wie das folgende Beispiel illustriert.
In dem Skript wird eine while-Schleife über alle Rückgabewerte des Kommandos getopts durchgeführt (alle an das Skript übergebenen Argumente). An dieses Kommando wird als Argument ein String übergeben, der mit einem Doppelpunkt beginnt und alle Buchstaben enthält, die als Optionsschalter berücksichtigt werden sollen (hier a, b, c und d). Optionen, die ein weiteres Argument benötigen (beispielsweise einen Dateipfad oder einen sonstigen benutzerdefinierten Wert), folgt ein weiterer Doppelpunkt (hier bei b).
Bei jedem Durchlauf der Schleife werden die an das Skript übergebenen Argument der Reihe nach in die Variable option geschrieben und mit der case-Anweisung geprüft. Ein an eine Option gebundenes Argument wird in die festgelegte Variable OPTARG geschrieben; dieser Variablenname kann nicht frei gewählt werden.
Es können nun auch Optionen in der üblichen kombinierten Schreibweise wie beispielsweise -acb verwendet werden. Optionen, die ein weiteres Argument benötigen, müssen als letztes in einer solchen Kette notiert werden.
In diesem Beispiel wird nun lediglich das Vorhandensein einer bestimmten Option mit echo bestätigt. In einem realen Skript würde man an dieser Stelle natürlich die benötigte Funktionalität einer Option implementieren.
while getopts ':ab:cd' option; do
case "$option" in
a) echo "Option -a aktiv.";;
b) echo "Option -b aktiv, Argument: $OPTARG";;
c) echo "Option -c aktiv.";;
d) echo "Option -d aktiv.";;
*) echo "unbekanntes Argument"
esac
done
Programmabbruch und Exit-Status
Jedes Shell-Kommando gibt nach dessen Ausführung einen Zahlenwert (den Exit-Status) zurück, aus dem ersehen werden kann, ob das Kommando eventuell zu einem Fehler geführt hat. Dieser Exit-Status kann aus der Variable ? ausgelesen werden. Ist dieser Wert gleich 0, hat es keinen Fehler gegeben. Ansonsten gibt der Wert die Art des Fehlers an, die von dem konkreten Kommando abhängt und beispielsweise in der entsprechenden Manpage beschrieben wird. Der Vorteil eines Exit-Status gegenüber einer Fehlermeldung besteht darin, dass der Exit-Status maschinenlesbar ist und so weiterverarbeitet werden kann.
Im obigen Beispiel könnte man nun das Skript so gestalten, dass bei einer unbekannten Option zusätzlich zu der Fehlermeldung ein entsprechender Exit-Status (hier: 42) zurückgegeben wird (und das Skript an dieser Stelle abgebrochen wird):
*) echo "unbekanntes Argument"; exit 42
Dieser Exit-Status kann nun nach der Ausführung des Skripts mit dem Kommando echo $?
abgefragt werden. Das Kommando exit kann auch ohne Angabe eines konkreten Exit-Status verwendet werden, wenn ein Skript an einer bestimmten Stelle abgebrochen werden soll.