Bash-Skripting-Guide für Anfänger
Dieser Artikel wurde für die folgenden Ubuntu-Versionen getestet:
Dieser Artikel ist größtenteils für alle Ubuntu-Versionen gültig.
Zum Verständnis dieses Artikels sind folgende Seiten hilfreich:
Dieser Artikel soll Interessierten die grundlegenden Möglichkeiten des Bash-Skriptens nahebringen. So werden hier kurz die wichtigsten Konstrukte angesprochen und diese mit einigen praktischen Beispielen etwas vertieft.
Einsatzgebiete von Bash-Skripten¶
Bashskripte sind geeignet um Programme, gemäß der eigenen Bedürfnisse, miteinander zu kombinieren und sie spielen eine zentrale Rolle im Alltag der Systemadministration. Hier im Wiki findet man bereits einige Beispiele, welche die breite Anwendbarkeit von Bashskripten ein wenig illustrieren.
Vorbereitung¶
Editor¶
Standardmäßig sind in den verschiedenen Ubuntuversionen folgende Editoren installiert:
Lubuntu (LXQt) : FeatherPad
Ubuntu MATE (MATE) : Pluma
Alle genannten Programme beherrschen die farbliche Hervorhebung von Befehlsstrukturen (Syntaxhervorhebung), was Übersichtlichkeit sowie Fehlervermeidung und -suche erheblich verbessert. Sollte man mit dem vorinstallierten Editor unzufrieden sein, gibt es aber viele mindestens ebenso gute Alternativen, die teilweise auf spezielle Aufgaben hin gestaltet sind.
Für die Kommandozeile empfehlenswert sind:
Name des Skriptes¶
Es ist sinnvoll, dass der Name des Skriptes dessen Funktion andeutet. Dabei wäre zu beachten, dass man keine Sonderzeichen verwendet, und es sollte nicht schon einen gängigen Systembefehl mit diesem Namen geben. Man sollte sein Skript z.B. nicht cp nennen, da es diesen Befehl schon gibt. Das Ganze lässt sich vermeiden, indem man einfach testet, ob es schon ein Programm mit dem gewünschten Skriptnamen gibt, und das geht folgendermaßen:
type mein_Skript.sh
Erhält man hier ein Resultat, ist der Name bereits vorhanden, und man sollte sich einen anderen ausdenken.
Ausführbar machen und aufrufen¶
Damit ein Skript ohne Angabe des Interpreters ausgeführt werden kann, muss es zuerst ausführbar gemacht werden. Dies geschieht z.B. in der Konsole durch folgenden Befehl.
chmod +x /pfad/zu/mein_Skript.sh
Mehr Informationen dazu findet man auch in Rechte.
Ist das Skript nun ausführbar gemacht, lässt es sich durch Angabe von Pfad und Namen starten[3]:
/pfad/zu/mein_Skript.sh
Einfacher geht dies, wenn man sich bereits im Verzeichnis befindet in welchem auch das Skript ist:
./mein_Skript.sh
Dabei sollte nicht vergessen werden, dass sich Skripte auch ausführen lassen, wenn sie selbst nicht ausführbar gemacht sind. Ein Aufruf des Interpreters mit dem Skriptnamen als Argument führt den Skriptinhalt ebenfalls aus:
bash mein_Skript.sh
Skript verfügbar machen¶
Man kann das Skript, nachdem es ausführbar gemacht wurde, in einen Ordner (z.B. ~/bin/) verlinken oder verschieben, welchen man der Umgebungsvariablen $PATH bekannt macht. Dies ist vor allem dann sinnvoll, wenn ein fertiges Skript von überall her einfach durch die Eingabe des Skriptnamens aufrufbar sein soll. In Ubuntu sorgt ein Mechanismus in der Datei ~/.profile dafür, dass ~/bin/ automatisch an den Anfang von $PATH gesetzt wird, sofern dieses Verzeichnis existiert.
Es sollte (insbesondere bei von mehreren Personen benutzten Rechnern und Servern) darauf geachtet werden, dass Skripte, die in globalen Verzeichnissen wie /usr/local/bin/ abgelegt werden, dem Benutzer root gehören und nur dieser in die Datei schreiben darf, ansonsten öffnet man eine Sicherheitslücke.
Auch Skripte in ~/bin/ können ein Sicherheitsproblem sein, wenn sie den gleichen Namen wie Systemprogramme haben. So könnte ein Angreifer mit einfachen Nutzerrechten beispielsweise ein Skript in diesen Ordner legen, das einfach sudo heißt und damit das Passwort des Nutzers abfangen.
Konkret sind folgende Befehle hilfreich um ein Skript allen Benutzern des Rechners sicher zur Verfügung zu stellen:
ls -l /usr/local/bin sudo chown root /usr/local/bin/mein_Skript.sh sudo chgrp root /usr/local/bin/mein_Skript.sh sudo chmod 755 /usr/local/bin/mein_Skript.sh
Ist das Skript wie oben beschrieben verfügbar gemacht worden, dann kann es einfacher aufgerufen werden mit folgendem Befehl (Der Punkt plus Schrägstrich entfällt):
mein_Skript.sh
Zu den Beispielen¶
Es gibt zwei verschiedene Typen von Beispielen in diesem Artikel.
Zum einen kann man interaktive Beispiele finden, welche dann folgendermaßen aussehen.
Befehl...
Ausgabe...
Dies bedeutet nichts anderes, als dass man ein solches Beispiel einfach durch Eintippen in eine Konsole, Zeile für Zeile, nachvollziehen kann.
Der zweite Typ ist der eines Skriptes. Diese Beispiele beginnen immer mit einer Shebang-Zeile, welche gleich noch erklärt wird, und sind zudem in einer Box mit Syntax-Highlighting gesetzt, damit man die Konstrukte besser erkennen kann. Einen solchen Text soll man zum Ausprobieren in eine Skriptdatei kopieren.
Das erste Skript¶
Die Shebang-Zeile¶
Ein Skript wird prinzipiell immer von einem ausführbaren Programm, seinem Interpreter, gelesen und von ihm Befehl für Befehl abgearbeitet. Das Skript selbst erzeugt keinen Prozess, sondern nur der ausführende Interpreter – zum Beispiel die Bash. Zur Ausführung eines Skriptes ist also der passende Interpreter zu starten und ihm muss mitgeteilt werden, welches Skript er abarbeiten soll. Ein solcher Aufruf hat diese Form:
bash mein_Skript.sh
Natürlich ist es möglich, bei jedem Aufruf seines Skriptes die ausführende Shell erneut anzugeben, doch diesen Aufwand möchte man oft nicht treiben. Üblicherweise gibt man im Skript die zu verwendende Shell in der ersten Zeile an. Eine solche erste Zeile nennt man Shebang-Zeile oder auch oft verkürzt nur "Shebang".
Eine Shebang-Zeile enthält drei Teile und hat diese allgemeine Struktur:
1 | #!INTERPRETER OPTION …
|
Die Zeile muss mit den beiden Zeichen
#!
beginnen. Diese Zeichenkombination ist der eigentliche Shebang und muss am Anfang der Datei stehen, d.h. davor ist nichts zulässig, auch keine Leerzeichen oder Leerzeilen oder Kommentarzeilen. Auch ein BOM (Byte Order Mark) zur Identifizierung der Zeichenkodierung ist nicht erlaubt. Nach dem Shebang darf aber die AngabeINTERPRETER
optional durch Leerzeichen abgetrennt werden.Der Platzhalter INTERPRETER ist durch das Programm zu ersetzen, welches dieses Skript abarbeiten soll. Da das Skript in der Syntax einer bestimmten Shell verfasst wird, ist hier das passende Programm und dieses mit seinem vollständigen absoluten Pfad anzugeben, da die Umgebungsvariable
PATH
an dieser Stelle nicht beachtet wird.OPTION …
: Diese Angabe ist optional und kann auch fehlen. Gemeint sind Optionen für das ProgrammINTERPRETER
, die seine Arbeitsweise modifizieren.
Im Rahmen dieses Artikels sind dies Beispiele für gültige Shebang-Zeilen:
1
#!/bin/bash
Man legt also für die Ausführung des Skriptes die Shell Bash fest.
1
#! /bin/bash
Das Leerzeichen dient nur nur Verbesserung der Lesbarkeit.
1
#! /bin/bash -e
Die Option
-e
weist Bash an, bei einem Fehler die weitere Abarbeitung des Skripts zu beenden. Ohne diese Option würde nach einem Fehler das Skript weiter ausgeführt.
Die Raute zu Beginn der Zeile identifiziert für die Bash diese Zeile als Kommentar; somit ignoriert die ausführende Shell selbst die Shebang-Zeile. Der Kernel hingegen erkennt beim Start der Datei durch diese Raute gefolgt vom Ausrufezeichen, dass dieses Skript von einem bestimmten Interpreter ausgeführt werden soll.
Hat das derart vorbereitete Skript Ausführungsrechte und wird es dann über
./mein_Skript.sh
aufgerufen, dann ist dieser Aufruf äquivalent hierzu:
/bin/bash ./mein_Skript.sh
Benutzt man eine andere Shell, so vermerkt man das in der Shebang-Zeile entsprechend, beispielsweise: #!/bin/zsh
. Dieser Artikel bezieht sich aber nur auf die Bash, weshalb /bin/bash verwendet wird. Damit wird deutlich die Aussage getroffen: „Dies ist ein Bash-Skript.“
In vielen anderen Anleitungen findet sich jedoch #!/bin/sh
. Was hat es damit auf sich? Die Bash ist eine komplexere Shell, die nicht auf allen unixartigen Systemen installiert sein muss im Gegensatz hingegen zur laut POSIX-Standard verpflichtend erforderlichen Shell /bin/sh. Hinsichtlich der sprachlichen Ausdrucksmittel stimmen die von POSIX geforderte sh und die Bash aber nicht überein, denn die Bash versteht mehr Konstrukte als für sh verpflichtend. Mit der Angabe #!/bin/sh
wird also eine andere Aussage gemacht: „Dieses Skript entspricht dem POSIX-Standard.“
Grundsätzlich gilt: Man muss einen Interpreter angeben, der alle im Skript benutzten Funktionen beherrscht. Es gilt nicht allgemein, „Shellskripte können /bin/sh verwenden“! Man muss sich stattdessen darüber im Klaren sein, dass man einen zur gewählten Syntax passenden Interpreter wählen muss. Eine falsche Deklaration in der Shebang-Zeile ist ein häufiger Fehler.
Ein Skript, welches mit #!/bin/sh
als POSIX-Shell deklariert wurde, kann sich bei einer erzwungenen Abarbeitung durch die Bash anders oder sogar fehlerhaft verhalten.
Hinweis:
Unter Ubuntu ist /bin/sh ein symbolischer Link auf die Dash. Welche Konsequenzen das hat, wird unter anderem im englischen Ubuntu-Wiki Eintrag zur DASH 🇬🇧 erklärt.
Kommentare einfügen¶
Kommentare sind wichtig, und man sollte regen Gebrauch davon machen. An schwierigen Passagen sollte man kleine Notizen hinterlegen, um festzuhalten, was man sich dabei gedacht hat und wie das Konstrukt funktioniert. So erleichtert man sich selbst, z.B. nach längeren Pausen, oder aber auch anderen, welche das Skript bearbeiten möchten, das Verständnis.
Kommentare beginnen mit dem Hash-Zeichen (#). Dies kann überall in einer Zeile stehen; außer innerhalb von Quotes. Die Bash ignoriert alles, was dahinter steht. Allgemein sieht das dann so aus:
# Dies ist ein Kommentar kein Kommentar # Dies ist ein Kommentar. echo "auch # kein Kommentar innerhalb" # , wohl aber außerhalb ...
Variablen – Teil 1¶
Variablen sind symbolische Namen für Werte und verleihen einem Skript große Flexibilität. Sie erlauben es, einen Wert an nur einer Stelle zu ändern und den Wert überhaupt zu ändern.
Variablen belegen¶
Variablen werden folgendermaßen belegt:
message=hallo
In diesem Beispiel wird der Variablen mit dem Namen message
der Wert hallo
zugewiesen. Im Gegensatz zu vielen anderen Programmiersprachen darf man bei der Shell keine Leerzeichen vor und nach dem Zuweisungszeichen =
verwenden!
Wenn die Zuweisung Leerzeichen oder andere Sonderzeichen enthält, sind Anführungszeichen notwendig:
message="hallo Welt"
oder das maskieren mittels Backslash:
message=hello\ world
Variablen lesen¶
Variablen werden mit echo ausgegeben. Dabei muss man ein Dollarzeichen $
vor den Namen der Variablen setzen. Für die oben mit der Zuweisung hallo
versehene Variable message
würde sich also Folgendes ergeben:
echo "$message"
hallo Welt
Variablen löschen¶
Man kann Variablen, die nicht mehr gebraucht werden, einfach mit dem Befehl unset
wieder löschen. Dies wird am vorherigen Beispiel mit der Variable message
weiterverfolgt.
echo "$message"
hallo Welt
unset message echo "$message"
Variablen abgrenzen¶
Gelegentlich kann es der Fall sein, dass man Variablen innerhalb einer Zeichenkette verwenden möchte. Dabei kommt es zu Schwierigkeiten, da die Konsole dann die Variable nicht mehr von dem sie umgebenden Zeichenkette unterscheiden kann. Um dieses Problem zu umgehen, verwendet man eine Schreibweise mit geschweiften Klammern. Ein Beispiel zeigt das Problem.
echo "$message"
hallo
echo "$messagelolo"
Hier wird nach dem Anhängen einer beliebigen Zeichenkette an die Variable $message
eine leere Variable ausgegeben. Dies liegt daran, dass die Bash versucht, die Variable mit dem Namen $messagelolo
auszugeben, welche nicht belegt und daher eben leer ist. Grenzt man die Variable jedoch durch die Schreibweise mit geschweiften Klammern ab, bekommt man folgendes Ergebnis.
echo "${message}lolo"
hallololo
Wie man sehen kann, wird hier die angehängte Zeichenkette lolo
an die Variable angehängt, ohne sich mit der Zeichenkette der Variablen zu vermischen, und man erhält das gewünschte Ergebnis.
Quoting¶
Erklärung des Quotings im Video Axel Beckert (Ubucon 2010) 🇩🇪
Befehle¶
Einige Sonderzeichen, Zeilenumbrüche und Leerzeichen haben in Shellskripten eine besondere Bedeutung. Bei einem einfachen Beispiel wird dies vielleicht noch nicht deutlich:
echo Hallo, Welt
Hallo, Welt
Doch spätestens, wenn dieser Befehl mit einigen Leerzeichen „geschmückt“ wird, zeigt sich sonderbares Verhalten:
echo Hallo, Welt
Hallo, Welt
Die zusätzlichen Leerzeichen gehen in der Ausgabe also verloren. Möchte man sie erhalten, muss die auszugebende Zeichenkette in Anführungszeichen gesetzt werden:
echo "Hallo, Welt"
Hallo, Welt
Die Anführungszeichen (engl. „Quotes“) haben hier folgende Bedeutung: Sie entbinden die Leerzeichen zwischen den beiden Worten von ihrer Funktion als Worttrenner. Dieser Vorgang, also das Entbinden eines bestimmten Zeichens von einer Sonderfunktion, wird auch Maskierung genannt.
Ein oder mehrere aufeinander folgende Leerzeichen haben im Normalfall die Funktion, Argumente für Befehle voneinander abzugrenzen. In der Vorverarbeitungsphase des Befehls würden ohne Anführungszeichen dadurch die beiden Zeichenketten „Hallo,“ und „Welt“ zu zwei eigenständigen Argumenten für den echo
-Befehl werden. Bei anderen Befehlen ist dies ein ganz natürliches und gewünschtes Verhalten:
ls -al /usr /tmp
Hier sorgen die Leerzeichen dafür, dass ls
die drei Argumente -al
, /usr
und /tmp
erhält. Die Shell übernimmt also die Aufgabe, derlei Zeichenkettenverarbeitung an zentraler Stelle zu erledigen. Dadurch braucht ls
dies nicht selbst zu tun – andere Programme wie cp
, mv
oder rm
müssten es dann nämlich auch selbst vornehmen. Das heißt auch, dass Programme keine Dateinamensmuster zu sehen bekommen:
ls -al *.ogg
Die ausführende Shell löst das Muster *.ogg
auf und ls
sieht nur noch die resultierende Liste an Dateinamen. Analog wird beispielsweise mit Variablennamen oder Umleitungen verfahren.
Derartige Vorverarbeitungen von der Shell wie die Auflösung von Mustern oder die Auftrennung in einzelne Worte werden durch Anführungszeichen verhindert. Das heißt, man könnte bei „Hallo, Welt“ genauso gut Folgendes schreiben:
echo Hallo," "Welt
Dieses Beispiel soll noch einmal verdeutlichen, dass Anführungszeichen nicht – wie in anderen Sprachen üblich – zur Kennzeichnung von Zeichenketten verwendet werden. Stattdessen heben sie die Sonderbedeutung von bestimmten Zeichen auf.
Möchte man also zum Beispiel den Stern von seiner Bedeutung „passe auf alle Zeichenketten“ (was im folgenden Beispiel auf „alle nicht-versteckten Dateien im aktuellen Verzeichnis“ hinausläuft) entbinden, dann muss man Anführungszeichen um ihn herum setzen. Man vergleiche:
echo *
bin include lib local man sbin share src var
Und:
echo "*"
*
Auch Zeilenumbrüche können wörtlich übernommen werden, wenn sie innerhalb von Anführungszeichen stehen – sie verlieren also ihre Bedeutung als „Ende des Befehls“:
echo "Dies ist eine > mehrzeilige Ausgabe. > Man hält es nicht für möglich."
Dies ist eine mehrzeilige Ausgabe. Man hält es nicht für möglich.
Variablen¶
Bisher wurden lediglich Befehle betrachtet. Bei der Verwendung von Variablen findet jedoch ein vergleichbarer Prozess statt. Dies gilt einerseits für die Variablenzuweisung und andererseits auch für das Auslesen von Variablen:
1 2 | foo="Hallo, Welt" echo "$foo" |
Ohne Anführungszeichen bei echo
würden wieder nur die Leerzeichen verlorengehen:
echo $foo
Hallo, Welt
Bei der Zuweisung jedoch handelt es sich um einen Sonderfall. Ohne Maskierung erhält man einen Fehler:
foo=Hallo, Welt
-bash: Welt: command not found
Dies passiert deshalb, weil Variablenzuweisungen auch nur für einen einzelnen Befehl gelten können, nämlich sofern sie unmittelbar vor ihm aufgeschrieben werden. Genau das ist hier der Fall. Die Zuweisung ist foo=Hallo,
und der Befehl wäre dann Welt
. Da es ein solches Programm nicht gibt, erhält man einen Fehler.
Der Vollständigkeit zuliebe sei hier ein „echtes“ Beispiel erwähnt, das korrekten Gebrauch von dieser Funktionalität macht. Mit date
kann der aktuelle Wochentag ausgegeben werden:
date +%A
Formatangaben beim Befehl date
muss ein + vorangestellt werden, %A
steht für den ausgeschriebenen Wochentag. Weitere Informationen findet man bei date.
Montag
Auf einem deutschen System ist diese Ausgabe wie erwartet auf Deutsch. Man kann nun die Variable $LC_ALL
vorübergehend nur für den Aufruf von date
umbelegen, sodass eine andere locale (Lokalisation: Sprache, Datumsformat, ...) benutzt wird. Hier wird die besondere „C“-Locale verwendet, welche eine englische Ausgabe bewirkt:
LC_ALL=C date +%A
Monday
Doppelte Anführungszeichen reichen jedoch nicht in allen Situationen aus, denn sie heben nicht die Bedeutung aller Sonderzeichen auf. Wie bereits am letzten Beispiel ersichtlich, behält das Dollarzeichen seine Sonderbedeutung. Was kann man nun tun, wenn man tatsächlich die Zeichenkette „$foo“ ausgeben möchte?
Hierzu gibt es einfache Anführungszeichen. Sie entbinden alle Zeichen von ihren Sonderfunktionen, bis auf das einfache Anführungszeichen selbst.
echo '$foo'
$foo
Hinweis:
Variablen, die mit Dateinamen belegt sein können, sollten in doppelten Anführungszeichen stehen. In Dateinamen sind alle Zeichen erlaubt außer der binären Null (\0) und dem Pfadtrenner '/', insbesondere also auch solche, die von der Shell ausgewertet werden würden wie '*' und '?'. Dies kann unerwünschte Folgen haben.
Insbesondere gilt dies natürlich für Dateinamen, die Leerzeichen enthalten, die leicht als Ende des Dateinamens missverstanden werden.
Maskierung mit Backslashes¶
Eine Alternative zum Setzen von Anführungszeichen stellt der Rückwärtsstrich ("backslash") dar. Das ihm folgende Zeichen wird wörtlich übernommen:
echo \$foo
$foo
Backslashes werden bei längeren Zeichenketten oder Befehlen jedoch schnell unübersichtlich. Deshalb sind in den meisten Fällen Anführungszeichen die bessere Wahl. Ein Backslash kann allerdings genutzt werden, um (doppelte) Anführungszeichen zu schachteln:
echo "Er sprach: \"Hallo, Welt\""
Er sprach: "Hallo, Welt"
Aber Vorsicht: Einfache Anführungszeichen lassen sich nicht schachteln. Dies liegt daran, dass sie, wie bereits erwähnt, alle Zeichen außer sich selbst wörtlich übernehmen. Folglich verliert auch der Backslash seine Funktion. Im folgenden Beispiel haben also die einfachen Anführungszeichen Vorrang gegenüber dem Backslash – sie heben seine Wirkung auf und nicht er ihre.
echo 'Dies ist ein Backslash: \'
Dies ist ein Backslash: \
Möchte man ein einfaches Anführungszeichen in einer solchen Situation ausgeben, so muss man zuerst den von ihnen umschlossenen Bereich beenden und danach einen neuen Bereich beginnen, der von doppelten Anführungszeichen umschlossen ist:
echo 'So sprach'"'"'s und ging ohne einen Backslash (\) weiter.'
So sprach's und ging ohne einen Backslash (\) weiter.
Noch einmal im Detail:
echo 'So sprach'"'"'s und ging ohne einen Backslash (\) weiter.' └────┬────┘└┬┘└─────────────────────┬─────────────────────┘ │ │ │ │ │ └ Dritter Bereich: Wieder von ' │ │ umschlossen, der Backlash │ │ verliert seine Sonderbedeutung. │ │ │ └ Zweiter Bereich: Von " umschlossen, enthält ein │ einzelnes '. │ └ Erster Bereich: Von ' umschlossen.
Einfacher ist natürlich der Verzicht auf die überflüssigen Anführungsstriche:
echo So sprach"'"s und ging ohne einen Backslash '(\)' weiter.
Alternative Wege mit Quoting:
echo 'So sprach'\''s und ging ohne einen Backslash (\) weiter.' echo "So sprach's und ging ohne einen Backslash (\) weiter."
Variablen – Teil 2¶
Ausgaben in eine Variable schreiben¶
Durch eine Befehlssubstitution ist es möglich, die Textausgaben eines beliebigen Befehls in eine Variable zu schreiben. Ein Beispiel soll hier zeigen, wie man die Liste der Player im System anzeigt.
1 2 3 4 5 6 7 8 9 | #!/bin/bash #Ausgaben in Variable schreiben #Variablendefinition suchwort=player liste=$(apropos $suchwort) echo " Player-Liste:" echo "$liste" |
Wie man sieht, wird alles zwischen $(
und )
stehende als Befehl ausgeführt und die Ausgabe in die Variable $liste
gespeichert. Danach werden die so gewonnenen Daten im Falle dieses Beispiels auf einmal durch echo
ausgegeben. Sie lassen sich so aber auch sehr gut über eine Schleife abarbeiten, dazu aber später mehr.
Hinweis:
Es gibt eine alternative Schreibweise zu diesem Operator, die Backticks `: NAME=`whoami`
. Der Einsatz empfiehlt sich aber nicht, da sie sich nicht ohne kompliziertes Maskieren schachteln lassen und, je nach Schriftart, schlecht vom Apostroph zu unterscheiden sind.
Einfache und doppelte Gänsefüßchen unterscheiden sich beim Einschließen von Variablen darin, dass bei "..."
Variablen expandiert werden, bei '...'
nicht.
Achtung!
Es ist verführerisch, die Ausgabe des Befehls ls
im Skript zu verarbeiten. Das wäre aber gleich in mehrerer Hinsicht gefährlich:
Leerzeichen und Zeilenwechsel in Dateinamen verursachen oft Probleme, da sie als Ende des Dateinamens interpretiert werden oder weil man die Eingabe zeilenweise verarbeitet und so den Dateinamen durchschneidet.
Abhängig vom locale und einigen anderen Einstellungen ersetzt
ls
manche Zeichen durch ein oder mehrere "?". Dies ist aber bei bash der Platzhalter für "jedes Zeichen" und führt leicht zu Mehrdeutigkeiten, ebenso wie der '*', sofern er im Dateinamen vorkommt.
Entsprechend wird man auch die Ausgabe von find
nicht parsen, sondern die entsprechenden Befehle mit der Option -exec
in einem Unterskript ausführen. Beispiele dazu findet man im Artikel find.
Dateien und Verzeichnisse lassen sich gefahrlos mit den bash-eigenen Funktionen abfragen (basename und dirname sind eigenständige Befehle):
1 2 3 4 5 6 7 8 9 | #!/bin/bash for file in ./*/*.png ; do echo "Diese Datei: $file" fname=$(basename "$file") echo "hat den Namen: $fname" fdir=$(dirname "$file") echo "und steht im Verzeichnis: $fdir" done |
Abschneiden von Mustern¶
Eine gewöhnungsbedürftige, aber doch sehr nette Funktion ist das Herausschneiden bestimmter Muster aus der Zeichenkette einer Variablen.
Schnitt-Funktionen | |
Funktion | Erklärung |
${variable%muster} | Entfernt rechts das kleinste passende Stück. |
${variable%%muster} | Entfernt rechts das größte passende Stück |
${variable#muster} | Entfernt links das kleinste passende Stück. |
${variable##muster} | Entfernt links das größte passende Stück |
An einem kleinen Beispiel soll nun eine Anwendungsmöglichkeit erläutert werden.
pfadname="/var/www/index.html" echo $pfadname
/var/www/index.html
echo "Pfad: ${pfadname%/*}"
Pfad: /var/www
echo "Dateiname: ${pfadname##*/}"
Dateiname: index.html
Hier sieht man, wie leicht man an Teile einer Variablen herankommt, ohne die ursprüngliche Variable zu verändern. Ergänzend sei hier noch gesagt, dass ein Metazeichen (*) zum Einsatz gekommen ist, welches für eine beliebige Zeichenkette an dieser Stelle steht. Dies ist natürlich nicht die einzige Möglichkeit. Hier kann jedes Zeichen oder beliebige Folge von Metazeichen zum Einsatz kommen.
Umgebungsvariablen¶
Es gibt diverse von der Shell selbst verwaltete Variablen. Diese Umgebungsvariablen lassen sich gut für verschiedene Aufgaben in Skripten verwenden, wie z.B. einer Fehlerkontrolle von Programmen, indem man den Rückgabewert ("exit status") prüft. In der folgenden Liste werden nur ein paar dieser Variablen aufgeführt, für weitere Informationen ist bitte der Artikel Umgebungsvariablen zu bemühen.
Variable | Erklärung |
HOME | Enthält das Homeverzeichnis des Benutzers. |
PATH | Suchpfad für Kommandos. |
PWD | Enthält das Aktuelle Verzeichnis. |
? | Enthält den Exitstatus des letzten Kommandos |
Zeichenketten bearbeiten¶
Eine in einer Variablen gespeicherten Zeichenkette (und nur eine solche) kann man vor der weiteren Verwendung noch modifizieren. Zusätzlich zu den oben gezeigten Möglichkeiten zum Abschneiden von Mustern kann man auch Zeichenketten mit der normalen Zeichenkettenbearbeitung einer Posix-Shell bearbeiten.
Der Ausdruck ${x/a/b}
ersetzt im Wert der Variablen x
das erste a
(Suchbegriff) durch ein b
(Ersatzbegriff). Wenn man den ersten Schrägstrich verdoppelt, dann werden alle Vorkommen des Suchbegriffs durch den Ersatzbegriff ersetzt. Für Such- und Ersatzbegriff können Globs und Variablenwerte verwendet werden. Das Beispiel macht also dasselbe wie $( echo $x | sed s/a/b/g )
, aber mit den Sprachmitteln der Posix-Shell.
Beispiele:
x=aaaaaaaaaaaaaaaaaaaarrrrrrrrrrrrg echo $x ${x/a/b} ${x//a/b}
aaaaaaaaaaaaaaaaaaaarrrrrrrrrrrrg baaaaaaaaaaaaaaaaaaarrrrrrrrrrrrg bbbbbbbbbbbbbbbbbbbbrrrrrrrrrrrrg
Natürlich kann man auch längere Such-/Ersatzbegriffe verwenden und der Ersatzbegriff darf auch leer sein, was zur Löschung des Suchbegriffs führt:
x='Das UbuntuUsers.de-Wiki ist toll.' echo ${x/./ und wird immer besser.}
Das UbuntuUsers.de-Wiki ist toll und wird immer besser.
Der in der Variablen gespeicherte Wert wird bei diesen Operationen nicht verändert.
Rechnen mit der Shell¶
Die Shell kann durch die interne Funktion $(( ... )) auch Integer-Berechnungen ausführen:
pingu@ubuntu:~$ echo $((100 / 3)) 33 pingu@ubuntu:~$ x="62" pingu@ubuntu:~$ echo $(($x + 12)) 74 pingu@ubuntu:~$ ((x++)) pingu@ubuntu:~$ echo "$x" 63
Es gilt hier auch Punkt vor Strich:
pingu@ubuntu:~$ a=$((3 + 4 * 2)) pingu@ubuntu:~$ echo "$a" 11 pingu@ubuntu:~$ b=$(((3 + 4) * 2)) pingu@ubuntu:~$ echo "$b" 14
Eine Aufstellung der verfügbaren Rechenarten in ihrer Vorrang-Reihenfolge findet sich im Abschnitt Shell Arithmetic 🇬🇧 des Bash-Skripting-Guide 🇬🇧.
Man kann abhängig vom System durchaus sehr große Zahlen berechnen, irgendwann erfolgt allerdings kommentarlos der Überlauf. Beim 32-bit-Hardy z.B.:
pingu@ubuntu:~$ echo $((10 ** 18)) 1000000000000000000 pingu@ubuntu:~$ echo $((10 ** 19)) -8446744073709551616
Für weitergehende Berechnungen müsste man dann auf die Rechenfunktionen externer Programme zurückgreifen, z.B. auf bc oder awk.
Arrays¶
Feldvariablen werden englisch als "Arrays" bezeichnet. Sie ermöglichen es, mehrere Werte innerhalb einer Variablen abzuspeichern. Die einzelnen Werte stehen dabei in Feldern des Arrays und können über den Index des Feldes einzeln angesprochen werden. Arrays lassen sich hervorragend über Schleifen verarbeiten, was etwas weiter unten noch beschrieben wird.
Arrays belegen¶
Um einen Wert in ein Array zu schreiben, ist es nötig, den Index mit anzugeben. Allgemein sieht das folgendermaßen aus:
Arrayname[i]=Wert
Ganz vorne steht der Name des Arrays, gefolgt vom Index, welcher durch eine Zahl in eckigen Klammern angegeben wird. Dann folgt wie auch schon bei den Variablen eine Zuweisung. In diesem Beispiel wird also der zugewiesene Wert „Wert“ in das i-te Feld des Arrays „Arrayname“ geschrieben.
Soll ein neues Array mit mehreren Werten auf einmal belegt werden, dann bietet sich folgende Schreibweise an:
Arrayname=(Dies sind vier Werte)
Die innerhalb der Klammern angegebenen Werte werden dabei implizit bei 0 beginnenden Indizes zugewiesen. Dieses Array enthielte also die vier Werte „Dies“, „sind“, „vier“ und „Werte“, wobei zum Beispiel „sind“ den Index 1 hätte.
Arrays können „Löcher“ haben, das heißt, Indizes müssen keine aufeinanderfolgenden Zahlen sein, und es müssen auch nicht alle Indizes ab 0 belegt sein.
Es können auch Elemente an das Ende eines Arrays angefügt werden. Das Ende ist dabei der höchste bereits existierende Index:
Arrayname+=(all diese sieben Elemente werden einzeln angefügt)
Quoting ist notwendig, falls Elemente Leer- oder andere Sonderzeichen enthalten sollen.
Arrays auslesen¶
Arrays lassen sich genau wie die Variablen mit Hilfe des Befehls echo
auslesen. Hierbei sind jedoch verschiedene Möglichkeiten gegeben. Es können gezielt Elemente mit einem bestimmten Index angesprochen werden:
echo ${array[2]}
Wird als Index ein „@
“ oder „*
“ angegeben, so kann auf das gesamte Array zugegriffen werden. In diesen Fällen gibt es allerdings Besonderheiten bezüglich doppelter Anführungszeichen um das Array herum:
${array[*]}
oder${array[@]}
: Alle Elemente mit Nachbearbeitung wie Globbing oder Word-Splitting."${array[*]}"
: Kein Globbing, Elemente getrennt durch erstes Zeichen aus$IFS
, Endergebnis ist ein Argument."${array[@]}"
: Kein Globbing, jedes Element wird ein eigenes Argument.
Hierzu einige verdeutlichende Beispiele. Angenommen, das Array wurde mit den folgenden drei Elementen belegt:
array=('Hallo, Welt!' '*' ' Noch ein paar Worte und Leerzeichen.')
Im Fall 1 expandiert das Element, das nur den Stern enthält, zu allen Dateien im aktuellen Verzeichnis, ebenso werden Worte an Leerzeichen getrennt:
echo ${array[*]}
Hallo, Welt! bin boot dev etc home lib lost+found media mnt opt proc root run sbin srv sys tmp usr var Noch ein paar Worte und Leerzeichen.
Die gelben Markierungen sollen zeigen, dass die resultierende Ausgabe keine zusammengehörige Zeichenkette ist, sondern aus mehreren Argumenten besteht. Ebenfalls ist zu beachten, dass die zusätzlichen Leerzeichen im dritten Element weggefallen sind.
Im Fall 2 hingegen entsteht ein einziges Argument, es findet keine Expansion statt, und die Leerzeichen bleiben erhalten:
echo "${array[*]}"
Hallo, Welt! * Noch ein paar Worte und Leerzeichen.
Als „Verbindungsstück“ zwischen den Elementen wird hier das erste Zeichen der Variablen $IFS
verwendet. Belegt man diese Variable um, so kann man auch das Verbindungsstück ändern:
IFS=';' echo "${array[*]}"
Hallo, Welt!;*; Noch ein paar Worte und Leerzeichen.
Das Ergebnis ist immernoch ein Argument, doch zwischen den Elementen steht jetzt kein Leerzeichen mehr, sondern ein Semikolon.
Hinweis:
Die Variable $IFS
erfüllt noch weitere Zwecke, zum Beispiel wird ihr Inhalt beim Wordsplitting verwendet. Sie sollte nur mit Vorsicht umbelegt werden. Im Zweifelsfalle sollte man sich den Inhalt vorher in eine andere Variable sichern und diesen Wert später wiederherstellen.
Fall 3 stellt den reinen Zugriff auf das Array dar. Änderungen an der Variablen $IFS
spielen keine Rolle mehr, da getrennte Argumente entstehen:
echo "${array[@]}"
Hallo, Welt! * Noch ein paar Worte und Leerzeichen.
Übung für den Leser: Verwende testweise ls
statt echo
und existierende Dateinamen als Arrayinhalt. Das sollte noch einmal herausstellen, was der Unterschied zwischen einem und mehreren Argumenten ist.
Wer sich nur die Anzahl der in einem Array enthaltenen Elemente ausgeben lassen will, kann dies mit dem folgenden Befehl tun:
echo ${#array[@]}
3
Arrays löschen¶
Arrays lassen sich genau wie Variablen mit dem Befehl unset
löschen.
unset array
Beispiel für ein Array¶
Das folgende Beispiel zeigt etwas ausführlicher den Umgang mit einem Array.
array[1]=Hallo echo ${array[@]}
Hallo
array[2]=Pingu echo ${array[@]}
Hallo Pingu
array[1]=Bye echo ${array[@]}
Bye Pingu
unset array echo ${array[@]}
An diesem Beispiel kann man ganz gut verschiedene Aspekte des Umgangs mit einem Array erkennen. Es zeigt wie ein Array angelegt wird, wie neue Felder belegt oder diese einfach verändert werden können. Schlussendlich wird das Array dann mit unset
gelöscht.
Assoziative Arrays¶
Assoziative Arrays, also Arrays mit Zeichenketten als Index, müssen zuerst mit declare -A
explizit erstellt werden, können danach aber wie normale Arrays benützt werden:
declare -A meinarray meinarray=( [zwei]="Zweiter Wert" [drei]="Dritter Wert" [vier]="Vierter Wert" ) meinarray[eins]="Ein Wert"
echo ${meinarray[eins]}
Ein Wert
Interaktion mit dem Benutzer¶
Oft ist es notwendig, dem Benutzer des Skriptes während dem Durchlauf mitzuteilen, was gerade passiert oder ihn aufzufordern, gewisse Angaben zu machen, die für die weitere Arbeit des Skriptes notwendig sind. Den Möglichkeiten, dies umzusetzen, widmet sich der folgende Abschnitt.
Ausgabe von Text¶
echo
kann man verwenden, um den Benutzer des Skriptes wissen zu lassen, was gerade passiert, oder um ihn aufzufordern, eine Eingabe zu machen.
1 2 3 4 | #!/bin/bash #Hallo Welt Skript echo "Hallo Welt" |
Die Shebang-Zeile wurde bereits beschrieben, ebenso Kommentare. Mit echo
gibt man Nachrichten auf der Standardausgabe aus, welche somit also am Bildschirm angezeigt werden. Dieses Beispiel würde folgende Ausgabe generieren.
Hallo Welt
Einlesen von Eingaben¶
Mit read
kann man Eingaben von der Standardeingabe einlesen und somit Benutzereingaben verarbeiten.
1 2 3 4 5 | #!/bin/bash #Begrüßung read -p "Geben sie ihren Namen ein:" name echo Hallo: $name |
In diesem Beispiel fordert man den Benutzer des Skriptes dazu auf, eine Eingabe zu machen. read
liest die Eingabe in die hinter read
stehende Variable $name
ein. Der Schalter -p
legt einen Prompt fest, der angezeigt wird. Die Ausgabe des Skriptes sieht dann wie folgt aus:
./begruessung.sh
Geben sie ihren Namen ein: Sab Hallo: Sab.
Man kann das auch in eine einzige Zeile packen, falls man die Kommandos direkt auf der Kommandozeile verwenden muss. Ein Beispiel:
read -p "Programm A (a) oder B (b) starten? Geben Sie a oder b ein und die Eingabetaste, Abbruch mit jeder anderen Taste ... " kommando; if [ $kommando == 'a' ]; then starte_programm_a; elif [ $kommando == 'b' ]; then starte_programm_b; else echo "Abbruch."; fi
Hier wird der Benutzer aufgefordert a oder b einzugeben, dann wird das jeweilige Programm ausgeführt. Die Eingabeaufforderung und die if-Verzweigung werden in einer Zeile abgesetzt. Man muss zwischen die einzelnen Befehle hier dann immer einen ";" setzen, um sie zu trennen.
Umleitungen¶
Umleitungen werden zu verschiedenen Zwecken verwendet. Hier werden nun ein paar dieser Möglichkeiten aufgelistet. Mehr zum Thema Umleitungen findet sich im Wiki unter dem Artikel Umleitungen.
Operator | Erklärung |
> | Dient der Umleitung der Standardausgabe in eine Datei. |
>> | Dient ebenfalls der Umleitung in eine Datei, hängt jedoch die Ausgabe an eine bereits bestehende Datei an anstatt diese zu überschreiben. |
| | Mit dem Pipeoperator lässt sich die Ausgabe eines Kommandos als Eingabe für ein anderes verwenden. |
< | Statt von der Standardeingabe (Tastatur) wird aus einer Datei gelesen |
Das Test-Kommando¶
Das Testkommando wird benötigt, um diverse Bedingungen zu testen. Es wird oft in Zusammenhang mit programmführenden Konstrukten verwendet. Test kennt verschiedene Operatoren, welche die Bedingungen des Testes beeinflussen. In der folgenden Tabelle sind die Möglichkeiten von Test aufgelistet.
Die Optionen des Test-Kommandos | |
Operator | Erklärung |
-d Verzeichnis | Ist wahr, wenn "Verzeichnis" existiert. |
-e Datei | Ist wahr, wenn Datei existiert. |
-f Datei | Ist wahr, wenn reguläre Datei existiert. |
-w Datei | Ist wahr, wenn die Datei existiert und Schreibzugriff erlaubt ist. |
-x Datei | Ist wahr, wenn Datei existiert und Ausführbar ist. |
-n String | Ist wahr, wenn String nicht leer ist. (Bsp. eine Variable) |
-z String | Ist wahr, wenn String leer ist. (Bsp. eine Variable) |
String1 = String2 | Ist wahr, wenn String1 gleich String2 ist. |
Zahl1 -eq Zahl2 | Ist wahr, wenn Zahl1 gleich Zahl2 ist. (-eq = equal) |
Zahl1 -lt Zahl2 | Ist wahr, wenn Zahl1 kleiner Zahl2 ist. (-lt = less than) |
Zahl1 -gt Zahl2 | Ist wahr, wenn Zahl1 größer Zahl2 ist. (-gt = greater than) |
Zahl1 -le Zahl2 | Ist wahr, wenn Zahl1 kleiner oder gleich Zahl2 ist. (-le = less or equal) |
Zahl1 -ge Zahl2 | Ist wahr, wenn Zahl1 größer oder gleich Zahl2 ist. (-ge = greater or equal) |
Zahl1 -ne Zahl2 | Ist wahr, wenn Zahl1 nicht gleich Zahl2 ist. (-ne = not equal) |
! foo | Ist wahr, wenn foo falsch ist, also eine Negation. |
Strings und Stringvariablen sollten stets gequotet werden, sonst kann es zu unerwarteten Ergebnissen führen (beispielsweise werden unmaskierte, leere Variablen mit dem Ausdruck -n
nicht erkannt). Wie man das Testkommando nun richtig anwendet, wird gleich im Abschnitt Programmführung noch gezeigt. Da das Testkommando durch die eckigen Klammern repräsentiert wird, wird es in Kontrollstrukturen nur selten explizit verwendet. Daher sollte man wissen, dass z. B. das Kommando
test -f Datei.txt
äquivalent ist zum Kommando
[ -f Datei.txt ]
Programmführung¶
Mit Verzweigungen und Schleifen steuert man den Ablauf eines Skripts.
If-Else-Anweisung¶
Die If-Verzweigung macht es möglich, nach einem Test auf eine Bedingung hin eine entsprechende Maßnahme zu ergreifen. Dabei ist neben test
auch jedes andere Kommando, welches einen Exit-Status zurückliefert, möglich. Das Konstrukt ist, ausgehend vom Testkommando, folgendermaßen aufgebaut.
1 2 3 4 | if [ Test-Bedingung ] then Befehl... fi |
In den eckigen Klammern kann man jetzt jede der zuvor kennengelernten Option des Testbefehls benutzen. Der Anweisungsblock wird mit fi
geschlossen. Man kann dieses Konstrukt auch noch etwas erweitern, um eine alternative Reaktion auf einen Test hinzuzufügen.
1 2 3 4 5 6 | if [ Test-Bedingung ] then echo "Ok, alles in Ordnung." else echo "Ahrg...Fehler." fi |
Dies wird durch den Zusatz else
ermöglicht. Zudem lässt sich dieses Konstrukt noch beliebig durch weitere Test-Bedingungen mit dem Zusatz elif
erweitern. Dies sieht folgendermaßen aus.
1 2 3 4 5 6 7 8 9 | if [ Test-Bedingung ] then Befehl... elif [ Test-Bedingung ] then Befehl... else Befehl... fi |
Ein Beispiel zeigt, wie man zwei Zeichenketten miteinander vergleicht und dem Ergebnis entsprechend eine Meldung dazu ausgibt. Wichtig zu beachten sind hier die Leerzeichen vor und hinter dem Vergleichsoperator sowie um die eckigen Klammern.
1 2 3 4 5 6 7 8 9 | #!/bin/bash echo "Wie ist Ihr Name?" read ANTWORT if [ "$ANTWORT" == "root" ] then echo "Hallo, Administrator." else echo "Hallo, Anwender." fi |
Ein Beispiel zeigt, wie man auf die Existenz einer Datei hin prüft und entsprechend dem Ergebnisse eine Meldung dazu ausgibt.
1 2 3 4 5 6 7 8 9 | #!/bin/bash #Dateitest if [ -f /home/Pingu/test.txt ] then echo "Die Datei test.txt in Pingus Home ist da." else echo "Die Datei test.txt in Pingus Home fehlt." fi |
Oder das ganze wird mit einer elif
-Konstruktion erweitert.
1 2 3 4 5 6 7 8 9 10 11 12 | #!/bin/bash # Dateitest if [ -f /home/Pingu/test.txt ] then echo "Die Datei test.txt in Pingus Home ist da." elif [ -d /home/Pingu/test.txt ] then echo "test.txt ist ein Verzeichnis" else echo "Die Datei test.txt in Pingus Home fehlt" fi |
Da neben dem Testkommando bei if-Konstrukten auch häufig das Kommando grep
zur Anwendung kommt, soll abschließend noch auf die insoweit abweichende Syntax hingewiesen werden:
1 2 3 4 | if grep -q "Muster" Dokument.txt then Befehl... fi |
Case-Anweisungen¶
Die case-Anweisung ist eine Mehrfachverzweigung und eine elegante Alternative zu verschachtelten if-else-Konstrukten, mit welchen prinzipiell dasselbe Ergebnis erzielbar ist.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #!/bin/bash dat="$1" # Schneide vor Dateiendung ab: ext=${dat/*./} case $ext in pdf) xpdf "$dat" ;; sh) shellcheck "$dat" ;; png|jpg|gif) eog "$dat" ;; wav|mp3) aplay "$dat" ;; *) echo Unbekanntes Dateiformat"!" ;; esac |
Es lassen sich auch Alternativen für eine Testbedingung angeben. Dazu wird das Pipe-Symbol "|" als Oder-Operator verwendet, wie oben gezeigt.
Hier wird die Variable ext
auf verschiedene, bekannte Dateinamenserweiterungen überprüft, etwa pdf
oder sh
. Ansonsten unbehandelte Muster können mit dem Jokerzeichen *)
abgefangen werden. Jeder Abschnitt muss durch ein doppeltes Semikolon ;;
vom folgenden nächtsn Abschnitt getrennt sein.
Schleifen¶
Es gibt in der Bash drei verschiedene Arten von Schleifen, welche unterschiedlich gut für einen jeweiligen Typ von Aufgabe verwendet werden können. Grundsätzlich wird es durch sie möglich, einen Befehl mehrmals auszuführen, bis eine Bedingung eintritt oder solange eine Bedingung erfüllt ist. Diese drei Typen sind unten aufgezeigt, im Folgenden wird jedoch nur auf die for
- und while
-Schleife näher eingegangen. Weitere Beispiele, insbesondere für eine until
-Schleife, findet man z. B. in diesem How-To zur Bash-Programmierung 🇬🇧.
for
-Schleifewhile
-Schleifeuntil
-Schleife
Eine for-Schleife kann 3 Formen annehmen. Eine Liste der Werte über die zu iterieren ist, kann ausdrücklich angegeben werden:
1 2 3 4 5 | params="a b c" for z in $params do echo $z done |
Output:
1 2 3 | a b c |
Diese Schleife beendet sich, sobald das letzte Element der Übergebenen Parameterliste abgearbeitet ist. Ein Beispiel soll ihre Arbeitsweise verdeutlichen.
1 2 3 4 5 6 | #!/bin/bash # for-schleife for wort in Hallo "du Welt" "da draußen" do echo "$wort" done |
Ausgabe:
Hallo du Welt da draußen
Der nachfolgende Code bezieht sich noch einmal auf das Beispiel, bei dem eine Variable mit Hilfe der Ausgabe eines Befehls gefüllt wurde. Dies kann sehr gut mit einer for
-Schleife verbunden werden:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #!/bin/bash # Variablendefinition inputdir="/home/foo/Bilder/ZuBearbeiten" outputdir="/home/foo/Bilder/Bearbeitet" # Abarbeiten der eingelesenen Bilder mit Hilfe einer For-Schleife # und dem Programm ImageMagick. for pic in "$inputdir"/*.png do picname=$(basename "$pic") echo "Bearbeite Bild: $picname" convert "$pic" -colorspace Gray "$outputdir/$picname" done |
Dieses Skript läuft über alle Namen der png-Bilddateien im Ordner inputdir und weist sie der Reihe nach der Variablen pic
zu. Die Bilddateien werden über die for
-Schleife abgearbeitet, wobei das Programm ImageMagick zum Einsatz kommt. Hierbei werden alle Farbbilder in ihre Grauwerte umgerechnet und dann im Ordner outputdir gespeichert. Man kann hieran ganz gut erkennen, wie eine for
-Schleife arbeitet. Jedes Element in der Liste wird einzeln herangezogen und abgearbeitet. Dazu bedient man sich einer Variable, welche diese jeweils aufnimmt. Die Schleife endet nach der Bearbeitung des letzten Listeneintrages.
Diese Methode lässt sich auch auf Arrays übertragen, jedoch sieht das Konstrukt etwas anders aus. Hier zunächst in allgemeiner Form:
1 2 3 4 | for wert in "${array[@]}" do Befehl... done |
Mit der zweiten Form der for
-Schleife lassen sich Berechnungen durchführen. Dies ist praktisch, wenn die Schleife eine festgelegte Anzahl von Durchgängen durchlaufen soll oder wenn für einen Befehl eine Zahlenreihe benötigt wird. Die Syntax hierzu lautet:
1 2 3 4 5 6 | for ((Anfangswert;Bedingung;Operation)) do Befehl1 Befehl2 # usw. done |
So kann man zum Beispiel Jahresarchivordner für die letzten zehn Jahre in einem Rutsch erstellen:
1 2 3 4 | for ((z=2004;z<=2014;z++)) do mkdir Archiv_"$z" done |
Kürzer wäre jedoch for z in 20{04..14}
, und da mkdir
mehrere zu erstellende Verzeichnisse entgegennehmen kann mkdir 20{04..14}
.
Für die Zahlenreihen kann man auch größere Abstände wählen. Will man für die Zahlenreihe Fünferschritte haben, dann muss die erste Zeile zum Beispiel
1 | for ((z=5;z<=50;z+=5)) |
lauten.
Die dritte Form verwendet für die Variable alle Shell- oder Funktionsparameter der Reihe nach:
1 2 3 4 | for x do echo $x done |
./parafor.sh a b c
Ausgabe:
1 2 3 | a b c |
Die while
-Schleife sieht allgemein folgendermaßen aus.
1 2 3 4 5 6 | while [ Test-Bedingung ] do Befehl1 Befehl2 # usw. done |
Die while
-Schleife wird solange ausgeführt, wie eine Bedingung zutrifft. Im nachfolgenden Beispiel wartet eine while
-Schleife, bis eine bestimmte Benutzereingabe stattgefunden hat.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #!/bin/bash # Warten auf das richtige Ergebnis. echo "Frage: Was ergibt 2 + 2" echo "" while ((answer != 4)) do read -p "Bitte ihre Eingabe:" answer case "$answer" in 4) echo richtig ;; *) echo falsch ;; esac done |
Eine while
-Schleife eignet sich auch besonders gut, um ohne Interaktion Datendateien abzuarbeiten. Hierzu ein weiteres Beispiel:
1 2 3 4 5 6 7 8 9 10 | #!/bin/bash # Liste aus einer .csv-Datei verarbeiten while read stapel do readarray -td\; zeile <<<"$stapel" echo " Vorname: ${zeile[0]} Name: ${zeile[1]} Funktion: ${zeile[2]}" done < liste.csv; echo |
Der Inhalt der Datei liste.csv
könnte wie folgt aussehen:
Mathilde;Meister;Hausmeisterin; Lars;Lernfreak;Schüler; Gerd;Geduldig;Lehrer; Adelheid;Alpha;Schulleiterin;
Ausgabe:
Vorname: Mathilde Name: Meister Funktion: Hausmeisterin Vorname: Lars Name: Lernfreak Funktion: Schüler Vorname: Gerd Name: Geduldig Funktion: Lehrer Vorname: Adelheid Name: Alpha Funktion: Schulleiterin
Damit das Einlesen per readarray
korrekt funktioniert, ist es wichtig, dass auch das letzte Feld mit dem in der Option -d
definierten Feldtrenner abgeschlossen wird. Insofern unterscheidet sich die Datei ein wenig von dem üblichen .csv
-Format. Die notwendige Änderung kann aber leicht mit dem Befehl
1 | sed -i 's/$/;/' liste.csv |
vorgenommen werden.
Parameter an ein Skript übergeben¶
Ein Skript kann bei seinem Start Parameter übernehmen, indem diese, durch Leerzeichen oder Tabulator (engl.: "whitespaces") getrennt, bei seinem Aufruf übergeben werden. Die Parameter befinden sich zur Laufzeit des Skripts in den Variablen $1 bis $9 und können dazu genutzt werden, das Skript in seiner Arbeit zu beeinflussen. Ein Beispiel soll hier zeigen, wie dies generell funktioniert.
1 2 3 4 5 6 7 8 | #!/bin/bash # willkommen if [ $1 == "Pingu" ] then echo "Hallo, kleiner Pingu!" else echo "Hallo, $1" fi |
Und jetzt der Aufruf des Skriptes:
./willkommen.sh Pingu
Hallo, kleiner Pingu!
./willkommen.sh Peter
Hallo, Peter
Die Variable $#
gibt die Anzahl der verwendeten Parameter aus. So kann man prüfen, ob das Skript sinnvoll verwendet wird:
1 2 3 4 5 6 7 8 9 10 11 | #!/bin/bash # Willkommen if [ $# -ne 1 ] then echo; echo "Verwendung: $0 Name"; echo elif [ $1 == "Pingu" ] then echo "Hallo, kleiner Pingu!" else echo "Hallo, $1" fi |
Mit Hilfe des Befehls shift
kann das Skript auf mehr als neun Parameter zugreifen. Shift verschiebt dabei alle Parameter, die auf der Kommandozeile übergeben wurden, eine Position nach links. In dem kurzen Beispiel werden kurzschließende Tests durchgeführt, die im Prinzip wie if
arbeiten.
1 2 3 4 5 6 7 8 | #!/bin/bash while [ "$1" != '' ] do [ $1 == "-b" ] && BACKUP="yes" && echo "Ok. Do a backup!" && shift [ $1 == "-r" ] && RESTORE="yes" && echo "Ok. Do a restore!" && shift [ $1 == "-c" ] && CLEAN="yes" && echo "Ok. Tidy up!" && shift done |
Die Bedingung, mit der die while
-Schleife initiiert wird, bedeutet soviel wie: Fahre fort, solange "$1" nicht leer ist. Werden nun mehrere Parameter an das Skript übergeben, so wird immer der an Position eins liegende und in "$1" gespeicherte bearbeitet. Ist dies geschehen, wird durch shift
der Parameter, welcher sich an der zweiten Position befindet und in "$2" gespeichert ist, an die erste Position gerückt, und die Schleife beginnt von vorne. Mehrere Befehle können durch doppelte Und-Zeichen "&&" zu kurzschließenden Tests verbunden werden, d.h. bei mehreren, mit && verbundenen Tests wird die Auswertung abgebrochen, sobald ein Teilausdruck falsch ist, weil dann der Gesamtausdruck falsch ist; umgekehrt wird bei mehreren veroderten Tests (||) abgebrochen, sobald einer wahr ergibt, da dann der Gesamtausdruck wahr ergibt. Dies kann Zeit sparen, wenn spätere Tests ignoriert werden können, aber mit Überraschungen verbunden sein, wenn man nicht berücksichtigt, dass spätere Tests Nebenwirkungen (engl. "side effects") haben sollen, die dann auch übersprungen werden.
In diesem Zusammenhang ist die Option extglob
interessant, mit der die übergebenen Parameter komplexen Tests unterzogen werden können. Einfacher, schneller und komfortabler können Parameter mit dem Befehl getopts
ausgewertet werden. Ein Beispiel soll zeigen, wie getopts
arbeitet.
1 2 3 4 5 6 7 8 9 10 11 12 13 | #!/bin/bash # Getopttest while getopts ':brcp:' OPTION do case "$OPTION" in b) echo "Ok, do a backup";; p) echo "Backup path is: $OPTARG";; r) echo "Ok, do a restore";; c) echo "Ok, tidy up afterwards";; *) echo "Unknown parameter" esac done |
Der Befehl getopts
arbeitet mit bereits vertrauten Konstrukten. Es kommen eine while
-Schleife und eine case
-Anweisung zum Einsatz. Zu beachten ist die spezielle Notation in der Parameterliste von getopts
: Der erste Doppelpunkt ist optional und schaltet die Anzeige von Fehlermeldungen ab. Dahinter stehen die Parameter als aufeinanderfolgende Buchstaben. Folgt einem Parameter ein Doppelpunkt, bedeutet dies, dass er ein Argument benötigt. Im oben dargestellten Beispiel ist das bei p
der Fall. Der Parameter p
nimmt als Argument einen Pfad auf. Auf diesen kann über die Variable "${OPTARG}" zugegriffen werden.
Funktionen¶
Funktionen helfen, die Übersichtlichkeit zu verbessern. Sie können mehrere Befehle zu semantischen Gruppen zusammenfassen. Wurden sie einmal definiert, können sie im Skript durch ihren Namen aufgerufen werden. Wichtig ist, dass die Definition der Funktion vor ihrem ersten Aufruf stattgefunden haben muss. Es ist also ratsam, zunächst die Funktionen zu definieren:
1 2 3 | funktionsname(){ Befehle... } |
Das Beispielskript zaehledateien
zeigt eine konkrete Funktion.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #!/bin/bash PFAD="/home/pingu/" # Funktionsdefinitionen wievieledateien() { anzahl=0 for file in "$PFAD"/* ; do (( anzahl++ )) done echo "Es befinden sich $anzahl Dateien und Ordner in $PFAD" } # Hauptteil wievieledateien |
Das Skript liefert nach seinem Aufruf:
./zaehledateien
folgende Ausgabe:
Es befinden sich 90 Dateien und Ordner in /home/pingu/
Die Kommentare #Hauptteil, #Funktionsdefinitionen,
usw. macht man in richtigen Skripten nicht - sie dienen nur hier der Verdeutlichung für Anfänger.
Nicht nur an das Skript, sondern auch an Funktionen können Parameter übergeben werden. Dies funktioniert wie die Übergabe von Skriptparametern, indem die Parameter an den Funktionsaufruf, zumeist durch Leerzeichen voneinander getrennt, angehängt werden. Das folgende Beispiel ergänzt das vorhergehende um Funktionsparameter. Wichtig ist auch, dass die an das Skript übergebenen Parameter in den Funktionen nicht "direkt" in Form ${X}
verwendet werden können, sondern "neu definiert" werden müssen (hier zaehlpfad=${1}
). Das kann vor Definition der Funktion oder auch erst in der Funktion selbst erfolgen.
1 2 3 4 5 6 7 8 9 10 11 | #!/bin/bash wievieledateien() { zaehlpfad="$1" for file in "$zaehlpfad"/* ; do (( anzahl++ )) done echo "Es befinden sich $anzahl Dateien und Ordner in $zaehlpfad" } wievieledateien /home/pingu wievieledateien /home/edit |
Grafische Menüs¶
Ein Skript kann ein Hilfsprogramm nutzen, um GUI-Elemente wie Auswahllisten, Datei-öffnen-Dialoge, Auswahlbuttons usw. zu erzeugen. Entweder entscheidet man sich für ein spezielles Hilfsprogramm, das günstigerweise automatisch mit einer Oberfläche installiert wird (z.B. GNOME: Zenity, KDE: KDialog, Textkonsole: dialog, etc.), oder man nutzt ein universelles Hilfsskript wie easybashgui 🇬🇧, das automatisch das jeweils passende, installierte Text- oder Grafik-Hilfsprogramm nutzt.
Der folgende Abschnitt soll erklären wie die Erstellung grafischer Dialoge mit Bash-Skripten prinzipiell funktioniert. Hauptsächlich wird dabei auf das Programm dialog (textbasiert) eingegangen.
Menüs mit Dialog¶
Dialog bietet eine konsolenbasierte Aufbereitung der darzustellenden Daten. Dabei reicht das Repertoire von einfachen Entscheidungsfenstern (Ja/Nein) über verschiedene Auswahlmenüs bis hin zur Möglichkeit der Darstellung einer Dateiauswahlliste. Ein einfaches Beispiel soll die Arbeit mit Dialog illustrieren.
1 2 3 4 | #!/bin/bash # Ja oder Nein dialog --yesno "Bist du ein Linuxfan?" 15 60 |
Dieses Beispiel erzeugt eine Box von 60 Zeichen Breite und 15 Zeilen Höhe mit dem in Anführungsstrichen stehenden Fragentext und zwei Schaltflächen für "Yes" und "No". Dialog speichert den Rückgabewert für die getroffene Auswahl in der allgemeinen Rückgabevariable $?
– im Fall dieser Yes/No-Box stehen die Rückgabewerte 0
für ja, 1
für nein und 255
für Abbruch ohne Auswahl. Im nächsten Schritt soll dieses Beispiel ausgebaut werden.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #!/bin/bash # erweiterter Dialog dialog --backtitle Umfrage --title "Erste Frage" --yesno "Sind sie ein Linuxfan?" 15 60 antwort=${?} if [ "$antwort" -eq "255" ] then echo "Abgebrochen" exit 255 fi if [ "$antwort" -eq "1" ] then dialog --backtitle Umfrage --title Kritikbox --msgbox "Na, dann wird es aber allerhöchste Zeit!" 15 40 fi if [ "$antwort" -eq "0" ] then dialog --backtitle Umfrage --title Schmeichelbox --msgbox "Hm, das habe ich mir fast schon gedacht!" 15 40 fi |
Im Beispiel wurden zusätzliche Optionen eingesetzt. So setzt --backtitle
den Titel des Hintergrundes, --title
setzt die Überschrift der Box. Zur --yesno
-Box kommen zwei Messageboxen (--msgbox
) hinzu, die nur eine Nachricht darstellen und dann über einen OK-Button geschlossen werden können. Diese Messageboxen erscheinen in Abhängigkeit von der Antwort, die im ersten Fenster gegeben wurde. Dieser Dialog soll nun weiter verfeinert werden.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | #!/bin/bash # erweiterter Dialog dialog --backtitle Umfrage --title "Erste Frage" --yesno "Sind sie ein Linuxfan?" 15 60 antwort=${?} if [ "$antwort" -eq "255" ] then echo "Abgebrochen" exit 255 fi if [ "$antwort" -eq "1" ] then dialog --backtitle Umfrage --title Kritikbox --msgbox "Na, dann wird es aber allerhöchste Zeit!" 15 40 fi if [ "$antwort" -eq "0" ] then dialog --backtitle Umfrage --title Lobbox --msgbox "Hm, das hab ich mir fast schon gedacht!" 15 40 auswahl=$(dialog --stdout --backtitle Umfrage --title Details --radiolist "Welche Desktop-Umgebung bevorzugen Sie? Sie können nur eine wählen." 16 60 5 \ "GNOME" "Die GNOME Desktop Umgebung" off \ "KDE" "Die KDE Desktopumgebung" on \ "Sonstige" "Eine andere hier nicht gelistete" off) fi case "$auswahl" in GNOME) dialog --backtitle Umfrage --title Reaktion --msgbox "Sehr schön Gnome!" 15 40 ;; KDE) dialog --backtitle Umfrage --title Reaktion --msgbox "Sehr schön KDE!" 15 40 ;; Sonstige) dialog --backtitle Umfrage --title Reaktion --msgbox "Hm Sonstige!" 15 40 ;; esac |
Dieses Beispiel geht nach einer positiven Antwort noch etwas weiter. Mit Hilfe einer Radioliste --radiolist
wird die bevorzugte Desktopumgebung erfragt. Eine Radioliste zeichnet sich dadurch aus, dass nur ein Eintrag aktiv sein kann. Sollen mehrere aktive Einträge erlaubt sein, sollte stattdessen --checklist
verwendet werden. Die Antwort aus der Radioliste wird in eine Variable geschrieben, um sie danach weiterverarbeiten zu können. Dies geschieht durch eine case
-Anweisung. Wichtig: Die Option --stdout
darf beim Schreiben in eine Variable nicht vergessen werden, sie sorgt für eine Ausgabe auf dem Bildschirm. Wäre sie nicht gesetzt, würde der Dialog nicht erscheinen.
Literaturhinweise¶
Shell-Programmierung – Einführung, Praxis, Referenz 🇩🇪 - von Jürgen Wolf, Rheinwerk <openbook>
Links¶
Shell Hauptartikel zur Shell mit vielen weiteren Links (intern + extern)
Bash – spezielles zur Bourne again shell
Tipps & Tricks, um Fehler zu vermeiden
Bash/Hilfe – Hilfe zu Befehlen und Kommandos der Bash aufrufen
ShellCheck – Werkzeug zur Analyse von Bash- und Shell-Skripten
Skripte Beispiele, welche die breite Anwendbarkeit von Bashskripten ein wenig illustrieren
extern
Shellscripts: http://www.bin-bash.de/scripts.html 🇩🇪