Shell-Scripting¶
Die Shell kann unmittelbar als Interpreter für einzelne Programmaufrufe genutzt werden. Darüber hinaus ist es ebenso möglich, mehrere Eingabezeilen miteinander zu kombinieren. Diese Methode, mehrere aufeinander folgende Programmaufrufe – ähnlich wie ein Kochrezept – in eine Textdatei zu schreiben und mittels dieser Datei von der Shell ausführen zu lassen, wird als „Shell-Scripting“ bezeichnet.
In den folgenden Abschnitten werden einige Möglichkeiten zum gezielten Scripten
der bash
, also der unter Debian, Ubuntu und Linux Mint am weitesten
verbreiteten Shell, näher vorgestellt.
Aufbau und Aufruf eines Shellskripts¶
Shell-Skripte sind Textdateien, die üblicherweise die Endung .sh
oder
überhaupt keine Endung besitzen.[1] In beiden Fällen sollte am Anfang der
Datei festgelegt werden, welcher Shell-Interpreter beim Aufruf des Skripts
genutzt wird. Dies erfolgt über den so genannten „Shebang“:[2]
#!/bin/bash
Die Zeichenkette #!
bewirkt dabei, dass die nachfolgende Anweisung
(gegebenenfalls mit zusätzlichen Argumenten) beim Aufruf des Programms
ausgeführt wird. Im obigen Beispiel wird also ein bash
-Prozess gestartet,
der die darauf folgenden Anweisungen und Programmaufrufe Zeile für Zeile
interpretiert.[3] Alle anderen Zeilen, die mit #
beginnen, werden als
Kommentare ignoriert.
Um das Shell-Skript aufzurufen, wird in einer Shell folgende Zeile eingegeben:
sh pfad/skriptdatei
Ebenso ist es möglich, die Skriptdatei mit chmod +x
ausführbar zu machen.
Dann kann sie – wie ein binäres Programm – direkt aufgerufen werden:
# Datei ausführbar machen:
cd pfad
chmod +x skriptdatei
# Skript aufrufen:
pfad/skriptdatei
Befindet man sich im gleichen Ordner wie die Skriptdatei, so kann diese mit
./skriptdatei
aufgerufen werden, da .
für den Namen des aktuellen
Verzeichnisses steht.
Möchte man bestimmte Skripte auch ohne explizite Angabe des Pfades aufrufen, so
empfiehlt es sich zu diesem Zweck mit mkdir ~/bin
ein eigenes
Unterverzeichnis im Home-Verzeichnis anzulegen und in die Konfigurationsdatei
~/.bashrc
folgenden Eintrag zu schreiben:
# Eigenes bin-Verzeichnis zum Systempfad PATH hinzufügen
# Prüfen ob das Verzeichnis existiert:
if [ -d "$HOME/bin" ] ; then
PATH="$HOME/bin:$PATH"
export PATH;
fi
Durch die Zeile PATH="$HOME/bin:$PATH"
wird das eigene bin
-Verzeichnis
an den Anfang des Systempfades gesetzt. Wird in einer Shell eine beliebige
Anweisung eingegeben, so wird zunächst der eigene bin
-Ordner und erst
anschließend alle anderen in der PATH
-Variable gespeicherten Verzeichnisse
nach einem entsprechenden Programm beziehungsweise Skript durchsucht.
Rückgabewerte und Verkettung von Programmen¶
Jedes Programm oder Shell-Skript sollte beim Beenden einen Wert liefern, der
angibt, ob das Skript fehlerfrei abgelaufen ist oder nicht. Üblicherweise wird
dazu das Programm bei einem fehlerfreien Durchlauf mit exit 0
beendet. Jeder
andere „Exit-Status“ zwischen 1
bis 255
deutet meistens auf einen Fehler
hin.[4] Der Rückgabewert wird in einem Skript an einer beliebigen Stelle
mittels exit num
festgelegt; das Skript wird dadurch unmittelbar
beendet.
Bei der interaktiven Benutzung der Shell wird der Exit-Status eines Skripts oder Programms nur selten benutzt, da mögliche Fehlermeldungen direkt vom Benutzer abgelesen werden können. In Shell-Skripten jedoch werden bestimmte Aktionen häufig in Abhängigkeit von anderen Aktionen ausgeführt; sollen beispielsweise Dateien verschoben werden, so dürfen die Originale nur dann gelöscht werden, wenn sie zuvor erfolgreich kopiert worden sind.
Die Operatoren &&
und ||
Mittels der Operatoren &&
und ||
kann man die Ausführung einer zweiten
Anweisung vom Rückgabewert der ersten Anweisung abhängig machen:
- Ein Ausdruck der Form
anweisung1 && anweisung2
bedeutet, dassanweisung2
nur dann ausgeführt wird, wennanweisung1
fehlerfrei ausgeführt beziehungsweise mit Exit-Status0
beendet wurde. - Ein Ausdruck der Form
anweisung1 || anweisung2
bedeutet, dassanweisung2
genau dann ausgeführt wird, wennanweisung1
mit einem Fehler beziehungsweise mit einem Exit-Status zwischen1
und255
beendet wurde.
Bedingte Anweisungen können auch mittels if
und case
implementiert
werden.
Ausgaben in Textdateien umlenken
Normalerweise geben Skripte und Programme Meldungen und Rückgabewerte als Text
im Shell-Fenster aus. Mit den Operatoren >
und >>
ist es allerdings auch
möglich, die Ausgabe in Text-Dateien umzuleiten:
Mit dem Operator
>
kann man die Ausgabe eines Programms in eine Datei umleiten, deren Name im Anschluss an das>
-Zeichen geschrieben wird:# Inhalt des Verzeichnisses und aller Unterverzeichnisse # in Text-Datei "folder-content.txt" schreiben: ls -R > folder-content.txt
Existiert die Datei noch nicht, so wird sie neu angelegt; als Eigentümer der Datei wird dabei der Benutzer eingetragen, der den Shell-Prozess ausführt.
Existiert die Datei schon, so wird sie zunächst geleert und anschließend neu beschrieben. Der Eigentümer und die Zugriffsrechte bleiben dabei erhalten. Damit das Überschreiben funktioniert, muss das Schreiben der Datei erlaubt sein.
- Nach dem gleichen Prinzip kann man mit dem Operator
>>
die Ausgabe eines Programms an eine Datei anhängen. Diese Variante findet insbesondere bei der Protokollierung einzelner Prozesse in Log-Dateien Anwendung.
Umgekehrt kann man mittels des Operators eine Datei angeben, aus der
gelesen werden soll. Selten wird diese Syntax von Programmen zwingend gefordert,
in den meisten Fällen kann eine einzulesende Datei auch ohne <
angegeben werden.
Beispielsweise sind die Anweisungen cat anyfile.txt
und cat <
anyfile.txt
identisch.
Fehlermeldungen umlenken
Für jeden Prozess öffnet die Shell drei Standard-Kanäle: Kanal 0
steht für
die Standard-Eingabe, Kanal 1
für die Standard-Ausgabe und 2
für den
Standard-Fehlerkanal.
Man kann jedem der Umleitungskommandos <
, >
und >>
eine dieser
Nummern für den jeweiligen Kanal voranstellen. So kann beispielsweise die
Fehlerausgabe einer Anweisung mittels 2> error-logfile.txt
in eine
entsprechende Log-Datei umgelenkt werden.
Als eine Besonderheit ist hierbei die Datei /dev/null
hervorzuheben. Diese
Datei dient als „Mülleimer“, es werden also alle Meldungen, die zu dieser Datei
umgelenkt werden, verworfen. So kann man beispielsweise mittels der Anweisung
any_program 2> /dev/null
die Ausgabe von Fehlern unterdrücken.
Pipelines („Pipes“)
Die Ausgaben eines Programms können nicht nur Dateien, sondern auch an andere
Programme weitergeleitet werden. Hierzu wird in Shell-Skripts der Operator |
(„Pipe“) in der Form anweisung1 | anweisung2
verwendet:
# Alle Dateien des aktuellen Verzeichnisses und aller
# Unterverzeichnisse anzeigen, die "txt" enthalten:
ls -R | grep txt
Bei einer solchen Verkettung von Programmen werden die Daten vom Interpreter in eine temporäre Datei (ebenfalls „Pipe“ genannt) abgelegt und so an das nächste Programm übergeben.
Pipes stellen ein vielseitiges Werkzeug dar, insbesondere in Kombination mit folgenden Programmen:
- Mit grep kann die Ausgabe eines Programms hinsichtlich bestimmter Suchmuster gefiltert werden.
- Mit tee kann die Standard-Ausgabe oder Standard-Fehlerausgabe sowohl auf
dem Bildschirm ausgegeben als auch in eine Datei geschrieben werden. Die
Syntax hierzu könnte also
anweisung | tee error-logfile.txt
lauten. - Mit
xargs
werden alle empfangenen Werte als Argumente der folgenden Anweisung übergeben. Beispielsweise würde die Anweisungfind ./ -name *foo* | xargs grep muster
alle Dateien, die „foo“ in ihrem Dateinamen enthalten, nach dem gegebenen Begriff oder Suchmuster durchsuchen (ohnexargs
würden hingegen die Dateinamen nachmuster
durchsucht).
Dateimuster und Variablen¶
Die Shell weist als Interpreter einigen Sonderzeichen eine besondere Bedeutung zu. Mit Hilfe solcher Zeichen (so genannten „Wildcards“) können Muster für Dateinamen einfach formuliert werden. Die Shell ersetzt dann bei der Ausführung die Muster dann durch die entsprechenden Dateinamen.
Zeichen | Bedeutung |
~ |
Der Pfad des Home-Verzeichnisses des aktuellen Benutzers |
* |
Eine beliebig lange Folge von Zeichen |
? |
Ein einzelnes beliebiges Zeichen |
/ |
Trennzeichen für Verzeichnis-Namen (Pfadangaben) |
[abc123] |
Eines der Zeichen, die in der Klammer vorkommen |
[a-z1-9] |
Eines der Zeichen im angegebenen Bereich |
[!abc] |
Ein beliebiges Zeichen, das nicht in der Klammer vorkommt |
Wohl am häufigsten wird das *
-Zeichen verwendet, das für eine beliebig lange
Folge von Zeichen steht; dabei ist auch die Länge Null explizit erlaubt.
Beispielsweise werden mittels ls *foo*
alle Dateien ausgegeben, die „foo“ in
ihrem Dateinamen beinhalten, egal welche Zeichen vorher oder nachher im
Dateinamen vorkommen. Mit ls *.txt
werden alle Dateien angezeigt, deren
Dateiname auf „.txt“ endet. Zu beachten ist hierbei jedoch eine Ausnahme: Der
Stern als Suchmuster ignoriert Dateien, deren Name mit einem Punkt beginnt, es
sei denn, man schreibt explizit .*txt
. Dadurch soll verhindert werden, dass
versehentlich Konfigurationsdateien gelöscht werden.
In den eckigen Klammern kann auch ein Buchstaben- oder Zahlenbereich in der Form
[a-z]
oder [0-9]
angegeben werden; auch eine Kombination der Form
[a-zA-Z0-9]
ist möglich, um ein beliebiges alphanumerisches Zeichen
auszudrücken. Diese Syntax funktioniert ebenso für ausschließende Klammern,
beispielsweise steht [!a-z]
für ein beliebiges Zeichen außer einem
Kleinbuchstaben.
Eine weitere besondere Bedeutung hat das Dollar-Zeichen $
: Es ersetzt den
unmittelbar (ohne Leerstelle) folgenden Variablennamen durch den in der
Variablen abgespeicherten Wert.
Möchte man die gewöhnliche Bedeutung eines Zeichens aufheben, so muss diesem das
Backslash-Zeichen \
vorangestellt werden. Dies betrifft sowohl die oben
angegebenen Sonderzeichen wie auch das Leerzeichen, das eigentlich zur Trennung
verschiedener Argumente genutzt wird, aber auch Bestandteil von Dateinamen sein
kann. In gleicher Weise muss den Zeichen ; & | ( ) < >
sowie \n
und
\t
(Zeilenende und Tabulator) ein Backslash vorangestellt werden, um die
jeweilige Sonderbedeutung aufzuheben.
Eine weitere bisweilen nützliche Ergänzung bieten geschweifte Klammern innerhalb
von Dateimustern. Diese eignen sich dazu, um an der gegebenen Stelle einen der
in den geschweiften Klammern stehenden, durch Komma-Zeichen voneinander
getrennten Namen einzusetzen. Beispielsweise gibt ls -R
~/data/{buecher,docs}/?*/{*.pdf,*djvu}
alle pdf
- und djvu
-Dateien in
den Unterverzeichnissen von ~/data/buecher
und ~/data/docs
auf. Diese
Liste kann dann beispielsweise mittels | grep
gezielt nach Einträgen
durchsucht werden.
Zuweisung von Variablen
Ähnlich wie in Programmiersprachen, so lassen sich auch in der Shell Werte in Variablen speichern. Allerdings sind nur Zeichenketten („Strings“) als Werte erlaubt.
Um einer Variablen einen Wert zuzuweisen, muss folgende Syntax verwendet werden:
variablenname=wert
Zwischen dem Variablennamen, dem Zuweisungsoperator =
und dem zu
speichernden Wert darf dabei kein Leerzeichen stehen; auf den Inhalt der
Variable kann wiederum mittels $variablenname
zugegriffen werden.
Mit der Anweisung set
kann in der Shell abgefragt werden, welche Variablen
aktuell gesetzt sind und welche Werte diese haben. Unter Umständen kann diese
Liste recht lang sein, so dass es nützlich ist, die Ausgabe von set
mittels
einer Pipe entweder an einen Pager wie less zu übergeben oder
mittels grep nach einem bestimmten Variablennamen zu filtern.
# Alle Variablen und ihre Werte mit less betrachten:
set | less
# Wert der Variablen EDITOR prüfen:
set | grep EDITOR
Variablen können in der Shell an jeder beliebigen Stelle genutzt werden. Trifft
der Shell-Interpreter auf ein $
-Zeichen, so wird der unmittelbar (ohne
Leerzeichen) folgende Variablenname durch den gespeicherten Variablenwert
ersetzt. Ist die angegebene Variable nicht definiert, so wird vom Interpreter an
dieser Stelle nichts eingesetzt.
Mittels unset variablenname
kann man eine Variable wieder löschen.
Exportieren von Variablen
Weist man in einer Shell einer Variablen einen Wert zu, so ist diese Variable
per Voreinstellung nur dem aktuellen Shell-Prozess bekannt. Möchte man eine
Variable auch in von der aktuellen Shell-Sitzung aus gestarteten Unterprozessen
nutzen, so kann sie mittels der export
-Anweisung zugänglich gemacht werden:
# Variable definieren:
my_var=fooo
# Variable öffentlich machen:
export my_var
Auch in diesem Fall kann die Variable mittels unset my_var
wieder gelöscht
werden. Wird dem gleichen Variablennamen erneut ein Wert zugewiesen, so wird die
Variable wieder als lokal angesehen und muss bei Bedarf erneut exportiert
werden.
Definition von Konstanten
Mittels der Anweisung readonly variablenname
kann eine Variable in eine
Konstante umgewandelt werden. Der Wert, den die Variable zu diesem Zeitpunkt
hat, kann später nicht mehr verändert werden, auch kann die Variable nicht mehr
mittels unset
gelöscht werden – sie ist sozusagen schreibgeschützt. Erst
mit dem Beenden der Shell wird die Konstante wieder gelöscht.
Mittels readonly
(ohne Variablennamen) kann eine Liste mit allen aktuell
definierten Konstanten ausgegeben werden.
Definition von Variablen-Listen
In einer Shell-Variable kann eine Liste mit mehreren Elementen mittels folgender Syntax abgespeichert werden:
var_liste=(
element_1
element_2
element_3
)
Wichtig ist hierbei wiederum, dass zwischen dem Namen der Variablenliste, dem
Zuweisungsoperator =
und der öffnenden Klammer kein Leerzeichen stehen
darf. Es können auch mehrere Elemente in eine Zeile geschrieben werden; die
Elemente werden durch Leerzeichen getrennt.
Auf die einzelnen Elemente der Variablenliste kann mittels ${var_liste[0]}
,
${var_liste[1]}
usw. zugegriffen werden, wobei der Index 0
für das erste
Listenelement und der Index 1
für das zweite Listenelement steht. Schreibt
man nur $var_liste
, so ist dies mit einem Zugriff auf das erste Element der
Liste identisch. Um alle Listenelemente auszugeben, muss hingegen
${var_liste[*]}
geschrieben werden.
Die Anzahl an Elementen einer Liste kann mittels ${#var_liste[*]}
ausgegeben
werden; mit ${#var_liste[num]}
wird ausgegeben, aus wie vielen (Text-)Zeichen das
Listenelement mit der Indexnummer num
besteht.
Ein neues Element kann folgendermaßen an eine bestehende Liste angefügt werden:
var_liste+=( element_4 )
Soll ein neues Element nicht am Ende der Liste, sondern vor einer bestimmten
Indexposition num
eingefügt werden, so kann man folgendes schreiben:
var_liste[num]+=( element_neu )
Mittels unset var_liste
kann die Variablenliste, mit unset
var_liste[num]
das Listenelement mit der Indexnummer num
wieder gelöscht
werden.
Besondere Shell-Variablen¶
Im folgenden werden einige Standard-Variablen aufgelistet, die automatisch definiert sind und häufig in Shell-Skripten vorkommen:
$0 |
Diese Variable enthält den Namen des aktuellen Prozesses, beispielsweise
/bin/bash .Im Fall eines laufenden Shellskripts entspricht
$0 dem Namen der
Skriptdatei. |
$1 bis $9 |
Diese Variablen enthalten die beim Aufruf des Skripts übergebenen
Argumente 1 bis 9 . |
$* |
Diese Variable enthält alle beim Aufruf des Skripts übergebenen
Argumente als eine einzelne Zeichenkette.
Die einzelnen Argumente sind
dabei durch Leerzeicheen getrennt.
|
$@ |
Diese Variable enthält alle beim Aufruf des Skripts übergebenen Argumente als Liste. |
$# |
Die Variable gibt die Anzahl der beim Aufruf des Skripts übergebenen Argumente an. |
$- |
Diese Variable enthält alle im aktuellen Prozess eingeschalteten Optionsbuchstaben. |
$? |
Diese Variable enthält den Exit-Status der zuletzt ausgeführten Anweisung. |
$$ |
Diese Variable enthält die Prozess-Nummer der Shell, in der das Skript ausgeführt wird. |
$! |
Diese Variable enthält die Prozess-Nummer des zuletzt erzeugten Hintergrundprozesses. |
Die Variable $$
ist insbesondere für die Erzeugung von temporären Dateien
von Bedeutung. Erzeugt ein Skript beispielsweise eine gleichnamige Datei
/tmp/$0
im /tmp
-Verzeichnis, so würde das Skript bei einem
gleichzeitigen Aufruf in verschiedenen Shell-Fenstern die gleiche Datei nutzen
und dabei mit großer Wahrscheinlichkeit jeweils Daten der anderen Prozesse
überschreiben. Verwendet man hingegen /tmp/$0.$$
als Namen für die temporäre
Datei, so bekommt jede ausführende Instanz des Skripts eine eigene Datei
zugewiesen.
Neben den obigen, minimalistisch benannten Variablen existieren weitere vordefinierte Variablen, die häufig in Shell-Skripten eingesetzt werden:
$EDITOR |
Diese Variable gibt an, welches Programm bevorzugt als Texteditor geöffnet werden soll. |
$HOME |
Diese Variable enthält den Namen des Home-Verzeichnisses des aktuellen Benutzers. |
$PAGER |
Diese Variable gibt an, welches Programm als Pager, also als Anzeigeprogramm für Textdateien geöffnet werden soll |
$PATH |
Diese Variable enthält alle Verzeichnisse, in denen bei Eingabe einer Shell-Anweisung nach einem entsprechenden Programm gesucht wird. Die Namen der einzelnen Verzeichnisse sind durch Doppelpunkte getrennt und werden in der angegebenen Reihenfolge durchsucht. |
$PS1 |
In dieser Variablen („Prompt String 1“) wird das Aussehen des
Eingabe-Prompts definiert. Üblicherweise steht $ für normale Benutzer
und # für den Systemverwalter. |
$PS2 |
In dieser Variablen („Prompt String 2“) wird definiert, wie der
Eingabe-Prompt im Fall eines Zeilenumbruchs aussehen soll.
Üblicherweise wird hierfür das Zeichen > verwendet. |
$TERM |
Diese Variable enthält den Namen des aktuellen Shell-Anzeige-Programms. |
$USER |
Diese Variable enthält den Namen des aktuellen Benutzers. |
Die aktuellen Werte aller Variablen können mittels der Anweisung printenv
angezeigt werden; eine einzelne Variable wie $EDITOR
kann mittels printenv
EDITOR
ausgegeben werden.
Üblicherweise werden die $EDITOR
und $PAGER
-Variablen in der
Konfigurationsdatei .bashrc
festgelegt:
# Vim als Editor festlegen:
export EDITOR=vim
# Less als Pager festlegen:
export PAGER=less
Werden die Variablen nicht vom Benutzer gesetzt, so wird üblicherweise vi
als Standard-Editor und cat
als Pager verwendet.
Wer keine Erfahrung mit Vim hat, kann an dieser Stelle
beispielsweise pico
, nano
, joe
oder emacs
verwenden, wobei die
letzten beiden gegebenenfalls mittels der gleichnamigen Pakete via apt installiert werden müssen.
Auswertung von Variablen¶
In manchen Fällen, beispielsweise beim Arbeiten mit Verzeichnis- und Dateinamen, kann es passieren, dass der Wert einer Variable nahtlos in weiteren Text übergehen soll. In diesem Fall muss der Name der Variablen in geschweifte Klammern gesetzt werden:
# Variable definieren:
zwei=ZWEI
echo eins$zweidrei
# Ergebnis: eins
echo eins${zwei}drei
# Ergebnis: einsZWEIdrei
Ist der Variablenname in geschweiften Klammern nicht definiert, so wird sie wie gewöhnlich vom Interpreter ausgelassen (durch „nichts“ ersetzt). Dieses Verhalten des Interpreters kann auf mehrere Arten beeinflusst werden:
- Schreibt man
${variablenname-standardwert}
, so wird an der Stelle der Variablen der angegebene Standardwert eingesetzt, sofern der Variablenname nicht definiert ist; Die Variable bleibt dabei undefiniert. - Schreibt man
${variablenname=standardwert}
, so wird ebenfalls an der Stelle der Variablen der angegebene Standardwert eingesetzt, sofern der Variablenname nicht definiert ist; die Variable wird dabei allerdings mit dem angegebenen Standardwert neu definiert. - Schreibt man
${variablenname?fehlermeldung}
, so wird geprüft, ob der angegebene Variablenname definiert ist. Ist er es nicht, so wird das Shellskript abgebrochen und die hinter dem?
angegebene Fehlermeldung angezeigt. Wird keine Fehlermeldung angegeben, so wird als Standard die Meldung „parameter null or not set“ ausgegeben.
Möchte man im umgekehrten Fall einen bestimmten Wert ausgeben, wenn eine
Variable definiert ist, so kann man die Syntax ${variablenname+wert}
verwenden. Beispielsweise Liefert ${one+yes}
den Wert yes
, wenn die
Variable one
definiert ist, andernfalls wird die angegebene Variable
ausgelassen (durch nichts ersetzt).
Quotings
In der Shell haben einfache Anführungszeichen, doppelte Anführungszeichen und so
genannte „Backticks“ (`
) eine jeweils eigene Bedeutung:
Innerhalb von doppelten Anführungszeichen kann ein beliebig langer Text als eine einzelne Zeichenkette eingegeben werden. Diese kann sich über mehrere Bildschirmzeilen erstrecken und Leerzeichen beinhalten, ohne dass diesen jeweils ein Backslash vorangestellt werden muss. Die Bedeutung des Dollar-Zeichens bleibt allerdings erhalten, so dass mittels
$variablenname
innerhalb doppelter Anführungszeichen der Wert einer Variablen wie gewohnt ausgewertet werden kann.Dateinamen-Erweiterungen, beispielsweise mittels des Stern-Zeichens
*
, sind hingegen innerhalb der Anführungszeichen nicht möglich.Innerhalb von einfachen Anführungszeichen kann ebenfalls ein beliebig langer Text als eine einzelne Zeichenkette eingegeben werden. Auch in diesem Fall kann sich der Text über mehrere Bildschirmzeilen erstrecken. Die Besonderheit bei der Benutzung von einfachen Anführungszeichen liegt darin, dass in diesem Fall sämtliche Sonderzeichen ihre Bedeutung verlieren, also auch keine Auswertung von Variablen mittels des Dollar-Zeichens
$
erfolgt.
- Backticks werden üblicherweise für Shell-Anweisungen genutzt, wobei die innerhalb der Backticks stehende(n) Anweisung(en) von der Shell durch ihren Rückgabewert ersetzt werden.
Soll das Ergebnis einer Shell-Anweisung wie eine Variable genutzt werden, so
kann dies alternativ zur Backticks-Notation auch mittels $(anweisung)
erfolgen. Diese Schreibweise ist im Allgemeinen sogar vorzuziehen, da sie meist
übersichtlicher und somit angenehmer zu lesen ist.
Kontrollstrukturen¶
Die folgenden Kontrollstrukturen können zur Steuerung eines Shell-Skripts verwendet werden, wenn einzelne Code-Blöcke nur unter bestimmten Bedingungen oder auch mehrfach ausgeführt werden sollen.
Fallunterscheidungen – if
, then
, else
Mit Hilfe von if
-Abfragen ist es möglich, Teile eines Shell-Skripts nur
unter bestimmten Bedingungen ablaufen zu lassen. Ist die if
-Bedingung wahr,
so wird der anschließende, durch then
gekennzeichnete Code ausgeführt, bis
das Schlüsselwort fi
(ein umgedrehtes if
) die bedingte Anweisung
abschließt.
Die grundsätzliche Syntax lautet also:
if bedingung
then
anweisungen
fi
Optional können nach einem if
-Block mittels elif
eine oder mehrere
zusätzliche Bedingungen formuliert werden, die jedoch nur dann untersucht
werden, wenn die erste if
-Bedingung falsch ist. Schließlich kann auch eine
else
-Bedingung angegeben werden, die genau dann ausgeführt wird, wenn die
vorherige Bedingung (beziehungsweise alle vorherigen Bedingungen bei Verwendung
eines elif
-Blocks) nicht zutreffen.
Insgesamt kann eine Fallunterscheidung beispielsweise folgenden Aufbau haben:
if bedingung1
then
anweisung1
elif bedingung2
then
anweisung2
else
anweisung3
fi
Um die Bedingungen zu formulieren, wird häufig die Shell-Anweisung test
verwendet. Mit dieser lassen sich zum einen Datei-Tests durchführen, zum
anderen auch Zahlenwerte und Zeichenketten miteinander vergleichen.
Um die zu einem Dateinamen gehörende Datei auf eine bestimmte Eigenschaft hin zu überprüfen, lautet die
test
-Syntax wie folgt:test option dateiname
Eine Auswahl an häufig verwendeten Prüfoptionen sind in der Datei-Test-Tabelle aufgelistet; eine vollständige Liste aller Optionen findet in den
test
-Manpages (man test
).
-d |
wahr, wenn Datei existiert und ein Verzeichnis ist |
-e |
wahr, wenn Datei existiert |
-f |
wahr, wenn Datei existiert und regulär ist (kein Verzeichnis, kein Link) |
-h oder -L |
wahr, wenn Datei existiert und ein Symlink ist |
-r |
wahr, wenn Datei existiert und lesbar ist |
-s |
wahr, wenn Datei existiert und nicht leer ist |
-w |
wahr, wenn Datei existiert und schreibbar ist |
-x |
wahr, wenn Datei existiert und ausführbar ist |
Anstelle von test option dateiname
kann auch kürzer [ option dateiname ]
geschrieben werden. In dieser Form kommen Test-Anweisungen sehr häufig bei
if
-Bedingungen vor.
Um ganzzahlige Werte miteinander zu vergleichen, können die Optionen
-eq
,-ne
,-gt
,-lt
,-ge
, und-le
verwendet werden.test zahl1 operator zahl2
Die möglichen Vergleichsoperatoren für Zahlen sind in der Integer-Test-Tabelle aufgelistet.
-eq |
wahr, wenn beide Zahlen gleich sind („equal“) |
-ne |
wahr, wenn beide Zahlen nicht gleich sind („not equal“) |
-gt |
wahr, wenn erste Zahl größer als zweite Zahl ist („greater than“) |
-lt |
wahr, wenn erste Zahl kleiner als zweite Zahl ist („less than“) |
-ge |
wahr, wenn erste Zahl größer oder gleich der zweiten Zahl ist („greater or equal“) |
-le |
wahr, wenn erste Zahl kleiner oder gleich der zweiten Zahl ist („less or equal“) |
Um eine einzelne Zeichenkette zu überprüfen, können die Optionen
-z
(„zero“) oder-n
(„non-zero“) verwendet werden. Mittest -z $mystring
wird beispielsweise getestet, ob die in der Variablenmystring
gespeicherte Zeichenkette die Länge Null hat.Um zwei Zeichenketten miteinander zu vergleichen, können die Operatoren
==
zum Test auf Gleichheit und!=
zum Test auf Ungleichheit verwendet werden. Beispielsweise kann mitif [ $mystring1 == $mystring2 ]
eine Bedingung für die Gleichheit vonmystring1
undmystring2
formuliert werden.
Möchte man mehrere Teilbedingungen zu einer einzigen Bedingung verknüpfen,
können die Optionen -a
(„and“) für eine UND-Bedingung und -o
(„or“) für
eine ODER-Bedingung eingesetzt werden. Wird einer (Teil-)Bedingung das
Negationszeichen !
vorangestellt, so wird der Wahrheitswert des
Bedingungsterms umgekehrt.
Mehrfach-Unterscheidungen – case
Sollen Anweisungen in Abhängigkeit des konkreten Werts einer Variablen oder
einer Test-Bedingung ausgeführt werden, so kann das Schlüsselwort case
genutzt werden. Dieses hat folgende Syntax:
case variable in
muster1) anweisung1 ;;
muster2) anweisung2 ;;
muster3) anweisung3 ;;
*) sonstige-anweisungen ;;
esac
Im obigen Beispiel kann anstelle variable
auch ein Ausdruck stehen, der
eine Zeichenkette als Ergebnis liefert.
Trifft ein Muster auf den Wert der Variablen zu, so wird die dahinter angegebene
Anweisung ausgeführt. Die case
-Struktur wird unmittelbar anschließend
beendet; bei mehreren passenden Mustern werden somit nur die Anweisungen beim
ersten zutreffenden Muster ausgeführt.
Das Muster darf jedes Suchmuster beinhalten,
das auch für Dateinamen erlaubt ist. Zwei oder mehrere einzelne Teilmuster
können dabei mittels |
-Zeichen zu einem Gesamt-Muster verbunden werden.
Das Suchmuster *
trifft auf jeden beliebigen Wert zu. Es kann daher
verwendet werden, um Anweisungen festzulegen, die genau ausgeführt werden, wenn
kein anderer Fall zutrifft. Da nach dem *
-Muster die case
-Struktur mit
Sicherheit beendet wird, darf es erst am Ende der möglichen Fälle aufgelistet
werden. Anschließend wird die case
-Struktur mittels esac
(ein
umgekehrtes case
) beendet.
Schleifen – for
, while
und until
In einer Shell stehen folgende Schleifentypen zur Verfügung:
Mittels einer
for
-Schleife kann eine Liste von Variablen elementweise abgearbeitet werden. Häufig wird als Liste ein Dateimuster verwendet, beispielsweise würdefor pic in *.png
allepng
-Dateien des Verzeichnisses in eine Liste speichern und bei jedem Durchlauf der Schleife die jeweils nächste solche Datei in der Variablenpic
ablegen. Sind alle Elemente der Liste abgearbeitet, wird diefor
-Schleife automatisch beendet.Die Anweisungen, die innerhalb der Schleife abgearbeitet werden sollen, werden durch die Schlüsselwörter
do
unddone
begrenzt. Einefor
-Schleife hat damit insgesamt folgende Form:for varname in var_list do echo "Doing something with $varname ..." done
In Kurzform, insbesondere bei einer einzelnen Schleifenanweisung, kann eine
for
-Schleife auch in eine Zeile geschrieben werden:for varname in var_list ; do echo "Doing something with $varname ..." ; done
Üblicherweise werden
for
-Schleifen zum Durchlaufen einer vorgegebenen Anzahl an Listenelementen verwendet.
Mittels einer
while
-Schleife kann eine beliebe Anzahl an Anweisungen, solange eine bestimmte Bedingung erfüllt ist, beliebig oft wiederholt werden:while [ $count -le 10 ] do echo "Hallo" count=$( expr $count + 1 ) done
Die Bedingung wird vor jedem Schleifendurchlauf geprüft, und sofern diese nicht erfüllt ist, wird die Schleife beendet. Stellt sich die Bedingung schon vor dem ersten Schleifendurchlauf als Falsch heraus, wird die Schleife somit komplett übersprungen.
Die
expr
-Anweisung wertet dabei den gegebenen arithmetischen Ausdruck aus und gibt das Ergebnis als Rückgabewert zurück. Anstelle$(expr $count +1)
kann auch kürzer$(($count + 1))
geschrieben werden. Für komplexere Berechnungen innerhalb eines Shell-Skripts sollte bc verwendet werden.Eine
while
-Schleife kann beispielsweise verwendet werden, um alle dem Skript beim Aufruf übergebenen Parameter auszulesen:while [ -n $1 ] do echo $1 shift done
Hierbei bewirkt die Funktion
shift
, dass die Nummerierung der Parameter$1
bis$9
nach links verschoben wird, aus$3
wird beispielsweise$2
und aus$2
wird$1
. Auf diese Weise können auch mehr als übergebene Parameter der Reihe nach abgearbeitet werden.
Hat man eine Bedingung in der Form
while not
, so kann dafür das Schlüsselwortuntil
verwendet werden. Mituntil
wird ebenfalls eine Schleife eingeleitet, wobei die angegebene Bedingung – wie bei einerwhile
-Schleife – vor jedem Schleifendurchgang geprüft wird.until [ $count -eq 10 ] do echo "Hallo" count=$(expr $count + 1) done
Mit while
und until
werden üblicherweise Endlos-Schleifen definiert,
die dann zu einem bestimmten Zeitpunkt mittels break
abgebrochen werden.
break und continue
Um den gewöhnlichen Schleifenverlauf zu verändern, akzeptiert der
Shell-Interpreter zwei Schlüsselwörter: break
und continue
:
Mittels
break
wird die Schleife komplett abgebrochen.Beispielsweise kann somit eine Endlos-Schleife unterbrochen werden, wenn eine bestimmte Bedingung eintritt:
while true do echo "Doing something.." if given_condition then break done
Mittels
continue
wird der aktuelle Schleifendurchlauf abgebrochen. Die Schleife wird dann mit dem nächsten Schleifendurchlauf fortgesetzt.Die
continue
-Anweisung wird häufig infor
-Schleifen eingesetzt, wenn beispielsweise alle Dateien eines Verzeichnisses abgearbeitet werden und nur in Sonderfällen zur nächsten Datei gegangen werden soll.
Die Anweisung break
kann bei Bedarf auch mit einer Zahl n
aufgerufen
werden, um in einer verschachtelten Schleife nur die innersten Ebenen
der Schleife zu verlassen (beispielsweise bricht break 1
nur die innerste
Schleifenebene ab). Ebenso kann die Anweisung continue
mit einer Zahl n
aufgerufen werden, um insgesamt Schleifendurchläufe zu überspringen.
Definition von Funktionen¶
In Shell-Skripten können, ähnlich wie in Programmiersprachen, Kombinationen von mehreren Anweisungen als Funktionen definiert und somit beliebig oft an verschiedenen Stellen eines Skripts aufgerufen werden.
Die grundlegende Syntax zur Definition eigener Funktionen ist folgende:[5]
funktionsname ()
{
anweisungen
}
Hat man eine Funktion definiert, so kann sie mittels funktionsname
aufgerufen werden; die in der Funktion enthaltenen Anweisungen werden dadurch
zeilenweise ausgeführt. Wird eine Funktion in der Konfigurationsdatei der Shell
definiert, so kann sie von jeder Shell-Sitzung aus wie ein gewöhnliches Programm
aufgerufen werden.
Innerhalb eines Shell-Skripts können wiederum Variablen genutzt werden;
beispielsweise können so beim Aufruf der Funktion zusätzlich angegebene
Argumente (die in den Variablen $1
, $2
, usw. gespeichert werden)
innerhalb der Funktion genutzt werden.
Persönlich nutze ich beispielsweise die im folgenden vorgestellte Funktion, um aus mit Inkscape erstellten Vektor-Graphik–Dateien (SVG) gewöhnliche PNG-Dateien zu erstellen. Die Funktion wird mit dem Dateinamen einer SVG-Datei als Argument aufgerufen, exportiert den kompletten Inhalt als gleichnamige PNG-Datei und komprimiert anschließend die Datei-Größe dieser Datei:
# Funktion zum Exportieren einer SVG-Datei:
INK ()
{
# Export als PNG-Datei mittels inkscape:
inkscape -z -d 150 -D $1 -e $(basename $1 .svg).png
# Komprimieren der PNG-Datei mittels pngnq:
pngnq -n 256 $(basename $1 .svg).png && rm $(basename $1 .svg).png
mv $(basename $1 .svg)-nq8.png $(basename $1 .svg).png
}
Wird die obige Funktion in die ~/.bashrc
beziehungsweise ~/.zshrc
aufgenommen, so kann sie nach einem erneuten Laden dieser Datei mittels
beispielsweise source ~/.zshrc
folgendermaßen genutzt werden:
# Funktion INK auf :
INK dateiname.svg
Als Ergebnis erhält man in diesem Fall eine zusätzliche Datei dateiname.png
im selben Verzeichnis.
Beenden einer Funktion mittels return
Mit return num
kann eine Funktion an jeder beliebigen Stelle beendet werden;
dabei wird num
als Exit-Status an die Shell zurück geliefert. Gibt es in einer Funktion keine
return
-Anweisung, so entspricht der Exit-Status der letzten Anweisung dem
Rückgabewert der Funktion.
Anmerkungen:
[1] | Die Dateinamen von Shell-Skripten sollten keine Zeichen außer Groß- und Kleinbuchstaben, Nummern und dem Unterstrich beinhalten; Umlaute und Sonderzeichen sollten, obwohl sie prinzipiell zulässig sind, vermieden werden. |
[2] | Auf die gleiche Weise kann man zu Beginn einer Skriptdatei auch einen
anderen Interpreter festlegen. Beispielsweise leiten #!/bin/awk -f
ein AWK-Skript oder /usr/bin/python3 ein Python3
-Skript ein. |
[3] | Möchte man in eine Zeile zwei oder mehrere Anweisungen schreiben, so
müssen diese durch ; getrennt werden. (Andernfalls würde die zweite
Anweisung als Argument der ersten Anweisung interpretiert werden.) |
[4] | Wird ein Shell-Skript nicht explizit mittels exit beendet, so
entspricht der Exit-Status dem Rückgabewert der zuletzt ausgeführten
Anweisung. |
[5] | Funktionsdefinitionen können, ebenso wie Variablen, mit unset
funktionsname gelöscht werden; sie können allerdings nicht an
Unterprozesse exportiert werden. |