Jede Anwendung der hier beschriebenen Hinweise und Tips erfolgt ausschließlich auf eigene Gefahr. Keine Haftung für die Richtigkeit der Darstellung. Jegliche Haftung für irgendwelche Schäden aufgrund dieses Textes ist ausgeschlossen.
Shells sind Kommandozeileninterpreter. Bash, die "Bourne Again shell", ist die erweitere Open-Source-Variante der Shell "sh", die Stephen Bourne in den 70er Jahren für das Betriebssystem Unix entwickelt hat.
Unter Linux ist Bash wohl am weitesten verbreitet, auch wenn andere Shells wie "sh", "ksh", "csh" und "tcsh" ebenfalls zur Verfügung stehen.
Bei der Shell-Programmierung erfolgt die Datenverarbeitung anders als in anderen Programmiersprachen in der Regel dadurch, daß die Ausgabe eines Befehls einem oder mehreren anderen Befehlen zur Weiterverarbeitung zugeführt wird.
Man verwendet die Shell-Befehle dann also wie mehrere Werkzeuge eines Werkzeugkastens.
Es gibt aber auch Kontrollstrukturen wie in anderen Programmiersprachen.
Bash-Skripte werden vor allem zur Automatisierung von Betriebssystemvorgängen oder zur Durchführung von Programminstallationen verwendet. Nachteile sind, daß sie nicht allzu schnell ablaufen und außerdem wenig portabel sind. Zudem sind sie für Menschen oft schlecht lesbar.
Als Kommandozeileninterpreter sucht Bash in einer Zeile nach einem Befehl und den dazu gehörenden Optionen.
Die Befehle sind wie bei Basic oder DOS englische Wörter, die jedoch erheblich verkürzt sind, um nicht so viel tippen zu müssen.
Die Linux-Befehle unterscheiden sich erheblich von DOS, auch wenn sie dasselbe tun.
Ein Bash-Befehl wäre z.B.
echo "Hallo Welt"
Im Gegensatz zu DOS beachtet Bash Groß- und Kleinschreibung (= case sensitive).
Die Shell versucht, alle angefangenen Eingaben automatisch zu vervollständigen, wenn man jeweils TAB eingibt.
Mit den Cursortasten kann man vorherige Eingaben wieder zurückholen.
Es gibt drei sogenannte Datenkanäle STDIN, STDOUT und STDERR, die von Programmen verwendet werden können:
STDIN ist der Standardeingabekanal. Er ist standardmäßig mit der Tastatur verbunden. Das bedeutet, wenn ein Programm Daten aus diesem Kanal zu lesen versucht, wartet es auf Tastatureingaben.
Der Standardausgabekanal STDOUT ist dagegen standardmäßig mit dem Bildschirm verbunden. Schreibt ein Programm Daten in diesen Kanal, erscheinen sie also gewöhnlich auf dem Bildschirm. Auf diese Weise arbeitet auch "echo": Es sendet das erhaltene Argument (oben "Hallo Welt") an STDOUT.
Der Kanal STDERR ist für Fehlermeldungen gedacht. Er ist standardmäßig ebenfalls mit dem Bildschirm verbunden.
In Bash kann man die Ausgabe von Befehlen statt nach STDOUT aber auch in eine Datei leiten. Dazu verwendet man die Zeichen ">" und ">>".
So erscheint bei
echo "Hallo Welt" > hallo.txt
nichts auf dem Bildschirm. Stattdessen findet sich die Ausgabe in der Datei "hallo.txt" im aktuellen Verzeichnis.
echo "Hallo Welt" >> hallo.txt
fügt dieser Datei noch einmal "Hallo Welt" an. Mit
cat hallo.txt
kann man sich den Inhalt der Datei "hallo.txt" anschauen.
Etwas Vorsicht ist bei der Umleitung mit ">" geboten: Sollte bereits eine Datei mit dem Namen der Ausgabedatei existieren, so wird sie ohne Warnung mit der Befehlsausgabe überschrieben. Das Problem ist bei ">>" geringer: Hier wird der Datei lediglich etwas angehängt, so daß es gegebenenfalls nachträglich wieder entfernt werden kann.
Ausgaben von Befehlen können aber nicht nur in Dateien geleitet werden, sondern auch in den Eingabekanal anderer Befehle.
Dazu verwendet man das Zeichen "|" (auf der Tastatur: "AltGR + <"). Ein Beispiel:
ls
zeigt die Namen der Dateien im aktuellen Verzeichnis über STDOUT auf dem Bildschirm an. Sind sehr viele Dateien in einem Verzeichnis, scrollt "ls" den Inhalt zu schnell nach oben. Mit
ls | less
kann man sich die Ausgabe von "ls" mit dem Tool "less" bildschirmweise anzeigen lassen.
Die Umleitung der Ausgabe eines Befehls in einen anderen mittels "|" (man nennt dies das Erstellen einer "Pipe") führt also zu einer Weiterverarbeitung der Daten durch den zweiten Befehl.
Die Ausgabeumleitung mit ">", ">>" und "|" gibt es übrigens auch unter DOS.
Man kann mit einem Editor (wie "vim", "emacs", "kate", "gedit", "joe" etc.) eigene Skripte erstellen. Das sind Dateien, die beliebig viele hintereinander auszuführende Konsolenkommandos enthalten (vergleichbar "autoexec.bat" unter DOS; die hier beschriebenen Bash-Skripte sollen aber nicht automatisch beim Systemstart ausgeführt werden).
Die erstellten Skripte kann man dann unter Linux, wenn man die nötigen Benutzerrechte hat und mit
chmod +x skript
das Attribut "ausführbar" für das Skript gesetzt hat, ausführen. Dies geht entweder mit einem erneuten Aufruf der Shell, also mit
bash skript
oder man führt das Skript ohne erneuten Aufruf der Shell aus. Ist das Skript im aktuellen Verzeichnis und ist dieses nicht in "$PATH" enthalten, muß man dazu aber noch angeben, daß man sich auf das aktuelle Verzeichnis bezieht. Dieses wird durch "." symbolisiert.
Zum Aufruf des Skripts im aktuellen Verzeichnis gibt man dann also
./skript
ein.
Man kann die Skripte dann in dem Verzeichnis
/usr/local/bin
ablegen. Da vom System für dieses Verzeichnis bereits standardmäßig die Variable "$PATH" gesetzt ist, lassen sich die Skripte dann von jedem Verzeichnis aus ausführen.
Es ist sinnvoll, in dem Skript anzugeben, daß es mit Bash ausgeführt werden soll. Dazu gibt man in der ersten Zeile des Skripts an:
#!/bin/bash
Diese erste Zeile im Skript nennt man "sh'bang": "sh'" steht für sharp (#), "bang" für das Ausrufezeichen.
Erstellt man also etwa eine solches Skript mit dem Inhalt
#!/bin/bash
echo "Hallo Welt"
nennt es "mine", setzt die Benutzerrechte auf "ausführbar" ("chmod +x mine") und bringt es nach "/usr/local/bin"("cp ./mine /usr/local/bin"), kann man von jedem Verzeichnis aus einfach "mine" eingeben, um das Skript zu starten.
Als Kommandozeileninterpreter sucht Bash in einer Zeile nach einem Befehl und den dazu gehörenden Optionen.
Vor der Ausführung des Befehls werden aber zudem noch bestimmte Sonderzeichen wie etwa "*" in der Zeile
cp * /home/user
ausgewertet. Diese Auswertung, bzw. Expansion erfolgt auch in Programmzeilen in Skripten.
Daher muß man gelegentlich aufpassen, zu was genau die Shell eine Programmzeile vor der jeweiligen Ausführung auswertet.
Die Optionen, bzw. Parameter der Shell-Befehle werden in der Regel durch Leerzeichen voneinander getrennt, z.B. in "ls -l -i -s -a".
Verwendet man Ausdrücke, die zu Befehlsparametern ausgewertet werden sollen, muß man also darauf achten, daß bei der Ausdrucksauswertung keine überschüssigen Leerzeichen entstehen, die sonst als Ende des jeweiligen Parameters gedeutet würden.
Setzt man Strings in einfache Anführungszeichen, z.B.
echo 'Hallo *'
deutet die Shell den Ausdruck trotz Leerzeichen als einheitliche Zeichenkette. Zudem erfolgt keine Expansion der gegebenenfalls enthaltenen Sonderzeichen (hier "*").
Setzt man Strings in doppelte Anführungszeichen, z.B.
echo "Hallo *"
so handelt es sich ebenfalls um eine einheitliche Zeichenkette. Die meisten Sonderzeichen werden nicht expandiert, einige aber doch, insbesondere "$", so daß z.B. der Inhalt einer Variablen "$a" in
a=Welt; echo "Hallo $a"
(zu Variablen siehe sogleich) Teil der Zeichenkette wird.
Die Skalarvariablen in Bash sind grundsätzlich Strings. Man definiert sie nur mit ihrem Variablennamen. Um später auf sie zuzugreifen, muß man ein "$" davorsetzen. Die Shell wertet dann den Variablennamen zum Inhalt der Variablen aus:
#!/bin/bash
a="Hallo Welt !"
echo $a
Diese Variablenexpansion erfolgt in jeder einzelnen Zeile des Skripts.
In der Zeile a="Hallo Welt !"
darf kein Leerzeichen zwischen "a", "=" und dem folgenden Wert stehen. Denn Leerzeichen trennen in Bash Anweisungen (wie z.B. Shell-Befehle) von Parametern (wie Befehlsoptionen (wie z.B. in "ls -l")). Nur wenn das Gleichheitszeichen in Verbindung mit den Variablennamen und -werten ohne Leerzeichen verwendet wird, kann die Shell erkennen, daß es sich um eine Variablenzuweisung handeln soll.
Es wurde bereits dargestellt, daß die Ausgabe von Befehlen mittels "|" anderen Befehlen über deren Dateneingabekanal zur Weiterverarbeitung zugeleitet werden kann ("Pipe").
Doch wie weist man einer Variablen die Ausgabe eines Befehls zu ? Variablen haben ja keine Dateneingabekanäle.
Dazu setzt man Befehle in sog. Backticks (`) (auf der Tastatur: "Shift + Taste rechts neben dem ?") oder zwischen mit "$" versehene runde Klammern, also:
$(Befehl)
Dann wird der Befehl ausgeführt und seine Ausgabe zu einem Ausdruck innerhalb der Befehlszeile ausgewertet. Dieser Ausdruck kann dann einer Variablen zugewiesen werden:
#!/bin/bash
a=$(ls -la)
echo $a
Oft ist es sinnvoll, den ausgewerteten Ausdruck nochmals von Anführungszeichen zu umschließen, um klarzustellen, daß es sich bei diesem trotz enthaltener Leerzeichen um einen einzigen zusammengehörenden Ausdruck und nicht um einzelne Wörter oder Zahlen handelt.
In BASIC kann man Folgendes machen:
10 FOR i=1 TO 10 20 PRINT i 20 NEXT i
Das geht auch in Bash:
#!/bin/bash
for ((i=1; i<=10; i++))
do
echo $i
done
for-Schleifen kann man (fast wie in der Sprache C) nach obigem Muster konstruieren: Zwischen zwei runde Klammmern schreibt man, jeweils durch Semikolon getrennt, drei Dinge:
Sodann schreibt man zwischen die Zeilen "do" und "done", welche Anweisungen während des Schleifendurchlaufs ausgeführt werden sollen.
Die Einrückung der Anweisungen in Anweisungsblöcken (wie z.B. oben um jeweils vier Leerzeichen) ist in Bash (anders als in Python) nicht unbedingt erforderlich, wird aber empfohlen, da dadurch die Lesbarkeit des Codes etwas erhöht wird.
Die Anweisung "break;" innerhalb eines Anweisungsblocks bewirkt den vorzeitigen Abbruch der Schleife. Das Programm wird dann unmittelbar nach der Schleife fortgesetzt. "break 2" bricht dabei zwei ineinander verschachtelte Schleifen gleichzeitig ab.
Die Anweisung "continue;" innerhalb des Anweisungsblocks bewirkt, daß die Schleife vorzeitig den nächsten Durchlauf startet, ohne daß die nach "continue;" folgenden Anweisungen noch ausgeführt werden.
Noch gebraüchlicher sind in Bash for-Schleifen, in denen die Schleifenvariable jeweils eines der nach dem Wort "in" folgenden Argumente repräsentiert:
#!/bin/bash
a="Geh Du alter Esel, hol Fisch."
for i in $a
do
echo $i
done
In der dritten Zeile wird $a zu den Wörtern expandiert, und zwar ohne Anführungszeichen. $i steht dann jeweils für eines dieser Wörter.
Benutzt man in dem Skript oben stattdessen die Zeile
for i in "Geh Du alter Esel, hol Fisch."
so steht $i für den ganzen String, und die Schleife wird nur einmal durchlaufen.
Neben for-Schleifen bietet Bash noch while-Schleifen, mit denen man das Programm für die Schleife von 1 bis 10 ebenfalls schreiben kann:
#!/bin/bash
i=1
while test $i -le 10
do
echo $i
let "i += 1"
done
Die Bedingung für die while-Schleife wird mit dem Befehl "test" erzeugt; Näheres siehe "man test".
Um "i = i + 1" zu erreichen, benutzt man die Zeile oben:
let "i += 1"
Dabei sind besonders die Leerzeichen zu beachten. Sie müssen genau so wie gezeigt geschrieben werden.
Um eine Variable um eins zu erhöhen, kann auch "let i++" verwendet werden.
Es gibt auch noch den Befehl:
i=$(expr $i + 1)
Aber der "let"-Befehl ist wesentlich schneller. Er ist Teil von bash ("builtin").
Will man das Skript mit der while-Schleife in der Shell interaktiv in einer Befehlszeile ausführen, müssen alle Befehle mit ";" abgetrennt werden. Allerdings darf dabei zwischen "do" und dem folgenden Befehl kein ";" stehen:
i=1; while test $i -le 10; do echo $i; let "i += 1"; done
BASIC:
10 LET a=1 20 IF a=1 THEN PRINT "a=1" 30 IF a<>2 THEN PRINT "a ist nicht 2." 40 IF a=2 THEN PRINT "a=2" ELSE PRINT "a ist nicht 2." 50 LET b=2 60 IF a=1 AND b=2 THEN PRINT "a=1, b=2" 70 IF a=1 OR b=2 THEN PRINT "a=1 oder b=2."
Bash:
#!/bin/bash
a=1
if [ $a -eq 1 ];
then
echo "a=1"
fi
if [ $a -ne 2 ];
then
echo "a ist nicht 2."
fi
if [ $a -eq 2 ];
then
echo "a=2."
else
echo "a ist nicht 2."
fi
b=2
if [ $a -eq 1 ] && [ $b -eq 2 ];
then
echo "a=1, b=2."
fi
if [ $a -eq 1 ] || [ $b -eq 2 ];
then
echo "a=1 oder b=2."
fi
Wie man sieht, ist die Form für if-Bedingungen:
if <test-Befehl-mit-Ausdruck>; then <Anweisungen>; else <Andere-Anweisungen>; fi
Der Ausdruck
[ ]ist eine Kurzform für den Befehl "test" (siehe "man test"). Dabei sind die Leerzeichen rechts, bzw. links innerhalb der eckigen Klammern unbedingt erforderlich.
Neben "else" gibt es noch "elif" (für "else if").
BASIC:
10 LET a=0 20 INPUT a 30 PRINT a
Bash:
#!/bin/bash
a=0
read a
echo $a
So wie "echo" eine Zeile nach STDOUT ausgibt (s.o.), liest "read" standardmäßig eine Zeile von STDIN.
Nachdem wir Schleifen, Eingaben und Bedingungen kennengelernt haben, können wir diese Programmiertechniken einmal verbinden.
Bitte finden Sie selbst heraus, was das folgende Skript tut und wie es es tut:
#!/bin/bash
kekse=""
while test -z $kekse || test $kekse != "KEKSE"
do
echo -n "Ich will KEKSE: "
read kekse
done
echo "Mmmm. KEKSE."
Das "||" in der while-Zeile steht für logisches ODER. Die zweite "test"-Prüfung in der while-Zeile wird nur ausgeführt, wenn nicht schon die erste Bedingung ($kekse gleich "") erfüllt ist.
Die erste Überprüfung ist notwendig, denn wenn $kekse gleich "" ist, würde $kekse zu nichts expandiert, so daß "test" für andere Vergleiche als für die Optionen "-z" und "-n", also etwa für "!="-Überprüfungen kein brauchbares Argument erhielte.
Das Skript kann mit Optionen aufgerufen werden, z.B.
./skript -a
Die Optionen wie "-a" werden in besonderen Variablen, "$1", "$2", "$3", usw. gespeichert, die so innerhalb des Skripts verarbeitet werden können. "$@" enthält alle erkannten Optionen in einem String. Die Anzahl der jeweils durch ein Leerzeichen getrennten Optionen kann über "$#" abgefragt werden.
In dem Beispiel oben enthält also "$1" im Skript den Ausdruck "-a".
Die Verarbeitung von Strings ist in Bash weniger komfortabel als in anderen Programmiersprachen.
Die Anzahl der Zeichen einer Variablen $a erhält man mit "${#a}".
"${a:2:3}" liefert einen Substring von 3 Zeichen ab Position 2 in der Variablen $a.
"${a:2}" liefert alle Zeichen von Position 2 bis zum Ende.
a=${a/from/to}
ersetzt das erste Vorkommen von "from" in der Variablen $a zu "to".
a=${a//from/to}
ersetzt jedes Vorkommen von "from" in der Variablen $a zu "to".
Arrays (Feldvariablen) sind in Shell-Skripts traditionell weniger gebräuchlich, werden aber in neueren Versionen von Bash durchaus unterstützt.
Mehrere Array-Elemente können gemeinsam zugewiesen werden, den Array-Stellen können aber auch direkt Werte zugewiesen werden.
Die erste Array-Stelle beginnt bei 0 (wie in den meisten anderen Programmiersprachen auch).
Die Anzahl der Array-Elemente kann ermittelt werden. Arrays können durchlaufen werden:
#!/bin/bash
arr=("Apfel" "Birne" "Pfirsich")
arr[3]="Banane"
arr[4]="Kirsche"
echo
echo "Das Array hat ${#arr[@]} Elemente:"
echo
for ((i=0; i<${#arr[@]}; i++))
do
echo ${arr[$i]}
done
echo
Für komplexere Skripte bietet Bash auch Funktionen. Allgemein sind das Unterprogramme, die Daten als Argumente erhalten und Ergebnisse zurückgeben können. Auf diese Weise können umfangreiche Aufgaben in viele kleine Teilaufgaben zerlegt werden:
#!/bin/bash
selfdefinedfunction ()
{
echo $1
}
selfdefinedfunction "Hallo"
Der Aufruf von "selfdefinedfunction" mit dem Parameter "Hallo" bewirkt, daß innerhalb der Funktion die Variable "$1" diesen Parameter enthält.
Lokale Variablen können innerhalb der Funktion mit dem Befehl "local" definiert werden.
Von der Shell aufgerufene Unterprozesse und eben auch Funktionen können einen Rückgabewert zurückliefern. Auf diesen kann über die Variable "$?" zugegriffen werden. Eigentlich ist das für Programme gedacht, die so z.B. melden, ob sie erfolgreich beendet wurden. Aber auf diese Weise können auch Funktionen in Bash-Skripten Werte zurückgeben:
#!/bin/bash
selfdefinedfunction ()
{
echo $1
return 5
}
selfdefinedfunction "Hallo"
echo $?
Manchmal möchte man nicht, daß z.B. Fehlermeldungen eines Programms auf dem Bildschirm ausgegeben werden.
Dies kann man durch Umleitung des Kanals STDERR verhindern:
echo "gone" &>/dev/null
In Shell-Variablen wird das Neue-Zeile-Zeichen durch Ersetzung von
$'\n'
erzeugt. Eine mehrzeilige Variable definiert man also z.B. durch:
#!/bin/bash
a="Zeile1"$'\n'"Hallo Welt"$'\n'"Zeile3"
echo "$a"
Da die Shell bei for-Schleifen im "Python-Stil" den nächsten Schleifendurchlauf bei einem Leerzeichen beginnt, kann man mit folgendem Skript aber nicht jede Zeile der Variablen a durchlaufen:
#!/bin/bash
a="Zeile1"$'\n'"Hallo Welt"$'\n'"Zeile3"
for i in $a
do
echo $i
done
Denn hier wird die Schleife ungewollt auch zwischen "Hallo" und "Welt" durchlaufen, so daß "Hallo" und "Welt" in zwei verschiedenen Zeilen ausgegeben werden.
Es geht auch nicht mit Anführungszeichen wie in folgendem Skript:
#!/bin/bash
a="Zeile1"$'\n'"Hallo Welt"$'\n'"Zeile3"
for i in "$a"
do
echo "$i"
done
echo "$i"
Denn hier wird $i sofort der ganze Inhalt der Variablen $a zugewiesen und nicht nur die jeweilige Zeile von $a.
Dies zeigt die letzte Ausgabe von $i nach der Schleife. Diese wurde insgesamt nur einmal durchlaufen.
Um jede Zeile von $a einzeln zu verarbeiten, benötigt man vielmehr folgende Konstruktion:
#!/bin/bash
a="Zeile1"$'\n'"Hallo Welt"$'\n'"Zeile3"
echo "$a" | while read i
do
echo $i
done
Dabei wird der Inhalt der Variablen $a über eine Pipe zeilenweise in eine while-Schleife geleitet, in der mit Hilfe von read die einzelnen Zeilen solange der Variablen $i zugewiesen werden, bis $a keine Zeilen mehr liefert.
Im Grundsatz ist Bash mehr auf den Umgang mit ganzen Dateien und Verzeichnissen als auf die Verarbeitung von Daten in Dateien ausgelegt.
Um bestimmte Zeilen aus Textdateien (oder Befehlsausgaben) herauszusuchen, kann man den Befehl "grep" verwenden.
Ruft man "grep" mit einem Suchwort und einem Dateinamen auf, so wird die angegebene Datei nach dem Suchstring durchsucht, und die gefundenen Zeilen werden ausgegeben. Der Befehl
grep -i wort datei.txt
durchsucht also die Datei "datei.txt" nach Zeilen, die den Begriff "wort" enthalten (wobei im Beispiel wegen der "grep"-Option "-i" Groß-/Kleinschreibung ignoriert wird) und gibt die gefundenen Zeilen aus.
"grep" wird auch oft in einer Pipe verwendet wie in "cat datei.txt | grep wort" oder "find | grep html".
Hat man mit "grep" bestimmte Zeilen gefunden, möchte man diese oft zerteilen, weil einen nur bestimmte Teile innerhalb dieser Zeile interessieren. Hierzu kann man "awk" verwenden:
Angenommen, eine Textdatei "datei.txt" enthält eine Zeile mit dem Inhalt:
Anton;Berta;Cäsar;Dora;Emil
Dann gibt
grep Berta datei.txt
diese Zeile aus. Um die Zeile zu zerteilen, muß "awk" einen Input erhalten, gesagt bekommen, bei welchem Zeichen die Trennung vorgenommen werden soll und was es mit dem Ergebnis tun soll. Folgender Befehl gibt in dem Beispiel das Teilstück "Anton" aus:
grep Berta datei.txt | awk -F ";" '{print $1}'
Das Trennzeichen für "awk" wird also mit der Option "-F" angegeben und die speziellen awk-Bearbeitungsbefehle für die Textteile (z.B. "print") in geschweifte Klammern gesetzt, die in einfachen Anführungszeichen eingeschlossen werden.
Änderungen in Textdateien kann man theoretisch mit dem Stream-Editor "sed" ("man sed") vornehmen.
Das alles wird aber ziemlich unkomfortabel. Daher setzt man für solche Operationen stattdessen häufig die Programmiersprache Perl ein.
Einzeilige Perl-Skripte lassen sich auch von Bash-Skripten aus aufrufen. So kann mit
perl -e 'print "Hello World\n";'
Text über Perl ausgegeben werden. Typischerweise wird aber in Bash-Skripten die Ausgabe eines Shell-Befehls in einen Perl-Einzeiler geleitet:
echo "Hello World" | perl -e 'while(<>){$_ =~ s/World/Planet/g; print $_;}'
In dem Beispiel wird die Ausgabe von "echo" durch Perl verändert und dann wiederum ausgegeben.
Die Perl-Konstruktion "while(<>){}" liest dabei solange Zeilen von STDIN, bis dort keine mehr ankommen.
Das "awk"-Beispiel oben mit "Anton" und "Berta" sähe mit Perl dann z.B. so aus:
grep Berta datei.txt | perl -e 'while(<>){@a = split(";"); print $a[0]."\n"}'