ubuntuusers.de

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:

Wiki/Icons/terminal.png 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:

Außer Leafpad beherrschen alle genannten 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:

  • mcedit aus dem Paket mc-data

  • Nano

  • VIM (nicht leicht für Anfänger, aber trotzdem sehr empfehlenswert)

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 …

  1. 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 Angabe INTERPRETER optional durch Leerzeichen abgetrennt werden.

  2. 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.

  3. OPTION …: Diese Angabe ist optional und kann auch fehlen. Gemeint sind Optionen für das Programm INTERPRETER, 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:

  1. ${array[*]} oder ${array[@]}: Alle Elemente mit Nachbearbeitung wie Globbing oder Word-Splitting.

  2. "${array[*]}": Kein Globbing, Elemente getrennt durch erstes Zeichen aus $IFS, Endergebnis ist ein Argument.

  3. "${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

Wiki/Icons/users.png

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-Schleife

  • while-Schleife

  • until-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.

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 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

Diese Revision wurde am 13. März 2024 15:25 von wxpte erstellt.
Die folgenden Schlagworte wurden dem Artikel zugewiesen: Shell, Programmierung, Einsteiger