Die GNU-Shell Bash (Bourne Again SHell)


Index

  1. Vorbemerkung
  2. Hinweise für tcsh-Nutzer
  3. Bash-Nutzung im Terminal-Emulator unter X11 und auf der Linux-Text-Konsole
  4. Aufruf der Bash
  5. Start einer interaktiven Bash
  6. Start einer nicht-interaktiven Bash
  7. Beendigung der Bash
  8. Konfigurationsdateien
  9. Modifikation von Shell-Optionen in einer laufenden Shell
  10. Modi der Bash
  11. Terminologie
  12. Kommentare
  13. Bash-Grammatik
  14. Einfache Kommandos
  15. Such-Pfad
  16. Ein-/Ausgabe-Umlenkung
  17. Metazeichen für Pfadnamens-Muster
  18. Variablen, Parameter und Felder
  19. Umgebungsvariablen
  20. Sonderzeichen und Quotierung
  21. Kommandozeilen-Editor und History
  22. Alias-Konstrukte (Aliase)
  23. Expandierungen
  24. Rechnen mit der Bash
  25. Test-Operatoren und Bedingungs-Ausdrücke
  26. Pipes (Pipelines) und T-Stücke
  27. Listen inklusive Hintergrund-Kommandos
  28. Job-Steuerung
  29. Zusammengesetzte Kommandos / Ablaufsteuerung
  30. Koprozesse
  31. Funktions-Definitionen
  32. Umgang mit Signalen
  33. Ausgewählte eingebaute Kommandos (Builtin-Kommandos)
  34. Flexible Prompt-Anpassung
  35. Einige Neuerungen in Bash 3.0
  36. Bash-Debugger
  37. Sichere Shell-Skripten
  38. Beispiel-Skripten

1. Vorbemerkung

Das vorliegende Material versucht, einen relativ umfangreichen Überblick über die Philosophie und den Leistungsumfang der Bash und vielfach allgemein der Unix-Shell-Schnittstelle zu geben, der auch bei der Verwendung anderer Shells (speziell der Korn- oder Z Shell) hilfreich sein kann. Es geht dabei nicht um den Ersatz der sehr umfangreichen, für die Klärung von Detail-Fragen meist unverzichtbaren Bash-Manuals, sondern um eine Ergänzung, die auf wichtigste oder typische Punkte hinweist und vielfach versucht, diese an instruktiven Beispielen (die in den Manuals kaum zu finden sind) zu erläutern.

Der Text soll dem Shell- bzw. Bash-Neuling den Einstieg in diese Materie erleichtern sowie dem Shell-Kenner das schnelle Nachschlagen spezieller Bash-Konstrukte ermöglichen und ihm vor allem auch die neuen Möglichkeiten der Bash aufzeigen, die bei der älteren Bourne-Shell noch nicht existierten und deren schrittweise Aneignung jedem aktiven Shell-Nutzer nur dringend empfohlen werden kann, da sie seine Arbeit deutlich effektivieren können.

Für Details, die hier nicht dargestellt sind, sei generell auf das Online- und das Referenz-Manual der Bash sowie geeignete Zusatz-Literatur verwiesen, die in Form gedruckter Bücher sowie freier Web-Seiten zur Verfügung steht. Hinweise dazu findet man auf den Seiten Links zu Bash, Z Shell, Shell-Programmierung etc. und Literaturempfehlungen zu Bash und Shell-Programmierung.

Unter Unix/Linux erreicht man die beiden Manuals in der Regel über die folgenden Kommandos:

Das Referenz-Manual existiert neben dieser einfachen Text-Form auch noch in den Formaten HTML, PDF, PS und DVI.

In der Regel benötigt man mehrere solche Dokumente, um vor allem knifflige Fragen zu klären, da moderne Shells über einen sehr großen Funktionsumfang verfügen, der vermutlich in keinem Dokument komplett dargestellt wird. Außerdem ist es oft interessant, die Sichtweise verschiedener Autoren zu einem Thema kennenzulernen. Generell ist es auch sehr lehrreich, bestimmte Antworten durch eigene Experimente mit der Shell zu finden.

Nachfolgend wird primär der Leistungsumfang der Bash-Versionen 2.X und 3.X dargestellt, wobei auf eine Auswahl der neuen Möglichkeiten (Features) der Bash 3 in einem eigenen Abschnitt gesondert hingewiesen wird.

Die Bash ist kompatibel zur Bourne-Shell und bietet zusätzlich viele nützliche Eigenschaften von ksh (Korn-Shell) und tcsh (TENEX C-Shell), bringt aber auch neue (Bash-spezifische) Elemente ein. Sie ist wie die zsh (Z Shell) eine leistungsfähige interaktive und nicht-interaktive Shell. Die Bash beabsichtigt, eine POSIX-konforme Implementierung gemäß POSIX 1003.2 (IEEE POSIX Shell and Tools specification) zu sein.

Die Bash steht auf einer Vielzahl von Plattformen zur Verfügung, u.a. für Linux, verschiedene Unix-Versionen und Windows. Ein Teil der Aussagen und Beispiele des vorliegenden Materials sind Unix-/Linux-spezifisch. Die anderen gelten generell für alle Plattformen.

An einigen Stellen wird im Text auch auf Unterschiede zu anderen Shells, speziell zur tcsh und zsh, hingewiesen, da diese beiden neben der Bash die am häufigsten genutzten (interaktiven) Shells sein dürften. Um langjährigen tcsh-Kennern (zu denen auch der Autor dieser Seite gehört) einen evtl. Umstieg auf die Bash zu erleichtern, wurde für sie ein eigener Abschnitt aufgenommen, der auf einige deutlich spürbare Unterschiede zwischen Bash und tcsh hinweist.

Es sei noch erwähnt, dass die zsh von den hier genannten Shells den größten Leistungsumfang bietet und sich einer zunehmenden Beliebtheit bei Profi-Shell-Nutzern erfreut. Sie umfasst weitgehend komplett die Funktionalität der Bash und der Korn-Shell (wenn auch teilweise mit anderen syntaktischen Mitteln bzw. anderer Ausprägung) und unterstützt gezielt viele Eigenschaften der tcsh. Zusätzlich verfügt sie über spezielle zsh-Features, die man in keiner anderen Shell findet. Dazu gehören z.B. erweiterte Möglichkeiten beim sog. Globbing (also der Nutzung von Mustern zur Pfadnamens-Expandierung), ein implizites tee (die gleichzeitige Ausgabe-Umlenkung in mehrere Dateien) und ein mehrzeiliger Kommandozeilen-Editor mit einem sehr ausgefeilten Vervollständigungs-System (programmierbare Wort-Vervollständigung).

Die gezielte Bereitstellung von Bash- oder tcsh-Features in der Z Shell sollen den Nutzern dieser beiden anderen Shells den Umstieg auf die Z Shell erleichtern. Eine Hilfestellung dazu gibt das Buch From Bash to Z Shell. Ob sich ein Umstieg auf eine andere Shell letztlich lohnt, muss jeder Anwender für sich entscheiden. Für viele Nutzer reicht der Umfang der Bash (oder auch der tcsh) sicher ohne weiteres aus. Sie werden vermutlich nie deren volle Leistung ausreizen. Eine gewisse Beschränkung der Komplexität hat auch Vorteile, weil man den Überblick besser behalten kann und so weniger Überraschungen erlebt.

Während die Bash 3 eine recht umfangreiche Unicode-Unterstützung bietet und bei der tcsh sowie bei der Z Shell Version 4.3 der Kommandozeilen-Editor Unicode-fähig ist, fehlt die Unicode-Unterstützung in der Z Shell bis zur Version 4.2. Daher ist die interaktive Nutzung älterer Versionen der Z Shell in einer Unicode-Umgebung (die zunehmend häufiger anzutreffen ist) teilweise sehr lästig und ggf. auch fehleranfällig, da man manchmal bestimmte Zeichen der Kommandozeile auf dem Bildschirm nicht mehr sieht. Diese Diskrepanz zwischen Anzeige und internem Inhalt ergibt sich nach dem Löschen von Mehrbyte-Zeichen, die zwar auf dem Bildschirm entfernt werden, im internen Kommandozeilen-Puffer aber teilweise erhalten bleiben, da nur ein Byte gelöscht wird.

Abschließend noch einige Bemerkungen zur Begriffs-Verwendung sowie zu typographischen Konventionen: Die Original-Begriffe der Shell-Programmierung wie auch allgemein der Informatik stammen aus dem Englischen und werden von alten "Shell-Hasen" oft auch im Original gebraucht. Im vorliegenden Text wurde vielfach geeigneten deutschen Entsprechungen der Vorzug gegeben. Solche allgemein bekannten Begriffe wie File für Datei und String für Zeichenkette werden dagegen verwendet.

Manche Termini lassen sich aber nur schwer übersetzen, z.B. das Substantiv Quoting oder das Verb quote, weil die wörtliche Übersetzung zitieren ungebräuchlich ist. Jeder Shell-Kenner weiß, dass es hier darum geht, die spezielle Bedeutung der vielen Sonderzeichen der Shell aufzuheben und statt dessen das betreffende Zeichen in seiner ursprünglichen Bedeutung (also als das Zeichen selbst) zu verwenden. Wir nutzen nachfolgend die eingedeutschten Begriffe quotieren bzw. Quotierung, die zumindest an das englische Original erinnern, sich aber in deutsche Satzkonstruktionen besser einfügen als z.B. die Konstruktion gequotet.

Beim Begriff History verzichten wir auf eine deutsche Übersetzung und verwenden statt dessen den englischen Ausdruck, weil er unter Shell-Kennern sofort klar und sehr gebräuchlich ist.

An verschiedenen Stellen des Textes wird die Syntax bestimmer Konstrukte erläutert und durch Beispiele illustriert. Dies erfolgt generell in einem Font fester Breite (Schreibmaschinenschrift). Die literal einzugebenden Zeichen bzw. Zeichenfolgen sind in Fettschrift angegeben, die symbolischen Platzhalter für Argumente u.ä. dagegen nicht.

In Syntax-Beschreibungen werden folgende Meta-Symbole genutzt:


2. Hinweise für tcsh-Nutzer

Ein Umstieg von der tcsh zur Bash ist aus Sicht des Autors unter Beachtung seiner eigenen praktischen Erfahrungen relativ problemlos möglich. Durch die sehr zahlreichen Konfigurationsmöglichkeiten kann man die Bash in vielen Punkten so einstellen, dass sie sich (weitgehend oder genau so) wie die tcsh verhält. An einigen Stellen muss man allerdings umlernen, da bestimmte Features nicht oder in anderer Form angeboten werden. Als Entschädigung für diesen Aufwand erhält man aber auch neue Möglichkeiten, die in der tcsh fehlen. Dazu kommt, dass die meisten Skripte ohnehin Bourne-Shell-Skripte oder sogar direkt Bash-Skripte sind. Wenn man also die Bash statt der tcsh auch für die interaktive Arbeit nutzt, hat man eine einheitliche Umgebung für die interaktive Arbeit und die Skript-Programmierung.

Denjenigen tcsh-Fans, die mit einer interaktiven Bash dennoch nicht gut leben können oder wollen, sei ein Blick auf die zsh empfohlen, die ja die Welten der Bourne- und C-Shell zu vereinigen versucht. Es gibt Berichte verschiedener Nutzer, die erfolgreich von der Bash oder der tcsh zur Z Shell gewechselt sind und sich in der neuen Umgebung sehr wohl fühlen. Natürlich muss man auch beim Wechsel zur Z Shell einige kleine Änderungen in Kauf nehmen, z.B. die Tatsache, dass die Anzeige mehrerer Auswahlmöglichkeiten bei der Wort-Vervollständigung unter und nicht wie gewohnt über der aktuellen Eingabezeile erfolgt.

Wer mit dem keineswegs geringen Leistungsumfang der tcsh in der Regel auskommt, weil er interaktiv meist nur oder gerade die Dinge benötigt, die die tcsh gut beherrscht, und für komplexere Aufgaben auf Skript-Dateien statt interaktiv eingegebener Skripte zurückgreift, kann natürlich auch problemlos die tcsh als Standard-Shell behalten und sich in den relativ seltenen Fällen, in denen komplexere Aufgaben anliegen, eine Bash oder Z Shell manuell starten. Der Vorteil dieser auch vom Autor längere Zeit praktizierten Arbeistweise besteht darin, dass man für die meisten seiner Aufgaben eine Umgebung nutzt, die man besonders gut kennt und in der man daher auch sehr effizient arbeiten kann. Bei diesem Vorgehen empfiehlt es sich, die genutzten Shells so weit wie möglich identisch zu konfigurieren (z.B. identische Aliase, Variablen-Einstellungen, Optionen etc.), um einen nahtlosen Shell-Wechsel zu unterstützen.

Hier nun die (keineswegs vollständige) Liste von Unterschieden zwischen Bash und tcsh, die dem Autor aufgefallen sind:


3. Bash-Nutzung im Terminal-Emulator unter X11 und auf der Linux-Text-Konsole

Typischerweise wird die Bash in einem Terminal-Emulator genutzt, der unter einer grafischen Oberfläche läuft. Das weit verbreitete Oberflächen-System KDE bietet z.B. als Terminal-Emulator das Kommando konsole an. Es gestattet, in einem Fenster mehrere Shell-Sitzungen parallel zu betreiben, so dass man mit einem einzigen Shell-Fenster auskommen kann und nicht für jede Shell ein eigenes Fenster benötigt, was die Übersichtlichkeit des Desktops erhöht.

Linux unterstützt mehrere virtuelle Terminals. Mit der Tastenkombination

Strg-Alt-Fn

kann man auf das virtuelle Terminal n umschalten.

Das virtuelle Terminal 7 ist meist das erste (und typischerweise das einzige) grafische Terminal. Die virtuellen Terminals 1 bis 6 bieten in der Regel eine Text-Konsole an. Dort wird nach einem erfolgreichen Anmelde-Vorgang eine Shell gestartet. Sie sind also nur über eine Shell vernünftig nutzbar.

Die Arbeit mit einer Text-Konsole kann sinnvoll oder nötig sein, wenn entweder das grafische Terminal (noch) nicht funktioniert oder durch einen anderen Nutzer gesperrt ist und man z.B. nur mal schnell seine Mails lesen oder eine Text-Datei editieren möchte. Dann kann man beispielsweise mit Strg-Alt-F1 auf die Text-Konsole 1 umschalten und dort die gewünschten Arbeiten erledigen.


4. Aufruf der Bash

Die allgemeine Aufruf-Syntax der Bash lautet:

  bash [Optionen] [Skript-Datei]

Hinweis: In der Regel kann man die Bash auch unter dem Namen sh aufrufen. Dadurch verändert sich allerdings ihr Verhalten in einigen Punkten, primär bei der Art der Auswertung von Konfigurationsdateien. Darauf wird im Abschnitt Konfigurationsdateien genauer eingegangen.

Die Bash kann (wie andere Shells auch)

genutzt werden.

Sie bietet eine Vielzahl von Optionen zur Beeinflussung ihres Verhaltens, so dass man sie sehr flexibel konfigurieren und gut an seine persönlichen Bedürfnisse anpassen kann.

Die meisten Optionen lassen sich innerhalb einer laufenden Shell jederzeit durch die eingebauten Kommandos set und shopt, die wir im Abschnitt Modifikation von Shell-Optionen in einer laufenden Shell genauer besprechen, abfragen und ändern. Zur Abfrage von Optionen können auch die nur lesbaren Variablen $- und SHELLOPTS genutzt werden. Eine existierende Umgebungs-Variable SHELLOPTS wird beim Bash-Start ausgewertet, so dass man der Shell auch darüber Optionen übergeben kann.

Zusätzlich zu allen einbuchstabigen Optionen von set stehen beim Aufruf der Bash die folgenden zusätzlichen Optionen zur Verfügung:

-c Kommando-String Ausführung der Kommandos des Kommando-Strings
-i Start einer interaktiven Shell
-l Start einer Login-Shell
-r Start der Shell im eingeschränkten Modus
-s Shell-Kommandos von der Standard-Eingabe lesen

Dies gestattet das Setzen von Positionsparametern einer interaktiven Shell.
-D Ausgabe der Liste aller Strings, die der Locale-spezifischen String-Übersetzung unterliegen, wenn die Locale nicht C oder POSIX lautet, wobei die Strings in Doppelapostrophe eingeschlossen werden und ihnen ein $ vorausgeht

Diese Option impliziert die Option -n , weswegen hier keine Kommandos ausgeführt werden.

Beispiel: Das Kommando

  bash -D -c 'echo $"stdout" stderr $"stdin"'

erzeugt die Ausgabe

  "stdout"
  "stdin"
-O[shopt-Option]
+O[shopt-Option]
Setzen oder Rücksetzen von shopt-Optionen

Mit -O wird die betreffende Option gesetzt, mit +O zurückgesetzt. Wenn die Angabe der shopt-Option fehlt, dann gibt die Bash die Liste aller verfügbaren shopt-Optionen zusammen mit ihrem Vorzugs-Wert auf die Standard-Ausgabe aus. Bei +O erfolgt diese Ausgabe in Form von shopt-Kommandos, mit denen man die Vorzugs-Werte einstellen könnte.
-
--
Diese funktionell äquivalenten Optionen (ein bzw. zwei Minus-Zeichen) kennzeichnen das Ende der Optionen-Liste. Dahinter folgende Argumente werden dann nicht mehr als Optionen gewertet, auch wenn sie mit einem Minus-Zeichen beginnen.

Das Manual erwähnt explizit noch die folgenden beiden Optionen, die allerdings zu den einbuchstabigen set-Optionen gehören:

-v Anzeige der Kommandozeilen, wie sie eingelesen wurden

Kurzform von verbose
-x Anzeige der Kommandozeilen, wie sie ausgeführt werden (nach allen Expandierungen bzw. Substitutionen)

Kurzform von xtrace

Die Bash akzeptiert auch eine Reihe mehrbuchstabiger Aufruf-Optionen. Diese müssen vor evtl. vorhandenen einbuchstabigen Optionen angegeben werden:

--debugger bei (gepatchter) Bash 3.0 Aufruf des Bash-Debuggers von Rocky Bernstein, dessen Interface dem des GNU-Debuggers GDB ähnelt
--dump-strings äquivalent zur Option -D
--dump-po-strings äquivalent zur Option -D, wobei die Ausgabe im PO-Format (Portable Object Format) von gettext erfolgt

Beispiel: Das zum oben bei Option -D verwendeten Kommando analoge Kommando

  bash --dump-po-strings -c 'echo $"stdout" stderr $"stdin"'

erzeugt hier die Ausgabe

  #: -c:0
  msgid "stdout"
  msgstr ""
  #: -c:0
  msgid "stdin"
  msgstr ""
--help kurzen Hilfetext ausgeben, der den Aufruf der Bash erläutert
--init-file Datei
--rcfile Datei
Verwendung der angegebenen Datei an Stelle der Datei ~/.bashrc als persönliche Initialisierungs- bzw. Konfigurations-Datei einer interaktiven Shell
--login äquivalent zur Option -l
--noediting Readline-Bibliothek (flexibler Kommandozeilen-Editor) in einer interaktiven Shell nicht verwenden
--noprofile systemweite oder persönliche Konfigurations-Dateien nicht einlesen, die normalerweise von einer Login-Shell ausgewertet werden
--norc die persönliche Konfigurations-Datei ~/.bashrc in einer interaktiven Shell nicht auswerten

Diese Option wird automatisch gesetzt, wenn man die Bash unter dem Namen sh aufruft.
--posix die Bash im POSIX-Modus starten
--restricted äquivalent zur Option -r
--verbose äquivalent zur Option -v
--version Versions-Information der Bash ausgeben und Shell dann sofort beenden

5. Start einer interaktiven Bash

Eine interaktive Shell (Bash) ist eine Shell, die

Hinweis: Man kann die Positionsparameter einer interaktiven Shell setzen, wenn man sie mit der Option -s aufruft, die erzwingt, dass die Shell ihre Kommandos von der Standard-Eingabe liest. Die inhaltlich identischen zwei Kommandos des folgenden Beispiels starten jeweils eine interaktive Shell, deren erste drei Positionsparameter auf die Werte 1, 2 und 3 gesetzt sind:

  bash -s 1 2 3
  bash -i -s 1 2 3

Die Variable PS1 für den primären Prompt (Bereitschaftszeichen der Shell) ist bei interaktiven Shells im Gegensatz zu nicht-interaktiven Shells gesetzt. Der Wert der Variablen $-, die die aktuell gesetzten einbuchstabigen Shell-Optionen anzeigt, enthält den Buchstaben i, um zu signalisieren, dass es sich um eine interaktive Shell handelt.

Man kann also in einem Skript und damit auch in einer Konfigurationsdatei wie ~/.bashrc feststellen, ob es sich um eine interaktive Shell handelt oder nicht. Hier ein Beispiel:

  case "$-" in
    *i*) echo interaktiv
         ;;
    *)   echo nicht interaktiv
         ;;
  esac

  if [[ -z $PS1 ]]
  then
    echo nicht interaktiv
  else
    echo interaktiv
  fi

Eine interaktive Shell kann konkret folgendermaßen gestartet werden:

Interaktive Shells zeigen durch einen Prompt die Bereitschaft zur Entgegennahme einer Kommandozeile an.

Die vier verfügbaren Bash-Prompts sind konfigurierbar, indem man den folgenden Prompt-Variablen geeignete Prompt-Strings zuweist, die dann in den entsprechenden Situationen als Prompts ausgegeben werden:

Prompt-VariableBedeutung
PS1Primär-Prompt
PS2Sekundär-Prompt (ab Zeile 2 bei mehrzeiligen Kommandos)
PS3Prompt für Menüs beim Kommando select
PS4Prompt für den Trace-Modus (Option -x)

Primär- und Sekundär-Prompt (PS1 und PS2) sind sehr flexibel anpassbar. Über speziell dafür vorgesehene Escape-Sequenzen kann man viele Informationen in den Prompts unterbringen. Ein Beispiel hierzu findet sich im Abschnitt Flexible Prompt-Anpassung.

Kommandos können meist schon "auf Vorrat" (also vor Erscheinen des nächsten Prompts) eingetippt werden. Sie werden in der Regel zwischengespeichert und später ausgeführt, wenn die Shell neue Kommandos akzeptieren kann.


6. Start einer nicht-interaktiven Bash

Auch für den Start einer nicht-interaktiven Bash gibt es mehrere Möglichkeiten:

  bash [Optionen] Skript-Datei [Argumente]

  bash -c Kommando-String [Argumente]

  Kommandos_von_stdin | bash

Das Argument Skript-Datei ist der Name einer Datei, deren Inhalt als Shell-Skript, also eine Folge von Shell-Kommandos betrachtet und von der Shell entsprechend interpretiert wird. Der Kommando-String ist eine Zeichenfolge, die die Shell ebenfalls als Folge von Shell-Kommandos interpretiert.

Hinter der Skript-Datei angegebene Argumente werden der Reihe nach als Positionsparameter $1 ... gesetzt. Argumente, die dem Kommando-String folgen, werden der Reihe nach den Parametern $0 (spezieller Parameter) sowie $1 ... (Positionsparameter) zugewiesen.

Hinweis: Die erste Zeile der Skript-Datei sollte keine Nicht-ASCII-Zeichen (z.B. Umlaute und ß) enthalten, da sonst die Bash die Abarbeitung mit der Bemerkung

  cannot execute binary file

verweigert.

Wie oben schon erwähnt, kann man speziell unter Linux die Bash normalerweise auch unter dem Namen sh statt bash aufrufen, wodurch sich u.a. die Auswertung von Konfigurationsdateien ändert.

Beispiel-Skript hello_world.sh:

  #!/bin/sh

  # der Shell-Variablen text den Wert Hello World zuweisen
  text='Hello World'
  # den Inhalt der Variablen text auf die Standard-Ausgabe ausgeben
  echo "$text"

Aufruf-Beispiele der Bash:

  # Skript hello_world.sh starten
  bash hello_world.sh

  # Skript hello_world.sh starten;
  # alle Zeilen anzeigen, wie sie eingelesen wurden (Option -v) und wie
  # sie (nach allen Expandierungen/Substitutionen) ausgeführt werden
  # (Option -x)
  bash -vx hello_world.sh

  # Ausgabe der Summe 3+4
  bash -c 'echo $((3+4))'

  # Kommando finger über die Standard-Eingabe übergeben und von
  # der Bash ausführen lassen
  echo finger | bash

  # Skript hello_world.sh unter Steuerung des Bash-Debuggers ausführen
  bash --debugger hello_world.sh

Anmerkung zur Interpreter-Zeile (Zeile 1) von Skripten:

Unix-Systeme sind in der Lage, Interpreter-Dateien auszuführen. Das sind Dateien, die mit einer Zeile folgender Form beginnen:

  #! absoluter_Pfad_zum_Interpreter [Optionen]

Beim Start einer ausführbaren Interpreter-Datei wird der angegebene Interpreter (z.B. die Shell, AWK, Perl oder Python) aufgerufen. Ihm übergibt der Unix-Kern drei Argumente:

Beispiele für Interpreter-Zeilen:

  #!/bin/sh

  #!/bin/bash

  #!/bin/sh -x

  #!/bin/sh -

Eine Interpreter-Zeile der Form

  #!/bin/sh -x -v

führt zu einer Fehlermeldung der Shell, da sie die Option -x -v nicht kennt. Man kann hier also keine Liste einzelner Optionen nutzen, weil der Kern alle Zeichen, die zum Optionsfeld der Interpreter-Zeile gehören, geschlossen als ein Argument an den Interpreter weitergibt. Allerdings wäre im konkreten Beispiel die Angabe

  #!/bin/sh -xv

mit dem gewünschten Effekt nutzbar.

Durch die Interpreter-Zeile der Form

  #!/bin/sh -

kann man verhindern, dass der Shell eine ungewollte Option übergeben wird, wenn man die Interpreter-Datei über einen Dateinamen aufruft, der einer Option gleicht.

Würde man beispielsweise ein Skript, dessen Interpreter-Zeile

  #!/bin/sh

lautet, über den Dateinamen -i (harter oder symbolischer Link auf die Skript-Datei) rufen, würde der Shell die Option -i übergeben werden, wodurch man eine interaktive Shell erhielte, das Skript in der Datei aber nicht ausgeführt würde.

Konkret könnte das so passieren:

  ln -s pfad_zum_skript /tmp/-i
  PATH=/tmp
  -i

Wenn es sich bei der Interpreter-Datei um ein Setuid-/Setgid-Programm handelt, könnte dies ggf. dazu führen, dass ein unberechtigter Nutzer eine interaktive Shell starten kann, die nicht mit seinen Rechten, sondern mit den Rechten des Eigentümers der Interpreter-Datei läuft (im Extremfall mit Root-Rechten).

Unter Linux und anderen modernen Unix-Systemen besteht allerdings keine derartige Gefahr, da Setuid-/Setgid-Skripten vom Kern aus Sicherheitsgründen nicht zugelassen werden. Außerdem ist zu erwähnen, dass die Bash nur dann eine Abweichung von realer und effektiver UID (User ID) bzw. GID (Group ID) gestattet, wenn sie im privilegierten Modus (privileged mode) läuft, was sie nur dann tut, wenn explizit die Option p bzw. privileged mit dem Kommando set oder durch die Aufruf-Option -p aktiviert wurde.

Da die Bash standardmäßig nicht im privilegierten Modus läuft, setzt sie nach dem Start die effektive auf die reale UID bzw. GID zurück, sofern diese Werte differieren. Wenn man in einer laufenden Bash den privilegierten Modus verlässt, indem man die Option p deaktiviert (set +p), wird die effektive UID bzw. GID auf den jeweils realen Wert zurückgesetzt.

Falls beim Bash-Start die reale und effektive UID und/oder GID differieren, werden unabhängig davon, ob der privilegierte Modus aktiviert ist, aus Sicherheitsgründen die Umgebungsvariablen ENV und BASH_ENV (und damit Konfigurationsdateien) nicht ausgewertet und Shell-Funktionen nicht aus der Umgebung vererbt. Außerdem wird die Umgebungvariable SHELLOPTS ignoriert.


7. Beendigung der Bash

Eine laufende Bash kann durch das Kommando

  exit

beendet werden.

Eine Login-Shell kann man zusätzlich mit dem Kommando

  logout

beenden. In einer Nicht-Login-Shell führt die Nutzung von logout dagegen zu einer Fehlermeldung:

  bash: logout: not login shell: use `exit'

Eine nicht-interaktive Shell terminiert automatisch immer dann, wenn das ihr zur Ausführung übergebene Skript beendet ist, wenn also die Shell beim Versuch, das nächste Kommando einzulesen, ein EOF (Dateiende-Signal, End Of File) bekommt.

Auch eine interaktive Shell kann man mittels EOF beenden. Dazu muss man mit einer geeigneten Tastenkombination (die meist Ctrl-D lautet, aber mit dem Kommando stty auch geändert werden kann) das EOF-Zeichen als erstes Zeichen einer Kommandozeile eingeben, ggf. mehrmals hintereinander. Der Wert der Shell-Variablen IGNOREEOF gibt dabei an, wie viele EOF-Zeichen hintereinander einzugeben sind, bis die Bash beendet wird. Wenn diese Variable einen nicht-numerischen Wert oder den leeren String als Wert hat, wird sie implizit mit dem Wert 10 angenommen. Falls IGNOREEOF nicht existiert, genügt ein einziges EOF-Zeichen zur Terminierung der Shell.

Außerdem kennt die Bash noch die Option ignoreeof. Sie kann man mit dem Kommando

  set -o ignoreeof

setzen. Dies entspricht der Zuweisung der Zahl 10 an die Variable IGNOREEOF:

  IGNOREEOF=10

Die Option ignoreeof wird von der Bash automatisch immer als gesetzt betrachtet, wenn die Variable IGNOREEOF existiert. Man kann diese Option also auch durch eine Zuweisung an IGNOREEOF setzen. Um sie zurückzusetzen, hat man folglich zwei Möglichkeiten:

  set +o ignoreeof

  unset IGNOREEOF
    # löscht die Variable IGNOREEOF

Eine in einem Terminal-Emulator unter einer grafischen Oberfläche ausgeführte Shell lässt sich auch durch Beendigung des Terminal-Emulators terminieren.


8. Konfigurationsdateien

Von der Art des Aufrufs der Bash hängt ab, welche Konfigurationsdateien sie in welcher Reihenfolge auswertet. Nachfolgend soll eine Auswahl von typischen Konstellationen beschrieben werden.

Wird die Bash nicht über das Kommando bash, sondern sh aufgerufen, verhält sie sich bzgl. der Konfigurationsdateien sowie des Bash-Modus anders.

Beim Aufruf der Bash über das Kommando bash gilt:

Beim Aufruf der Bash über das Kommando sh gilt:

Um also Shell-Skripten ohne Beeinflussung von Konfigurationsdateien auszuführen, sollte man die Bash über das Kommando sh rufen:

  sh skript.sh

Als Interpreter-Zeile von Shell-Skripten empfiehlt sich daher eine der folgenden Formen:

  #! /bin/sh

  #! /bin/sh -

Eine im POSIX-Modus gestartete Bash folgt dem POSIX-Standard für Konfigurationsdateien. Interaktive Shells expandieren dort die Variable ENV, werten das Ergebnis als Dateiname und führen die Kommandos aus, die in dieser Datei stehen. Andere Konfigurationsdateien werden generell nicht gelesen.

Eine interaktive Shell nutzt standardmäßig die GNU-Bibliothek Readline als Benutzerschnittstelle. Sie bietet einen komfortablen und sehr flexibel konfigurierbaren Kommandozeilen-Editor sowie einen Zugriff auf eine History-Liste (Liste früherer Kommandos). Durch die Bash-Option --noediting kann die Verwendung der Readline-Bibliothek unterbunden werden.

Zur Initialisierung der Readline-Bibliothek dient die Datei, deren Name in der Umgebungsvariablen INPUTRC steht. Sie wird beim Start einer Bash eingelesen. Wenn die Variable INPUTRC nicht gesetzt ist, versucht die Shell, der Reihe nach die beiden Dateien /etc/inputrc und ~/.inputrc zu lesen. Erläuterungen dazu folgen weiter unten.

Hinweis: Das Syntax-Highlighting des weit verbreiteten Editors Vim schaltet standardmäßig nur dann in den Bash-Modus um, wenn in Zeile 1 des Skripts

  #! /bin/bash

steht. Um auch bei Bash-Skripten, die mit

  #! /bin/sh

beginnen, den Bash-Modus des Highlightings nutzen zu können, muss man der Vim-Variablen is_bash den Wert 1 zuweisen. Diese Zuweisung sollte in der Datei ~/.vimrc erfolgen:

  let is_bash=1

9. Modifikation von Shell-Optionen in einer laufenden Shell

Shell-Optionen werden mit den Kommandos set und shopt angezeigt und modifiziert. Mit dem von der klassischen Bourne-Shell stammenden set kann man nur auf einen Teil der Optionen, mit dem Bash-spezifischen shopt dagegen auf sämtliche Optionen zugreifen. Eine komplette Beschreibung der Bash-Optionen findet man im Online- oder Referenz-Manual. Dabei empfiehlt es sich vor allem, die Abschnitte zu den eingebauten Kommandos set und shopt sowie zur Aufruf-Syntax der Bash zu konsultieren.

Fast alle mit set veränderbaren Optionen haben neben einer Langform auch noch eine einbuchstabige Kurzform, beispielsweise xtrace und x. Daher sind die folgenden beiden Anweisungen funktionell identisch:

  set -o xtrace
  set -x

Das Minus-Zeichen bei -o und -x bewirkt das Setzen (Aktivieren) der betreffenden Option (im Beispiel also xtrace bzw. x). Um eine solche Option wieder zurückzusetzen bzw. zu deaktivieren, verwendet man statt des Minus-Zeichens ein Plus-Zeichen:

  set +o xtrace
  set +x

Man kann mit einem set-Kommando auch mehrere Optionen setzen und rücksetzen, wobei sich Kurz- und Langform mischen lassen, z.B. so:

  set +o xtrace -m -o pipefail
    # xtrace zurücksetzen und m sowie pipefail setzen

Hinweis: Mit set kann man auch die Positionsparameter einer Shell-Instanz oder einer Shell-Funktion setzen und löschen sowie alle Shell-Variablen mit ihren aktuellen Werten ausgeben lassen. Das wird im Abschnitt Variablen, Parameter und Felder beschrieben.

Das Kommando shopt kennt die Option -s zum Setzen und -u zum Rücksetzen einer Option. Hier zwei Beispiele:

  shopt -s dotglob
  shopt -u dotglob

Statt einer einzelnen Option kann man auch eine Liste angeben:

  shopt -s dotglob nullglob extglob
  shopt -u promptvars shift_verbose

Ein gleichzeitiges Setzen und Rücksetzen von Optionen ist bei shopt aber nicht möglich.

Um mit shopt auch die Optionen von set zu manipulieren, muss man zusätzlich die Option -o verwenden, wie das folgende Beispiel zeigt:

  shopt -u -o xtrace pipefail
  shopt -s -o xtrace pipefail

  oder
  
  shopt -uo xtrace pipefail
  shopt -so xtrace pipefail

Als Argumente sind hier die langen set-Optionen anzugeben.

Die aktuellen Werte der set-Optionen kann man sich durch die Kommandos

  set -o
  shopt -o

  set +o

anzeigen lassen. Sie unterscheiden sich lediglich hinsichtlich des Ausgabe-Formats. Die ersten beiden geben Zeilen der Form

  allexport       off
  braceexpand     on

aus, die vorn die Option und dahinter deren aktuellen Wert enthalten.

Das dritte Kommando generiert set-Kommandos, mit denen man die aktuelle Einstellung wiederherstellen könnte, z.B.:

  set +o allexport
  set -o braceexpand

Die Anzeige der ausschließlich mit shopt einstellbaren Optionen erfolgt so:

  shopt

Hier erhält man Zeilen der Form

  cdable_vars     off
  cdspell         on

Wenn dabei hinter shopt noch Options-Namen als Argumente folgen, werden nur die Einstellungen der genannten Optionen ausgegeben:

  shopt failglob gnu_errfmt huponexit
    # Beispiel-Ausgabe:
    #   failglob        off
    #   gnu_errfmt      off
    #   huponexit       off

Mit den beiden Kommandos

  shopt -s
  shopt -u

kann man sich die gesetzten bzw. nicht gesetzten shopt-Optionen anzeigen lassen. Für die set-Optionen funktioniert dies analog so:

  shopt -s -o
  shopt -u -o

  oder
  
  shopt -so
  shopt -uo

Bei all diesen Anzeige-Kommandos bewirkt die Option -p, dass die Anzeige in Form von Kommandos erfolgt, mit denen man die aktuellen Einstellungen wiederherstellen könnte:

  shopt -p
    # Format der Ausgabe:
    #   shopt -u shift_verbose
    #   shopt -s sourcepath
    #   shopt -u xpg_echo

  shopt -po
    # Format der Ausgabe:
    #   set +o verbose
    #   set +o vi
    #   set +o xtrace

Die shopt-Option -q bewirkt, dass keine Ausgaben erfolgen, sondern das Ergebnis der Abfrage über den Exit-Status mitgeteilt wird. Er lautet 0, wenn alle abgefragten Optionen gesetzt sind, ansonsten 1.

Beispiele:

  shopt -q lithist sourcepath

  shopt -qo physical histexpand

Die Liste der aktuell aktiven einbuchstabigen Optionen kann man auch der Shell-Variablen $- entnehmen. Wenn also z.B. die Option x gesetzt ist, kommt der Buchstabe x im Wert von $- vor. Ein Beispiel-Wert könnte himxBCH lauten. Die analoge Liste der langen set-Optionen ist der Variablen SHELLOPTS zu entnehmen, die z.B. den Wert

  braceexpand:emacs:hashall:histexpand:interactive-comments:monitor:noclobber:physical

haben könnte.

Eine existierende Umgebungsvariable SHELLOPTS wird beim Start der Bash ausgewertet, sofern die Shell nicht im privilegierten Modus läuft. Somit kann man über sie von außen Optionen setzen. In einer laufenden Bash ist SHELLOPTS aber nur lesbar. Eine Wertzuweisung an diese Variable ist daher nicht möglich. Um aus einer Bash eine andere Bash mit einem modifizierten Wert von SHELLOPTS zu starten, kann man das Kommando env verwenden, wie folgendes Beispiel zeigt:

  env SHELLOPTS=xtrace bash

Beispiele zur Anzeige und Modifikation von Optionen, die die wichtigsten Punkte nochmal zusammenfassen:

  set -x            # Option x bzw. xtrace aktivieren
  shopt -so xtrace  # dito mit shopt

  set +x            # Option x bzw. xtrace deaktivieren
  shopt -uo xtrace  # dito mit shopt
                
  # für die set-Option vi gibt es keine Kurzform:
  set -o vi         # Option vi und damit den Vi-Modus des Kommandozeilen-Editors
                    # aktivieren (der Emacs-Modus wird damit automatisch deaktiviert)
  set +o vi         # Option vi und damit den Vi-Modus deaktivieren (der Emacs-Modus
                    # wird dabei nicht automatisch aktiviert)

  # Die analogen beiden shopt-Kommandos sehen so aus:
  shopt -so vi
  shopt -uo vi

  set -o            # Anzeige der aktuellen Einstellung aller set-Optionen
  shopt -o          # dito mit shopt

  set +o            # dito, aber in Form von set-Kommandos
  shopt -po         # dito mit shopt

  shopt -s dotglob  # Option dotglob aktivieren
  shopt -u dotglob  # Option dotglob deaktivieren

  shopt             # Anzeige der aktuellen Einstellung aller shopt-Optionen

  shopt -s          # Anzeige aller gesetzten shopt-Optionen
  shopt -u          # Anzeige aller zurückgesetzten shopt-Optionen
  shopt -so         # Anzeige aller gesetzten set-Optionen
  shopt -uo         # Anzeige aller zurückgesetzten set-Optionen

  echo $SHELLOPTS   # Anzeige der aktiven set-Optionen in der Langform
  echo $-           # Anzeige der aktiven einbuchstabigen set-Optionen

10. Modi der Bash

Die Bash kann in verschiedenen Modi ausgeführt werden, auf die wir hier kurz hinweisen wollen. Standardmäßig läuft die Bash im nativen Modus, den wir im vorliegenden Text bei der Erläuterung von Sachverhalten allgemein unterstellen und nicht gesondert erwähnen.

Weitere Modi sind:

Der mit der Option p bzw. privileged aktivierbare privilegierte Modus wurde bereits beim Start einer nicht-interaktiven Bash beschrieben.

Der eingeschränkte Modus wird aktiviert, wenn man die Bash mit der Option -r bzw. --restricted oder unter dem Namen rbash aufruft. In diesem Modus stehen aus Sicherheitsgründen bestimmte Möglichkeiten der Bash nicht zur Verfügung, z.B. der Verzeichnis-Wechsel mit cd. Damit eignet sich der eingeschränkte Modus z.B. recht gut für die Realisierung von Gast-Accounts. Details zu den Einschränkungen findet man im Online-Manual unter RESTRICTED SHELL.

Der POSIX-Modus wird aktiviert, wenn beim Aufruf der Bash die Option --posix angegeben wurde oder die Umgebungsvariable POSIXLY_CORRECT existierte. In einer laufenden Shell kann man den POSIX-Modus durch

  set -o posix

einschalten. Auch der Aufruf der Bash unter dem Namen sh hat den POSIX-Modus zur Folge, da er hierbei nach dem Einlesen der Konfigurationsdateien automatisch aktiviert wird.

Anmerkung: Man kann die Bash bei der Übersetzung aus den Quellen so konfigurieren, dass sie standardmäßig im POSIX-Modus läuft. Davon ist aber abzuraten.

Im POSIX-Modus orientiert sich die Bash strenger am Standard POSIX 1003.2 und ändert daher an verschiedenen Stellen ihr Verhalten gegenüber dem nativen Modus. Diese Änderungen sind im Referenz-Handbuch, nicht aber im Online-Manual beschrieben und werden in der folgenden Liste aufgeführt, wobei wir nicht alle genannten Punkte im restlichen Text diskutieren werden:

  1. Die Bash verhält sich generell so, also ob die Option checkhash gesetzt wäre. Beim Aufruf eines Kommandos, dessen Hash-Tabellen-Eintrag veraltet ist, wird daher automatisch eine neue Suche des Kommandos im Such-Pfad (PATH) veranlasst.

  2. Die Nachricht, die die Bash ausgibt, wenn ein Hintergrund-Job mit einem Exit-Status ungleich 0 (also fehlerhaft) beendet wird, lautet

      Done(Exit-Status)

    In runden Klammern wird also der Exit-Status angegeben.

  3. Die Nachricht, die die Bash ausgibt, wenn ein Job suspendiert wurde, lautet

      Stopped(Signal-Name)

    In runden Klammern wird also der Signal-Name des Stopp-Signals angegeben, z.B. SIGTSTP.

  4. Die vom eingebauten Kommando bg ausgegebene Beschreibung der in den Hintergrund verschobenen Jobs enthält keine Information darüber, ob der Job der aktuelle oder der vorher aktuelle Job ist.

  5. Reservierte Wörter, die in einem Kontext auftauchen, in dem reservierte Wörter erkannt werden, unterliegen keiner Alias-Expandierung.

  6. Unabhängig von der Option promptvars wird in den Werten der Prompt-Variablen PS1 und PS2 das Zeichen ! zur History-Nummer und die Zeichenfolge !! zu ! expandiert. Im Gegensatz zu den sonstigen Spezialzeichen des Bash-Prompts geht diesen beiden hier kein Backslash voran.

  7. Statt der normalen Bash-Konfigurationsdateien wird nur die Datei gelesen, deren Name in der Shell-Variablen ENV steht.

  8. Die Tilde-Expandierung erfolgt nicht in allen Zuweisungen einer Zeile, sondern nur in denjenigen, die einem Kommando-Namen vorausgehen.

  9. Der Vorzugswert der Variablen HISTFILE lautet ~/.sh_history. Das ist die Default-History-Datei.

  10. kill -l gibt alle Signal-Namen getrennt durch Leerzeichen auf einer einzigen Zeile aus, wobei das Präfix SIG weggelassen wird.

  11. Das Builtin-Kommando kill akzeptiert keine Signal-Namen mit dem SIG-Präfix.

  12. Nicht-interaktive Shells werden beendet, wenn eine mit dem Kommando source bzw. . einzulesende Datei nicht gefunden wird.

  13. Nicht-interaktive Shells werden beendet, wenn ein auszuwertender arithmetischer Ausdruck einen Syntaxfehler aufweist.

  14. Umlenkungs-Operatoren realisieren in nicht-interaktiven Shells keine Pfadnamens-Expandierung in dem dahinter folgenden Wort.

  15. Umlenkungs-Operatoren realisieren in dem dahinter folgenden Wort keine Aufspaltung in Wörter (Word Splitting).

  16. Funktionsnamen müssen gültige Shell-Bezeichner sein. Sie dürfen also nur Buchstaben, Ziffern und Unterstriche enthalten und dürfen nicht mit einer Ziffer beginnen. Die Verwendung ungültiger Namen bei einer Funktionsdefinition führt in nicht-interaktiven Shells zu einem fatalen Fehler, also zum Beenden der Shell.

  17. Die POSIX-Spezial-Builtin-Kommandos (POSIX special builtins), die man sich mit enable -s anzeigen lassen kann, werden vor gleichnamigen Shell-Funktionen ausgeführt. Zu diesen Spezial-Builtin-Kommandos gehören folgende:

      .
      :
      break
      continue
      eval
      exec
      exit
      export
      readonly
      return
      set
      shift
      source
      trap
      unset
  18. Wenn die Spezial-Builtin-Kommandos einen Fehler-Status liefern, wird eine nicht-interaktive Shell beendet. Zu den fatalen Fehlern gehören beispielsweise die Angabe inkorrekter Optionen, Umlenkungs-Fehler und Fehler bei Variablen-Zuweisungen, die einem Kommando vorausgehen.

  19. Wenn cd sein Zielverzeichnis über die Shell-Variable CDPATH findet, dann enthält der dabei an die Variable PWD zugewiesene Wert keine symbolischen Links. Implizit wird also cd -P ausgeführt.

  20. Wenn die Shell-Variable CDPATH gesetzt ist, wird beim Kommando cd nicht automatisch das aktuelle Verzeichnis implizit angehängt. Somit schlägt cd fehl, wenn das als Argument angegebene Ziel-Verzeichnis zwar im aktuellen, aber in keinem der explizit in CDPATH genannten Verzeichnisse vorkommt.

  21. Eine nicht-interaktive Shell wird mit einem Fehler-Status beendet, wenn hinter einer fehlerhaften Variablen-Zuweisung (z.B. einer Zuweisung an eine nur lesbare Variable) kein Kommando folgt.

  22. Eine nicht-interaktive Shell wird mit einem Fehler-Status beendet, wenn die Iterations-Variable einer for-Schleife oder die Auswahl-Variable einer select-Anweisung nur lesbar ist.

  23. Die Prozess-Substitution ist nicht verfügbar.

  24. Variablen-Zuweisungen, die vor POSIX-Spezial-Builtin-Kommandos stehen, verbleiben dauerhaft in der Umgebung, sind also auch nach der Ausführung des betreffenden Builtin-Kommandos gültig.

  25. Variablen-Zuweisungen, die vor dem Aufruf einer Shell-Funktion stehen, verbleiben dauerhaft in der Umgebung, wenn innerhalb der Funktion ein POSIX-Spezial-Builtin-Kommando ausgeführt wurde.

  26. Die Builtin-Kommandos export und readonly zeigen ihre Ausgaben in dem von POSIX 1003.2 geforderten Format an.

  27. Das Builtin-Kommando trap zeigt Signal-Namen ohne das Präfix SIG an.

  28. Das Builtin-Kommando trap untersucht nicht, ob es sich bei seinem ersten Argument um eine Signal-Spezifikation handelt, was im nativen Modus zur Folge hätte, dass für alle genannten Signale die Original-Einstellung reaktiviert würde, also diejenige, die beim Start der Bash aktiv war. Im POSIX-Modus kann man die Original-Einstellung von Signalen reaktivieren, wenn man als erstes Argument eine gültige Signal-Nummer oder ein Minus-Zeichen angibt, wobei sich letzteres empfiehlt.

  29. Die Builtin-Kommandos . und source suchen nicht im aktuellen Verzeichnis nach der einzulesenden Datei, nachdem diese über den Such-Pfad PATH nicht gefunden wurde.

  30. Sub-Shells, die zum Zwecke der Kommando-Substitution gestartet werden, erben den Wert der Option -e von ihrer Eltern-Shell. Im nativen Modus deaktiviert die Bash diese Option generell für derartige Sub-Shells.

  31. Alias-Expandierungen sind standardmäßig immer verfügbar, auch in nicht-interaktiven Shells.

  32. Das eingebaute Kommando alias zeigt die Liste der Alias-Definitionen ohne das Präfix alias an, es sei denn, die Option -p wurde angegeben.

  33. Wenn das eingebaute Kommando set ohne Optionen aufgerufen wird, zeigt es keine Namen und Definitionen von Shell-Funktionen an.

  34. Wenn das eingebaute Kommando set ohne Optionen aufgerufen wird, zeigt es Variablenwerte ohne umschließende Quotierungs-Zeichen an, es sei denn, sie enthalten Shell-Metazeichen. D.h., das Auftreten nicht-druckbarer Zeichen im Variablenwert zieht keine Quotierung nach sich.

  35. Wenn das eingebaute Kommando cd im logischen Modus (Option -L) aufgerufen wird und der aus dem logischen Pfad zum aktuellen Verzeichnis sowie dem als Argument angegebenen Verzeichnis generierte Pfad nicht existiert, dann schlägt cd fehl, statt in den physischen Modus (Option -P) zurückzufallen.

    Ein Beispiel hierzu findet sich bei der Beschreibung des Kommandos cd.

Folgende Features von POSIX 1003.2 sind in der Bash nicht implementiert:

  1. Zuweisungen beeinflussen die Ausführungs-Umgebung aller Builtin-Kommandos und nicht nur die Umgebung der POSIX-Spezial-Builtin-Kommandos.

  2. Wenn die Bash eine Sub-Shell kreiert, um ein Shell-Skript auszuführen, das in einer (laut Zugriffsrechten) ausführbaren Datei ohne Interpreter-Zeile (#!) gespeichert ist, dann übergibt sie an die Variable $0 des Skripts den durch eine PATH-Suche ermittelten absoluten Pfadnamen des Skripts und nicht das ggf. abweichende Kommando, das der Nutzer angegeben hat.

  3. Wenn die Bash ein im Suchpfad PATH gefundenes externes Skript mit dem Kommando . bzw. source einliest, dann prüft sie in den Zugriffsrechten die Ausführbarkeit und nicht die Lesbarkeit des Skripts, als ob sie eine Kommando-Suche ausführen würde.


11. Terminologie

Im Online-Manual der Bash wird folgende Terminologie verwendet:

Leerraum (blank) Leerzeichen oder Tabulator
Wort oder Token (word, token) Zeichenfolge, die von der Shell als einzelne Einheit betrachtet wird
Name oder Bezeichner (name, identifier) Wort, das nur aus alphanumerischen Zeichen und Unterstrichen (underscores) besteht und mit einem Buchstaben oder Unterstrich beginnt
Metazeichen (metacharacter) Zeichen mit (nicht durch Quotierung aufgehobener) Sonderbedeutung, das als Worttrenner wirkt:
  |  &  ;  (  )  <  >  Leerzeichen  Tabulator
Anmerkung: In der Literatur wird der Begriff Metazeichen oft weiter gefasst. Darunter versteht man häufig alle Zeichen, die in der Shell standardmäßig eine Sonderbedeutung haben und somit nicht für sich selbst stehen. Wir werden nachfolgend meist auch diese allgemeinere Bedeutung unterstellen.
Operator (control operator) Token mit Steuerfunktion:
  ||  &  &&  ;  ;;  (  )  |  Newline
Reservierte Wörter (reserved words) Wörter mit (nicht durch Quotierung aufgehobener) Sonderbedeutung für die Shell, wenn sie als erstes Wort eines einfachen Kommandos oder als drittes Wort eines case- oder for-Kommandos stehen:
  !  case  do  done  elif  else  esac  fi  for  function
  if  in  select  then  until  while  {  }  time  [[  ]]

Anmerkung: Die Variable IFS (Internal Field Separator) wird bei der Bash nur an zwei Stellen im Rahmen des Word Splitting (Aufteilung eines Strings in Wörter) verwendet:

  1. nach Expandierungen/Substitutionen bei der Kommandozeilen-Auswertung,

  2. beim Kommando read zum Zerlegen des eingelesenen Strings.

Beim Aufspalten von Kommandozeilen in einzelne Token spielt IFS dagegen keine Rolle. Hier werden nur die o.g. Metazeichen als Trenner verwendet.

Beispiele zur Nutung von IFS:

  # alten Wert von IFS merken
  ORIG_IFS=$IFS

  # IFS auf den Doppelpunkt setzen
  IFS=:

  # über den Datei-Deskriptor 3 die Datei /etc/passwd zum Lesen öffnen
  exec 3< /etc/passwd

  # in einer while-Schleife die Zeilen der Datei vom Deskriptor 3 einlesen,
  # die einzelnen Wörter einer Zeile einem Feld (Array) zuordnen und über eine
  # for-Schleife die Feld-Elemente (Wörter der Zeile) auf separaten Zeilen
  # ausgeben
  while read -a felder -u 3
  do
    for i in "${felder[@]}"
    do
      echo "$i"
    done
    echo '======='
  done

  # IFS auf den alten Wert setzen, um z.B. Listen normal expandieren zu lassen
  IFS=$ORIG_IFS

  ZAHLEN="1 2 3 4 5 6"
  for i in $ZAHLEN
  do
    echo "$i"
  done

Komplexeres Beispiel: SCRIPTS/passwd_ausgabe.sh

Bei der Besprechung der Aufspaltung in Wörter wird die Verwendung von IFS noch detaillierter diskutiert.


12. Kommentare

Ein mit einem Doppelkreuz beginnendes Wort leitet einen Kommentar ein:

  # Ich bin ein Kommentar.
  echo 123 # Ausgabe von 123

Wenn die (standardmäßig eingeschaltete) Option interactive_comments aktiv ist, kann man Kommentare auch in interaktiven Shells verwenden. Mit den folgenden beiden Kommandos lässt sich diese Option ein- bzw. ausschalten:

  shopt -s interactive_comments

  shopt -u interactive_comments

13. Bash-Grammatik

Die Bash-Grammatik kennt sechs zentrale Elemente:

  1. einfache Kommandos

  2. Pipelines

  3. Listen

  4. zusammengesetzte Kommandos

  5. Koprozesse (ab Bash 4)

  6. Funktions-Definitionen


14. Einfache Kommandos

Syntaktischer Aufbau:

Das erste Wort spezifiziert das auszuführende Kommando. Die Folgewörter sind Optionen/Argumente, die an das Kommando weitergegeben werden.

Mit dem Kommando type kann man den Typ eines Kommandos ermitteln. Fünf Typen sind möglich:

  1. eingebaut (builtin), also ein internes Kommando der Shell

  2. extern, also ein externes Programm im Dateisystem, das von der Shell aufgerufen wird

  3. Alias, also der Name eines über das Kommando alias definierten Alias-Konstrukts, der als Abkürzung oder alternativer Bezeichner für ein anderes Shell-Konstrukt dient

  4. Funktion

  5. reserviertes Wort

Alle Kommandos liefern einen Rückkehrwert. Er entspricht dem Exit-Status des Kommandos oder 128+n, falls das Kommando durch Signal n beendet wurde.

Typischerweise signalisiert der Exit-Status 0 den "Erfolg" (also die erfolgreiche bzw. fehlerfreie Ausführung eines Kommandos). Andere Werte weisen in der Regel auf einen Fehler hin.

Der Exit-Status lässt sich abfragen und zur Programmsteuerung nutzen. Den Exit-Status eines Shell-Skripts kann man durch das Kommando exit gezielt setzen.

Bei gleichnamigen Konstrukten gilt folgende Ausführungs-Reihenfolge:

  1. Alias

  2. reserviertes Wort

  3. Shell-Funktion

  4. eingebautes Kommando

  5. externes Kommando

Anmerkung: Es ist zulässig, auch reservierte Wörter als Alias-Namen oder Namen von Shell-Funktionen zu verwenden.

Es gibt verschiedene Möglichkeiten zur Beeinflussung der Ausführungs-Reihenfolge:

Beispiele für einfache Kommandos:

  id      # UID/GIDs des aktuellen Nutzers anzeigen
  pwd     # aktuelles Verzeichnis anzeigen
  cd /etc # Wechsel des aktuellen Verzeichnisses nach /etc

  type cd # Typ des Kommandos cd ermitteln

  # mit Muster für Datei-/Pfadnamen und Ausgabe-Umlenkung:
  #
  # -  ho* bezeichnet Dateien, deren Name mit ho beginnt
  # -  [a-gp]* bezeichnet Dateien, deren Name mit einem der Buchstaben
  #    a bis g oder p beginnt
  # -  [!a]*.? bezeichnet Dateien, deren Name nicht mit dem Buchstaben
  #    a beginnt und dessen vorletztes Zeichen ein Punkt ist
  # -  [^a]*.? ist eine andere Notation für [!a]*.?
  # -  > /tmp/erg.txt lenkt die Standard-Ausgabe in die Datei /tmp/erg.txt um,
  #    wobei eine existierende Datei überschrieben wird, sofern die Option
  #    noclobber nicht aktiv ist
  # -  >> /tmp/erg.txt lenkt die Standard-Ausgabe in die Datei /tmp/erg.txt um,
  #    wobei eine existierende Datei fortgeschrieben wird
  #
  # Achtung:
  # Welche Zeichen zu einem Zeichenbereich, z.B. [a-g], gehören, hängt
  # von der sog. Locale (Lokalisierung, Anpassung der landessprachlichen
  # Einstellungen) ab, in der die Bash bzw. ein Bash-Kommando ausgeführt wird.
  # 
  # So umfasst z.B. der Zeichenbereich [a-z] in der Locale C nur
  # genau alle Kleinbuchstaben. In der Locale de_DE.UTF-8 gehören neben
  # den Kleinbuchstaben auch noch die korrespondierenden Großbuchstaben sowie
  # Umlaute und andere von den Grundbuchstaben durch diakritische Zeichen
  # abgeleitete Zeichen dazu. 
  #
  # Mit Hilfe der POSIX-Zeichenklassen kann man Locale-unabhängige
  # Zeichenbereiche bilden. Z.B. umfasst die POSIX-Zeichenklasse lower
  # alle Kleinbuchstaben.
  #
  # Nutzungsbeispiel:
  #   echo [[:lower:]]*
  #
  # Weitere Informationen dazu finden sich im Abschnitt
  # Metazeichen für Pfadnamens-Muster.
  #
  ls -ld ho* [a-gp]* > /tmp/erg.txt
  echo [!a]*.? >> /tmp/erg.txt
  echo [^a]*.? >> /tmp/erg.txt

  # Datei /tmp/erg.txt anzeigen
  cat /tmp/erg.txt

  # Wurzel des Dateibaums löschen (funktioniert nicht; erzeugt eine Fehlermeldung)
  rm /
  # Umgebungsvariable LC_ALL auf C setzen
  # (die Locale C bewirkt englische Fehlermeldungen)
  LC_ALL=C rm /

  # $? hält den Exit-Status des letzten Vordergrund-Kommandos
  echo $?

  # Exit-Status 1 setzen und Skript beenden
  exit 1


  # Beispiele für gleichnamige Konstrukte

  # Aliase pwd und if definieren
  alias pwd='echo Alias pwd'
  alias if='echo Alias if'

  # Shell-Funktionen pwd und if definieren
  function pwd()
  {
    echo Funktion pwd
  }

  function if()
  {
    echo Funktion if
  }

  # Aufrufe von pwd
  pwd           # Alias-Expandierung zu echo Alias pwd
  \pwd          # Funktions-Aufruf; Ausführung von echo Funktion pwd
  builtin pwd   # Aufruf des eingebauten Kommandos pwd
  command pwd   # Aufruf des (eingebauten) Kommandos pwd
  /bin/pwd      # Aufruf des externen Kommandos /bin/pwd
  
  enable -n pwd # eingebautes Kommando pwd deaktivieren
  pwd           # externes Kommando pwd rufen, z.B. /bin/pwd (laut Such-Pfad)
  enable pwd    # eingebautes Kommando pwd wieder aktivieren

  # Aufrufe von if
  if            # Alias-Expandierung zu echo Alias if
  \if           # Funktions-Aufruf; Ausführung von echo Funktion if
                # Durch die Quotierung wird if nicht als reserviertes Wort
                # betrachtet.
  unalias if    # den Alias if löschen
  if            # if wirkt nun als reserviertes Wort und leitet
                # eine (vollständige oder unvollständige) Alternative ein

  # Hinweis zu type:
  type if
    # Ausgabe: if is a shell keyword
    # Da der Alias if gelöscht wurde, wird if korrekt als
    # reserviertes Wort gewertet. Allerdings kann man mit type
    # nicht erkennen, dass man mit \if eine Funktion aufruft:
  type \if
    # Ausgabe wie oben: if is a shell keyword
    #
    # Dieses Verhalten ist logisch:
    # An das eingebaute Kommando type wird das Argument if übergeben.
    # Der Backslash als Quotierungs-Zeichen wird ganz normal entfernt.
    #
    # Wenn wir allerdings den Alias if wieder definieren, meldet type,
    # dass es sich um einen Alias handelt:
  alias if='echo Alias if'
  type if
    # Ausgabe: if is aliased to `echo Alias if'

15. Such-Pfad

Der Such-Pfad ist eine Liste von durch je einen Doppelpunkt voneinander getrennten Verzeichnissen, die nach externen Kommandos zu durchsuchen sind:

  # Suchpfad anzeigen
  echo "$PATH"
  # /tmp als erstes Element in den Suchpfad aufnehmen
  PATH="/tmp:$PATH"

Die Bash verwaltet eine Hash-Tabelle, in der sie sich automatisch alle bereits aufgerufenen externen Kommandos zusammen mit deren absolutem Pfad merkt. Wenn ein externes Kommando aufgerufen werden soll, prüft die Bash zuerst, ob dieses Kommando in der Hash-Tabelle zu finden ist. Falls ja, verwendet sie den dort gespeicherten absoluten Pfad. Anderenfalls durchsucht sie die in PATH genannten Verzeichnisse nach dem Kommando. Wurde es gefunden, ergänzt die Bash diesen Treffer in der Hash-Tabelle.

Wenn die shopt-Option checkhash aktiv ist, prüft die Bash, ob ein in der Hash-Tabelle gefundenes Kommando existiert, bevor sie versucht, es auszuführen. Wenn es nicht mehr existiert, wird der veraltete Eintrag gelöscht und eine normale PATH-Suche ausgeführt. Bei deaktivierter Option checkhash wird ein veralteter Pfad aus der Hash-Tabelle ohne Prüfung verwendet, was dann natürlich zu einem Fehler führt.

Die Hash-Tabelle kann mit dem eingebauten Kommando hash angezeigt und modifiziert werden.


16. Ein-/Ausgabe-Umlenkung

Standardmäßig startet die Shell die Kommandos mit drei offenen Dateien (Kanälen):

Kanalbezeichnung Kanalnummer bzw. Datei- oder File-Deskriptor
Standard-Eingabe (stdin) 0
Standard-Ausgabe (stdout) 1
Standard-Fehler (stderr) 2

Diese Kanäle sind im Standardfall mit dem Terminal verbunden. Mittels der Ein-/Ausgabe-Umlenkung kann man sie mit einer anderen Quelle oder Senke verbinden: reguläre Datei, Named Pipe (benannte Pipe, FIFO), Gerätedatei (z.B. /dev/zero oder /dev/null),  ...

Durch explizite Angabe können auch die Datei-Deskriptoren >= 3 in der Shell genutzt werden. Generell sind nur natürliche Zahlen als Datei-Deskriptor zulässig.

Umlenkungen können sich auf einzelne Kommandos, Kommando-Gruppen sowie die aktuelle Shell im Ganzen beziehen. Letzteres legt die Standard-Umlenkungen für alle nachfolgend ausgeführten Kommandos fest.

Umlenkungen können an beliebigen Stellen innerhalb eines einfachen Kommandos stehen oder einem Kommando folgen. Sie werden von links nach rechts ausgewertet. Ihre Reihenfolge ist daher teilweise wichtig:

  # stdout/stderr gemeinsam nach file umlenken
  ls > file 2>&1

  # nur stdout nach file umlenken;
  # stderr wird zuvor auf die vorherige Einstellung von stdout umgelenkt
  ls 2>&1 > file

Umlenkungs-Operatoren bestehen aus einem oder zwei Zeichen. Operatoren für Eingabe-Umlenkungen beginnen mit dem Zeichen < und Operatoren für Ausgabe-Umlenkungen mit dem Zeichen >. Wenn in den folgenden Beschreibungen die Angabe des Datei-Deskriptors fehlt, beziehen sich Eingabe-Umlenkungen immer auf die Standard-Eingabe (Deskriptor 0) und Ausgabe-Umlenkungen immer auf die Standard-Ausgabe (Deskriptor 1).

Das Wort, das dem Umlenkungs-Operator folgt, unterliegt standardmäßig einer Reihe von Expandierungen: Expandierung geschweifter Klammern, Tilde-Expandierung, Parameter-, Kommando- und arithmetische Substitutionen, Aufspaltung in Wörter, Pfadnamens-Expandierung sowie Entfernung der Quotierungs-Zeichen. Wenn im Ergebnis mehr als ein Wort entsteht, meldet die Bash einen Fehler.

Die Bash kennt bei den Umlenkungen verschiedene Spezialdateien:

  /dev/fd/fd          # Deskriptor fd
  /dev/stdin          # Deskriptor 0
  /dev/stdout         # Deskriptor 1
  /dev/stderr         # Deskriptor 2
  /dev/tcp/host/port  # TCP-Verbindung zu Host/Port
  /dev/udp/host/port  # UDP-Verbindung zu Host/Port

Nachfolgend werden die verschiedenen Umlenkungen mit den dafür vorgesehenen Umlenkungs-Operatoren (Metazeichen) aufgelistet, wobei die Operatoren zur Hervorhebung rot geschrieben sind:

Eingabe-Umlenkung:

Format:

  [n]<Wort

Die Datei, deren Name sich durch die Expandierungen von Wort ergibt, wird zum Lesen geöffnet und mit dem Deskriptor n (Standard: 0) verbunden.

Ausgabe-Umlenkung:

Format:

  [n]>Wort

Die Datei, deren Name sich durch die Expandierungen von Wort ergibt, wird zum Schreiben geöffnet und mit dem Deskriptor n (Standard: 1) verbunden. Falls die Datei noch nicht existiert, wird sie erzeugt. Andernfalls wird sie auf die Länge Null gekürzt (der alte Inhalt geht also verloren).

Wenn die set-Option noclobber gesetzt ist, scheitert die Ausgabe-Umlenkung, wenn die Ausgabe-Datei bereits existiert.

Eine existierende Datei kann (sofern man die nötigen Rechte dafür besitzt) durch eine Ausgabe-Umlenkung überschrieben werden, wenn man die Option noclobber deaktiviert oder den Umlenkungs-Operator

  [n]>|Wort

verwendet.

Ausgabe-Umlenkung mit Anfügung:

Format:

  [n]>>Wort

Die Datei, deren Name sich durch die Expandierungen von Wort ergibt, wird zum Schreiben im Anfüge-Modus geöffnet und mit dem Deskriptor n verbunden. Eine existierende Datei wird also fortgeschrieben. Eine noch nicht existente Datei wird (wie oben bei der Ausgabe-Umlenkung) erzeugt.

Gemeinsame Umlenkung von Standard-Ausgabe und -Fehlerstrom:

Formate:

  &>Wort
  >&Wort

Standard-Ausgabe und -Fehlerstrom werden in die Datei umgelenkt, deren Name sich durch die Expandierungen von Wort ergibt. Beide genannten Formate sind funktionell identisch und äquivalent zu

  >Wort 2>&1

Der Operator &> sollte generell bevorzugt werden.

Bei gesetzter Option noclobber scheitert diese gemeinsame Umlenkung ebenso wie die oben beschriebene einfache Ausgabe-Umlenkung. Man kann sich allerdings auch hier über noclobber hinwegsetzen:

  >|Wort 2>&1

Hinweis: Der bei tcsh und zsh existierende Operator |& zur gemeinsamen Umlenkung von Standard-Ausgabe und -Fehlerstrom in eine Pipe steht bei der Bash erst ab Version 4 zur Verfügung. Bis Bash 3 muss der Standard-Fehlerstrom explizit vor dem Pipe-Zeichen umgelenkt werden:

  # Nutzung |& bei tcsh und zsh
  diff -ru verzeichnis1 verzeichnis2 |& vim -
  # äquivalentes Kommando bei der Bash
  diff -ru verzeichnis1 verzeichnis2 2>&1 | vim -

Hier-Dokumente:

Format:

  <<[-]Wort
    Hier-Dokument
  Begrenzer

Die Standard-Eingabe wird auf ein Hier-Dokument (here document), d.h. eine Zeilenfolge umgelenkt, die an der aktuellen Stelle eines Shell-Skripts (also "hier") angegeben ist. Dadurch kann man die im Rahmen einer Eingabe-Umlenkung zu lesenden Daten im aktuellen Shell-Skript unterbringen und so auf eine externe Datei verzichten.

Der Begrenzer entspricht dem Wort nach der Entfernung evtl. Quotierungs-Zeichen. Er steht in der Regel alleine auf einer Zeile. Hinter ihm folgt also sofort ein Newline-Zeichen. Leerräume hinter dem Begrenzer sind generell nicht zulässig. Dem Begrenzer dürfen Tabulatoren vorausgehen, sofern vor dem Wort ein Minus-Zeichen steht. Andere Zeichen vor dem Begrenzer sind unzulässig.

Ein - vor Wort bewirkt das Streichen von Tabulator-Zeichen (nicht aber Leerzeichen!!) am Anfang jeder Zeile des Hier-Dokuments einschließlich der Begrenzer-Zeile. Damit kann man ein Hier-Dokument im Shell-Skript geeignet einrücken, ohne seinen Inhalt zu verändern.

Das Wort selbst unterliegt keiner Parameter-, Kommando-, arithmetischen und Pfadnamens-Expandierung. Allerdings werden standardmäßig im Hier-Dokument Parameter-, Kommando- und arithmetische Expandierungen durchgeführt und die Zeichenfolge Backslash Newline ignoriert.

Die Expandierungen kann man selektiv unterbinden, indem man die Sonderbedeutung der Zeichen

  \  $  `

durch Voranstellen eines Backslashs aufhebt.

Wenn man (mindestens) ein Zeichen von Wort quotiert, werden Expandierungen innerhalb des Hier-Dokuments komplett unterbunden.

Hier-Strings:

Format:

  <<<Wort

Das ist eine Variante der Hier-Dokumente. Das Wort wird expandiert und dann im Rahmen der Eingabe-Umlenkung als Eingabe verwendet. Dabei wird hinten an das Expansions-Ergebnis immer automatisch ein Newline-Zeichen (Zeilenschaltung) angehängt.

Duplizierung von Datei-Deskriptoren:

Formate:

  [n]<&Wort
  [n]>&Wort

Format 1:

Der Operator <& dupliziert Eingabe-Datei-Deskriptoren. Wenn im Ergebnis der Expandierung von Wort eine Ziffernfolge (also eine natürliche Zahl) entsteht, dann wird der Deskriptor n (Standard: 0) als Kopie des durch Zahl benannten Deskriptors eingerichtet. Beide Deskriptoren verweisen dann auf dieselbe Datei.

Falls die Zahl keinen zum Lesen geöffneten Deskriptor benennt, erfolgt eine Fehlermeldung.

Sofern sich bei der Expandierung von Wort ein Minus-Zeichen ergibt, dann wird der Deskriptor n (Standard: 0) komplett geschlossen, unabhängig davon, ob er zum Lesen und/oder Schreiben geöffnet war.

Format 2:

Der Operator >& dupliziert Ausgabe-Datei-Deskriptoren. Wenn im Ergebnis der Expandierung von Wort eine Ziffernfolge (also eine natürliche Zahl) entsteht, dann wird der Deskriptor n (Standard: 1) als Kopie des durch Zahl benannten Deskriptors eingerichtet. Beide Deskriptoren verweisen dann auf dieselbe Datei.

Wenn die Zahl keinen zum Schreiben geöffneten Deskriptor benennt, erfolgt eine Fehlermeldung.

Wenn sich bei der Expandierung von Wort ein Minus-Zeichen ergibt, dann wird der Deskriptor n (Standard: 1) komplett geschlossen, unabhängig davon, ob er zum Lesen und/oder Schreiben geöffnet war.

Wenn n fehlt und Wort weder zu einer Ziffernfolge noch zu einem Minus-Zeichen expandiert, dann liegt der obige Spezialfall der gemeinsamen Umlenkung von Standard-Ausgabe und -Fehlerstrom vor.

Verschieben von Datei-Deskriptoren:

Formate:

  [n]<&Wort-
  [n]>&Wort-

Wort muss hier zu genau einer natürlichen Zahl expandiert werden. Format 1 verschiebt den Datei-Deskriptor Zahl nach Datei-Deskriptor n (Standard: 0) und schließt danach Deskriptor Zahl. Format 2 verschiebt den Datei-Deskriptor Zahl nach Datei-Deskriptor n (Standard: 1) und schließt danach Deskriptor Zahl.

Datei-Deskriptoren zum Lesen und Schreiben öffnen:

Format:

  [n]<>Wort

Die Datei, deren Name sich durch die Expandierungen von Wort ergibt, wird zum Lesen und Schreiben geöffnet und mit dem Deskriptor n (Standard: 0) verbunden. Eine nicht existierende Datei wird erzeugt.

Hinweis zu Leerräumen bei Umlenkungs-Operatoren:

Das hinter einem Operator folgende Wort kann dem Operator unmittelbar folgen oder auch durch Leerräume von ihm getrennt sein. Die Wirkung unterscheidet sich dabei nicht. Daher sind die folgenden beiden Zeilen inhaltlich identisch:

  ls > /tmp/ls.txt
  ls >/tmp/ls.txt

Zwischen einer optionalen Zahl n, die dem Operator vorausgeht, sowie dem Operator selbst darf kein Leerraum stehen. Zwischen den Zeichen eines aus 2 Zeichen bestehenden Operators (z.B. <>) darf ebenfalls kein Leerraum stehen. Das folgende Beispiel soll dies veranschaulichen:

  # korrekte Umlenkung des Deskriptors 2 (stderr) auf Deskriptor 1 (stdout):
  exec 2>& 1

  # falsch:
  exec 2 >& 1
  exec 2 > & 1
  exec 2> & 1

Sofern einem Operator keine Zahl n vorausgeht, muss der Operator von einem vorausgehenden Wort nicht durch Leerräume getrennt werden. Die folgenden Zeilen sind deshalb wirkungsgleich:

  ls>& /tmp/ls.txt
  ls >&/tmp/ls.txt
  ls >& /tmp/ls.txt

Beispiel zu Deskriptor-Manipulationen: Skript SCRIPTS/deskr.sh

  #!/bin/sh
  #
  # Deskriptor-Manipulationen in der Bash
  #
  # 5.9.2008
  
  # die folgenden beiden read-Kommandos lesen 2 Mal die erste (und damit
  # dieselbe) Zeile aus $file, da durch die getrennten Umlenkungen die Datei 2
  # Mal separat geöffnet wird
  file=/etc/services
  read -r < $file ; echo "$REPLY"
  read -r < $file ; echo "$REPLY"
  
  echo '====='
  # die ersten 10 Zeilen aus $file einlesen und mit vorangestellter Zeilennummer
  # ausgeben
  for ((i = 0; i < 10; i++))
  {
    read -r ; echo "$i: $REPLY"
  } < $file
  
  echo '====='
  # $file unter FD (File-Deskriptor) 3 zum Lesen öffnen
  exec 3< $file
  # die ersten beiden Zeilen von FD 3 lesen und ausgeben; für den Zugriff auf den
  # Deskriptor 3 nutzt Variante 1 die Eingabe-Umlenkung und Variante 2 die Option
  # -u von read
  read -r <&3 ; echo "$REPLY"
  read -r -u 3 ; echo "$REPLY"
  # FD 3 wieder schließen
  exec 3<&-
  
  echo '====='
  # FD 0 nach FD 3 duplizieren und $file unter FD 0 öffnen
  exec 3<&0 < $file
  # die ersten beiden Zeilen von FD 0 lesen
  read -r ; echo "$REPLY"
  read -r ; echo "$REPLY"
  
  echo '====='
  # FD 3 (Kopie des alten FD 0) nach FD 0 verschieben und FD 3 schließen
  exec <&3-
  # noch eine Zeile vom alten (beim Start des Skripts aktuellen) FD 0 lesen
  read -r ; echo "$REPLY"

Aufruf des Skripts:

  echo xyz | bash deskr.sh

Beispiel zur bidirektionalen TCP-Kommunikation unter Verwendung des Operators <>: Skript SCRIPTS/get_http.sh

  #!/bin/sh
  #
  # simples Beispiel, das eine Web-Seite via GET holt
  #
  # 6.4.2006
  
  # Server-Adresse, Port, URL
  SERVER=localhost
  PORT=80
  URL=/
  
  # Deskriptor 3 mit dem HTTP-Server verbinden
  exec 3<>"/dev/tcp/$SERVER/$PORT"
  
  # GET-Anforderung senden
  echo "GET $URL" >& 3
  
  # Antwort lesen und ausgeben
  while read -ru 3
  do
    echo "$REPLY"
  done

Weitere Beispiele:

  # Eingabe-Umlenkung (stdin) durch <

  # alle Terminal-Einstellungen von /dev/ttyS0 (serielle Schnittstelle 1 bzw. COM1)
  # anzeigen
  stty -a </dev/ttyS0

  # Dateinamen aus der Datei file_list lesen und an ls -ld weitergeben;
  # die 3 folgenden Kommandos sind inhaltlich identisch, auch wenn die Eingabe-Umlenkung
  # an verschiedenen Stellen auftaucht
  xargs < file_list ls -ld
  < file_list xargs ls -ld
  xargs ls -ld < file_list

  # Einlesen des Version-Strings des SSH-Dämons an Port 22 auf localhost
  read -r ssh_vers < /dev/tcp/localhost/22

  # Ausgabe-Umlenkung (stdout) durch >
  ls -l > /tmp/ls.out

  # Ausgabe-Umlenkung (stdout) durch >|
  # existierende Datei wird trotz Option noclobber überschrieben
  ls -l >| /tmp/ls.out

  # Ausgabe-Umlenkung (stdout) durch >>
  # falls die Datei existiert, wird die Ausgabe angehängt
  ls -l /etc >> /tmp/ls.out

  # stdout und stderr getrennt umlenken
  touch /tmp/z
  rm -fv /tmp/z / > /tmp/out 2> /tmp/err

  # stdout und stderr zusammen umlenken
  # Variante 1 (Reihenfolge der Umlenkungen ist wichtig!):
  rm -f / > /tmp/rm.out 2>&1
  # Variante 2:
  rm -f / &> /tmp/rm.out
  # Variante 3 wie bei tcsh (Variante 2 sollte bevorzugt werden):
  rm -f / >& /tmp/rm.out

  # Hier-Dokument: Eingabe-Umlenkung durch <<
  name=Otto
  # Variante 1 (ohne Löschung von Tabs am Zeilenanfang:
  #   cat <<!
  # Variante 2 (mit Löschung von Tabs am Zeilenanfang):
  cat <<-!
    Hallo,
    ich bin ein Hier-Dokument mit Substitutionen
    $name
    $(date)
  !
  # Hier dient das ! als Begrenzer. Da es nicht quotiert ist,
  # werden im Hier-Dokument Parameter-, Kommando- und arithmetische
  # Expandierungen vorgenommen.
  
  # Im folgenden Beispiel wird der Begrenzer quotiert. Dadurch
  # werden alle Expandierungen im Hier-Dokument unterbunden.
  cat <<-\!
    Hallo,
    ich bin ein Hier-Dokument ohne Substitutionen:
    $name
    $(date)
  !

  # Hier-String: Eingabe-Umlenkung durch <<<
  cat <<< "Ich bin ein Hier-String: $name"
  
  # vier Newline-Zeichen via Standard-Eingabe an ein Kommando übergeben
  wc -c <<< $'\n\n\n'

17. Metazeichen für Pfadnamens-Muster

Jedes Zeichen eines bei der Pfadnamens-Expandierung genutzten Pfadnamens-Musters, das nicht zu den unten genannten Spezial- bzw. Metazeichen gehört, steht für sich selbst. Wenn also beispielsweise ein Muster an einer bestimmten Stelle das Zeichen a enthält, dann passen nur jene Pfadnamen zu diesem Muster, die an der entsprechenden Stelle ein a haben (wobei bei aktiver Option nocaseglob auch der Großbuchstabe A passt).

Das NUL-Zeichen (Zeichen mit dem Wert 0) kann nie in einem Muster vorkommen. Ein Backslash quotiert das Folgezeichen eines Musters, hebt also dessen Sonderbedeutung auf. Die Sonder- bzw. Metazeichen müssen quotiert werden, wenn sie literal verwendet werden sollen.

Standardmäßig kennt die Bash folgende Metazeichen für Pfadnamens-Muster, die im Rahmen der Pfadnamens-Expandierung eine Sonderbedeutung haben:

* beliebige (auch leere) Zeichenfolge
? ein beliebiges Zeichen
[...] Zeichenmenge
[!...] negierte Zeichenmenge
[^...] negierte Zeichenmenge (andere Notation für [!...])

Um eine Zeichenmenge anzugeben, kann man alle zur Menge gehörenden Zeichen einzeln aufzählen. Sofern die schließende eckige Klammer (]) auch zur Menge gehört, muss sie als erstes Zeichen angegeben werden, z.B. so: [[abc].

Wenn unmittelbar hinter der öffnenden eckigen Klammer einer Zeichenmengen-Angabe das Zeichen ! oder ^ folgt, handelt es sich um eine negierte Menge. D.h., genau die zwischen den eckigen Klammern angegebenen Zeichen gehören nicht zur Menge, was zur Folge hat, dass alle nicht angegebenen Zeichen zur Menge gehören.

Zusätzlich sind in Zeichenmengen auch Bereichsangaben möglich. Ein solcher Bereich wird in Form eines durch einen Bindestrich getrennten Zeichenpaares notiert (z.B. [a-g]) und umfasst die beiden explizit aufgeführten Bereichsbegrenzungen sowie alle Zeichen, die laut Sortierung zwischen den beiden Bereichsgrenzen liegen.

Achtung: Welche Zeichen zu einem Zeichenbereich gehören, hängt von der sog. Locale (Lokalisierung, Anpassung der landessprachlichen Einstellungen) ab, in der die Bash bzw. ein Bash-Kommando ausgeführt wird. Relevant sind hier die folgenden drei Umgebungsvariablen, die in der genannten Reihenfolge konsultiert werden:

  1. LC_COLLATE

  2. LC_ALL

  3. LANG

Die erste Variable, die gesetzt ist, wird verwendet.

So umfasst z.B. der Zeichenbereich [a-z] in der Locale C bzw. POSIX nur genau alle Kleinbuchstaben. In der Locale de_DE.UTF-8 gehören neben den Kleinbuchstaben auch noch die korrespondierenden Großbuchstaben sowie Umlaute und andere von den Grundbuchstaben durch diakritische Zeichen abgeleitete Zeichen dazu.

Mit Hilfe der nachfolgend dargestellten POSIX-Zeichenklassen kann man Locale-unabhängige Zeichenmengen bilden:

alnum alphanumerische Zeichen, also Buchstaben und Ziffern
alpha Buchstaben
ascii ASCII-Zeichen
blank Leerzeichen und Tabulator
cntrl Steuerzeichen
digit Ziffern
graph druckbare Zeichen ([:print:]) ohne Leerzeichen
lower Kleinbuchstaben
print druckbare Zeichen inkl. Leerzeichen, also [:graph:] plus Leerzeichen
punct Interpunktionszeichen: druckbare Zeichen, die weder zu den alphanumerischen noch den Whitespace-Zeichen gehören:
  ` ^ ~ < = > | _ - , ; : ! ? ' " () [ ] { } @ $ * \ & # % +
Bei aktivierter shopt-Option dotglob gehört zusätzlich der Punkt (.) zu den Interpunktionszeichen.
space Whitespace-Zeichen: Leerzeichen, Horizontal- und Vertikal-Tabulator, Newline (Zeilenschaltung), Carriage Return (Wagenrücklauf), Form Feed (Seitenvorschub)
upper Großbuchstaben
word alphanumerische Zeichen und Unterstreichungszeichen (_), also diejenigen Zeichen, aus denen man gültige Bezeichner bilden kann
xdigit Hexadezimal-Zeichen in Groß- und Kleinschreibung:
  0 1 2 3 4 5 6 7 8 9 a A b B c C d D e E f F

Um nun unabhängig von der Locale die Menge aller Kleinbuchstaben zu beschreiben, verwendet man die POSIX-Zeichenklasse lower:

  # Ausgabe aller Dateinamen des aktuellen Verzeichnisses, die mit
  # einem Kleinbuchstaben beginnen
  echo [[:lower:]]*

Wie das Beispiel zeigt, ist für die Angabe einer POSIX-Zeichenklasse innerhalb einer Zeichenmengen-Definition die Syntax

  [:Zeichenklassen-Name:]

zu verwenden.

Anmerkung: Es sei hier der Vollständigkeit halber erwähnt, dass noch zwei indirekte Möglichkeiten zur Zeichenklassen-Beschreibung existieren, die auf der Angabe eines Beispiel-Zeichens bzw. -Symbols beruhen. Nach Meinung des Autors sind sie für die tägliche Praxis aber ohne Belang. So kann man durch Angaben der Form [=c=] Äquivalenzklassen beschreiben. Das konkrete Beispiel würde alle Zeichen einschließen, die dasselbe Kollations-Gewicht wie das Zeichen c haben, also bei der Sortierung diesem gleichwertig sind. Mit der Angabe [.symbol.] wird die Ordnungsklasse beschrieben, deren Name dem Kollations-Symbol symbol entspricht. Ordnungsklassen können Zeichen und Zeichenfolgen enthalten. Alle Elemente einer Ordnungsklasse werden bzgl. der Sortierung als gleichwertig betrachtet (denkbar wären im Deutschen das Zeichen ä und die Zeichenfolge ae).

Das folgende Beispiel soll die Nutzung von Metazeichen sowie deren Quotierung veranschaulichen:

  # Zunächst legen wir in einem neuen Verzeichnis mehrere Dateien an, die in
  # ihrem Namen Metazeichen von Pfadnamens-Mustern enthalten:
  mkdir neu
  cd neu
  touch a1b 'a?b' 'a??b' 'ab?c' 'a\' 'a\b' 'a\?b' 'a\?bc' 'ab\c' 'a\bc*' 'abcd' 'a[b]c'
  
  # Option nullglob zurücksetzen, so dass Muster, auf die kein
  # Pfadname passt, nicht gelöscht, sondern unverändert erhalten werden
  shopt -u nullglob

  # Die folgenden Muster enthalten Metazeichen sowie Quotierungen:
  echo ?\?*
    # Pfadnamen, die an der zweiten Stelle ein Fragezeichen haben:
    #   a?b a??b

  echo ?\\?*
    # Pfadnamen, die an der zweiten Stelle einen Backslash
    # haben und mindestens 3 Zeichen lang sind:
    #   a\b a\?b a\?bc a\bc*

  echo ?\\\??
    # Pfadnamen, die an der zweiten Stelle einen Backslash
    # und an der dritten Stelle ein Fragezeichen haben sowie
    # genau 4 Zeichen lang sind:
    #   a\?b

  echo *\*
    # Pfadnamen, die auf einen Stern enden:
    #   a\bc*

  echo a\b
    # Hierbei handelt es sich nicht um ein Pfadnamens-Muster,
    # da keines der Metazeichen vorkommt. Somit wird die Zeichenfolge ab
    # ausgegeben.
    
  echo a[b?]*
    # Pfadnamen, die mit einem a beginnen, an zweiter
    # Stelle ein b oder ein ? haben:
    #   a?b a??b ab?c ab\c abcd
   
  echo a[!b?]*
  echo a[^b?]*
    # Pfadnamen, die mit einem a beginnen und an zweiter
    # Stelle weder ein b noch ein ? haben:
    #   a\ a1b a\b a\?b a[b]c a\?bc a\bc*
   
  echo a[b
    # Hierbei handelt es sich nicht um ein Pfadnamens-Muster,
    # da der Bereich nicht beendet ist, also die schließende Klammer ] fehlt.
    # Somit wird die Zeichenfolge a[b ausgegeben.

  echo a[?]c
    # Pfadnamen, die mit einem a beginnen, an zweiter
    # Stelle ein ? und an dritter Stelle ein c haben sowie genau
    # 3 Zeichen lang sind. Da es keinen passenden Namen gibt, wird das
    # Muster unverändert ausgegeben: a[?]c

  echo a\[?\]c
    # Pfadnamen, die mit der Zeichenfolge a[ beginnen und
    # mit der Zeichenfolge ]c enden, wobei zwischen diesen beiden
    # Zeichenfolgen genau ein beliebiges Zeichen steht:
    #   a[b]c

Standardmäßig ist die Option dotglob nicht gesetzt. Dann muss ein Punkt am Anfang eines Musters oder unmittelbar hinter einem Schrägstrich explizit angegeben werden:

  # alle Dateinamen (inkl. Punkt-Dateien) des aktuellen Verzeichnisses anzeigen
  echo .* *

  # alle Dateinamen des Verzeichnisses /tmp anzeigen
  echo /tmp/.* /tmp/*
  # durch Brace-Expandierung kann man das kürzer schreiben
  echo /tmp/{.*,*}

  # Option dotglob setzen
  shopt -s dotglob

  # jetzt kann der Punkt entfallen
  echo *
  echo /tmp/*
    # Hinweis: Die speziellen Einträge . und .. werden hier im Gegensatz
    # zum Muster .* nicht mit expandiert.

Weitere Beispiele finden sich im Abschnitt Pfadnamens-Expandierung.

Mit shopt -s extglob lassen sich erweiterte Musterangaben zuschalten, die aus der Korn-Shell stammen. Dabei steht Muster-Liste für eine durch Pipe-Striche (|) getrennte Liste von Mustern:

@(Muster-Liste) deckt genau ein Vorkommen der angegebenen Muster ab
+(Muster-Liste) deckt mindestens ein Vorkommen der angegebenen Muster ab
*(Muster-Liste) deckt kein, ein oder mehrere Vorkommen der angegebenen Muster ab
?(Muster-Liste) deckt kein oder genau ein Vorkommen der angegebenen Muster ab
!(Muster-Liste) deckt die Strings ab, die durch keines der angegebenen Muster abgedeckt werden
  shopt -s extglob
  touch aaac aac abbc abc acc accc
  echo a@(a|b|c)c    # aac abc acc
  echo a*(a|b|c)c    # aaac aac abbc abc acc accc
  echo !(aa*|*cc)    # abbc abc

Die o.g. POSIX-Zeichenklassen lassen sich auch bei den erweiterte Mustern verwenden.

Beispiel:

  shopt -s extglob
  # Dateinamen, die nur aus Kleinbuchstaben und Punkten bestehen,
  # wobei auch hier die speziellen Einträge . und .. nicht mit
  # expandiert werden.
  echo +([[:lower:]]|.)

18. Variablen, Parameter und Felder

Die Bash unterstützt einfache (skalare) Variablen, die genau einen Wert speichern können, und eindimensionale Feld-Variablen (Felder, Arrays), in denen man eine Reihe von Werten ablegen kann, auf die man über Indexe zugreift. Manche Variablen/Felder werden von der Shell automatisch gepflegt bzw. steuern ihre Arbeit. Alle anderen können vom Shell-Anwender frei für eigene Zwecke eingesetzt werden.

Im Bash-Manual wird der Oberbegriff Parameter gebraucht. Ein Parameter ist ein Gebilde (entity), das Werte speichert. Er wird durch einen Namen, eine Zahl oder ein Sonderzeichen benannt. Bei Variablen handelt es sich lt. Manual um Parameter, die einen Namen haben. Dies trifft auch auf Felder bzw. Feld-Variablen zu. Als Variablen- bzw. Feld-Namen sind Bezeichner zulässig. Bei den durch Zahlen benannten Parametern handelt es sich um Positionsparameter. Die durch Sonderzeichen benannten Parameter bezeichnet man als spezielle Parameter. In der Literatur werden die Begriffe Parameter und Variable öfter auch synonym gebraucht.

Eine nutzereigene einfache Variable wird dynamisch angelegt, wenn man ihr über das Konstrukt

  Variablen-Name=[Wert]

einen Wert zuweist. Analog wird ein Feld dynamisch angelegt, wenn man einem Feld-Element durch das Konstrukt

  Feld-Name[Index]=[Wert]

einen Wert zuweist. Der Wert ist jeweils optional. Wenn man ihn weglässt, wird die leere Zeichenkette zugewiesen.

Felder haben keine festgelegte Maximalgröße. Sie werden durch nichtnegative ganze Zahlen (0 bis n) indexiert, wobei die Index-Werte nicht lückenlos vergeben werden müssen.

Beispiele:

  name=Anton
  feld[3]=Berta

Dadurch wird der Variablen name der Wert Anton und dem Element mit Index 3 der Feld-Variablen feld der Wert Berta zugewiesen. Das Feld feld hat nach dieser Zuweisung nur dieses eine Element, sofern es vor der Zuweisung noch nicht existierte.

Wenn man einer existierenden Feld-Variablen durch ein Konstrukt der Form

  Feld-Name=[Wert]

einen Wert zuweist, so entspricht dies einer Zuweisung an das Element mit Index 0. Würde man der oben angelegten Feld-Variablen feld durch die Anweisung

  feld=Dora

den String Dora zuweisen, wäre dies analog zu

  feld[0]=Dora

Damit hätte feld nun 2 Elemente: feld[0] mit dem Inhalt Dora und feld[3] mit dem Inhalt Berta.

Anmerkung: Will man den Namen einer existierenden Feld-Variablen nachfolgend als Name einer einfachen Variablen verwenden, genügt es also nicht, dem unindexierten Namen einen String als Wert zuzuweisen. Es ist vorher erforderlich, das Feld mit dem Kommando unset zu löschen. Hier ein Beispiel:

  unset feld
  feld='Wert einer einfachen Variablen'

Man kann auch einzelne Feld-Elemente löschen, indem man bei unset hinter dem Feld-Namen einen Index angibt, z.B.:

  unset feld[0]

Hier wird nur das erste Feld-Element (mit Index 0) gelöscht. Wenn man als Index die speziellen Werte @ und * verwendet, wird wie beim kompletten Verzicht auf eine Index-Angabe das gesamte Feld gelöscht.

Bei den in eckigen Klammern angegebenen Feld-Indexen muss es sich nicht um ganzzahlige Konstanten handeln. Allgemein sind arithmetische Ausdrücke zulässig, z.B. so:

  i=8
  feld[2*i]=99

Dadurch wird dem Feld-Element mit Index 16 der Wert 99 zugewiesen.

Mit einer zusammengesetzten Zuweisung der Form

  Feld-Name=([Wert1 ... Wertn])

kann man eine Feld-Variable initialisieren, ihr also die angegebenen Werte als Feld-Elemente zuweisen, wodurch ein nicht existentes Feld natürlich ebenfalls angelegt wird. Ein evtl. zuvor existierender Inhalt einer gleichnamigen einfachen oder Feld-Variablen geht dabei verloren. Mit einer solchen Zuweisung kann man also aus einer einfachen Variablen eine Feld-Variable machen, z.B. so:

  # Variable abc löschen
  unset abc

  # Variable abc als einfache Variable initialisieren
  abc='einfache Variable'

  # Variable abc anzeigen
  declare -p abc
    # Anzeige: declare -- abc="einfache Variable"

  # Variable abc als Feld initialisieren und anzeigen
  abc=(1 2 3)
  declare -p abc
    # Anzeige: declare -a abc='([0]="1" [1]="2" [2]="3")'
    # Die Variable abc ist nun eine Feld-Variable und keine einfache
    # Variable mehr.

Bei der Feld-Initialisierung hat jeder Wert die allgemeine Form

  [Index]=String

Es genügt allerdings auch, nur den String anzugeben. Der String muss aus Sicht der Shell generell ein separates Wort sein. Strings, die aus mehreren Wörtern bestehen, weil sie Worttrenner enthalten, sind daher geeignet zu quotieren. Die Quotierung ist natürlich auch nötig, wenn die Sonderbedeutung von Metazeichen aufgehoben werden soll.

Ein explizit angegebener Index benennt das Feld-Element, dem der nachfolgende String zuzuweisen ist. Ein nur aus dem String bestehender Wert wird demjenigen Element zugewiesen, dessen Index um eins größer als der Index des Elements ist, dem zuletzt ein Wert zugewiesen wurde. Beim ersten Wert hat dieser implizite Index den Wert 0.

Beispiele:

  feld=(1 2 3)
    # Variable feld mit den 3 Elementen 1, 2 und 3
    # initialisieren; es werden die impliziten Indexe 0, 1 und 2 verwendet

  # Mit expliziter Indexierung ließe sich dieselbe Initialisierung so schreiben:
  feld=([0]=1 [1]=2 [2]=3)

  feld_zwei=(\$ '2 3' '`<>\')
    # Da hier die Feld-Elemente Worttrenner und sonstige Metazeichen enthalten,
    # nutzen wir eine geeignete Quotierung:
    # Element 0: $
    # Element 1: 2 3
    # Element 2: `<>\

Bei Aufruf des Kommandos set ohne Argumente werden u.a. die Belegungen aller Variablen und Felder ausgegeben, und zwar in Form von Zuweisungen, mit denen man diese Variablen/Felder mit ihrem derzeitigen Wert initialisieren würde. Für unser obiges feld generiert set folgende Ausgabe:

  feld=([0]="1" [1]="2" [2]="3")

Man kann sie gezielt durch

  set | grep '^feld='

herausfiltern. Alternativ kann man den oben schon benutzen Weg

  declare -p feld

verwenden, der allerdings eine etwas andere Ausgabe generiert:

  declare -a feld='([0]="1" [1]="2" [2]="3")'

Hier noch ein anderes Beispiel für eine Feld-Initialisierung, wobei explizite und implizite Indexierung zusammen verwendet werden:

  feld=([9]=1 2 [14]=3)
    # dies entspricht folgender Anweisung:
    # feld=([9]="1" [10]="2" [14]="3")

Mit dem Kommando declare kann man unter Verwendung der Option -a Variablen explizit als Feld-Variablen deklarieren, womit man sie gleichzeitig anlegt, falls sie nicht existieren. So deklariert man z.B. mit

  declare -a feld

das leere Feld feld, sofern diese Variable noch nicht existierte. Wenn feld schon eine Feld-Variable war, wird durch declare nichts verändert. War feld dagegen eine skalare Variable, dann wird sie in eine Feld-Variable umgewandelt, wobei der bisherige Wert zum Wert des Feld-Elements mit Index 0 wird.

Anmerkungen:

Statt
  declare -a Name
kann man auch
  declare -a Name[Index]

schreiben. Der Index wird dabei ignoriert. Beide Angaben sind demnach identisch.

Mit den Kommandos declare und readonly kann man einer Feld-Variablen Attribute zuweisen. Diese gelten dann für alle Elemente des Feldes, z.B.:

  declare -a -i -r feld=(12*3 9+7 40/8)
    # Durch -i wird feld als Feld ganzer Zahlen vereinbart.
    # Zuweisungen an die Elemente werden daher arithmetisch evaluiert.
    # Die Option -r besagt, dass die Elemente des Feldes nur lesbar sind.
    # Nach der Initialisierung sind deshalb keine Zuweisungen mehr möglich.
    # Auch eine Feld-Löschung mit unset wird von der Bash verhindert.
    # Das Attribut 'nur lesbar' lässt sich auch nicht wieder entfernen.
    #
    # äquivalente Anweisung:
    #   declare -air feld='([0]="36" [1]="16" [2]="5")'

  # Ein Feld kann auch nachträglich mit dem Attribut 'nur lesbar' versehen
  # werden:
  declare -r feld
  # oder
  readonly feld

Auch bei declare kann man die obige Mehrfach-Zuweisung zur Initialisierung von Feldern nutzen. Außerdem kann man mit einer declare-Anweisung mehrere Felder anlegen und initialisieren, was folgendes Beispiel zeigt:

  declare -a feld=(2 3 4) feld1=([9]=1 2 [14]=3) feld2
    # dies entspricht:
    #   feld=([0]="2" [1]="3" [2]="4")
    #   feld1=([9]="1" [10]="2" [14]="3")
    #   feld2=()

Mehrere Wertzuweisungen können auf einer Zeile stehen. Sie sind jeweils durch mindestens ein Leerzeichen oder einen Tabulator voneinander zu trennen. Ein Semikolon als Trenner ist nicht nötig. Hier ein Beispiel:

  var1=wert1 var2= var3=wert3 ...
  feld1=(wert1 wert2 wert3) feld2=() feld3=(wert4 wert5 wert6) ...

Wichtig: Vor dem Zuweisungs-Operator (d.h. dem Gleichheitszeichen) darf kein Leerraum stehen. Als Wert wird immer das erste Wort hinter dem Gleichheitszeichen verwendet, das auch leer sein kann. Wenn es sich beim Wert um einen String handelt, der Worttrenner enthält, ist er geeignet zu quotieren.

Variablenwerte haben keine festgelegte Maximalgröße. Die Shell stellt den jeweils benötigten Speicherplatz dynamisch zur Verfügung. Das gesamte Speichermanagement obliegt komplett der Shell. Der Nutzer muss sich darum nicht kümmern und hat keinen Einfluss darauf.

Werte von Variablen und Feld-Elementen sind generell Strings. Bei einer arithmetischen Evaluierung werden sie automatisch als Zahlen interpretiert. Bash-Strings werden intern durch ein NUL-Byte (Byte mit dem Dezimal-Wert 0) beendet, wie dies in der Sprache C auch üblich ist. Daher können Bash-Strings keine NUL-Bytes enthalten.

Das Sonderzeichen $ vor einem Parameter-Namen leitet eine Parameter- bzw. Variablen-Expandierung ein. Ausdrücke der Form

  $Parameter-Name
  ${Parameter-Name}

werden von der Shell durch den Inhalt des genannten Parameters ersetzt, so dass man mit ihnen einen Parameter bzw. eine Variable referenzieren, d.h. auf ihren Inhalt bzw. Wert zugreifen kann, wie folgende Beispiele zeigen:

  $var       # Inhalt der Variablen var
  ${var}     # Inhalt der Variablen var
  ${feld[3]} # Inhalt des Elements mit Index 3 des Feldes feld
  $7         # Inhalt des Positionsparameters 7
  ${11}      # Inhalt des Positionsparameters 11
  $$         # Prozess-ID des aktuellen Prozesses

Wenn der referenzierte Parameter nicht existiert, so wird standardmäßig das gesamte Konstrukt stillschweigend (also ohne Meldung) durch den leeren String ersetzt. Bei gesetzter set-Option u bzw. nounset prüft die Bash dagegen, ob ein referenzierter Parameter existiert. Wenn nicht, erscheint eine Fehlermeldung der Form

  var: unbound variable

Eine nicht-interaktive Shell wird danach abgebrochen.

Durch die Option nounset kann man sich davor schützen, dass man aus Versehen den leeren String als Wert einer ungesetzten Variablen verwendet. Allerdings muss man ggf. seine Skripten anpassen, wenn man die Option nutzen möchte. Hier ein Beispiel:

  # Variable var löschen
  unset var
  # Option nounset setzen
  set -u

  # testen, ob Variable var leer ist

  # Variante 1, die bei deaktivierter Option nounset funktioniert, hier
  # aber eine Fehlermeldung bewirkt, weil innerhalb des Tests mit
  # dem Konstrukt $var ein nicht existierender Parameter referenziert
  # wird:
  [[ -z $var ]] && echo leer

  # Variante 2, die wie gewünscht funktioniert, weil das
  # zum Test auf leere bzw. nicht existierende Parameter
  # dienende Konstrukt ${var-} im Falle der Nicht-Existenz von
  # var durch den leeren String ersetzt wird:
  [[ -z ${var-} ]] && echo leer

Beim Zugriff auf Feld-Elemente sowie auf Positionsparameter, deren Nummer größer als 9 und damit nicht mehr einstellig ist, müssen die geschweiften Klammern in oben gezeigter Weise verwendet werden. Bei Feldern sind die Klammern erforderlich, um Konflikte mit der Pfadnamens-Expandierung zu vermeiden. Bei einfachen Variablen sind sie dagegen nur dann nötig, wenn hinter dem Parameter-Namen ein Zeichen folgt, das syntaktisch noch zum Namen gehören könnte, also ein alphanumerisches Zeichen oder ein Unterstrich (_). Um Missverständnisse sicher zu vermeiden, kann man die geschweiften Klammern auch generell setzen, um der Shell exakt mitzuteilen, bis wohin sich der Parameter-Name erstreckt.

Bei Verwendung der speziellen Feldindexe @ und * wird das Konstrukt durch alle Elemente des betreffenden Feldes substituiert. Diese beiden Indexe verhalten sich nur dann unterschiedlich, wenn das Konstrukt in Doppelapostrophe eingeschlossen ist. Dann wird

  "${Feld-Name[*]}"

zu einem einzigen Wort expandiert, das alle Feld-Elemente enthält. Diese sind jeweils durch das erste Zeichen der Variablen IFS voneinander getrennt. Wenn IFS leer ist, wird ein einzelnes Leerzeichen als Trenner genutzt. Hat IFS den leeren String als Wert, wird gar kein Trenner eingefügt.

Bei

  "${Feld-Name[@]}"

wird dagegen jedes Feld-Element zu einem separaten Wort expandiert. Wenn das Feld keine Elemente besitzt, wird das gesamte Konstrukt zum leeren String expandiert.

Der Ausdruck

  ${#Feld-Name[Index]}

expandiert zur Länge des Feld-Elements ${Name[Index]}. Bei Verwendung der speziellen Indexe @ und * wird der Ausdruck zur Anzahl der Feld-Elemente expandiert.

Wenn man eine Feld-Variable ohne Index-Angabe referenziert, wird immer das Element mit Index 0 angesprochen.

Bei einer Wert-Zuweisungen an eine Variable unterliegen die Werte einer Tilde- und Parameter-Expandierung, einer Kommando-Substitution, einer arithmetischen Expandierung sowie der Entfernung von Quotierungs-Zeichen. Eine Aufspaltung in Wörter erfolgt dagegen außer beim Wert

  "$@"

(Liste aller Positionsparameter) nicht. Daher kann man den Wert einer Variablen einer anderen Variablen ohne Quotierung zuweisen:

  var1="1 2 3"
  var2=$var1 # var2="$var1" ist auch möglich

Eine Pfadnamens-Expandierung erfolgt bei der Zuweisung ebenfalls nicht.

Anmerkung: Das bei der Feld-Initialisierung in runde Klammern eingeschlossene Konstrukt, also die Folge der Wert-Zuweisungen an die Feld-Elemente, unterliegt standardmäßig einer Aufspaltung in Wörter, wobei ganz normal die in der Variablen IFS enthaltenen Zeichen als Wortrenner fungieren. Dies ist speziell dann zu beachten, wenn diese Wörter aus einer Expandierung stammen, wie folgendes Beispiel zeigt:

  unset IFS
    # IFS auf den Standard-Wert setzen
  feld=($(echo $'1 2\n3 4\n5 6'))
    # Diese Zuweisung intialisiert ein sechselementiges Feld. Die Zahlen von 1 bis
    # 6 sind die Feld-Elemente:
    #   feld=([0]="1" [1]="2" [2]="3" [3]="4" [4]="5" [5]="6")
    # Möchte man statt dessen die einzelnen vom Kommando echo ausgegebenen
    # Zeilen als separate Feld-Elemente speichern, liesse sich dies erreichen,
    # indem man der Variablen IFS ein Newline zuweist:
  IFS=$'\n'
  feld=($(echo $'1 2\n3 4\n5 6'))
    # entspricht: feld=([0]="1 2" [1]="3 4" [2]="5 6")

Zuweisungs-Anweisungen können auch als Argumente der eingebauten Kommandos alias, declare, typeset, export, readonly und local auftreten.

Man kann eine Variable mit

  declare -i Variablenname

explizit als Integer-Variable (Ganzzahl-Variable) deklarieren, was zur Folge hat, dass Zuweisungen an diese Variable arithmetisch evaluiert werden. Der Wert einer Integer-Variablen ist aber weiterhin ein String.

Variablen, Feld-Elemente und komplette Felder können, sofern sie nicht mit dem Attribut readonly (nur lesbar) gekennzeichnet sind, jederzeit durch eine weitere Zuweisung modifiziert und durch das eingebaute Kommando unset vernichtet werden.

Positionsparameter sind spezielle Variablen, die die Argumente eines Shell-Skripts oder einer Shell-Funktion beinhalten. Sie sind mit 1 beginnend durchnummeriert und werden mittels Parameter-Expandierung durch Konstrukte der Art

  $1
  $5
  ${11}

angesprochen. Wie schon erwähnt, müssen Nummern größer als 9 in geschweifte Klammern eingeschlossen werden, da ohne Klammern nur einstellige Nummern akzeptiert werden.

Innerhalb eines Skripts oder einer Funktion kann man die Positionsparameter unter Verwendung des eingebauten Kommandos set neu belegen. Eine Wertzuweisung mit dem Gleichheitszeichen ist dagegen nicht möglich.

Wird set ohne Argumente aufgerufen, dann gibt es alle existierenden Shell-Variablen zusammen mit ihren Werten in Form von Zuweisungen aus, die die aktuellen Werte an die betreffenden Variablen zuweisen würden, z.B.:

  EDITOR=vim

Die Nutzung von set zum Setzen und Rücksetzen von Shell-Optionen wird im Abschnitt Modifikation von Shell-Optionen in einer laufenden Shell beschrieben. Mit einem set-Kommando kann man gleichzeitig oder getrennt Optionen verändern und Positionsparameter setzen oder löschen.

Wenn Optionen und Positionsparameter gleichzeitig verändert werden, dann müssen die Optionen immer vor den Werten der Positionsparameter stehen, z.B.:

  set -x 1 2 3
    # Option x aktivieren sowie die ersten 3 Positionsparameter
    # neu setzen und evtl. weiter hinten folgende löschen:
    # 1 --> $1 
    # 2 --> $2 
    # 3 --> $3

Eine solche mit set realisierte Zuweisung an die ersten n Positionsparameter löscht evtl. vorhandene weitere Positionsparameter (im Beispiel also ab Positionsparameter 4).

Sofern der zu setzende neue Wert des ersten Positionsparameters mit einem Minus-Zeichen beginnt und somit eine Option sein könnte, so muss man die Options-Liste von der Liste der Positionsparameter-Werte trennen. Dies kann durch zwei spezielle Optionen geschehen:

  -- (zwei Minus-Zeichen)
  -  (ein Minus-Zeichen)

Wenn hinter -- keine Argumente folgen, werden alle existieren Positionsparameter gelöscht. Wenn dagegen bei - keine Argumente folgen, bleiben die Positionsparameter unverändert. Alle hinter - oder -- folgenden n Argumente werden den ersten n Positionsparametern zugewiesen. Evtl. weiter hinten folgende Positionsparameter werden dabei wie beschrieben gelöscht. Bei Verwendung der Option - werden automatisch die Optionen x und v deaktiviert.

Mit dem eingebauten Kommando

  shift [n]

kann man die Positionsparameter um n Positionen nach links verschieben, wodurch die bisherigen ersten n Positionsparameter vernichtet werden und der Parameter $#, der die Anzahl der Positionsparameter angibt, um n vermindert wird. Wenn n fehlt, wird eine 1 angenommen.

Das Argument n muss eine Zahl zwischen 1 und $# sein, um eine Verschiebung zu bewirken. Wenn n den Wert 0 hat oder größer als $# ist, erfolgt keine Verschiebung.

Hier ein Beispiel:

  # 7 Positionsparameter setzen
  set a b c d e f g

  # alle Positionsparameter löschen
  set --

  # Positionsparameter 1 auf den Wert -a setzen
  set -- -a
    # set-Optionen -x und -v bleiben unverändert
  set - -a
    # set-Optionen -x und -v werden zurückgesetzt

  # Option x setzen und alle Positionsparameter löschen
  set -x --

  # Option x setzen und die ersten drei Positionsparameter
  # auf die Werte
  #    1
  #   -2
  #   -4
  # setzen (und den Rest automatisch löschen)
  set -x 1 -2 -4

  # Option x setzen und die ersten zwei Positionsparameter
  # auf die Werte
  #   -1
  #    2
  # setzen (und den Rest automatisch löschen)
  set -x - -1 2
    # set-Optionen -x und -v werden zurückgesetzt;
    # -x wird also zuerst gesetzt und dann gleich wieder deaktiviert
  set -x -- -1 2
    # set-Optionen -x und -v bleiben unverändert

  # Positionsparameter um 3 Stellen nach links verschieben
  shift 3
  # die Positionsparameter lauten nun: d e f g
  # $# hat jetzt den Wert 4

Unter Nutzung von shift und $# kann man z.B. folgendermaßen die Argumente eines Shell-Skripts in einer Schleife durchlaufen:

  while (($#))
  do
    # Der Schleifenkörper wird für jeden Positionsparameter 1 Mal durchlaufen.
    # Durch shift stehen der Reihe nach alle Positionsparameter unter $1 zur Verfügung.
    ((++i))
    echo "Argument $i: $1"
    shift
  done

Neben den Positionsparametern gibt es auch noch spezielle Parameter, die von der Shell automatisch gepflegt werden. Auf sie kann man nur lesend zugreifen, d.h., die explizite Zuweisung eines Wertes an diese Parameter ist nicht möglich. Der Zugriff erfolgt wieder über die Parameter-Substitution, wobei dem $ hier ein spezielles Zeichen folgt, das den betreffenden Parameter benennt.

Die folgende Tabelle zeigt die speziellen Parameter der Bash, wobei gleich der Ausdruck angegeben wird, den man zum Zugriff auf den betreffenden Parameter benötigt:

$* Expandierung aller Positionsparameter

Wenn dieser Ausdruck in doppelte Apostrophe eingeschlossen wird, dann entsteht genau ein Wort, das der Reihe nach alle Positionsparameter enthält, die jeweils durch das erste Zeichen der Variablen IFS getrennt sind, also "$1c$2c...", wenn c das erste Zeichen von IFS ist. Falls IFS ungesetzt ist, wird ein Leerzeichen als Trenner verwendet: "$1 $2 ...". Wenn IFS die leere Zeichenkette als Wert hat, dann wird kein Trenner eingesetzt: "$1$2...".

Wenn es keine Positionsparameter gibt, dann expandiert $* zu nichts (das Konstrukt wird also gelöscht) und "$*" zum leeren String.
$@ Expandierung aller Positionsparameter

Wenn dieser Ausdruck in doppelte Apostrophe eingeschlossen wird, dann wird jeder Positionsparameter als ein separates Wort expandiert. "$@" entspricht also
  "$1" "$2" ...
Wenn es keine Positionsparameter gibt, dann expandieren $@ und "$@" zu nichts, d.h., sie werden gelöscht.
$# Anzahl der Positionsparameter
$? Exit-Status der zuletzt im Vordergrund ausgeführten Pipeline
$- Liste der aktuellen einbuchstabigen Shell-Optionen
$$ Prozess-ID der Shell

Innerhalb einer durch () gestarteten Sub-Shell wird weiterhin die Prozess-ID der aktuellen (Eltern-)Shell und nicht die Prozess-ID der Sub-Shell gemeldet.
$! Prozess-ID des zuletzt gestarteten Hintergrund-Kommandos
$0 Name der aktuellen Shell oder des aktuellen Shell-Skripts (Argument 0 der Argumente-Liste des aktuellen Prozesses)
$_ nach dem Shell-Start: absoluter Pfadname der aktuellen Shell gemäß Argumente-Liste

nachfolgend: letztes Argument des vorherigen Kommandos in expandierter Form

Beispiele:

  # Wertzuweisung für einfache Variablen
  name='Normalverbraucher, Otto'
  zahl=45

  # bei der folgenden Wertzuweisung ist eine Quotierung möglich, aber
  # nicht nötig, da die Shell den Wert von name nicht in Wörter aufteilt:
  name2=$name
  name2="$name"

  # hier ist eine Quotierung dagegen nötig:
  name3="$name $zahl"

  # Variable löschen
  unset zahl

  # Initialisierung eines Feldes; die Feld-Elemente mit den Index-Werten
  # 0, 1, 2 werden mit den Strings a, b und c belegt; weitere Elemente
  # hat das Feld danach nicht
  feld=(a b c)

  # Ausgabe von Feld-Element 0
  echo $feld
  echo "$feld"
  echo "${feld[0]}"

  # Wertzuweisung an Feld-Element 3
  feld[3]=d

  # Löschung von Feld-Element 2
  unset feld[2]

  # Vernichtung des gesamten Feldes
  unset feld

  # Zuweisung des leeren Feldes (0 Elemente)
  feld=()

  # Zugriff auf einfache Variablen
  echo "$name"
  echo "${name}kar"

  # Zugriff auf Felder

  # Feld in Liste umwandeln
  for i in "${feld[@]}"
  do
    echo "$i"
  done
  
  echo ====

  # über Feldindexe zugreifen
  for i in 0 1 2 3
  do
    echo "${feld[$i]}"
  done

  echo ====

  # in Zählschleife über Feldindexe zugreifen
  for (( i = 0; i < ${#feld[@]}; i++ ))
  do
    echo "${feld[$i]}"
  done

  echo ====

  # musterbasierte Zeichenersetzung auf alle Feld-Elemente anwenden

  # alle Dateinamen unter /etc in das Feld files übernehmen
  files=(/etc/*)
  # in allen Feld-Elementen von links alle Zeichen bis zum letzten
  # Schrägstrich streichen und so nur den eigentlichen Dateinamen belassen
  # (logisch also das Kommando basename anwenden)
  echo ${files[@]##*/}
  
  # Positionsparameter neu setzen:
  #   $1 --> 1
  #   $2 --> 2 3
  #   $3 --> 4
  # Da $2 einen Wert bekommen soll, der ein Leerzeichen enthält, muss man ihn
  # geeignet quotieren.
  set 1 '2 3' 4 

  # alle Positionsparameter in einer Schleife ausgeben
  for i in "$@"
  do
    echo "$i"
  done
    # Wenn es keine Positionsparameter gibt, wird der Körper dieser
    # Schleife nicht (also 0 Mal) ausgeführt. Gleiches gilt, wenn man statt
    # "$@" die Ausdrücke $@ und "$@" angeben würde,
    # weil in allen 3 Fällen die Liste hinter in leer ist.
    #
    # Dagegen wird die folgende Schleife ein Mal durchlaufen, wobei i den
    # leeren String als Wert hat:
  for i in "$*"
  do
    echo "$i"
  done
  
  # aktuelle Prozess-ID und Exit-Status des letzten Vordergrund-Kommandos ausgeben
  echo $$ $?

19. Umgebungsvariablen

Über Umgebungsvariablen lassen sich Daten an Kind-Prozesse weitergeben (als Alternative zu Dateien und Pipes). Manche Umgebungsvariablen werden von der Shell selbst ausgewertet bzw. gepflegt und steuern ihre Arbeit. Alle anderen sind frei in Shell-Kommandos verwendbar. Mit der Shell kann man die Umgebungsvariablen des Shell-Prozesses bequem auslesen und meist auch verändern. Auf sie wird wie auf normale Variablen zugegriffen.

Beispiele:

  # komplette Anzeige der Umgebung
  env
  printenv

  # gezielte Anzeige, z.B. Umgebungsvariable LC_ALL
  echo "$LC_ALL"
    # Der Zugriff erfolgt also wie bei normalen Variablen mittels
    # Parameter-Substitution.
    
  # Variablen können mit dem eingebauten Kommando export bzw. declare
  # exportiert, also zu Umgebungsvariablen erklärt werden. Wenn man ihnen beim Export
  # oder nachfolgend Werte zuweist, schreibt man diese automatisch in die Umgebung:
  export XYZ # Umgebungsvariable XYZ exportieren; damit wird sie aber noch nicht angelegt
  XYZ=abc    # der Umgebungsvariablen XYZ einen Wert zuweisen und sie damit anlegen
  
  # Das Kommando export beherrscht Wert-Zuweisungen.
  # Dies war bei der Bourne-Shell noch nicht der Fall.

  # LC_ALL dauerhaft auf C setzen, was u.a. englische Fehlermeldungen
  # bei den nachfolgenden Kommandos bewirkt
  export LC_ALL=C
  rm /
    # meldet
    #   rm: cannot remove directory `/': Is a directory
    # statt
    #   rm: Entfernen von Verzeichnis „/“ nicht möglich: Ist ein Verzeichnis

  # Die Umgebungsvariablen, die einem Shell-Prozess übergeben wurden, sind
  # automatisch exportiert, typischerweise z.B. HOME. Um sie zu verändern,
  # reicht also eine Wertzuweisung aus. export kann benutzt werden, ist
  # aber unnötig:
  HOME=/tmp
  export HOME=/tmp

  # Anzeige aller exportierten Variablen
  export -p

  # Man kann auch Funktionen exportieren:
  function abc() { echo ABC; }
  export -f abc
    # Eine Funktion wird über eine Umgebungsvariable weitergegeben, die
    # den Namen der Funktion trägt. Im obigen Beispiel sähe das so aus:
    #   abc=() {  echo abc
    #   }
    # Die Variable heißt also abc. Ihr Wert beginnt mit ().
    # Hinter ABC kommt ein Newline-Zeichen, da dieses an Stelle
    # des Semikolons gespeichert wurde. Daher steht die abschließende
    # Klammer in der Anzeige auf einer neuen Zeile.

  # Liste der exportierten Funktionen anzeigen
  export -pf # mit Funktions-Definition
  export -pF # ohne Funktions-Definition

  # Löschung von Exporten
  export -n LC_ALL # Variable LC_ALL
  export -nf abc   # Funktion abc
    # Damit wird nur der Export aufgehoben. Die Variablen und Funktionen bleiben
    # aber erhalten. Derartige Variablen existieren dann als normale Shell-Variablen weiter.
    # Nicht mehr exportierte Funktionen werden beim Aufruf von Kommandos nicht mehr an diese
    # weitergegeben.

  # Alternativ-Notation für Export
  declare -x  # Anzeige aller exportierten Variablen
  declare -xf # Anzeige aller exportierten Funktionen mit Definition
  declare -xF # Anzeige aller exportierten Funktionen ohne Definition

20. Sonderzeichen und Quotierung

Unter Quotierung (Quoting) versteht man die Aufhebung der Sonderbedeutung von Zeichen und Wörtern. Die folgende Tabelle listet eine Vielzahl allgemeiner Sonderzeichen (Metazeichen) der Bash auf und erläutert deren typische Bedeutung:

Leerzeichen und Tabulator Trennzeichen für Wörter
Newline Zeilenende; Übergabe der Zeile an die Shell zur Abarbeitung
> Ausgabe-Umlenkung
>> Ausgabe-Umlenkung mit Anhängung an eine Datei
>| Ausgabe-Umlenkung mit Überschreiben von Dateien trotz Option noclobber
&>
>&
gemeinsame Umlenkung von stdout und stderr
< Eingabe-Umlenkung
<> Datei zum Lesen und Schreiben öffnen
<&
>&
Duplizierung von Deskriptoren
<&...-
>&...-
Verschiebung von Deskriptoren
<< Hier-Dokument
<<< Hier-String
| Pipe
* Platzhalter für einen beliebigen (auch leeren) String bei der Pfadnamens-Expandierung
? Platzhalter für genau 1 Zeichen bei der Pfadnamens-Expandierung
[...] Zeichenmenge bei der Pfadnamens-Expandierung

Durch [!...] oder [^...] wird eine negierte Zeichenmenge beschrieben.
?(...)
*(...)
+(...)
@(...)
!(...)
erweiterte Muster bei der Pfadnamens-Expandierung (bei gesetzter Option extglob)
~ Home-Verzeichnis
; Trennzeichen zwischen sequentiell auszuführenden Shell-Kommandos
& Hintergrund-Kommando
`...` Backtick/Backquote; veraltete Form der Kommando-Substitution
$(...) neue Form der Kommando-Substitution
(...) Liste, die in einer Sub-Shell auszuführen ist
...; } Liste, die in der aktiven Shell auszuführen ist

Die geschweiften Klammern sind genau genommen keine Metazeichen, sondern Schlüsselwörter der Shell.
$ Präfix der Variablen-/Parameter-Expandierung:
  $0 $9 ${10} $* $@ $$
  $var ${var} ${var##*a} ${var:-leer}
  $feld[index]} $feld[@]} ...
$((...)) arithmetische Expandierung
\ quotiert das Folgezeichen
'...' starke Quotierung der in Apostrophe eingeschlossenen Zeichen
"..." schwache Quotierung der in Doppelapostrophe eingeschlossenen Zeichen
= Wertzuweisungen an Variablen und Alias-Namen
&& Kommando-Trenner einer UND-Liste
|| Kommando-Trenner einer ODER-Liste
# Beginn eines Kommentars
((...)) arithmetische Auswertung eines Ausdrucks
/ Verzeichnis-Trennzeichen in Pfadnamen
! History-Expandierung und logische Negation bei Pipes
^ als erstes Zeichen der Kommandozeile: Quick-Substitution bei der History-Expandierung
: Präfix für History-Modifikatoren

In speziellen Situationen (z.B. bei regulären Ausdrücken) kann es weitere Sonderzeichen geben. Manche Sonderzeichen haben je nach Kontext auch weitere Bedeutungen, die oben nicht genannt sind.

Für die Quotierung existieren folgende Möglichkeiten:

Backslash \ Aufhebung der Sonderbedeutung des Folgezeichens
einfache Apostrophe '...' starke Quotierung: Aufhebung der Sonderbedeutung aller Zeichen zwischen den begrenzenden Apostrophen
doppelte Apostrophe "..." schwache Quotierung: Aufhebung der Sonderbedeutung der meisten Zeichen zwischen den begrenzenden Doppelapostrophen. Folgende drei Zeichen behalten ihre Sonderbedeutung:
  $  `  \
Der Backslash behält seine Sonderbedeutung vor
  $  `  "  \  Newline
Wörter der Form $'string' ANSI-C-Quotierung: Interpretation von ANSI-C-Escape-Sequenzen (s. unten)
Wörter der Form $"string" Locale-spezifische String-Übersetzung

Mit dem Backslash und einfachen Apostrophen kann man folglich alle Arten der Expandierungen unterbinden. Mit Doppelapostrophen bleiben History-Expandierungen sowie Variablen-, Kommando- und arithmetische Expandierungen aktiv.

Die Zeichenfolge Backslash Newline gestattet die logische Fortsetzung der aktuellen Kommandozeile auf der folgenden Eingabe-Zeile. Damit lassen sich lange logische Zeilen übersichtlicher gestalten, weil man sie auf mehrere physische Zeilen verteilen kann. Falls eine Zeile mit einem Pipe-Strich, der Zeichenfolge && oder || endet (wenn diesen also unmittelbar das Newline-Zeichen folgt), wird die nächste Zeile bei Shells der Bourne-Shell-Familie (im Gegensatz zur C-Shell-Familie) automatisch als logische Fortsetzung betrachtet. Eine explizite Kennzeichnung durch Backslash Newline ist dann also nicht nötig.

Beispiele:

  # Hilfs-Funktion zur Anzeige aller ihrer Argumente auf einer separaten Zeile
  function show_args()
  {
    local nr=0
    for arg
      # Kurzform für: for arg in "$@"
    do
      echo "$((++nr)): |$arg|"
    done
    echo '--------'
  }

  # lange logische Zeile durch Backslash Newline physisch teilen
  echo lange \
    Zeile

  # lange logische Zeile durch |, &&, || physisch teilen
  ls |
    wc -l
  
  true &&
    echo true

  false ||
    echo true
  
  # Nutzung verschiedener Quotierungs-Möglichkeiten
  echo 'ho* [a-c]*'
    # Ausgabe: ho* [a-c]*

  show_args a\ b
  show_args 'a b'
  show_args "a b"
    # Ausgabe: a b

  name=Otto
  echo "\$name hat den Wert '$name'"
    # Ausgabe: $name hat den Wert 'Otto'

  echo '"`$\'\'
    # Ausgabe: "`$\'

  # Wie das letzte Beispiel zeigt, kann man quotierte Strings auch lückenlos
  # aufeinander folgen lassen. Die Shell verkettet sie dann auch lückenlos.
  # Dies wollen wir uns an einem weiteren Beispiel nochmal genauer ansehen:
  echo 'Der Inhalt von $name lautet'\ "'$name'"
    # Dem durch (rot markierte) einfache Apostrophe quotierten String folgt hier
    # ein mittels Backslash quotiertes Leerzeichen sowie anschließend ein durch
    # (hellgrün markierte) Doppelapostrophe quotierter String. An das Kommando
    # echo wird folglich genau ein Wort mit dem Inhalt
    #
    #  Der Inhalt von $name lautet 'Otto'
    #
    # übergeben.
    #
    # Das durch Backslash quotierte Leerzeichen wurde nur zu
    # Demonstrationszwecken eingefügt. In der Praxis würde man es sicher nicht
    # notieren, weil man ja das Leerzeichen auch bequem durch die nachfolgenden
    # Doppelapostrophe schützen kann.
    #
    # Doppelapostrophe kann man also nutzen, um einfache Apostrophe zu
    # quotieren. Analog kann man einfache durch Doppelapostrophe quotieren.
    # Statt dieser wechselseitigen Quotierung kann man auch Backslash-Folgen
    # innerhalb von Doppelapostrophen verwenden, was bei Ausdrücken mit vielen
    # Backslash-Sequenzen evtl. etwas unübersichtlich wird.
    #
    # Obige Ausgabe könnte man auch so erreichen:
  echo "Der Inhalt von \$name lautet '$name'"

Die Bash unterstützt die ANSI-C-Quotierung. D.h., Wörter der Form $'string' gestatten die Nutzung von ANSI-C-Sequenzen:

\aAlert (Bell; akustisches Signal)
\bBackspace (Rückschritt)
\eEscape
\fFormfeed (Seitenvorschub)
\nNewline (Zeilenvorschub)
\rCarriage Return (Wagenrücklauf)
\tHorizontal-Tabulator
\v Vertikal-Tabulator
\\Backslash
\’Apostroph
\nnnOktalzahl (1 bis 3 Ziffern)
\xHHHexadezimal-Zahl (1 oder 2 Ziffern)
\cxCtrl-x

Beispiel:

  echo $'\nHallo\nWelt'
    # Vor und hinter Hallo wird ein Zeilenvorschub ausgegeben.

Beispiel für eine Locale-spezifische String-Übersetzung in der Locale de_DE.UTF-8:

  # Nutzung des Message-Katalogs /usr/share/locale/de/LC_MESSAGES/tar.mo
  export TEXTDOMAINDIR=/usr/share/locale/de/LC_MESSAGES
  export TEXTDOMAIN=tar

  echo $"stdout"
    # Ausgabe des Strings Standardausgabe als deutsche Übersetzung
    # von stdout

Hinweis: Eine solche .mo-Datei ist eine Binärdatei, die man mit dem Kommando msgfmt aus einer Text-Datei erzeugen kann. Mit dem Kommando msgunfmt kann man wieder die Text-Form herstellen:

  msgunfmt /usr/share/locale/de/LC_MESSAGES/tar.mo

21. Kommandozeilen-Editor und History

Die Bash unterstützt über die Readline-Bibliothek einen Vi- und Emacs-Modus für den Kommandozeilen-Editor und den Zugriff auf die als History bezeichnete Kommando-Historie (eine Liste von früher ausgeführten Kommandos). Der Emacs-Modus ist der Standard und sollte bevorzugt verwendet werden.

Im Emacs-Mode wird vielfach die Meta-Taste benötigt. Unter Linux dient standardmäßig die linke Alt-Taste als Meta-Taste. Alternativ kann man an Stelle der Tastenkombination Meta Taste auch die Tastenfolge ESC und dann Taste eingeben.

Beispiel: Um die Tastenkombination Meta w einzugeben, kann man entweder nacheinander die Tasten ESC und w oder gleichzeitig die Tasten Alt_links und w betätigen.

Die History wird nur bei aktiver Option history gepflegt, was bei interaktiven Shells standardmäßig der Fall ist.

Setzen/Rücksetzen der History-Option:

  set -o history
  set +o history

Die Shell verwaltet für die ausgeführten Kommandos zwei getrennte Nummern: eine Kommando-Nummer und eine History-Nummer, die aus verschiedenen Gründen differieren können. Die History-Nummer gibt die Position eines Kommandos in der History-Liste an, während die Kommando-Nummer alle von einem Shell-Prozess ausgeführten Kommandos von eins beginnend fortlaufend durchnummeriert und daher nie dekrementiert wird. Die History-Nummer kann sich dagegen verringern, wenn die History-Liste mit dem Kommando history partiell oder komplett gelöscht wird.

Die Kommando-Nummer des nächsten einzugebenden Kommandos kann man sich als Teil des Prompt-Strings PS1 unter Nutzung der Sequenz \# ausgeben lassen. Für die entsprechende History-Nummer steht die Sequenz \! zur Verfügung. Weitere Details findet man im Abschnitt Flexible Prompt-Anpassung.

Nach dem Start einer Bash bzw. nach der kompletten Löschung der History-Liste wird die History-Nummer auf den Wert eins gesetzt. Die History-Nummern der in der History gespeicherten Kommandos werden immer fortlaufend vergeben, müssen aber nicht bei eins beginnen. Der Wert der Shell-Variablen HISTSIZE legt fest, wie viele Kommandos in der History zu speichern sind. Wenn diese maximale Anzahl erreicht ist, wird vor der Aufnahme eines weiteren Kommandos das erste Kommando der History gelöscht. Dabei ändern sich aber im Gegensatz zur o.g. Löschung durch das Kommando history die History-Nummern der Kommandos nicht, weswegen sie nun nicht mehr bei eins beginnen.

Wenn HISTSIZE nicht gesetzt ist oder den leeren String als Wert hat, dann hat die History keine festgelegte maximale Größe. Ist der Wert dagegen Null, wird keine History-Liste geführt.

Da eine History-Liste auch Kommandos anderer Shells aus einer History-Datei einlesen kann, ist es möglich, dass die History-Nummer größer als die Kommando-Nummer ist.

Die Shell-Variable HISTFILE benennt eine Datei (Standard ist ~/.bash_history), in der die letzten n History-Einträge automatisch beim Beenden einer interaktiven Shell gespeichert werden. Die Zahl n entspricht dem Wert der Shell-Variablen HISTFILESIZE. Durch Löschen von HISTFILE (unset HISTFILE) kann man das Speichern der History verhindern.

Mehrzeilige Kommandos kann die Bash in verschiedener Form in die History übernehmen. Bei (mit shopt) gesetzter Option cmdhist werden mehrzeilige Kommandos geschlossen in einem History-Eintrag gespeichert, ansonsten bildet jede Zeile einen separaten History-Eintrag.

Bei aktiver Option cmdhist hängt die konkrete Speicherung der mehrzeiligen Kommandos von der (ebenfalls mit shopt einstellbaren) Option lithist ab. Ist sie gesetzt, speichert die Bash die Zeilen möglichst literal, also unter Erhaltung der eingebetteten Newline-Zeichen, so dass man beim Zugriff auf derartige History-Einträge tatsächlich ein mehrzeiliges Konstrukt im Kommandozeileneditor der Bash angeboten bekommt und bearbeiten kann (ggf. auch im externen Editor, der sich mit Ctrl-X Ctrl-E bequem starten lässt).

Andernfalls werden die Newlines geeignet durch Semikolons ersetzt, d.h., die Bash speichert das mehrzeilige Kommando in einer funktionell äquivalenten einzeiligen Form und bietet dann beim History-Zugriff auch nur diese im Kommandozeileneditor an.

Beispiel:

  # mehrzeiliges Kommando
  for i in 1 2 3
  do
    echo "$i"
  done

  # funktionell äquivalentes einzeiliges Kommando
  for i in 1 2 3; do   echo "$i"; done

Anmerkung: Der Kommandozeilen-Editor der Z Shell unterstützt ebenfalls optional die mehrzeilige Darstellung. Die tcsh übernimmt generell nur die erste Zeile mehrzeiliger Kommandos in die History, so dass man nur auf diese zugreifen kann.

Mit der Shell-Variablen HISTCONTROL kann man steuern, wie Einträge in der History gespeichert werden. Diese Variable enthält eine Liste von Optionen, die durch je einen Doppelpunkt voneinander getrennt sind:

Unbekannte Optionen werden ignoriert. Wenn HISTCONTROL leer ist oder nur ungültige Optionen enthält, werden alle Zeilen in der History gespeichert, sofern dies laut der Shell-Variablen HISTIGNORE zulässig ist. Die Zeilen ab Zeile zwei eines mehrzeiligen zusammengesetzten Kommandos werden nicht getestet und ohne Beachtung von HISTCONTROL in die History übernommen.

Beispiel zur Einstellung von HISTCONTROL:

  HISTCONTROL=ignoreboth:erasedups

Die Shell-Variable HISTIGNORE enthält eine Liste von Pfadnamens-Mustern, die auch wieder durch je einen Doppelpunkt voneinander getrennt sind. Zeilen, die auf diese Muster passen, werden nicht in die History aufgenommen. Jedes Muster wird implizit am linken Zeilenrand verankert und muss der gesamten Zeile entsprechen, wenn es wirken soll. Ein * wird also nicht implizit an das Muster angehängt.

Jedes Muster von HISTIGNORE wird gegen eine Zeile getestet, nachdem die Tests von HISTCONTROL durchgeführt wurden. Zusätzlich zu den normalen Pfadnamens-Mustern der Shell steht das Zeichen & zur Verfügung, das der vorherigen History-Zeile entspricht. Die Sonderbedeutung des Ampersands kann durch das Voranstellen eines Backslashs aufgehoben werden. Die Zeilen ab Zeile zwei eines mehrzeiligen zusammengesetzten Kommandos werden wiederum nicht getestet und ohne Beachtung von HISTIGNORE in die History übernommen.

Beispiel zur Einstellung von HISTIGNORE:

  HISTIGNORE=';&:#*'

Hiermit würde man das Abspeichern von Zeilen vermeiden, die entweder aus einem Semikolon gefolgt von der Vorgänger-Zeile in der History bestehen oder mit einem Doppelkreuz beginnen (Kommentare). Von den Zeilen

  echo
  ;echo
  #echo

würde dann nur die erste in die History aufgenommen werden.

Da der Doppelpunkt als Trenner zwischen den Mustern dient und im Gegensatz zum Ampersand nicht quotiert werden kann, steht er in den Mustern nicht zur Verfügung.

Tasten-Bindungen (key-bindings), d.h. Zuordnungen von Editor-Funktionen oder Makros (Strings) zu Zeichenfolgen, die über die Tastatur eingegeben werden, sind über das Kommando bind und die Datei $INPUTRC bzw. ~/.inputrc einstellbar. Diese Konfigurationsdatei wird beim Start einer Bash gelesen und dient der Initialisierung der Readline-Bibliothek. Neben Tastenbindungen kann man dort mit set auch bestimmte Readline-Variablen verändern, die das Verhalten des Kommandozeilen-Editors beeinflussen (z.B. akustische und visuelle Signale unterdrücken, mit denen im Standardfall auf Fehlersituationen hingewiesen wird).

Zur Bezeichnung von Sondertasten oder speziellen Zeichen in Tasten-Bindungen existieren Escape-Sequenzen, z.B. \M- für das Meta-Präfix und \C- für das Control-Präfix. Außerdem stehen hier die Konstrukte $if, $else und $endif zur Realisierung von Alternativen sowie $include zum Einbinden weiterer Readline-Konfigurationsdateien zur Verfügung.

Eine detaillierte Beschreibung der Readline-Konfiguration findet man im Online-Manual der Bash sowie im Info-File zu Readline, das man unter Linux normalerweise mit dem Kommando

  info readline

aufrufen kann.

In einer laufenden Bash kann man Tasten-Bindungen mit dem Kommando bind manipulieren. Hier einige Beispiele:

  # die Zeichenfolge ESC Leerzeichen für die History-Expandierung wie bei tcsh verwenden
  bind '"\M- ":history-expand-line'
    # Wenn man z.B. hinter dem History-Ausdruck !echo die Tastenfolge
    # ESC Leerzeichen eingibt, wird dieses History-Ausdruck nach Möglichkeit
    # durch die letzte History-Zeile ersetzt, die mit echo beginnt.
    
  # Ctrl-O führt das Kommando ls aus
  bind -x '"\C-o":ls'

  # Liste aller verfügbaren Readline-Funktionen ausgeben
  bind -l

  # alle Readline-Funktionen mit zugehörigen Tasten-Bindungen in einer wieder
  # einlesbaren Form ausgeben
  bind -p

  # alle Readline-Variablen mit zugehörigen Werten in einer wieder einlesbaren
  # Form ausgeben
  bind -v

Anmerkungen:

Man muss bei bind und auch in den von der Bash gelesenen Readline-Konfigurationsdateien sehr genau auf die Syntax der Angaben achten, da inkorrekte Angaben ohne Fehlermeldung ignoriert werden. So ist es z.B. wichtig, dass vor dem Doppelpunkt, der die Tastenfolge von der auszuführenden Aktion trennt, kein Leerzeichen steht.

In Experimenten hat der Autor festgestellt, dass man bestimmte Tasten-Bindungen weder mit bind noch mit einem analogen Konstrukt in der Readline-Konfigurationsdatei ändern kann. Das Handbuch sagt dazu nichts aus.

Beispiel für Zeilen aus der Datei ~/.inputrc:

  # Konfiguration der Readline-Bibliothek
  
  # Readline im Emacs-Modus starten (das ist der Standard)
  set editing-mode emacs
  
  # keine akustischen oder visuellen Signale aussenden, um Fehler/Probleme zu
  # melden
  set bell-style none
  
  # falls bei Vervollständigungen (die typischerweise mittels Tab-Taste erfolgen)
  # mehr als eine Möglichkeit existiert, sollen alle Möglichkeiten immer sofort
  # angezeigt werden
  set show-all-if-ambiguous on
  
  # bei der tcsh steht standardmäßig die Tastenfolge "Escape Leerzeichen" zur
  # Verfügung, mit der man Bang-Ausdrücke (z.B. "!rm") expandieren lassen kann;
  # bei der Bash kann man dasselbe Verhalten erreichen, indem man die Tastenfolge
  # "Escape Leerzeichen" an die Readline-Funktion "history-expand-line" oder
  # "magic-space" bindet; letztere fügt nach der History-Expandierung automatisch
  # noch ein Leerzeichen in die Kommandozeile ein
  "\e ": history-expand-line
  #"\e ": magic-space
  
  # Tilde-Expandierung bei Wort-Vervollständigungen durchführen
  set expand-tilde on

Auch hier werden Kommentare durch ein # gekennzeichnet.

Die Bash unterstützt eine sehr leistungsfähige Vervollständigung von Wörtern der Kommandozeile, wodurch sich viel Tippaufwand sparen lässt und viele Tippfehler vermieden werden. Diese Vervollständigung ist kontextbezogen und programmierbar (man spricht von einer programmable completion). Zu ihrer Steuerung dient das Kommando complete.

Beispiele:

  # bei bunzip2 und bzcat nur Dateinamen komplettieren, die auf .bz2 enden
  complete -f -X '!*.bz2' bunzip2 bzcat 
    # -f --> Dateinamens-Vervollständigung (Alternativ-Notation: -A file)
    # -X --> Pfadnamens-Muster, das dem Streichen von Elementen aus der
    #        bei der Vervollständigung erstellten Liste dient; durch das Präfix !
    #        wird die Wirkung negiert, d.h., die Einträge, die auf das Muster passen,
    #        bleiben erhalten (bei uns also Elemente, die auf .bz2 enden)

  # bei unalias nur Alias-Namen komplettieren
  complete -a unalias

  # bei type und which nur Kommando-Namen komplettieren
  complete -c command type which

  # nutzereigene Vervollständigungs-Funktion; sie expandiert die Namen unter
  # /etc/sysconfig und gibt diese über das Feld COMPREPLY zurück
  function lsc_compl()
  {
    # COMP_WORDS: Array aller Wörter der aktuellen Kommandozeile
    # COMP_CWORD: Index für das Feld COMP_WORDS, der auf das Wort an der
    #             aktuellen Cursor-Position verweist
    COMPREPLY=($(cd /etc/sysconfig ; echo ${COMP_WORDS[$COMP_CWORD]}*))
  }

  # Funktion, deren Argumente durch lsc_compl komplettiert werden sollen
  function lsc()
  {
    for i in "$@"
    do
      ls -ld "/etc/sysconfig/$i"
    done
  }

  # festlegen, dass bei Kommando lsc die Vervollständigungs-Funktion lsc_compl
  # verwendet wird
  complete -F lsc_compl lsc

Das Builtin-Kommando compgen zur Generierung von Vervollständigungs-Ergebnissen bietet Unterstützung bei der Realisierung eigener Vervollständigungs-Funktionen, wie folgendes Beispiel aus dem Bash Completion Project zeigt:

  # This function completes on signal names
  #
  _signals()
  {
    local i

    # standard signal completion is rather braindead, so we need
    # to hack around to get what we want here, which is to
    # complete on a dash, followed by the signal name minus
    # the SIG prefix
    COMPREPLY=( $( compgen -A signal SIG${cur#-} ))
    for (( i=0; i < ${#COMPREPLY[@]}; i++ )); do
        COMPREPLY[i]=-${COMPREPLY[i]#SIG}
    done
  }

  # nutzen ließe sich diese Funktion für das Kommando kill z.B. so:
  function sig_helper()
  {
    local cur=${COMP_WORDS[COMP_CWORD]}
    _signals
  }

  complete -F sig_helper kill

  # Anmerkung: Im Bash Completion Project erfolgt die Nutzung von _signals
  # etwas anders, um dem Nutzer noch mehr Komfort zu bieten.

Der Funktions-Umfang von Readline ist sehr groß. In der Praxis dürfte eine sinnvolle Auswahl genügen, die man sich auch merken kann. Die folgende Tabelle enthält ausgewählte Tastenkombinationen bei Nutzung der standardmäßig aktiven Tasten-Bindungen im Emacs-Modus:

Cursortasten links/rechts Cursor ein Zeichen nach links/rechts positionieren
Cursortasten auf/ab History rückwärts/vorwärts durchlaufen
TAB (Tabulatortaste) "intelligente" Vervollständigung des aktuellen Arguments links vom Cursor
Ctrl-X * Expandierung des Pfadnamens-Musters links vom Cursor
Meta-^ History-Expandierung (für mit ! beginnende Ausdrücke)
ENTER oder Ctrl-M Übergabe der Kommandozeile zur Auswertung an die Shell
Pos1 oder Ctrl-A Cursor an den Zeilenanfang positionieren
Ende oder Ctrl-E Cursor ans Zeilenende positionieren
Meta-b Cursor nach links bis zum nächsten Wortanfang positionieren
Meta-f Cursor nach rechts bis hinter das nächste Wortende positionieren
Meta-u alle Zeichen von der Cursor-Position bis zum nächsten Wortende nach rechts in Großbuchstaben umwandeln
Meta-l alle Zeichen von der Cursor-Position bis zum nächsten Wortende nach rechts in Kleinbuchstaben umwandeln
Backspace oder Ctrl-H Zeichen links vom Cursor löschen
Ctrl-D Zeichen unter dem Cursor löschen
Ctrl-K ab Cursor bis Zeilenende löschen
Ctrl-W nach links bis zum nächsten Wortanfang löschen
Ctrl-X Backspace oder Ctrl-U nach links bis zum Zeilenanfang löschen
Meta-d nach rechts bis zum nächsten Wortende löschen
Ctrl-V das Folgezeichen literal in die Kommandozeile einfügen

Beispiel: Durch Ctrl-V Ctrl-M wird das Zeichen Ctrl-M (Wagenrücklauf) in die Kommandozeile eingefügt. Ctrl-M ohne Präfix Ctrl-V entspricht der ENTER-Taste und würde die Kommandozeile zur Auswertung an die Shell übergeben.
Interrupt-Zeichen Kommandozeile abbrechen

Das Interrupt-Zeichen lautet typischerweise Ctrl-C. Es kann mit dem Kommando stty eingestellt werden und bewirkt das Senden des Signals SIGINT.
Ctrl-L Bildschirm löschen und aktuelle Kommandozeile an den oberen Bildschirm-Rand verschieben
Meta-< Sprung zum Anfang der History
Meta-> Sprung zum Ende der History
Ctrl-R inkrementelle History-Suche rückwärts
Ctrl-S inkrementelle History-Suche vorwärts

Achtung: Oft ist Ctrl-S das Stopp-Zeichen für die Terminal-Flusskontrolle und wirkt dann auch so (Start der gestoppten Ausgabe mit Ctrl-Q). Diese Einstellung kann mit dem Kommando stty geändert werden, z.B. folgendermaßen auf Ctrl-G:

  stty stop '^G'
Meta-. das letzte Wort der letzten Zeile einfügen
Meta-n Meta-. das n-te Wort der letzten Zeile einfügen
Meta-Ctrl-E die aktuelle Zeile expandieren, wie es die Bash tut

Zuerst werden eine Alias- und History-Expandierung realisiert. Danach folgen alle Wort-Expandierungen der Shell (also alle Expandierungen mit Ausnahme der Klammern-, Tilde- und Pfadnamens-Expandierungen).
Ctrl-X Ctrl-E die aktuelle Kommandozeile im externen Editor öffnen und das Resultat ausführen

Die Bash versucht der Reihe nach, die Editoren $FCEDIT, $EDITOR und emacs zu starten.
Ctrl-X Ctrl-V Versionsinformationen zur aktuellen Bash-Instanz ausgeben

Für den Zugriff auf die History stehen neben den recht umfangreichen Möglichkeiten der Readline-Bibliothek auch Kommando-Konstrukte zur Verfügung, die von der tcsh übernommen wurden. Sie werden standardmäßig mit einem auch als Bang bezeichneten Ausrufezeichen (!) eingeleitet und haben allgemein folgendes Format:

  !Ereignisbezeichner[:Wortbezeichner[:Modifikatoren]]

Anmerkung: Möchte man statt des Ausrufezeichens ein anderes Zeichen verwenden, so muss man dieses als erstes Zeichen der Shell-Variablen histchars eintragen. Davon ist aber abzuraten. Wir gehen nachfolgend davon aus, dass immer das Ausrufezeichen verwendet wird.

Diese History-Substitution bzw. -Expandierung steht nur dann zur Verfügung, wenn die Option histexpand bzw. H gesetzt ist, was bei interaktiven Shells standardmäßig der Fall ist. Diese Option kann folgendermaßen gesetzt/rückgesetzt werden:

  set -H
  set -o histexpand
  set +H
  set +o histexpand

Hinweis: Diese zur History-Expandierung genutzten Konstrukte werden nicht in der ursprünglichen, sondern in expandierter Form in die History übernommen.

Beispiel:

  echo eins zwei drei
  echo !$

Der History-Ausdruck !$ wird durch das letzte Wort des vorigen Kommandos ersetzt. In die History wird daher die Zeile

  echo drei

und nicht

  echo !$

aufgenommen.

Falls dem Ausrufezeichen eines der folgenden Zeichen folgt, wird die History-Expandierung verhindert:

Achtung: Die History-Expandierung mittels ! kann nur durch Backslash und einfache Apostrophe, nicht aber durch Doppelapostrophe aufgehoben werden.

Beispiel: Die beiden Kommandos

  : eins zwei drei
  echo \!$ '!$' "!$"

erzeugen die Ausgabe

  !$ !$ drei

Die Bash unterstützt mehrere Ereignisbezeichner (event designators), mit denen man auf Zeilen der History Bezug nehmen kann:

!n Kommandozeile n
!-n aktuelle Kommandozeile minus n
!! voriges Kommando (Abkürzung für !-1)
!string letzte Kommandozeile, die mit string beginnt
!?string? letzte Kommandozeile, die string enthält

Das ? am Ende kann entfallen, wenn hinter string ein Newline-Zeichen (also das Zeilenende) folgt.
^string1^string2^ Schnell-Substitution: Wiederholung des letzten Kommandos, wobei string1 durch string2 ersetzt wird

Diese Substitution ist eine Kurzform für
  !!:s/string1/string2/
(s. unten bei Modifikatoren). Sie wirkt nur, wenn sie unmittelbar am Anfang einer Kommandozeile steht. Es darf also auch kein Leerzeichen vorausgehen. Das abschließende ^ kann entfallen, wenn hinter dem Ausdruck ein Newline-Zeichen folgt.

Anmerkung: Möchte man statt des Hütchens (^) ein anderes Zeichen verwenden, so muss man dieses als zweites Zeichen der Shell-Variablen histchars eintragen. Davon ist aber abzuraten.
!# die komplette bisher geschriebene Kommandozeile

Wenn der Ereignisbezeichner hinter dem Ausrufezeichen fehlt, bezieht sich das History-Konstrukt immer auf die letzte Zeile der History.

Auf die einzelnen Wörter einer History-Zeile kann man über optionale Wortbezeichner (word designators) zugreifen. Fehlt der Wortbezeichner, so bezieht sich das History-Konstrukt auf die gesamte durch den Ereignisbezeichner gewählte History-Zeile.

Standardmäßig wird ein Ereignisbezeichner vom Wortbezeichner durch einen Doppelpunkt (:) getrennt. Der Doppelpunkt kann entfallen, wenn der Wortbezeichner mit einem der Zeichen

  ^  $  *  -  %

beginnt.

Die Wörter der History-Zeilen werden mit Null beginnend von links nach rechts durchnummeriert. Wörter werden im Rahmen der History-Expandierungen in die aktuelle Kommandozeile jeweils so eingefügt, dass sie durch einzelne Leerzeichen voneinander getrennt sind.

Die folgende Tabelle listet die verfügbaren Wortbezeichner auf:

0 nulltes Wort einer Zeile (Kommando-Wort)
n n-tes Wort der Zeile
^ erstes Argument, also Wort 1
$ letztes Argument
% Wort, das bei der letzten String-Suche über ?string? passte (gefunden wurde)
x-y Wortbereich von Wort x bis Wort y; -y entspricht 0-y
* alle Wörter mit Ausnahme des nullten; identisch mit 1-$
x* Abkürzung für x-$
x- Abkürzung für x-$, wobei aber das letzte Wort ausgelassen wird (also Wort x bis zum vorletzten Wort)

Hinter einem Wortbezeichner kann eine Folge von Modifikatoren (modifiers) stehen, wobei jeder Modifikator in der Regel durch einen Doppelpunkt eingeleitet wird:

h head: entfernt die letzte Pfadnamenskomponente, also den Dateinamen
t tail: entfernt alle führenden Pfadnamenskomponenten und erhält den Dateinamen
r entfernt das abschließende Suffix der Form .xxx und belässt den Basisnamen
e extension: entfernt alles bis auf das abschließende Suffix (die letzte Dateinamens-Erweiterung)
p print: Ausgabe des neuen Kommandos, ohne es auszuführen
q quote: Alle substituierten Wörter werden geschlossen durch einfache Apostrophe quotiert, um weitere Substitutionen zu verhindern.
x extended quote: Die substituierten Wörter werden einzeln durch einfache Apostrophe quotiert, um weitere Substitutionen zu verhindern.
s/alt/neu/ substitution: Das erste Auftreten der Zeichenfolge alt wird durch neu ersetzt. Statt / können auch beliebige andere Trenner verwendet werden. Der abschließende Trenner ist optional, wenn danach das Zeilenende folgt. Wenn der Trenner in alt oder neu vorkommt, ist er durch einen Backslash zu quotieren. Wenn ein Ampersand (&) in neu vorkommt, wird es durch alt ersetzt. Ein Backslash hebt diese Sonderbedeutung von Ampersand auf. Wenn alt der leeren Zeichenfolge entspricht, so wird statt dessen der Wert von alt aus der letzten History-Substitution verwendet. Wenn es keine solche gab, so wird der letzte Such-String aus dem Ereignisbezeichner der Form !?string[?] verwendet.
& wiederholt die letzte Substitution
g global: Bewirkt, dass die Substitutionen der Modifikatoren s oder & global erfolgen, also jedes Auftreten von alt durch neu ersetzt wird.

Beispiel:

  :gs/alt/neu/

Statt g kann auch der Modifikator a verwendet werden.

G Global: Der folgende Modifikator s oder & wird genau einmal auf jedes Wort der History-Zeile angewendet.

Hinweis: Die Modifikatoren s und & folgen den Modifikatoren g, a und G also unmittelbar und nicht nach einem Doppelpunkt.

Zur Anzeige und Manipulation der History-Liste dienen die eingebauten Kommandos history und fc (fix command), deren Anwendung nachfolgend an einigen Beispielen demonstriert wird.

Mit fc kann man Teile der History in einen Editor laden lassen und dort modifizieren. Nach dem Speichern des Modifikations-Ergebnisses wird dieses von der Bash ausgegeben und ausgeführt. Falls man nichts ausführen möchte, weil man z.B. die falschen Kommandos in den Editor geladen hat, sollte man alle geladenen Kommandos im Editor löschen und dann die leere Datei abspeichern.

Den zu verwendenden Editor kann man mit der Option -e angeben, z.B.:

  fc -e pico

Ohne diese Option verwendet die Bash den Editor, der als Wert der Shell-Variablen FCEDIT festgelegt wurde. Existiert diese Variable nicht, wird der Wert der Shell-Variablen EDITOR verwendet. Falls auch diese nicht existiert, wird der Editor vi gestartet.

Das Kommamndo fc kennt zwei Aufruf-Formen:

  fc [-e Editor] [-nlr] [Anfang] [Ende]
  fc -s [Muster=Ersatz] [Kommando]

Bei Form 1 wird der durch Anfang und Ende begrenzte Kommando-Bereich spezifiziert. Die beiden Bereichsgrenzen können als Strings oder als Zahlen angegeben werden. Strings selektieren das letzte Kommando, das mit diesem String beginnt. Zahlen werden als History-Nummer gewertet, wobei die Bash negative Zahlen als Offset zur aktuellen History-Nummer interpretiert.

Wenn die Angabe Ende fehlt, so wird sie bei der History-Anzeige auf die aktuelle History-Nummer und bei der History-Modifikation auf den Wert Anfang gesetzt. Folglich werden bei der Anzeige die Kommandos von Anfang bis zum Ende der Liste ausgegeben. Bei der Modifikation wird genau das Kommando Anfang ausgewählt.

Wenn auch die Angabe Anfang fehlt, wird bei der Modifikation das letzte Kommando der History gewählt und bei der Anzeige der Wert Anfang auf -16 gesetzt, so dass maximal die letzten 16 Kommandos angezeigt werden.

Die Option -n unterdrückt die Ausgabe der History-Nummern. Die Option -r kehrt die Ordnung der Kommandos um, d.h., sie werden in umgekehrter Reihenfolge ausgegeben bzw. in den Editor geladen. Wenn die Option -l aktiv ist, werden die History-Kommandos auf die Standard-Ausgabe ausgegeben, ansonsten wie oben beschrieben in den Editor geladen.

Bei Form 2 wird das gewählte Kommando erneut ausgeführt, nachdem alle Vorkommen von Muster durch Ersatz ersetzt wurden. Für die Kommando-Auswahl stehen wieder die oben beschriebenen Zahlen oder Strings zur Verfügung. Fehlt Kommando, so wird das letzte Kommando der History selektiert.

Kommando-Beispiele zur Arbeit mit der History:

  # komplette Anzeige der History
  history
    # Vor jedem Kommando wird bei history die History-Nummer angezeigt.
    # Wenn die Shell-Variable HISTTIMEFORMAT gesetzt ist, wird ab Bash 3.0
    # hinter der History-Nummer auch noch ein Zeitstempel ausgegeben.
    
    # Anmerkung:
    # Geänderte History-Einträge werden durch einen Stern (*) vor dem
    # Kommando und hinter einer evtl. angezeigten History-Nummer
    # gekennzeichnet. Die Änderung eines History-Eintrags (und damit das Setzen
    # des Sterns) kann man erreichen, indem man z.B. durch Betätigung der
    # Cursor-Tasten auf/ab einen bestimmten Eintrag in den
    # Kommandozeilen-Editor lädt, diesen dort modifiziert und durch eine
    # nachfolgende erneute Betätigung der Cursor-Taste auf oder ab in die History
    # zurückspeichert, ohne ihn auszuführen.
    # Eine Beendigung der Modifikation durch ENTER erzeugt keinen Stern, da ja
    # hierbei der modifizierte Eintrag ausgeführt und dadurch in der Regel als
    # neuer Eintrag hinten an die History-Liste angefügt wird.
    
  fc -l  # Anzeige mit History-Nummern
  fc -ln # Anzeige ohne History-Nummern
         # Im Unterschied zu history gibt fc generell keinen Zeitstempel aus.

  # Anzeige der letzten 7 History-Kommandos
  history 7
  fc -l -7

  # Anzeige der History-Kommandos 7 bis 9
  fc -l 7 9

  # Anzeige des vorvorletzten und vorletzten History-Kommandos
  fc -l -3 -2

  # Suche in der History mittels grep
  history | grep ls
    # Anzeige aller Kommandos, die ls enthalten

  # Korrektur von Kommandos mit dem Editor
  fc        # letztes Kommando der History-Liste
  fc 1      # Kommando 1
  fc 5 10   # Kommandos 5 bis 10
  fc 5 echo # Kommandos 5 bis zum letzten Kommando, das mit echo beginnt
  fc -r 7 x # Kommandos 7 bis zum letzten Kommando, das mit x beginnt,
            # wobei diese in umgekehrter Reihenfolge in den Editor geladen werden
  fc -1     # letztes Kommando
  fc -2     # vorletztes Kommando

  # Kommando 8 wiederholen und jedes x durch a ersetzen
  fc -s x=a 8

  # letztes mit man beginnendes Kommando wiederholen und jedes ls
  # durch echo ersetzen
  fc -s ls=echo man

  # History komplett löschen
  history -c

  # Kommando 5 der History löschen
  history -d 5

  # aktuelle History durch den Inhalt der History-Datei ~/history ersetzen
  history -r ~/history

  # aktuelle History in die History-Datei ~/history sichern
  history -w ~/history


  # letzte Zeile nochmal ausführen
  !!

  # letztes Kommando, das mit h beginnt, nochmal ausführen
  !h

  # !$ steht für das letzte Wort des letzten Kommandos
  wc -l !$

  # Alternativ könnte man auch notieren:
  wc -l !:$
  wc -l !!$
  wc -l !!:$

  # Schnell-Substitution:
  ls -l /etc/passwd
    # letztes Kommando nochmal ausführen, aber -l durch nichts ersetzen,
    # also löschen
    ^-l^
      # Ergebnis der Substitution: ls /etc/passwd

    # letztes Kommando nochmal ausführen, aber -l durch -i ersetzen
    ^-l^-i
      # Ergebnis der Substitution: ls -i /etc/passwd

    # Falls man an das Ergebnis der obigen Schnell-Substitution noch etwas anhängen
    # möchte, benötigt man das abschließende ^. Im folgenden Beispiel wird
    # /tmp/ angehängt:
    ^-l^-i^ /tmp
      # Ergebnis der Substitution: ls -i /etc/passwd /tmp

    # Ohne das abschließende ^ sähe es so aus:
    ^-l^-i /tmp
      # Ergebnis der Substitution: ls -i /tmp /etc/passwd
      # Hier wird -l durch -i /tmp ersetzt.

  # Über die Shell-Variable HISTTIMEFORMAT ist ab Bash 3.0 die Einstellung des
  # Formats des Zeitstempels möglich, der bei history ausgegeben wird:
  HISTTIMEFORMAT='(%d.%m.%Y) %H:%M '
    # In Klammern wird hier das Datum und dahinter die Uhrzeit angegeben,
    # z.B. (08.01.2006) 19:47

  # Unterschied zwischen den Modifikatoren g und G
  : 1 2 3453 1 2 3453
  !*:gs/3/9/:p
    # expandiert zu 1 2 9459 1 2 9459
    #
    # Alle Vorkommen der 3 werden durch 9 ersetzt.
    #
    # Erläuterung des Konstrukts: Da der Ereignisbezeichner fehlt, bezieht sich
    # der History-Ausdruck auf die vorherige Zeile (die letzte Zeile der
    # History). Als Wortbezeichner dient der *, der alle Argumente
    # auswählt. Er muss vom Ereignisbezeichner nicht durch einen Doppelpunkt
    # getrennt werden.
    # Hinter dem Wortbezeichner folgen die beiden Modifikatoren g und
    # s, die nicht durch Doppelpunkt getrennt werden dürfen. Sie
    # realisieren eine globale Substitution von 3 durch 9.
    # Der anschließend folgende Modifikator :p bewirkt, dass das Ergebnis
    # der History-Substitution nur in die Kommandozeile übernommen (also
    # angezeigt) wird, ohne es der Shell zur Auswertung zu übergeben.

  : 1 2 3453 1 2 3453
  !*:Gs/3/9/:p
    # expandiert zu 1 2 9453 1 2 9453
    #
    # Im Unterschied zum obigen Konstrukt wird hier das jeweils erste
    # Vorkommen der 3 in einem Wort durch 9 ersetzt.

22. Alias-Konstrukte (Aliase)

Ein Alias bzw. Alias-Name ist ein Kurz- oder Alternativ-Name für ein anderes (meist längeres oder schwer zu merkendes) Shell-Konstrukt. Er wird speziell in interaktiven Shells benutzt, um den Tippaufwand zu reduzieren oder sich leichter merkbare Kommandos zu schaffen.

Im Rahmen einer Alias-Definition wird dem Alias-Namen ein Ersetzungstext zugewiesen. Dazu dient das eingebaute Kommando alias:

  alias Aliasname=Ersetzungstext [Aliasname=Ersetzungstext ...]

Einem bereits existierenden Alias-Namen kann man durch eine solche Alias-Definition jederzeit einen anderen Ersetzungstext zuweisen.

Mit dem ebenfalls eingebauten Kommando unalias lässt sich eine Alias-Definition wieder aufheben:

  unalias Aliasname [Aliasname ...]

Mit dem Kommando alias kann man sich auch alle Alias-Definitionen oder eine ausgewählte Alias-Definition auflisten lassen:

  # alle:
  alias

  # ausgewählte:
  alias Aliasname [Aliasname ...]

Der Ersetzungstext unterliegt den typischen Expandierungen (z.B. Variablen-, Kommando- und arithmetische Substitution) und muss aus Sicht der Shell ein einzelnes Wort sein. Ggf. ist also bei der Alias-Definition eine geeignete Quotierung nötig, z.B.:

  alias rm='rm -i'
  alias xyz="echo Alias-Definition am $(date) durch $USER \; 3+4=$((3+4))"

Anmerkung: Das Semikolon im Alias xyz wurde gesondert quotiert, da es sonst als Trenner zwischen zwei Kommandos interpretiert wird. Alternativ hätte man z.B. den Alias auch unter Verwendung einfacher Apostrophe als Quotierungs-Zeichen definieren können:

  alias xyz="echo 'Alias-Definition am $(date) durch $USER ; 3+4=$((3+4))'"

Im Rahmen der Auswertung von Kommandozeilen führt die Bash bei Bedarf eine Alias-Expandierung durch. D.h., sie ersetzt das erste Wort eines einfachen Kommandos, das einem Alias-Namen entspricht, durch den Ersetzungstext, sofern dieses Wort nicht quotiert und nicht das Ergebnis einer anderen Expandierung (z.B. Parameter-Expandierung) ist.

Eine Alias-Expandierung findet in der Bash allerdings nur dann statt, wenn die Option expand_aliases gesetzt ist. Bei interaktiven Shells ist dies standardmäßig der Fall. Bei nicht-interaktiven Shells muss die Option explizit gesetzt werden:

  shopt -s expand_aliases

Als Ersetzungstext ist ein beliebiges (gültiges) Shell-Konstrukt möglich, das von der Shell so ausgewertet wird, als hätte es an Stelle des Alias-Namens in der Kommandozeile gestanden. Im Gegensatz zum Ersetzungstext dürfen im Alias-Namen keine Shell-Metazeichen, keine Quotierungs-Zeichen und keines der Zeichen

  /  $  `  =

verwendet werden.

Wenn das erste Wort des Ersetzungstextes ein gültiger Alias-Name ist, der nicht dem gerade zu expandierenden Alias-Namen entspricht, erfolgt eine weitere Alias-Expandierung. Bei identischen Namen unterbleibt diese weitere Alias-Expandierung, da die Bash generell rekursive Alias-Expandierungen unterbindet. Das gilt z.B. auch für Konstellationen folgender Art:

  alias xx=yy
  alias yy=xx

Wenn der Ersetzungstext mit einem Leerzeichen endet, dann unterliegt auch das Wort, das einem Alias-Namen folgt, einer Alias-Expandierung.

Bash-Aliase unterstützen im Gegensatz zu Shell-Funktionen (und tcsh-Aliasen) keine Argumente.

Zur Vermeidung ungewollter Effekte sollten Alias-Definitionen immer auf einer separaten Zeile und außerhalb zusammengesetzter Kommandos und damit auch außerhalb von Shell-Funktionen erfolgen. Alias-Definitionen innerhalb einer Shell-Funktion sind erst wirksam, nachdem die Funktion das erste Mal ausgeführt wurde.

Aliase, die innerhalb eines Funktionskörpers genutzt werden, werden expandiert, wenn die Shell die Funktionsdefinition auswertet, und nicht, wenn die Funktion ausgeführt wird. Hier ein Beispiel:

  alias rm='rm -i'
  function fkt()
  {
    rm /tmp/xxx
  }

Im Funktionskörper wird rm durch rm -i ersetzt. Die Bash speichert sich also folgende Funktionsdefinition, die man sich durch das Kommando

  declare -f fkt

anzeigen lassen kann:

  fkt ()
  {
      rm -i /tmp/xxx
  }

Zum Zeitpunkt der Ausführung dieser Funktion wird also der Alias rm nicht mehr benötigt. Man kann ihn bis dahin löschen oder anders definieren:

  unalias rm
  fkt

Der Aufruf der Funktion fkt bewirkt trotzdem die Ausführung des Kommandos

  rm -i /tmp/xxx

Funktionen sind allgemein flexibler als Alias-Konstrukte. Es empfiehlt sich daher, möglichst Funktionen statt Alias-Definitionen zu verwenden.

Wietere Beispiele:

  # Alias definieren
  alias l='ls -l'

  # alle Alias-Definitionen auflisten lassen
  alias

  # Definition des Alias l auflisten lassen
  alias l

  # Alias nutzen
  l /tmp # wird zu ls -l /tmp expandiert

  # Alias-Definition aufheben
  unalias l
  l /tmp

  # rm als weiteren Alias definieren
  alias rm='rm -i'
  # Nutzung des Alias rm
  rm file
  # durch Quotierung kann man die Alias-Expandierung unterbinden
  \rm file
  'rm' file
  "rm" file
    # hier wird also die Shell-Funktion rm (sofern definiert) bzw.
    # das Standard-Kommando rm ausgeführt

  # tcsh-Alias mit Argument und äquivalente Bash-Funktion
  #
  # Ziel: Alias bzw. Funktion psg definieren, die die
  # Prozess-Status-Liste mittels AWK nach einem Muster durchsucht und
  # dabei das Such-Kommando selbst ausblendet
  #
  # Aufruf-Beispiel:
  psg 'kmix.*session'
    # sucht nach einem Prozess-Eintrag, in dem irgendwo der String
    # kmix und weiter hinten der String session vorkommt
  
  # Realisierung als tcsh-Alias:
  #
  #  alias psg 'eval set muster=\!:1:q ; ps auxww | sed 1d | awk "/$muster/" | fgrep -v "awk /$muster/"'
  #
  # Durch das History-Konstrukt \!:1:q greift der Alias auf das erste Argument
  # hinter dem Alias-Namen zu, im obigen Beispiel also auf kmix.*session.

  # Realisierung einer funktionell äquivalenten Bash-Funktion:
  function psg()
  {
    ps auxww | sed 1d | awk "/$1/" | fgrep -v "awk /$1/"
  }
    # Mit $1 greift die Bash-Funktion auf das erste Argument
    # hinter dem Funktionsnamen zu, im obigen Beispiel also auf kmix.*session.

  # Anmerkung:
  # In der tcsh stehen nur Aliase und keine Funktionen zur Verfügung.
  # Das ist ein Nachteil der tcsh gegenüber den Shells der
  # Bourne-Shell-Familie, da Funktionen allgemein eine höhere Flexibilität
  # als Aliase bieten, unabhängig davon, ob die Aliase Argumente akzeptieren
  # oder nicht.

  # Alias-Expandierung erfolgt nicht rekursiv:
  alias ls='ls -F'
  ls # Expandierung zu ls -F
     # Das ls im Ersetzungstext wird nicht nochmal expandiert.

  # wiederholte Alias-Expandierung
  alias a1='echo alias a1'
  alias a2='echo alias a2'
  alias a3='echo alias a3 ' # mit Leerzeichen am Ende
  a1 a2  # Expandierung zu echo alias a1 a2
  a3 a2  # Expandierung zu echo alias a3 echo alias a2

23. Expandierungen

Wenn der Bash eine Kommandozeile zur Ausführung übergeben wird, führt sie bei Bedarf (d.h. beim Auftreten nicht quotierter Zeichen mit Sonderbedeutung) eine Reihe von Expandierungen (Substitutionen) durch, bevor sie die Zeile wirklich ausführt. Expandierungen können prinzipiell alle Wörter bzw. Stellen der Kommandozeile betreffen. Sie sind also nicht auf Kommandonamen oder Argumente beschränkt. Durch eine geeignete Quotierung mit Backslash (\), Apostroph (') und Doppelapostroph (") kann man Expandierungen komplett oder teilweise unterdrücken.

Expandierungen erfolgen in einer festen Reihenfolge. Die folgende geordnete Liste nennt die einzelnen Schritte, die bei der Auswertung einer Kommandozeile durchlaufen werden:

  1. History-Expandierungen

  2. Zerlegung der Kommandozeile in einzelne Wörter (Token), wobei die unter Terminologie genannten Metazeichen als Trenner dienen

  3. Alias-Expandierungen

  4. Expandierung geschweifter Klammern (brace expansion)

  5. Tilde-Expandierung (tilde expansion)

  6. von links nach rechts in der Zeile:

    Diese vier Expandierungen erfolgen also nicht nacheinander, sondern gewissermaßen simultan. Die Zeile wird daher nicht vier Mal, sondern nur einmal von links nach rechts durchlaufen, um alle vier genannten Expandierungen zu realisieren.

  7. Aufspaltung in Wörter (word splitting)

  8. Pfadnamens-Expandierung (pathname expansion)

  9. Entfernung der Quotierungs-Zeichen (quote removal)

Die Klammern-, Tilde- und Pfadnamens-Expandierungen können die Anzahl der Wörter einer Zeile verändern. Alle anderen Expandierungen substituieren normalerweise ein Wort durch ein anderes Wort. Einzige Ausnahme sind die Expandierungen "$@" (Liste der Positionsparameter) und "${feld[@]}" (Liste aller Feld-Elemente).

Aus der festen Reihenfolge der Expandierungen resultiert, dass man ggf. eine weitere Auswertungs-Runde mittels eval erzwingen muss:

  # der Reihe nach in die Home-Verzeichnisse der Nutzer egon, berta und anton
  # wechseln (um dort bestimmte Kommandos auszuführen)
  for user in egon berta anton
  do
    eval cd ~$user
    # Da die Tilde-Expandierung vor der Parameter-Expandierung erfolgt,
    # führt das (bei tcsh und zsh auf Grund einer anderen Expandierungs-
    # Reihenfolge wie gewünscht funktionierende) Kommando
    #
    #  cd ~$user
    #
    # bei der Bash dazu, dass an das Kommando cd ein Argument der Form ~egon
    # übergeben wird, was einen Fehler zur Folge hat. Um den Ausdruck ~egon
    # durch den absoluten Pfad zum Home-Verzeichnis von Nutzer egon ersetzen
    # zu lassen, muss eine weitere Auswertung durch das Kommando eval
    # erzwungen werden.

    # hier die gewünschten Kommandos ausführen ...
  done

Wenn bei einfachen Kommandos der Kommando-Name einem nicht quotierten Alias-Namen entspricht, erfolgt die weiter oben beschriebene Alias-Expandierung. Entsteht der Name eines einfachen Kommandos erst im Ergebnis einer Expandierung, erfolgt standardmäßig keine Alias-Expandierung. Um sie auch hier zu erreichen, benötigt man wieder das Kommando eval:

  # Alias al definieren
  alias al='echo Alias'

  # den Alias-Namen al an die Variable cmd zuweisen
  cmd=al

  # Kommando ausführen, dessen Name in cmd steht
  $cmd
     # Die Bash expandiert hier $cmd zu al. Dieses Kommando existiert nicht.
     # Daher bekommt man eine Fehlermeldung: bash: al: command not found

  # Mit eval wird der Alias expandiert, so dass echo Alias ausgeführt wird:
  eval $cmd

Nachfolgend sollen die verschiedener Expandierungen detailliert dargestellt und an Beispielen erläutert werden, wobei wir auf History- und Alias-Expandierungen nicht eingehen, weil diese an anderer Stelle ausführlich besprochen werden. Zur leichteren Orientierung in dem relativ langen Text werden Syntax-Angaben rot hervorgehoben:


24. Rechnen mit der Bash

Die Bash beherrscht die arithmetische Evaluierung, also die Auswertung arithmetischer Ausdrücke. Sie unterstützt keine Gleitpunkt-Operationen, sondern nur eine Ganzzahl-Arithmetik (Integer-Arithmetik) fester Länge ohne Überlauf-Kontrolle. Eine Division durch Null wird allerdings abgefangen und als Fehler gemeldet. Bei den Interger-Werten handelt es sich um vorzeichenbehaftete 64-Bit-Werte in Zweierkomplement-Darstellung.

Bash bietet dieselben arithmetischen Operatoren mit demselben Vorrang und derselben Assoziativität wie die Programmiersprache C. Zusätzlich steht mit ** ein Potenzierungs-Operator zur Verfügung:

Hier ein kurzer Überblick über die Operatoren (fallend nach Vorrang geordnet):

id++ id-- Post-Inkrement and -Dekrement von Variablen
++id --id Prä-Inkrement and -Dekrement von Variablen
- + unäres Minus und Plus
! ~ logische und bitweise Negation
** Exponentiation
* / % Multiplikation, Division, Divisionsrest (Modulo-Division)
+ - Addition, Subtraktion
<< >> bitweise Links- bzw. Rechtsverschiebung
<= >= < > Vergleiche
== != Test auf Gleichheit und Ungleichheit
& bitweises UND
^ bitweises XOR (Exklusiv-ODER)
| bitweises ODER
&& logisches UND
|| logisches ODER
Ausdruck ? Ausdruck : Ausdruck ternärer Operator (Bedingungstest)
= *= /= %= += -= <<= >>= &= ^= |= Zuweisungen
Ausdruck1 , Ausdruck2 Komma-Operator

Operatoren werden entsprechend ihrem Vorrang ausgewertet. Durch runde Klammern kann man den Vorrang und damit die Auswertungs-Reihenfolge ändern, da geklammerte Teilausdrücke zuerst ausgewertet werden.

Shell-Variablen sind als Operanden zulässig. Vor der Auswertung eines Ausdruck wird eine Parameter-Substitution durchgeführt. Innerhalb eines Ausdrucks kann auf den Wert einer Variablen Bezug genommen werden, indem man deren Namen angibt, wobei das vorangehende Dollar-Zeichen (das normalerweise für eine Parameter-Substitution benötigt wird) entfallen kann. Eine leere oder ungesetzte (nicht existente) Variable wird zu 0 evaluiert.

Wie weiter oben schon erwähnt wurde, kann man eine Variable durch das Kommando

  declare i Variablenname

explizit als Integer-Variable deklarieren. Dies hat zur Folge, dass alle Wertzuweisung automatisch arithmetisch evaluiert werden. Um den Wert einer Variablen in einem arithmetischen Ausdruck verwenden zu können, ist es allerdings nicht nötig, die Variable als Integer-Variable zu deklarieren.

Für Integer-Konstanten wird die Syntax

  [Basis#]Zahl

verwendet. Die Angabe der Basis ist optional. Wird sie weggelassen, nimmt die Bash automatisch die Basis 10 an.

Konstanten, die mit dem Präfix 0 beginnen, werden als Oktalzahlen interpretiert. Die Präfixe 0x und 0X kennzeichnen Hexadezimal-Zahlen.

Als Basis sind die natürlichen Zahlen von 2 bis 64 zulässig. Die Ziffern größer als 9 werden der Reihe nach durch Kleinbuchstaben, Großbuchstaben sowie die Zeichen @ und _ repräsentiert. Wenn die Basis kleiner als 37 ist, können wahlweise Klein- und Großbuchstaben verwendet werden, um die Ziffern zwischen 10 und 35 darzustellen.

Eine arithmetische Evaluierung erfolgt in folgenden Situationen:

  1. im Rahmen einer arithmetischen Expandierung: $((Ausdruck))

  2. beim eingebauten Kommando let:

      let Ausdruck [Ausdruck ...]
  3. bei der Wertzuweisung an eine explizit deklarierte Integer-Variable.

Die einzelnen Ausdrücke von let müssen aus Sicht der Shell jeweils separate Wörter sein. Ggf. muss man sie also geeignet quotieren. Der Exit-Status von let ist 1, falls der letzte Ausdruck den Wert Null liefert, sonst 0.

Die arithmetische Ausdrucks-Auswertung mit

  ((Ausdruck))

entspricht folgendem let-Kommando:

   let "Ausdruck"

Daher kann man innerhalb des arithmetischen Ausdrucks auch eine Kommando-Substitution nutzen.

Beispiele:

  echo $((9#11 + 10))    # 10+10 --> Ausgabe: 20
  a=9
  echo $((a--)) $((--a)) # Ausgabe: 9 7

  ((a == 7)) && echo OK  # Ausgabe: OK, da a den Wert 7 hat
  (($a == 7)) && echo OK # dito; hier mit $ vor dem Variablenamen

  let a=9+10 a=a+1       # $? hat den Wert 0, a den Wert 20
  declare -i a           # a als Integer-Variable deklarieren
  a=a**2                 # a ergibt sich zu a hoch 2 (400)

  # Nutzung der Kommando-Substitution:
  ((b = `echo 1` + 1))   # b bekommt den Wert 2
  ((b = $(echo 1) + 1))  # dito

  # Durch den Komma-Operator kann man mehrere Ausdrücke syntaktisch zu einem
  # einzigen Ausdruck zusammenfassen. Das folgende Beispiel zeigt dies:
  ((a=9, b=10))
  # Hier wird also der Variablen a die Zahl 9 und der Variablen b
  # die Zahl 10 zugewiesen. Dieses Konstrukt entspricht folgendem let-Kommando:
  let "a=9, b=10"
  # Man kann bei let natürlich auch den einfachen Apostroph sowie den
  # Backslash zum Quotieren benutzen:
  let 'a=9, b=10'
  let a=9,\ b=10
  # Bei Verzicht auf das Leerzeichen nach dem Komma-Operator ist gar keine
  # Quotierung nötig, da die Shell dann genau ein Argument an let übergibt:
  let a=9,b=10
  # An das Kommando let kann man die beiden Zuweisungen als separate
  # Ausdrücke übergeben, was inhaltlich denselben Effekt hätte.
  # Der Komma-Operator ist hier also nicht nötig:
  let a=9 b=10

25. Test-Operatoren und Bedingungs-Ausdrücke

Bedingungs-Ausdrücke (conditional expressions) werden durch das zusammengesetzte Kommando [[ sowie durch die beiden funktionell identischen eingebauten Kommandos test und [ genutzt, um Datei-Attribute zu testen sowie arithmetische und String-Vergleiche durchzuführen.

Die Bash bietet die in der folgenden Tabelle genannten unären und binären Test-Operaten (primaries) zur Bildung von Bedingungs-Ausdrücken an. Wenn ein Datei-Argument dieser Operatoren die Form /dev/fd/n hat, dann wird der Datei-Deskriptor n getestet. Falls das Datei-Argument /dev/stdin, /dev/stdout oder /dev/stderr lautet, dann wird der korrespondierende Deskriptor (also 0, 1 oder 2) getestet.

-a Datei wahr, wenn Datei existiert
-b Datei wahr, wenn Datei existiert und eine blockorientierte Gerätedatei ist
-c Datei wahr, wenn Datei existiert und eine zeichenorientierte Gerätedatei ist
-d Datei wahr, wenn Datei existiert und ein Verzeichnis (Directory) ist
-e Datei wahr, wenn Datei existiert
-f Datei wahr, wenn Datei existiert und eine reguläre Datei ist
-g Datei wahr, wenn Datei existiert und ihr Set-Group-ID-Bit gesetzt ist
-h Datei wahr, wenn Datei existiert und symbolischer Link ist
-k Datei wahr, wenn Datei existiert und ihr Sticky-Bit gesetzt ist
-p Datei wahr, wenn Datei existiert und sie eine benannte Pipe (FIFO) ist
-r Datei wahr, wenn Datei existiert und lesbar ist
-s Datei wahr, wenn Datei existiert und eine Länge größer 0 hat
-t fd wahr, wenn Datei-Deskriptor fd geöffnet ist und sich auf ein Terminal bezieht
-u Datei wahr, wenn Datei existiert und ihr Set-User-ID-Bit gesetzt ist
-w Datei wahr, wenn Datei existiert und schreibbar ist
-x Datei wahr, wenn Datei existiert und ausführbar ist
-O Datei wahr, wenn Datei existiert und ihr Eigentümer der effektiven User-ID des aktuellen Shell-Prozesses entspricht
-G Datei wahr, wenn Datei existiert und ihre Gruppe der effektiven Gruppen-ID des aktuellen Shell-Prozesses entspricht
-L Datei wahr, wenn Datei existiert und ein symbolischer Link ist
-S Datei wahr, wenn Datei existiert und ein Socket ist
-N Datei wahr, wenn Datei existiert und seit dem letzten Lesen modifiziert wurde (die Zugriffszeit liegt vor der Modifikationszeit)
Datei1 -nt Datei2 wahr, wenn Datei1 (gemäß der Modifikationszeit) neuer ist als Datei2 oder wenn Datei1 existiert und Datei2 nicht
Datei1 -ot Datei2 wahr, wenn Datei1 (gemäß der Modifikationszeit) älter ist als Datei2 oder wenn Datei2 existiert und Datei1 nicht
Datei1 -ef Datei2 wahr, wenn sich Datei1 und Datei2 auf dieselbe Geräte- und Inode-Nummer beziehen, also identische Files sind
-o Option wahr, wenn die (mit set einstellbare) Shell-Option Option aktiv ist
-z String wahr, wenn String die Länge 0 hat
-n String wahr, wenn String eine Länge größer 0 hat
String identisch mit -n String
String1 == String2 wahr, wenn die beiden Strings gleich sind

Bei strikter POSIX-Konformität muss man = statt == verwenden.
String1 != String2 wahr, wenn die beiden Strings ungleich sind
String1 < String2 wahr, wenn String1 in der lexikografischen Ordnung der aktuellen Locale vor String2 steht
String1 > String2 wahr, wenn String1 in der lexikografischen Ordnung der aktuellen Locale hinter String2 steht
Argument1 Operator Argument2 wahr, wenn der arithmetische Vergleich der beiden ganzzahligen Operanden Argument1 und Argument2 unter Verwendung des binären Operators Operator wahr ist

Die folgende Tabelle nennt die möglichen Operatoren und deren Bedeutung:

Operatorsymbolische Bedeutungverbale Bedeutung
-eq = gleich (equal)
-ne != ungleich (not equal)
-lt < kleiner als (less than)
-le <= kleiner gleich (less than or equal)
-gt > größer als (greater than)
-ge >= größer gleich (greater than or equal)

Beispiele:

  -1 -eq 17
  45 -lt 90

26. Pipes (Pipelines) und T-Stücke

Unter einer Pipe oder Pipeline versteht man eine Folge von mindestens einem (einfachen oder zusammengesetzten) Kommando, die durch das Pipe-Zeichen | getrennt sind.

Syntax einer Pipeline:

  [time [-p]] [ ! ] Kommando [ | Kommando2 ... ]

Die Standard-Ausgabe (stdout) von Kommando wird dabei über eine Pipe mit der Standard-Eingabe (stdin) von Kommando2 verbunden.

Der Exit-Status der Pipe entspricht standardmäßig dem Exit-Status des letzten Pipe-Kommandos. Das Ausrufezeichen (!) vor der Pipe bewirkt die logische Negation des Exit-Status.

Das reservierte Wort time vor einer Pipeline bewirkt, dass nach der Beendigung der Pipe deren Laufzeit gemeldet wird. Die Option -p von time verändert nur die Darstellung der Zeitangaben, indem sie das POSIX-Format aktiviert.

Die Bash wartet, bis alle Kommandos der Pipe beendet wurden, bevor sie einen Status meldet.

Über das Feld PIPESTATUS können Pipe-Fehler erkannt werden, da der Exit-Status jedes Kommandos der jeweils zuletzt ausgeführten Pipe als Element dieses Feldes vermerkt wird. Ab Bash 3.0 wird zusätzlich die Option pipefail unterstützt. Wenn sie gesetzt ist, entspricht der Pipe-Status

Beispiele:

  # Zeilenanzahl der Ausgabe von ls -l ermitteln
  ls -l | wc -l
  
  # Exit-Status anzeigen
  echo $?
  
  # obiges ls-Kommando wiederholen, aber Exit-Status negieren
  ! !l
    # Da dem ersten ! ein Leerzeichen folgt, wird hier keine
    # History-Expandierung durchgeführt. Das zweite Wort ist
    # dagegen ein gültiger History-Ausdruck, der entsprechend
    # expandiert wird.
  
  # Status-Anzeige (Kommando echo) wiederholen
  !e

  # Das Kommando tee fügt ein T-Stück in die Pipe ein, d.h.,
  # tee dupliziert die von stdin gelesenen Daten. Eine Kopie wird nach
  # stdout und in jede der als Argument benannten Dateien (im Beispiel
  # nur in die Datei /tmp/ls.txt) ausgegeben.
  ls -l | tee /tmp/ls.txt | wc -l
  ls -l | tee /tmp/ls.txt | wc -l > /tmp/wcl.txt

  # Anmerkung: Die zsh beherrscht ein implizites tee. Man
  # kann die Standard-Ausgabe eines Kommandos gleichzeitig in
  # mehrere Dateien und ggf. noch zusätzlich in eine Pipe
  # umlenken:
  ls -l > /tmp/ls.txt | wc -l
  ls -l > /tmp/ls.txt > /tmp/ls2.txt | wc -l

  # allen Verzeichnissen unterhalb des aktuellen Verzeichnisses (.) für
  # die Gruppe und die Welt (go) das Ausführungs-Recht (+x) setzen
  find . -type d | xargs chmod go+x

  # Zeitmessung der Berechnung der MD5-Summen aller Dateien
  # im Verzeichnis /boot
  time md5sum /boot/* >/dev/null 2>/dev/null
    # Verzeichnisse werden von md5sum ignoriert.
    # Die Ausgaben des Kommandos werden durch Umlenkung nach /dev/null
    # vernichtet. Daher sehen wir nur die Zeitangaben.

  # Alternative (if) unter Verwendung der Negation des Exit-Status
  # des Kommandos cd
  if ! cd /etc 2> /dev/null
  then
    echo cd schlug fehl
    exit 1
  fi

  # Pipe starten, deren mittleres Kommando fehlschlägt
  true | false | true
  # Pipe-Status anzeigen
  echo ${PIPESTATUS[*]}

Eine Fehlerkontrolle für Pipes lässt sich z.B. über eine Funktion check_pipe_error zentralisieren, die man in einer separaten Datei ablegen und dann per source einbinden kann:

  # Datei check_pipe_error.sh
  
  # interne Funktion zur Überprüfung des PIPESTATUS
  # $1 --> PIPESTATUS
  # $2 --> optional; falls "0", dann Fehlermeldung unterdrücken
  function check_pipe_error_intern()
  {
    local i=1 rc
    for rc in $1
    do
      if ((rc))
      then
        [[ $2 != 0 ]] && echo "Fehler bei Pipe-Kommando $i (Rueckgabewert: $rc)"
        return 1
      fi
      ((i++))
    done
    return 0
  }
  
  # Überprüfung des PIPESTATUS, um einen Fehler in einer Pipe aufzuspüren
  function check_pipe_error()
  {
    check_pipe_error_intern "${PIPESTATUS[*]}" $1
  }

Nutzung in einem anderen Skript:

  source check_pipe_error.sh
 
  datenbank_abfrage | awk -f filter.awk | sort > resultat.txt
    # Wir unterstellen hier, dass das Kommando datenbank_abfrage
    # eine Datenbank-Abfrage realisiert, seine Daten über ein Pipe an ein
    # AWK-Filter-Programm übergibt und dessen Ergebnis über eine weitere Pipe
    # an das Kommando sort weiterleitet, um am Ende gefilterte und sortierte
    # Abfrage-Ergebnisse zu erhalten.

  # Wichtig!!! Die Funktion check_pipe_error muss unmittelbar nach der
  # zu prüfenden Pipe gerufen werden, um eine ungewollte Veränderung des
  # Inhalts von PIPESTATUS zu vermeiden.
  check_pipe_error || exit 1
  # ohne Fehlermeldung in check_pipe_error:
  #   check_pipe_error 0 || exit 1
  echo Abfrage OK

27. Listen inklusive Hintergrund-Kommandos

Unter einer Liste versteht die Bash folgendes:

Innerhalb einer Liste kann statt eines Semikolons auch eine Folge von Newline-Zeichen als Trenner zwischen den Kommandos (Pipelines) fungieren.

Kommandos, die durch ein Semikolon oder eine Newline-Folge voneinander getrennt sind, werden streng sequentiell ausgeführt. Die Shell wartet also immer das Ende eines Kommandos ab, bevor sie das nächste Kommando interpretiert und ausführt. Der Exit-Status einer solchen Liste entspricht dem Exit-Status des letzten ausgeführten Kommandos der Liste.

Beispiel:

  # die Kommandos date und uptime sequentiell ausgeführen
  date ; uptime
  
  # Alternativ-Notation mit Newline statt Semikolon:
  date
  uptime

Mit dem Operator && wird eine UND-Liste gebildet. Das Konstrukt

  Kommando1 && Kommando2

ist eine Kurzform der folgenden unvollständigen Alternative:

  if Kommando1
  then
    Kommando2
  fi

Kommando2 wird also nur dann ausgeführt, wenn Kommando1 erfolgreich (also mit dem Exit-Status 0) beendet wurde.

Beispiel:

  # die Datei /etc/passwd ausgeben, wenn sie lesbar ist
  [[ -r /etc/passwd ]] && cat /etc/passwd

Der Exit-Status einer UND-Liste entspricht dem Exit-Status des letzten ausgeführten Kommandos der Liste.

Mit dem Operator || wird eine ODER-Liste gebildet. Das Konstrukt

  Kommando1 || Kommando2

ist eine Kurzform der folgenden unvollständigen Alternative mit negierter Bedingung:

  if ! Kommando1
  then
    Kommando2
  fi

Kommando2 wird also nur dann ausgeführt, wenn Kommando1 mit einem Fehler (also einem Exit-Status ungleich 0) beendet wurde.

Beispiel:

  # eine Meldung ausgeben, wenn die Datei /etc/shadow nicht lesbar ist
  [[ -r /etc/shadow ]] || echo '/etc/shadow nicht lesbar'

Der Exit-Status einer ODER-Liste entspricht dem Exit-Status des letzten ausgeführten Kommandos der Liste.

Hinweis: Da die Kommandos der mit den Operatoren

  ;  &&  ||  Newline

gebildeten Listen sequentiell ausgeführt werden, können Dateien, die von einem Kommando der Liste neu erzeugt wurden, in einem nachfolgenden Kommando über die Pfadnamens-Expandierung gefunden werden, da das Nachfolge-Kommando erst nach dem Abschluss des vorangehenden Kommandos interpretiert und ausgeführt wird. Das folgende Beispiel soll dies verdeutlichen:

  rm -f /tmp/x*
  touch /tmp/xyz && echo /tmp/x*
    # Das Kommando rm löscht alle Dateien im Verzeichnis /tmp, deren
    # Name mit x beginnt. Durch touch wird die Datei /tmp/xyz erzeugt.
    # Die Pfadnamens-Expandierung des zweiten Kommandos der Liste (echo)
    # findet dann die Datei /tmp/xyz.

Wenn hinter einem Kommando der Operator & folgt, dann führt die Shell dieses Kommando im Hintergrund, d.h. in einer Sub-Shell (also einem Kind-Prozess der aktuellen Shell) aus, auf deren Ende die Shell nicht wartet. Sie kehrt sofort mit dem Exit-Status 0 zurück. Hintergrund-Kommandos werden daher auch als asynchrone Kommandos bezeichnet. Alle sonstigen Kommandos sind synchrone Kommandos, da sie von der Shell im Vordergrund, d.h. im aktuellen Shell-Prozess ausgeführt werden und die Shell auf deren Ende wartet, bevor sie das Folge-Kommando ausführt.

Das Beispiel-Skript bg.sh soll unten im Hintergrund ausgeführt werden. Es hat folgenden Inhalt:

  #!/bin/sh
  #
  # hängt aller 3 Sekunden einen Zeitstempel an /tmp/bg_out.txt an

  # den Datei-Namen an die Variable file zuweisen
  file=/tmp/bg_out.txt

  # Signal-Handler für die ersten 15 Signale installieren (bei Signal 9
  # geht das zwar nicht, aber man kann es problemlos angeben)
  for (( i=1 ; i < 16 ; i++ ))
  do
    trap "echo Signal $i ; exit 1" $i
  done

  echo "Los geht's"

  # Datei löschen
  rm -f $file

  # Endlos-Schleife
  while ((1))
  do
    # Datum an die Datei anhängen
    date >> $file
    # 3 Sekunden schlafen
    sleep 3
  done

Das Skript wird nun im Hintergrund gestartet:

  ./bg.sh &

Mit dem Kommando

  tail -f /tmp/bg_out.txt

lassen wir uns das Wachstum des Inhalts der vom Hintergrund-Skript periodisch fortgeschriebenen Datei /tmp/bg_out.txt anzeigen, um die Arbeit des Skripts zu kontrollieren.

Wenn ein im Hintergrund und damit in einer Sub-Shell ausgeführtes Kommando seine Ausführungs-Umgebung durch Variablenzuweisungen oder eingebaute Kommandos (z.B. Verzeichnis-Wechsel oder Änderung der Signalbehandlung) modifiziert, so wirken diese Änderungen nicht auf die aktuelle Shell (den Eltern-Prozess) zurück. Daher ändert z.B. das Kommando

  a=9 && cd /tmp &

weder den Wert der Variablen a noch das aktuelle Verzeichnis der aktuellen Shell, da die gesamte UND-Liste im Hintergrund ausgeführt wird.

Die Prozess-Nummer des letzten Hintergrund-Prozesses steht im Parameter $! zur Verfügung. Sie kann man verwenden, um dem Hintergrund-Prozess mit dem Kommando kill ein Signal zu senden oder um mit dem Kommando wait auf dessen Beendigung zu warten. Neben Prozess-Nummern akzeptieren diese beiden Kommandos auch Job-Spezifikationen (s. Job-Steuerung). Mit den Mitteln der Job-Steuerung lassen sich Hintergrund-Prozesse auch wieder in den Vordergrund holen und Vordergrund-Prozesse in den Hintergrund verlagern.

Das Kommando

  wait [n]

wartet auf die Beendigung des über seine Job-Spezifikation oder Prozess-ID spezifizierten Jobs oder Prozesses und gibt dessen Exit-Status zurück. Wenn man eine Job-Spezifikation als Argument angibt, wird auf das Ende aller Prozesse der Pipeline gewartet, die den Job bildet.

Wenn das optionale Argument n fehlt, wartet wait auf alle derzeit aktiven Kind-Prozesse. Der Exit-Status ist dann 0. Wenn n einen nicht existenten Prozess bezeichnet, lautet der Exit-Status 127. Ansonsten entspricht er dem Exit-Status des letzten Jobs oder Prozesses, auf den gewartet wurde.

Im folgenden Beispiel starten wir ein Kommando im Hintergrund. Anschließend werden parallel zum Hintergrund-Kommando weitere Kommandos des aktuellen Skripts ausgeführt. Mit wait wird danach auf das Ende des Hintergrund-Kommandos gewartet:

  # Hintergrund-Kommando starten
  sleep 5 && echo Hintergrund &
  # Prozess-ID des Hintergrund-Kommandos merken
  pid=$!
  # eine Schleife 3 Mal durchlaufen
  for ((i = 1; i < 4; i++))
  do
    echo "Vordergrund $i"
    sleep 1
  done
  # auf das Ende des Hintergrund-Kommandos warten
  wait $pid
  # danach weiter im Vordergrund
  echo 'Hintergrund-Kommando beendet'

Da wir in diesem Beispiel nur genau ein Hintergrund-Kommando gestartet haben, könnten wir hier statt

  wait $pid

auch das argumentlose

  wait

verwenden, um auf alle Hintergrund-Kommandos zu warten. Die Angabe einer Prozess-ID oder Job-Spezifikation ist nur dann nötig, wenn man mehrere Hintergrund-Kommandos bzw. -Jobs hat und selektiv auf das Ende eines bestimmten Prozesses oder Jobs warten will.

Statt der Prozess-ID $pid hätten wir auch eine Job-Spezifikation angeben können, z.B. %1 für Job 1, %+ für den aktuellen Job oder %sleep für den Job, der mit sleep beginnt.

Anmerkung: Die Ermittlung einer Job-Nummer ist evtl. aufwendiger als die Ermittlung der Prozess-ID, da es keinen zu !$ analogen Parameter gibt, der diese Nummer bereitstellt. Wenn man allerdings nur einen Job hat, weiß man sicher, dass er die Nummer 1 trägt.

Abschließend wollen wir noch den Vorrang der Listen-Operatoren diskutieren, der dann von Bedeutung ist, wenn man Listen unter Verwendung verschiedener Operatoren konstruiert.

Die Operatoren && und || haben denselben Vorrang. Dieser ist höher als der Vorrang der Operatoren ; und &, wobei auch diese beiden denselben Vorrang haben.

Durch die Verwendung von runden oder geschweiften Klammern, mit denen man zusammengesetzte Kommandos bilden kann, lässt sich der Vorrang ändern.

Die folgenden Beispiele sollen dies verdeutlichen:

  sleep 5 ; echo 1 &
    # Da ; und & denselben Vorrang haben, bezieht sich & nur auf das Kommando
    # echo. Dieses wird im Hintergrund ausgeführt, nachdem mit sleep 5
    # Sekunden gewartet worden war.

  { sleep 5 ; echo 1 ; } &
    # Hier wird die gesamte Kommando-Liste in den Hintergrund verlagert. Dort wird
    # dann 5 Sekunden gewartet und anschließend eine 1 ausgegeben.

  sleep 5 && echo 1 &
    # Die Liste sleep 5 && echo 1 wird durch & im Hintergrund ausgeführt.
    # Da && einen höheren Vorrang als & hat, bezieht sich & auf die gesamte Liste.
    # Mit sleep wird hier 5 Sekunden gewartet. Nach der erfolgreichen
    # Beendigung dieses ersten Kommandos wird dann mit echo eine 1 ausgegeben.

  sleep 5 && { echo 1 & }
    # Hier bezieht sich & nur auf das Kommando echo, da die geschweiften
    # Klammern ein zusammengesetztes Kommando bilden, das eine Liste in der
    # aktuellen Shell ausführt. Nach der erfolgreichen Beendigung von sleep
    # wird daher echo 1 im Hintergrund ausgeführt.

  sleep 5 && ( echo 1 & )
    # Im Unterschied zum vorigen Beispiel verwenden wir hier runde statt geschweifter
    # Klammern. Dadurch wird eine Liste in einer Sub-Shell statt in der aktuellen Shell
    # ausgeführt, was aber hier keinen funktionellen Unterschied zur Folge hat.

  ! sleep 5 || ( echo 1 & )
    # Hier wird die Funktionalität des vorigen Beispiels durch eine ODER-
    # statt einer UND-Liste erbracht. Daher wurde der Exit-Status des ersten
    # Kommandos negiert.

  true && false || echo falsch && echo richtig
    # Die Ausgabe lautet:
    #   falsch
    #   richtig
    # Da && und || denselben Vorrang haben, wird zuerst die linke UND-Liste
    # true && false ausgeführt. Sie hat den Exit-Status 1. Diese UND-Liste ist das
    # erste Kommando einer ODER-Liste. Deren zweites Kommando, also echo falsch, wird
    # ausgeführt, weil das erste Kommando mit einem Fehler-Code beendet wurde.
    # Der Exit-Status der ODER-Liste lautet 0, weil deren zweites Kommando erfolgreich
    # zurückkehrte. Die gesamte ODER-Liste ist das erste Kommando einer weiteren UND-Liste.
    # Da die ODER-Liste erfolgreich war, wird das zweite Kommando der letzten UND-Liste,
    # also echo richtig, ebenfalls ausgeführt.

  true || false && echo richtig || echo falsch
    # Die Ausgabe lautet richtig. Die linke ODER-Liste ist erfolgreich.
    # Daher wird echo richtig als zweites Kommando der UND-Liste ausgeführt.
    # Diese Liste ist ebenfalls erfolgreich, weswegen das zweite Kommando der
    # letzten ODER-Liste nicht zur Ausführung gelangt.
    
  true || { false && echo richtig || echo falsch ; }
    # Hier erfolgt keine Ausgabe. Das Gesamt-Konstrukt ist eine ODER-Liste.
    # Da deren erstes Kommando, also true, erfolgreich ist, wird das
    # zweite Kommando überhaupt nicht ausgeführt.

  true && { false && echo richtig || echo falsch ; }
    # Die Ausgabe lautet falsch. Das Gesamt-Konstrukt ist eine UND-Liste.
    # Da deren erstes Kommando, also true, erfolgreich ist, wird das
    # zweite Kommando ausgeführt, bei dem es sich um eine ODER-Liste handelt,
    # deren erstes Kommando wiederum eine UND-Liste (false && echo richtig) ist.
    # Die UND-Liste ist nicht erfolgreich, daher kommt echo falsch zur Ausführung.

28. Job-Steuerung

Die Job-Steuerung gestattet, Prozesse selektiv zu stoppen (suspendieren, suspend) und später wieder weiterlaufen zu lassen (resume). Sie steht in der Bash zur Verfügung, wenn die set-Option m bzw. monitor aktiv ist, was in interaktiven Shells dem Standard entspricht. Typischerweise nutzt man die Job-Steuerung durch eine interaktive Schnittstelle, die durch das Zusammenspiel des Terminal-Treibers mit der Bash realisiert wird.

Jede von der Bash gestartete Pipeline wird als Job betrachtet und in eine Job-Liste eingetragen, die die jeweils aktiven Jobs einer Bash enthält und die man sich mit dem Kommando jobs ausgeben lassen kann. In dieser Ausgabe wird der aktuelle, d.h. zuletzt angehaltene oder im Hintergrund gestartete Job durch ein + und der zuvor aktuelle Job durch ein - gekennzeichnet.

Alle Jobs sind nummeriert und somit über ihre Job-Nummer immer eindeutig identifizierbar. Neben der Nummer gibt es weitere Möglichkeiten der Job-Identifizierung, die wir unten besprechen. Die Job-Nummern werden nicht generell fortlaufend vergeben. Ein Job, der neu in die Job-Liste eingetragen wird, erhält die Nummer 1, wenn die aktuelle Liste leer ist (unabhängig davon, ob sie zuvor schon Jobs enthielt oder nicht). Andernfalls wird ihm die um eins erhöhte höchste Job-Nummer der Liste zugeordnet (z.B. die 8, wenn die bisher höchste Job-Nummer die 7 war), so dass es keine Rolle spielt, ob es unterhalb der höchsten Nummer noch freie Nummern gibt, da sie hier nicht wiederverwendet werden.

Wenn die Bash einen Job im Hintergrund (asynchron) ausführt, also nicht auf seine Beendigung wartet, dann gibt sie eine Zeile der Form

  [1] 9876

aus, die anzeigt, dass Job 1 gestartet wurde, wobei der letzte Prozess der Pipeline dieses Jobs die ID 9876 hat.

Die durch Tastatur-Eingaben generierten Signale (z.B. SIGINT, das typischerweise durch Ctrl-C erzeugt wird) werden nur an Prozesse im Vordergrund gesendet. Hintergrund-Prozesse erhalten diese Signale nicht. Nur Vordergrund-Prozesse dürfen vom Terminal lesen. Hintergrund-Prozesse erhalten dagegen das Signal SIGTTIN, wenn sie eine Lese-Operation auf das Terminal anwenden. Dieses Signal stoppt standardmäßig einen Prozess, kann allerdings auch vom Prozess explizit abgefangen werden.

Bei aktiver Terminal-Option tostop ist es einem Hintergrund-Prozess auch nicht erlaubt, auf das Terminal zu schreiben. Er erhält dann beim Schreibversuch das Signal SIGTTOU, welches ihn standardmäßig stoppt, allerdings auch abgefangen werden kann. Diese Option lässt sich mit dem Kommando

  stty tostop

setzen und mit

  stty -tostop

wieder zurücksetzen.

Wenn das Betriebssystem eine Job-Steuerung unterstützt, wird sie von der Bash genutzt. Wenn der Nutzer das Suspend-Zeichen (typischerweise Ctrl-Z, wobei man mit dem Kommando stty die aktuelle Einstellung ausgeben und auch ein anderes Suspend-Zeichen einstellen kann) eingibt, während ein Vordergrund-Job läuft, wird dieser sofort suspendiert. Die Shell weist darauf mit einer Zeile der Form

  [1]+ Stopped Pipeline

hin. Die Zahl in Klammern ist wieder die Job-Nummer. Am Ende der Zeile wird die konkret gestoppte Pipeline angegeben, was z.B. so aussehen kann:

  [1]+ Stopped cat | read

Als Nebeneffekt der Suspendierung werden alle noch nicht vom Terminal gelesenen Eingabe-Daten verworfen.

Durch die Suspendierung erhält die Shell die Steuerung zurück. Anschließend kann der Nutzer mittels der Kommandos zur Job-Steuerung den Job-Status manipulieren: Mit bg (background) kann man suspendierte Jobs in den Hintergrund verlagern. Mit fg (foreground) lassen sich suspendierte oder Hintergrund-Prozesse in den Vordergrund holen. Das Kommando kill sendet Signale an Jobs oder Prozesse und das Kommando wait wartet auf die Beendigung eines Jobs oder Prozesses.

Anmerkung: bg und fg unterstützen nur Job-Spezifikationen als Argument, wogegen kill und wait sowohl Job-Spezifikationen als auch Prozess-IDs akzeptieren.

Die Suspendierung eines Jobs erfolgt intern dadurch, dass den Prozessen des Jobs das Signal SIGSTOP gesendet wird. Die oben beschriebene Eingabe des Suspend-Zeichen (typischerweise Ctrl-Z) bewirkt genau das, allerdings kann man damit nur den Vordergrund-Job beeinflussen. Um laufende Hintergrund-Jobs zu stoppen, muss man sie entweder mit fg in den Vordergrund holen und danach mit Ctrl-Z suspendieren oder ihnen mit dem Kommando kill -STOP das SIGSTOP senden.

Anmerkung: Die tcsh sowie die Korn-Shell kennen hierfür das Kommando stop, das bei der tcsh ein eingebautes Kommando und bei der Korn-Shell ein Alias für kill -STOP ist.

Die Bash unterstützt verschiedene Formen von Job-Spezifikationen, wobei generell das Zeichen % eine Job-Spezifikation einleitet:

%n Job Nummer n
%+ und %% der aktuelle Job der Job-Liste (der zuletzt gestoppte Vordergrund-Job bzw. der zuletzt gestartete Hintergrund-Job)
%- der vorher aktuelle Job der Job-Liste
%Name Name ist ein Präfix des Kommandos, mit dem der Job gestartet wurde. Es muss eindeutig sein, da die Bash sonst einen Fehler meldet.
%?Teil-String Teil-String ist ein String, der irgendwo in der Kommandozeile eines Jobs vorkommt, wobei auch dieser (wie oben das Präfix) eindeutig sein muss, da die Bash sonst einen Fehler meldet.

Wenn man auf einer Kommandozeile nur eine Job-Spezifikation angibt, wird dies von der Bash als Synonym für das Kommando

  fg Job-Spezifikation

interpretiert. Man holt als den spezifizierten Job in den Vordergrund und beendet damit automatisch auch eine evtl. Suspendierung. Analog wird

  Job-Spezifikation &

als Synonym für

  bg Job-Spezifikation

betrachtet, so dass der spezifizierte Job in den Hintergrund verlagert und dadurch seine Suspendierung beendet wird. Intern wird dem Job dabei das Signal SIGCONT gesendet, das man auch mit kill -CONT senden kann.

Wenn die Shell-Variable auto_resume gesetzt ist, werden einfache Kommandos, die nur aus einem Wort bestehen und keine Umlenkungen enthalten, als Kandidaten für die automatische Wiederaufnahme eines gestoppten Jobs betrachtet, wobei hier keine Mehrdeutigkeiten zulässig sind. Das eingegebene Wort (der Wiederaufnahme-Kandidat) aktiviert denjenigen Job, dessen Kommando-Zeile zu diesem Wort passt. Wenn es mehrere passende Jobs gibt, wird der letzte passende der Liste ausgewählt (jener mit der höchsten Job-Nummer).

Wenn auto_resume den Wert exact hat, dann muss das Wort exakt der Kommando-Zeile entsprechen bzw. die Kommando-Zeile als Präfix enthalten. Wenn beispielsweise die Job-Liste die gestoppten Jobs

  abcd
  abc

enthält und abc eine höhere Job-Nummer als abcd hat, dann startet das Wort abcd den Job abc, da er vom Namen her passt (weil abc ein Präfix von abcd ist) und in der Liste hinter abcd steht.

Wenn auto_resume den Wert substring hat, dann muss das Wort ein Teil-String der Kommando-Zeile sein. Dies entspricht der Funktionalität der Job-Spezifikation mit %?String. Alle anderen Werte von auto_resume bewirken, dass das Wort am Anfang der Kommando-Zeile vorkommen muss. Dies entspricht der Funktionalität der Job-Spezifikation mit %String.

Die Shell registiert immer unverzüglich die Status-Änderungen von Jobs. Normalerweise meldet sie solche Änderungen aber erst dann auf der Standard-Ausgabe, wenn sie den nächsten Prompt ausgibt, um andere Ausgaben nicht zu stören. Sofern die set-Option -b aktiv ist, meldet die Bash Job-Status-Änderungen sofort.

Jedes Kind (Prozess eines Jobs) einer Bash, das beendet wird, sendet ein Signal SIGCHLD. Ein mit dem Kommando trap eingerichteter Signal-Handler für SIGCHLD wird dadurch aktiviert. Wenn also z.B. eine aus drei Prozessen bestehende Pipe beendet ist, wurde der Signal-Handler für SIGCHLD drei Mal aufgerufen.

Beim Versuch, eine Bash zu beenden, die noch gestoppte Jobs hat, wird eine Warnung ausgegeben. Wenn man danach sofort wieder das Beenden der Shell anweist, ohne ein anderes Kommando auszuführen, werden die gestoppten Jobs sowie die Shell ohne weitere Warnung beendet.

Beispiele:

  # Job-Liste anzeigen
  # + kennzeichnet den aktuellen (zuletzt angehaltenen
  #   oder im Hintergrund gestarteten) Job
  # - kennzeichnet den zuvor aktuellen Job
  jobs

  # aktuellen Job in den Vordergrund holen
  fg

  # Job Nummer 1 in den Vordergrund holen und durch Ctrl-C abbrechen
  fg %1
  Ctrl-C

  # Job Nummer 2 in den Vordergrund holen und durch Ctrl-Z suspendieren
  %2
  Ctrl-Z

  # Hintergrund-Job starten
  ./bg.sh &
  # den aktuellen Job beenden
  kill %+

  # Vordergrund-Job starten
  ./bg.sh
  # Vordergrund-Job suspendieren
  Ctrl-Z
  # in den Hintergrund verlagern (hebt die Suspendierung auf)
  bg
  # im Hintergrund suspendieren
  kill -STOP %+
  # Suspendierung aufheben und dadurch im Hintergrund weiterlaufen lassen
  %+&
  # auf das Ende des Hintergrund-Prozesses warten
  wait    # ohne Argument wird auf das Ende aller Kind-Prozesse gewartet
  wait %1 # wartet auf das Ende von Job 1

  # Mit disown kann man die Job-Liste manipulieren:
  disown %1    # Job 1 aus der Liste löschen
  disown       # den aktuellen Job aus der Liste löschen
  disown -r    # alle laufenden (running) Jobs aus der Liste löschen
  disown -a    # alle Jobs aus der Liste löschen
  disown -h %1 # Job 1 so markieren, dass kein SIGHUP mehr an ihn geschickt wird
  disown -ha   # alle Jobs so markieren, dass kein SIGHUP mehr an sie geschickt wird

  # Nutzung von auto_resume
  disown -a             # Job-Liste löschen
  unset auto_resume     # Variable auto_resume löschen
  abcd                  # Kommando abcd starten
  Ctrl-Z                # abcd stoppen
  abc                   # Kommando abc starten
  Ctrl-Z                # abc stoppen
  jobs                  # Job-Liste anzeigen:
                        # [1]-  Stopped                 abcd
                        # [2]+  Stopped                 abc
  auto_resume=exact     # Variable auto_resume auf exact setzen
  abcd                  # Wiederaunahme des Kommandos abc (nicht abcd)
  Ctrl-Z                # abc stoppen
  auto_resume=substring # Variable auto_resume auf substring setzen
  d                     # Wiederaunahme des Kommandos abcd, da nur dort der Sub-String d vorkommt
  Ctrl-Z                # abcd stoppen
  auto_resume=x         # Variable auto_resume auf beliebigen anderen Wert setzen
  d                     # Fehler: es gibt kein mit d beginnendes Kommando:
                        # bash: d: command not found
  ab                    # Wiederaunahme des Kommandos abc, weil es mit ab beginnt

Mit disown aus der Job-Liste gelöschte Jobs werden nicht beendet. Ihnen sendet die Shell aber keine Signale mehr, also auch kein Signal SIGHUP, das einen Prozess beendet, wenn er es nicht abfängt. Eine interaktive Shell sendet SIHUP in folgenden Situationen an alle Jobs:

Mit disown -h kann man gezielt das Senden des SIGHUP verhindern, ohne den Job aus der Job-Liste zu streichen.

Alternativ kann man das externe Kommando nohup verwenden, um einen Prozess so zu starten, dass er gegen SIGHUP immun ist, wobei hier seine Ausgabe in eine Datei umgelenkt wird, weswegen disown bequemer sein dürfte.

Anmerkung: Mit dem Kommando suspend kann man eine Bash selbst suspendieren. Sie wird dann erst weiter ausgeführt, wenn sie das Signal SIGCONT erhält. Zum Suspendieren einer Login-Shell muss bei suspend die Option -f (force) angegeben werden.


29. Zusammengesetzte Kommandos / Ablaufsteuerung

Die folgende nummerierte Liste zeigt die zusammengesetzten Kommandos (compound commands) der Bash, zu denen auch Kommandos zur Ablaufsteuerung gehören. Die Syntax-Angabe ist jeweils rot dargestellt, um sie optisch hervorzuheben:

  1. (Liste)

    Die Liste wird in einer Sub-Shell, also einem Kind-Prozess der aktuellen Shell ausgeführt. Variablenzuweisungen und eingebaute Kommandos der Liste, die die Ausführungs-Umgebung der Sub-Shell modifizieren (z.B. Verzeichnis-Wechsel oder Änderung der Signalbehandlung) haben daher keine Auswirkung auf die Umgebung der aktuellen Shell. Der Exit-Status dieses Konstrukts entspricht dem Exit-Status der ausgeführten Liste.

    Diese Sub-Shell-Liste wird meist dort eingesetzt, wo man eine Kommando-Gruppe in einer geänderten Umgebung ausführen möchte, ohne die Umgebung der aktuellen Shell zu beeinflussen. Häufig geht es darum, einige Kommandos mit einem geänderten aktuellen Verzeichnis abzuarbeiten.

    Beispiel:

      (cd /tmp && touch daten)
      pwd

    Die Sub-Shell wechselt hier ins Verzeichnis /tmp und legt dort die Datei daten an. Der Verzeichnis-Wechsel hat aber keine Auswirkungen auf die aktive Shell. Deren aktuelles Verzeichnis bleibt unverändert. Davon kann man sich an Hand der Ausgabe von pwd überzeugen.

  2. { Liste; }

    Die Liste wird in der aktuellen Shell ausgeführt. Sie muss mit einem Semikolon oder einem Newline-Zeichen abgeschlossen werden. Dieses Konstrukt dient als Gruppierungs-Kommando zur Bildung einer Kommando-Gruppe, das man z.B. benutzen kann, um für mehrere Kommandos gemeinsam eine Ein- oder Ausgabe-Umlenkung zu formulieren. Sein Exit-Status entspricht dem Exit-Status der ausgeführten Liste.

    Bei den runden Klammern der oben erläuterten Sub-Shell-Liste handelt es sich um Metazeichen der Shell, die daher als Worttrenner wirken. Im Gegensatz dazu sind die geschweiften Klammern des Gruppierungs-Kommandos keine Worttrenner, sondern reservierte Wörter der Bash, die nur dort auftreten dürfen, wo reservierte Wörter zulässig sind. D.h., vor und hinter jeder der beiden Klammern muss ein Worttrenner stehen.

    Konkret bedeutet dies, dass die öffnende Klammer durch mindestens einen Leerraum oder ein Newline-Zeichen von der Liste getrennt werden muss. Die schließende Klammer kann unmittelbar hinter dem abschließenden Semikolon der Liste folgen, allerdings sollte man der Übersichtlichkeit halber vor der schließenden Klammer auch mindestens einen Leerraum oder ein Newline-Zeichen angeben.

    Beispiel:

      { cd /tmp ; touch daten ; }
      pwd

    Da hier im Gegensatz zum obigen Sub-Shell-Beispiel die Liste innerhalb der aktuellen Shell ausgeführt wird, wird deren aktuelles Verzeichnis nach /tmp gewechselt.

    In diesem Beispiel macht die Verwendung der Gruppierung keinen praktischen Sinn. Man kann die geschweiften Klammern weglassen, ohne die Funktionalität zu ändern, da in beiden Fällen die drei angegebenen Kommandos sequentiell in der aktuellen Shell ausgeführt werden.

    Das folgende Beispiel zeigt eine sinnvolle Anwendung des Gruppierungs-Kommandos, die es gestattet, eine gemeinsame Ein-/Ausgabe-Umlenkung für die Kommando-Gruppe zu formulieren und diese Kommando-Gruppe als ein Kommando einer Pipeline zu verwenden:

      {
        echo 'Daten von stdin:'
        cat
        echo =============
        echo 'Inhalt der Datei "daten"'
        cat daten
        echo =============
      } < /etc/hosts 2>/tmp/error | tee /tmp/out.txt

    Durch die Eingabe-Umlenkung liest das erste cat-Kommando die Datei /etc/hosts. Alle Ausgaben von echo und cat werden über die Pipe an das Kommando tee weitergegeben, das sie auf die Standard-Ausgabe (bei interaktiven Shells in der Regel das Terminal) sowie in die Datei /tmp/out.txt schreibt. Evtl. Fehlermeldungen der Kommando-Gruppe (z.B. bei Nicht-Existenz der Datei daten) werden in die Datei /tmp/error umgelenkt.

    Das nächste Beispiel zeigt die Verwendung einer Kommando-Gruppe in einer UND- bzw. ODER-Liste:

      [[ -d /tmp ]] && { echo /tmp existiert ; cd /tmp ; }
    
      [[ -d /tmp ]] || { echo /tmp existiert nicht ; exit 1 ; }

    Wenn bei der UND-Liste das Verzeichnis /tmp existiert, wird die Kommando-Gruppe ausgeführt. Sie weist durch eine Ausschrift auf die Existenz von /tmp hin und wechselt in dieses Verzeichnis.

    Bei der ODER-Liste wird die Kommando-Gruppe abgearbeitet, wenn /tmp nicht existiert. Dann erfolgt eine Fehlermeldung sowie die Beendigung der Shell mit dem Exit-Status 1.

    Die Kommando-Gruppierung kann man auch für eine Fehlerbehandlung in Skripten benutzen. Im folgenden Beispiel wird das Skript mit dem Exit-Status 1 beendet, wenn die Kommando-Gruppe einen Exit-Status ungleich 0 liefert:

      {
        cd /tmp &&
        wc -l file1 > file2 &&
        echo Ergebnis: $(< file2)
      } || exit 1

    Bei dem Gesamt-Konstrukt handelt es sich um eine ODER-Liste, deren zweites Element das einfache Kommando exit 1 ist. Statt eines einfachen Kommandos kann man natürlich auch eine weitere Kommando-Gruppe angeben:

      {
        cd /tmp &&
        wc -l file1 > file2 &&
        echo Ergebnis: $(< file2)
      } ||
      {
        echo "Kommando schlug fehl; Exit-Status: $?"
        exit 1
      }

    Da in den letzten beiden Beispielen die Kommandos der Kommando-Gruppe eine UND-Liste bilden, liefert die Gruppe nur dann den Exit-Status 0 (also Erfolg), wenn jedes Kommando der Gruppe erfolgreich ausgeführt wurde. Sobald eines der Kommandos fehlschlägt, wird dessen Exit-Status als Exit-Status der Gruppe zurückgeliefert.

    Anmerkung: In den genannten beiden Beispielen kann man auf die Gruppierung der UND-Liste durch geschweifte Klammern verzichten, weil das rechte Kommando der ODER-Liste generell nur dann ausgeführt wird, wenn die UND-Liste nicht erfolgreich war, da die Operatoren && und || denselben Vorrang haben. Die Gruppierung ist allerdings dennoch sinnvoll, da sie im allgemeinen die Lesbarkeit des Konstrukts erhöht und außerdem die Möglichkeit bietet, den Exit-Status der gesamten Kommando-Gruppe zu negieren, was wir im nächsten Beispiel zeigen, sowie für die Gruppe geschlossen eine Ein-/Ausgabe-Umlenkung zu formulieren.

    Da eine Kommando-Gruppe wiederum ein Kommando und somit eine einelementige Pipeline ist, lässt sich ihr Exit-Status auch negieren, z.B. so:

      if ! {
        cd /tmp &&
        wc -l file1 > file2 &&
        echo Ergebnis: $(< file2)
      }
      then
        echo 'Kommando-Gruppe schlug fehl'
        exit 1
      fi

    Durch die Negation enthält der spezielle Parameter $? hier den negierten Exit-Status der Kommando-Gruppe, im Fehlerfalle also immer eine 0 und im Erfolgsfalle immer eine 1.

    Möchte man im Rahmen der Fehlerbehandlung auf den wirklichen Exit-Status der Kommando-Gruppe zugreifen können, muss man auf die Negation verzichten. Die folgenden beiden Beispiele zeigen eine mögliche Implementierung:

      if {
        cd /tmp &&
        wc -l file1 > file2 &&
        echo Ergebnis: $(< file2)
      }
      then
        echo 'Kommando-Gruppe war erfolgreich'
      else
        echo "Kommando-Gruppe schlug fehl; Exit-Status: $?"
        exit 1
      fi
      
      # andere Implementierung:
      {
        cd /tmp &&
        wc -l file1 > file2 &&
        echo Ergebnis: $(< file2)
      }
      ret=$?
      if ((ret))
      then
        echo "Kommando-Gruppe schlug fehl; Exit-Status: $ret"
      else
        echo 'Kommando-Gruppe war erfolgreich'
      fi

    Wenn man im Erfolgsfalle nichts tun möchte, kann man statt echo das leere Kommando verwenden, dessen Name ein Doppelpunkt ist:

      if (($#))
      then
        # wir tun nichts, da die Anzahl der Argumente ($#) ungleich Null ist
        :
      else
        echo 'Fehler, da keine Argumente angegeben wurden'
      fi

    Ein Kommentar allein statt des leeren Kommandos genügt nicht, da dann das Kommando if eine falsche Syntax hat, weil mindestens ein Kommando im then- und else-Zweig stehen muss.

    Anmerkung:

    Wenn man eine Kommando-Gruppe in den Hintergrund verlagert, wird analog zur Ausführung einfacher Hintergrund-Kommandos zuerst die Sub-Shell kreiert und dann dort die Kommando-Gruppe ausgeführt. Daher wirken die innerhalb der Kommando-Gruppe realisierten Zuweisungen, Verzeichnis-Wechsel etc. nur in der im Hintergrund gestarteten Sub-Shell, nicht aber in der aktuellen Shell, wie folgendes Beispiel demonstriert:

      a=5
      cd /
      { a=6 ; cd /tmp ; } &
      echo "$a"
      pwd

    Nach Ausführung der Kommando-Gruppe hat die Variable a weiterhin den Wert 5. Die Shell steht auch weiterhin im Verzeichnis /.

    Wenn man ohnehin vor hat, eine Kommando-Gruppe im Hintergrund auszuführen, kann man also zur Gruppierung auch die geschweiften statt der runden Klammern (mit denen eine Sub-Shell-Liste erzeugt wird) nutzen. In beiden Fällen wird eine Rückwirkung auf die Umgebung der aktuellen Shell verhindert, weil die Kommando-Gruppe in der Sub-Shell zur Ausführung kommt.

    Will man eine solche Rückwirkung verhindern, die Kommando-Gruppe aber synchron ausführen, dann muss man die Sub-Shell-Liste verwenden, weil dann die Kommando-Gruppe wieder in der Sub-Shell ausgeführt wird, die aktive Shell aber auf deren Ende wartet.

  3. [[ Ausdruck ]]

    Dieses Konstrukt dient der Auswertung eines Test-Ausdrucks, mit dem man Datei-Attribute überprüfen sowie arithmetische und String-Vergleiche durchführen kann. Bei erfolgreichen Tests wird der Exit-Status 0 geliefert, sonst 1.

    Die im Test-Ausdruck nutzbaren Operatoren sind im Abschnitt Test-Operatoren und Bedingungs-Ausdrücke detailliert beschrieben.

    Tests der Form [[ ... ]] werden von der Bash intern ausgewertet. Dieses Konstrukt stammt von der Korn-Shell und ist auch in der Z Shell zu finden. Es wird dabei also kein externes Kommando gestartet.

    Die alte Bourne-Shell kannte diesen Test noch nicht. Dort musste man auf das externe Kommando test zurückgreifen, das den Alternativ-Namen [ trägt. Der Test-Ausdruck ist hier mit dem Wort ] abzuschließen: [ ... ].

    Die alte Form des Tests steht auch bei den neueren Shells wie Korn-Shell, Bash und Z Shell zur Verfügung, wobei hier das Kommando test bzw. [ ein eingebautes Kommando ist. Zusätzlich existiert weiterhin das externe Kommando test, das aber ggf. weniger Operatoren als das entsprechende Builtin-Kommando unterstützt.

    Die beiden Test-Formen sind in ihrer Anwendung ähnlich, da sie dieselbe Menge von Bedingungs-Ausdrücken unterstützen. Details sind aber verschieden. Bei der neuen Form [[ ... ]] erfolgt im Unterschied zur alten Form innerhalb des Test-Ausdrucks

    1. keine Aufspaltung in Wörter (Word Splitting) und

    2. keine Pfadnamens-Expandierung.

    Meist wird genau dieses Verhalten gewünscht. In der alten Test-Form muss die Aufspaltung in Wörter bei Bedarf durch geeignete Quotierung erreicht werden. Hier ein Beispiel:

       var="1 2 3"
    
       # Bei test und [ ist eine Quotierung nötig:
       test -n "$var" && echo var ist nicht leer
       [ -n "$var" ] && echo var ist nicht leer
    
       # Bei [[ kann die Quotierung entfallen:
       [[ -n $var ]] && echo var ist nicht leer

    Test-Operatoren wie -f (zum Test auf eine reguläre Datei) müssen bei [[ ... ]] unquotiert notiert werden, um in der gewünschten Weise zu funktionieren. Bei test ist dagegen eine Quotierung erlaubt.

    Beispiele für zulässige Konstrukte:
       [ -f xyx ] && echo xyz existiert
       [ '-f' xyx ] && echo xyz existiert
       [[ -f xyx ]] && echo xyz existiert

    Unzulässig ist dagegen z.B.:

       [[ '-f' xyx ]] && echo xyz existiert

    Bei den Operatoren == und != wird der rechte Operand als Muster betrachtet und gemäß den Regeln für Muster bei der Pfadnamens-Expandierung interpretiert. Beliebige Teile des Musters können quotiert werden, wodurch für sie ein exakter String-Vergleich bewirkt wird.

    Ab Bash 3.0 unterstützt [[ ... ]] auch noch den Operator =~, der denselben Vorrang wie == und != hat und dessen rechter Operand als erweiterter regulärer Ausdruck gemäß POSIX 1003.2 interpretiert wird (s. dazu regexp(7) im Online-Manual).

    Test-Ausdrücke können bei [[ ... ]] durch folgende Operatoren kombiniert werden, die hier fallend nach ihrem Vorrang angegeben sind:

    ( Ausdruck )
    • gibt den Wert von Ausdruck zurück
    • kann zur Veränderung des normalen Operator-Vorrangs genutzt werden
    ! Ausdruck
    • logische Negation von Ausdruck
    Ausdruck && Ausdruck
    • logisches UND
    • wahr, wenn beide Ausdrücke wahr sind
    Ausdruck || Ausdruck
    • logisches ODER
    • wahr, wenn mindestens einer der beiden Ausdrücke wahr ist

    Die runden Klammern dürfen bei [[ nicht quotiert werden. Bei test dagegen müssen sie quotiert werden, da sie von der Shell als Zeichen mit Sonderbedeutung (Metazeichen) angesehen werden.

    Bei && und || wird der zweite Ausdruck nicht ausgewertet, wenn bereits nach Auswertung des ersten Ausdrucks das Resultat des Gesamtausdrucks feststeht (sog. Short-Circuit-Evaluation).

    Die folgenden Beispiele zeigen verschiedene Anwendungen von Tests:

      name=Otto
      [[ $name == O* ]] && echo "$name beginnt mit O"
      [[ $name != O* ]] && echo "$name beginnt nicht mit O"
    
      [[ -d /tmp ]] && echo '/tmp ist ein Verzeichnis (Directory)'
      [[ ! -d /tmp ]] && echo '/tmp ist kein Verzeichnis (Directory)'
      [[ -r /etc/passwd ]] && echo '/etc/passwd ist lesbar'
    
      [[ -n $BASH ]] && echo Variable BASH ist nicht leer
      [[ -z $BASH ]] && echo Variable BASH ist leer oder existiert nicht
    
      [[ $name = Otto ]] || echo 'Inhalt von Variable "name" ist nicht Otto'
    
      [[ " 17" -eq 17 ]] && echo beide Operanden haben den numerischen Wert 17
    
      [[ $a < $b ]] && echo 'Wert von Variable "a" ist lexikografisch kleiner als Wert von Variable "b"'
      [[ $a -lt $b ]] && echo Wert von Variable a ist numerisch kleiner als Wert von Variable b
    
      [[ $name == O* || ((${#name} == 10)) ]] &&
        echo "$name beginnt mit O oder ist 10 Zeichen lang"
    
      [[ $name == O* && ((${#name} == 10)) ]] &&
        echo "$name beginnt mit O und ist 10 Zeichen lang"
    
      [[ ( ${name:0:1} == Z || ${name:0:1} == O ) && ( ${#name} -gt 3 ) ]] &&
        echo "$name beginnt mit Z oder O und hat eine Länge größer 3"
    
      # Auch die erweiterten Pfadnamens-Muster sind bei == und != nutzbar.
      # Im folgenden Kommando wird getestet, ob das aktuelle Skript vom Nutzer
      # max oder moritz gerufen oder über einen Namen gestartet wurde, der
      # den Bestandteil tausch trägt:
      if [[ $USER == @(max|moritz) || $0 == *tausch* ]]
      then
        echo 'Bedingung erfüllt'
        # hier können weitere Kommandos folgen
      fi
        # Anmerkung: Über symbolische Links kann man einem Skript sehr bequem
        # verschiedene Namen zuordnen. In Abhängigkeit vom Namen, der über
        # den Parameter $0 ermittelbar ist, kann das Skript dann verschiedene
        # Aktionen ausführen.
    
      # Unterstützung regulärer Ausdrücke bei Bash 3.0:
      [[ $name =~ '^O..o$' ]] && echo 'Wert der Variablen "name" entspricht dem Muster "O..o"'
  4. for Name [ in Wort-Liste ] ; do Liste ; done

    Diese Konstruktion dient dem sequentiellen Durchlaufen einer Liste. Wenn der optionale Teil in Wort-Liste fehlt, wird die Liste der gesetzten Positionsparameter (Argumente eines Shell-Skripts oder einer Shell-Funktion) durchlaufen.

    Der Exit-Status dieser for-Schleife entspricht dem Exit-Status der ausgeführten do-Liste. Wenn die Wort-Liste leer ist, wird kein Kommando des Schleifenkörpers ausgeführt. Der Exit-Status ist dann 0.

    Beispiel:

      for i in $(seq 1 10) *
      do
        echo "$i"
      done

    Die Variable i durchläuft zunächst die Liste der ganzen Zahlen von 1 bis 10, die durch das externe Kommando seq erzeugt wird, und danach die durch die Pfadnamens-Expandierung des Musters * gebildete Liste aller Dateinamen des aktuellen Verzeichnisses.

    Ab Bash 3.0 kann man statt seq auch die Brace-Expandierung mit Sequenz-Ausdruck benutzen:

      for i in {1..10} *
      do
        echo "$i"
      done
  5. for (( Ausdruck1 ; Ausdruck2 ; Ausdruck3 )) ; do Liste ; done

    Dieses Konstrukt erinnert an die for-Schleife, wie man sie z.B. bei den Programmiersprachen C oder AWK findet. Allerdings gilt gegenüber anderen Sprachen bei der Bash die Einschränkung, dass die drei Ausdrücke immer nur numerisch ausgewertet werden, so dass keine String-Operationen nutzbar sind.

    Zuerst wird der Ausdruck1 ausgewertet. Danach wird der Ausdruck2 wiederholt so lange ausgewertet, bis er den Wert 0 hat. In diesem Falle endet die Schleife. Solange Ausdruck2 einen Wert ungleich 0 hat, wird der Schleifenkörper (also die do-Liste) ausgeführt und danach Ausdruck3 ausgewertet. Alle drei Ausdrücke sind optional. Jeder fehlende Ausdruck wird zu 1 ausgewertet.

    Der Exit-Status dieser for-Schleife entspricht dem Exit-Status der do-Liste oder 1, wenn einer der Ausdrücke ungültig ist.

    Beispiel:

      for (( i=1 ; i < 10 ; i++ ))
      do
        echo "$i"
      done

    Hier wird eine Zählschleife realisiert, deren Körper 9 Mal durchlaufen wird. Die Variable i bekommt der Reihe nach die ganzen Zahlen von 1 bis 10 zugewiesen. Solange ihr Wert kleiner als 10 ist, wird der Schleifenkörper ausgeführt und somit der Wert von i ausgegeben.

  6. select Name [ in Wort-Liste ] ; do Liste ; done

    Dieses Konstrukt ermöglicht die recht bequeme Gestaltung einfacher Text-Menüs. Die Elemente der Wort-Liste werden mit 1 beginnend aufsteigend nummeriert als Menü auf die Standard-Ausgabe geschrieben. Falls der optionale Teil in Wort-Liste fehlt, werden die gesetzten Positionsparameter verwendet.

    Nach der Ausgabe dieses Menüs wird Prompt PS3 ausgegeben, eine Zeile von der Standard-Eingabe eingelesen und in der Variablen REPLY gespeichert. Die Umgebungsvariable TMOUT ist zur Einstellung eines Timeouts nutzbar.

    Wurde eine Nummer eingelesen, die mit einem Element der Wort-Liste korrespondiert (die also als Nummer eines Menü-Eintrags vorkommt), so erhält die Variable Name das zugehörige Wort der Wort-Liste (also den Menü-Eintrag) als Wert. Wurde der leere String eingelesen, werden die nummerierte Wort-Liste sowie PS3 erneut ausgegeben. Falls EOF (End Of File) gelesen wurde, wird select beendet. Bei jedem anderen eingelesenen Wert wird Name mit dem leeren String initialisiert.

    Nach jeder nichtleeren Auswahl wird der Schleifenkörper (do-Liste) bis zum ersten Kommando break oder (falls kein break erreicht wird) bis zum Ende ausgeführt. Durch break wird select beendet. Ohne break schließt sich an die Abarbeitung der do-Liste eine erneute Auswahl an, wobei aber das Auswahl-Menü nicht erneut ausgegeben wird. Falls man vor jeder Auswahl das Menü wieder anzeigen lassen möchte, muss man select in einer geeigneten Schleife wiederholen.

    Der Exit-Status von select entspricht dem Exit-Status der do-Liste oder ist 0, falls keine do-Liste ausgeführt wurde.

    Das folgende Beispiel, das auch unter SCRIPTS/select.sh zu finden ist, soll die Nutzung von select verdeutlichen. Die Kommentare enthalten Erläuterungen zu den einzelnen Kommandos und Sachverhalten:

      #!/bin/sh
      #
      # Beispiel zu select
      #
      # 14.1.2006
      
      # Durch die while-Schleife wird das Auswahl-Menü vor jeder Auswahl ausgegeben.
      while ((1))
      do
        # Wir löschen vor select immer die Variable name. An einem leeren String
        # in name erkennen wir EOF, da select in diesem Falle die Variable
        # unverändert lässt.
        unset name
        select name in Anton Berta Caesar
        do
          # Wenn name nicht leer ist, liegt eine korrekte Auswahl vor. Dann brechen
          # wir select ab, um vor der nächsten Auswahl wieder das Menü angezeigt
          # zu bekommen. Bei einer ungültigen Auswahl ist name leer. Dadurch wird
          # der Körper von select bis zum Ende abgearbeitet, was zur Folge hat,
          # dass wir select nicht beenden, so dass sich eine neue Auswahl ohne
          # Ausgabe des Menüs anschließt.
          [[ -n $name ]] && break
        done
                  
        # Bei EOF brechen wir das Skript ab. Mit break statt exit könnte man auch 
        # nur die while-Schleife abbrechen. Falls select innerhalb einer Funktion
        # ausgeführt würde, könnte man mit return statt exit diese Funktion
        # abbrechen.
        [[ -z $name ]] && { echo 'Abbruch ohne Auswahl' ; exit ; }
        echo "Auswahl: $name"
      done
    
  7. case Wort in [ [(] Muster [ | Muster ] ... ) Liste ;; ] ... esac

    Mit case kann man eine Mehrfach-Verzweigung bzw. eine Fall-Auswahl realisieren. Das Wort wird expandiert und anschließend der Reihe nach mit jedem angegebenen Muster verglichen, wobei die Regeln der Pfadnamens-Expandierung zur Anwendung kommen. Sobald ein Muster passt, wird die zugehörige Liste ausgeführt und die case-Anweisung beendet. D.h., weitere Muster werden dann nicht getestet.

    Der Exit-Status von case ist 0, wenn kein Muster passt. Ansonsten entspricht er dem Exit-Status der ausgeführten Liste.

    Beispiel:

      read -p 'Antwort (j/n): ' antwort
      case $antwort in
        j|J) echo Ja
             ;;
        n|N) echo Nein
             ;;
          *) echo ungültige Auswahl
             ;;
      esac
  8. if Liste; then Liste; [ elif Liste; then Liste; ] ... [ else Liste; ] fi
    

    Mit dem Kommando if lassen sich vollständige und unvollständige Alternativen realisieren. Zuerst wird die if-Liste (also die Liste hinter if) ausgeführt. Wenn deren Exit-Status 0 lautet, wird die (erste) then-Liste ausgeführt und die Alternative beendet. Andernfalls wird begonnen, der Reihe nach alle existierenden elif-Listen auszuführen. Sobald eine den Exit-Status 0 hat, wird die zugehörige then-Liste ausgeführt und die Alternative beendet. Andernfalls wird eine existierende else-Liste ausgeführt.

    Der Exit-Status der Alternative ist 0, wenn keine then- oder else-Liste ausgeführt wurde, wenn also keine Bedingung wahr war, d.h. den Exit-Status 0 lieferte. Andernfalls entspricht der Exit-Status der Alternative dem Exit-Status der ausgeführten then- bzw. else-Liste.

    Beispiel:

      read -p 'Zahl: ' zahl
      if [[ $zahl -gt 15 ]]
      then
        echo "$zahl ist größer als 15"
      elif [[ $zahl -gt 5 ]]
      then
        echo "$zahl ist größer als 5"
      else
        echo "$zahl ist kleiner gleich 5"
      fi
  9. while Liste; do Liste; done
    until Liste; do Liste; done

    Diese beiden Konstrukte dienen der Realisierung von Abweis-Schleifen. Die while-Schleife führt die do-Liste so lange aus, solange die Liste (Bedingung) hinter while den Exit-Status 0 liefert. Die until-Schleife entspricht der while-Schleife mit negiertem Test. Die do-Liste wird also so lange ausgeführt, solange die Liste (Bedingung) hinter until einen Exit-Status ungleich 0 liefert.

    Der Exit-Status der Abweis-Schleifen entspricht dem Exit-Status der ausgeführten do-Liste oder 0, falls keine do-Liste ausgeführt wurde.

    Beispiel:

      zahl=5
      while ((zahl > 0))
      do
        echo "$zahl"
        ((zahl--))
      done
    
      zahl=5
      until ((zahl == 0))
      do
        echo "$zahl"
        ((zahl--))
      done

    Diese beiden Schleifen sind funktionell identisch. Sie dekrementieren schrittweise den Inhalt der Variablen zahl von 5 auf 0 und geben diesen Inhalt aus, solange er größer als 0 ist.

Die Bash ermöglicht auch eine Schleifensteuerung. Die mit for, while, until und select realisierbaren Schleifen können durch die Kommandos break und continue beeinflusst werden:

Optional kann bei break und continue ein numerisches Argument n angegeben werden, um sich auf die n-te umschließende Schleife zu beziehen. n hat den Standard-Wert 1.

Beispiele:

  # Endlos-Einlese-Schleife
  while true
  do
    # Zeile von stdin einlesen
    read
    # Doppelkreuz-Kommentare übergehen
    [[ $REPLY == \#* ]] && continue
    # bei leerer Zeile Einlese-Schleife stoppen
    [[ -z $REPLY ]] && break
    # eingelesene Zeile verarbeiten (ausgeben)
    echo "$REPLY"
  done
  
  # break mit Argument:
  # Sobald hier die beiden Variablen i und j denselben
  # numerischen Wert haben, werden beide Schleifen beendet.
  for i in 3 4 5
  do
    for j in 4 5 6
    do
      echo "$i $j"
      ((i == j)) && break 2
    done
  done

  echo Ende der beiden for-Schleifen

30. Koprozesse

Im Gegensatz zu ksh, zsh und gawk unterstützt die Bash vor Version 4 keine Koprozesse:

  cat |&     # Koprozess in der ksh einrichten
  coproc cat # Koprozess in der zsh einrichten

  # bidirektionale Kommunikation mit dem Koprozess in ksh/zsh
  # 
  # Zur Kommunikation mit dem Koprozess existiert bei den Kommandos
  # print und read die Option -p. Außerdem kann man mit
  # dem Umlenkungs-Operator <& p Daten vom Koprozess lesen und
  # mit dem Operator >& p Daten zum Koprozess senden.
  print -p abc
  echo def >& p
  read -p ; echo "$REPLY"
  read <& p ; echo "$REPLY"

  # Koprozess-Beispiel bei gawk: Sortierung von Zeichenketten
  # unter Verwendung des Unix-Standard-Kommandos sort
  BEGIN {
    SORT = "sort"
    # 3 Zeichenketten an sort senden
    print "def" |& SORT
    print "xyz" |& SORT
    print "abc" |& SORT
    # die Verbindung in Richtung von gawk zu sort schließen;
    # dadurch erhält sort ein EOF (Dateiende-Signal, End Of File)
    # und beginnt die Sortierung
    close(SORT, "to")
    # die sortierten Daten in einer Schleife zurücklesen und ausgeben
    while ((SORT |& getline str) > 0)
    {
      print str
    }
  }

Ab Version 4 stehen auch in der Bash Koprozesse zur Verfügung. Das sind Shell-Kommandos, die wie bei der Z-Shell mit dem reservierten Wort coproc eingeleitet werden. Die komplette Syntax sieht so aus:

  coproc [NAME] Kommando [Umlenkungen]

Dadurch wird der Koprozess NAME erzeugt. Falls NAME nicht angegeben wird, verwendet die Bash den Default-Namen COPROC. NAME darf nur dann spezifiziert werden, wenn es sich beim Koprozess-Kommando um ein zusammengesetztes Kommando handelt. Bei einem einfachen Kommando würde NAME sonst als dessen erstes Wort interpretiert werden.

Ein solcher Koprozess wird asynchron in einer Sub-Shell ausgeführt, als ob das Kommando mit dem Operator & abgeschlossen worden wäre, allerdings wird hier zusätzlich eine bidirektionale Pipe zwischen der aufrufenden Shell und dem Koprozess etabliert. Im Kontext der aufrufenden Shell wird eine Feld-Variable NAME erzeugt. Die Standardausgabe des Koprozess-Kommandos ist mit einer Pipe verbunden, deren Dateideskriptor in NAME[0] hinterlegt wird. Analog steht in NAME[1] der Dateideskriptor der Pipe, die mit der Standardeingabe des Koprozess-Kommandos verbunden ist. Die Pipe wird eingerichtet, bevor die optionalen Umlenkungen des Kommandos coproc aktiv werden, so dass die Dateideskriptoren der Pipe dort nutzbar sind.

Die Prozess-ID der den Koprozess ausführenden Sub-Shell ist als Wert der Variable NAME_PID verfügbar. Das eingebaute Kommando wait kann genutzt werden, um auf die Beendigung des Co-Prozeses zu warten. Dessen Exit-Status entspricht dem Exit-Status des Koprozess-Kommandos.

Hinweis: Eine Bash-Instanz gestattet zu jeder Zeit maximal einen aktiven Koprozess. Details hierzu findet man unter Only one coprocess at a time.

Beispiel für Bash-Koprozesse: SCRIPTS/coprocess.sh

  #!/bin/sh
  #
  # Koprozesse ab Bash 4
  #
  # 4.5.2014
  
  # einen Koprozess mit einem einfachen Kommando erzeugen; awk konvertiert alle
  # von der Standardeingabe gelesenen Zeilen in Großschreibung und gibt das
  # Resultat auf die Standardausgabe aus; durch fflush() wird trotz aktiver
  # Bufferung eine Ausgabe ermöglicht; siehe den Punkt "Buffering" unter
  # http://wiki.bash-hackers.org/syntax/keywords/coproc?s[]=coproc#pitfalls
  coproc awk '{print toupper($0) ; fflush() }'
  
  # wir übergeben in einer Schleife 3 Strings an die Standardeingabe des
  # Koprozesses und lesen dessen Ausgabe zurück und geben sie aus; die
  # Deskriptoren der bidirektionalen Pipe zum Koprozess stehen im Feld COPROC
  for text in aaa bbb ccc
  do
    # eine Zeile an den Koprozess senden
    echo "$text" >&${COPROC[1]}
    # eine Zeile vom Koprozess lesen
    read line <&${COPROC[0]}
    echo "$line"
  done
  
  # den Koprozess mit kill und wait sauber beenden;
  # ohne Umlenkung auf /dev/null bekämen wir von wait eine Nachricht der Art
  #   coprocess.sh: Zeile 30: 27410 Beendet                 coproc COPROC awk '{print toupper($0) ; fflush() }'
  kill $COPROC_PID
  wait $COPROC_PID &> /dev/null
  
  echo ====
  
  # einen zweiten Koprozess erzeugen; diesmal geben wir ihm den expliziten Namen
  # "copr"; daher muss das Koprozess-Kommando ein zusammengesetztes Kommando
  # sein; die Standardausgabe des Koprozess-Kommandos ist zunächst mit der
  # bidirektionalen Pipe verbunden; durch die Umlenkung >&3; wird sie auf den
  # Dateideskriptor 3 umgelenkt, den wir durch eine äußere Umlenkung mit der
  # Standardausgabe des aufrufenden Skripts verbinden, so dass dort automatisch
  # die Ausgaben des Koprozesses erscheinen
  { coproc copr { awk '{print tolower($0) ; fflush() }'; } >&3; } 3>&1
  
  for text in aaa bbb ccc
  do
    echo "$text" >&${copr[1]}
  done
  
  # das explizite Vernichten des Koprozesses durch
  #  kill $copr_PID
  #  wait $copr_PID
  # kann hier entfallen, da dies die Bash am Ende selber erledigt
  
  echo Ende

31. Funktions-Definitionen

Die Bash gestattet die Definition eigener Funktionen. Bei einer Shell-Funktion handelt es sich um mit Shell-Mitteln definiertes Konstrukt, das wie ein einfaches Kommando gerufen werden kann und ein zusammengesetztes Kommando (den Funktionskörper) mit einem neuen Satz von Positionsparametern (nummerierte Argumente, die über Konstrukte der Art $1, $2, ${11} angesprochen werden) ausführt.

Syntax einer Funktions-Definition:

  [function] Name () { Liste; }

Anmerkung: Wenn das reservierte Wort function angegeben wurde, können die runden Klammern hinter dem Funktionsnamen entfallen.

Der Exit-Status einer Funktion entspricht im Standardfall dem Exit-Status des letzten Kommandos, das in der Funktion ausgeführt wurde. Mit dem eingebauten Kommando

  return [n]

kann man eine Funktion beenden und gleichzeitig den Exit-Status gezielt auf den Wert n setzen. Wenn n fehlt, wird eine 0 angenommen.

Beispiele:

  # Programm-Beendigung mit Fehlermeldung nach Fehler
  function error()
  {
    # optionales Argument ist Fehlernummer
    local nr=$1
    # Standard-Fehlernummer ist 0
    [[ -n $nr ]] || nr=0
    echo "Das Programm wird wegen Fehler $nr beendet."
    exit 1
  }

  # Prozess-Liste nach einem AWK-Muster durchsuchen
  # interne Funktion zum Durchsuchen der Prozess-Status-Liste, wobei das
  # Such-Kommando ausgeblendet werden soll
  function psg_intern()
  {
    ps auxww | sed 1d | $1 "$2" | fgrep -v "$1 $2"
  }

  # ps grep -- Ausgabe von ps nach AWK-Muster durchsuchen
  function psg()
  {
    psg_intern awk "/$1/"
  }
  
  # analog psg(), aber ohne Unterscheidung Groß-/Kleinschreibung
  function psgi()
  {
    psg_intern 'awk -v IGNORECASE=1' "/$1/"
  }

  # Funktion mit return-Kommando
  # Ein Exit-Status 0 zeigt den Erfolg der Funktion an.
  # Ein Exit-Status größer 0 benennt das Kommando, das fehlschlug.
  function putzen()
  {
    cd /tmp/xxx || return 1
    rm *.bak    || return 2 
  }
  
  # Funktion putzen aufrufen und Exit-Status auswerten
  putzen
  exit_status=$?
  ((exit_status)) && echo "Fehler bei Kommando $exit_status"

Wenn man eine Funktion innerhalb einer Shell ausführt, dann kann sie (im Gegensatz zu externen Kommandos, die immer in einem Kind-Prozess laufen) die Ausführungsumgebung dieser Shell verändern. Um eine solche Veränderung der Umgebung zu verhindern, muss man die Funktion in einer Sub-Shell ablaufen lassen. Das soll an folgendem Beispiel demonstriert werden:

  function xyz()
  {
    zzz=888
    cd /etc/init.d
  }

  unset zzz
  cd /
  
  ( xyz )
    # Funktion xyz synchron in einer Sub-Shell ausführen
  pwd
  echo zzz=$zzz
    # Ausgabe:
    #  /
    #  zzz=
    # Wie erwartet wurde in der aktuellen Shell das aktuelle Verzeichnis nicht geändert
    # und die globale Variable zzz nicht angelegt.

  xyz &
    # Funktion xyz asynchron in einer Sub-Shell ausführen
  wait
    # auf das Ende des Hintergrund-Kommandos warten
  pwd
  echo zzz=$zzz
    # Ausgabe:
    #  /
    #  zzz=
    # Auch hier wurde erwartungsgemäß in der aktuellen Shell das aktuelle Verzeichnis
    # nicht geändert und die globale Variable zzz nicht angelegt.
  
  xyz
    # Funktion xyz synchron in der aktuellen Shell ausführen
  pwd
  echo zzz=$zzz
    # Ausgabe:
    #  /etc/init.d
    #  zzz=888
    # Im Gegensatz zu den beiden vorangegangenen Aufrufen hat die Funktion
    # xyz die Umgebung der aktuellen Shell verändert. Deren neues
    # aktuelles Verzeichnis lautet /etc/init.d. Außerdem wurde die
    # globale Variable zzz mit dem Wert 888 angelegt.

Lokale Variablen können mit den Kommandos local oder declare (bzw. dem mittlerweile als veralt geltenden typeset) angelegt werden:

  name=Max

  function print_name()
  {
    echo "$name"
    local name=Otto
    declare name1=Berta
    echo "$name $name1"
  }

  print_name
  echo "$name $name1"

Lokale Variablen sind nur in der Funktion, in der sie definiert werden, und in allen Funktionen, die von der definierenden Funktion aufgerufen werden, sichtbar.

Alle nicht lokalen Variablen sind global, also im gesamten Skript sichtbar. Wenn eine Funktion einer noch nicht existierenden Variablen, die nicht als lokale Variable deklariert wurde, einen Wert zuweist, dann wird diese Variable als neue globale Variable angelegt. Das hatten wir oben in der Funktion xyz gesehen, die die globale Variable zzz anlegte.


32. Umgang mit Signalen

Signale gehören bei Unix/Linux-Systemen zu den Mitteln der Interprozess-Kommunikation. Sie haben eine Nummer sowie einen symbolischen Namen, die beide zur eindeutigen Identifizierung des Signals genutzt werden können. So trägt z.B. das Signal 9 die Bezeichnung SIGKILL. Zum Umgang mit Signalen existieren in der Bash die eingebauten Kommandos kill und trap.

Mit kill, das auch bei der Job-Steuerung eine Rolle spielt, kann man Signale an andere Prozesse senden. Neben dem eingebauten Kommando kill gibt es auch noch ein externes Kommando kill. Allerdings bieten alle modernen Shells ein eigenes kill an, das sich zwischen den verschiedenen Shells mehr oder minder deutlich unterscheidet. Nachfolgend wollen wir uns daher nur auf das Bash-interne kill beziehen.

Mit trap kann man die Reaktion auf die vom Shell-Prozess empfangenen Signale festlegen, indem man jeweils bestimmten Signalen einen entsprechenden Signal-Handler zuordnet.

Wie man z.B. der Seite signal(2) des Online-Manuals, die den Systemruf signal() beschreibt, entnehmen kann, kennt Unix/Linux drei Arten von Signal-Handlern:

  1. eine nutzerdefinierte Funktion (nutzerdefinierter Code)

  2. die Konstante SIG_IGN zum Ignorieren eines Signals

  3. die Konstante SIG_DFL zur Reinstallation des Vorzugs-Verhaltens des Unix-/Linux-Kerns, das je nach Signal verschieden sein kann:

Bei einem Signal-Handler der Bash handelt es sich um eine Folge von Shell-Kommandos, die ausgeführt wird, wenn die Shell eines der Signale empfängt, denen der Handler zugeordnet wurde bzw. die dem Handler zugeordnet wurden. Eine derartige von der Shell verwaltete Verknüpfung eines Handlers mit einem Signal wird auch als Trap bezeichnet.

Wenn ein solcher Signal-Handler existiert und mindestens ein Kommando umfasst, dann werden die zugehörigen Signale abgefangen und im (vom Shell-Anwender frei programmierbaren) Handler gezielt behandelt. Dies entspricht dem obigen Handler Nummer 1 (benutzerdefinierte Funktion).

Anmerkung: Die Signale SIGKILL und SIGSTOP können generell nicht abgefangen werden, weil dies der Unix-/Linux-Kern nicht zulässt.

Ein Signal-Handler der Bash kann auch leer sein, also kein Kommando enthalten. In diesem Falle werden die zugehörigen Signale ignoriert (entspricht SIG_IGN). Die Zuordnung eines leeren Handlers wird aber von der Shell trotzdem in der Liste der aktiven Traps verwaltet.

Die Bash bietet die Variante SIG_DFL nicht direkt an. D.h., man kann nicht gezielt das Vorzugs-Verhalten des Kerns bzgl. der Signal-Behandlung reinstallieren. Statt dessen ermöglicht die Shell die Reinstallation derjenigen Einstellungen der Signal-Behandlung, die sie von ihrem Eltern-Prozess geerbt hat. Sobald für ein Signal dieser geerbte Handler reinstalliert wurde, taucht dieses Signal nicht mehr in der Liste der aktiven Traps auf.

Anmerkung: Vom Eltern-Prozess kann die Shell nur die Einstellungen SIG_DFL und SIG_IGN erben. Eine andere Möglichkeit lässt der Kern nicht zu. Sobald nämlich ein Prozess eine andere Anwendung startet, werden alle Signale, die mit einem benutzerdefinierten Signal-Handler verbunden sind, auf SIG_DFL gesetzt. Die mit SIG_IGN verbundenen Signale bleiben dagegen unangetastet, d.h., sie werden auch im neu gestarteten Programm ignoriert.

Die folgende Liste beschreibt, wie die Bash mit empfangenen Signalen umgeht:

Das Kommando kill hat zwei Syntax-Varianten:

  # Variante 1:
  kill [-s Signal-Spezifikation | -n Signal-Nummer | -Signal-Spezifikation] [PID | Job-Spezifikation] ...

  # Variante 2:
  kill -l [Signal-Spezifikation | Exit-Status]

Variante 1 sendet das durch eine Signal-Spezifikation oder eine Signal-Nummer angegebene Signal an diejenigen Prozesse, die durch ihre Prozess-ID (PID) bzw. eine Job-Spezifikation benannt wurden. Eine Signal-Spezifikation ist entweder ein Signal-Name wie SIGKILL (wobei das SIG-Präfix optional ist und nicht zwischen Groß- und Kleinschreibung unterschieden wird) oder eine Signal-Nummer. Wenn kein Signal spezifiziert wurde, dann sendet kill immer das Signal SIGTERM.

Hier einige Beispiele:

  # Signal 10 (SIGUSR1) an Job Nummer 1 senden:
  kill -s USR1 %1
  kill -s SIGUSR1 %1
  kill -n 10 %1
  kill -10 %1
  kill -USR1 %1
  kill -SIGUSR1 %1
    # Die Bash sendet das Signal an alle Prozesse des Jobs.

  # Signal 10 (SIGUSR1) an Prozess 9253 senden:
  kill -s USR1 9253
  kill -s SIGUSR1 9253
  kill -n 10 9253
  kill -10 9253
  kill -USR1 9253
  kill -SIGUSR1 9253

Der Exit-Status der Variante 1 von kill lautet 0, wenn mindestens ein Signal erfolgreich gesendet wurde (d.h. an einen von evtl. mehreren Prozessen eines Jobs), und 1, wenn ein Fehler, eine ungültige Option oder ein ungültiges Argument auftrat.

Die Variante 2 von kill dient zur Anzeige von Signal-Nummern und -Namen. Wenn nur das Argument -l angegeben wird, listet kill alle existierenden Signal-Namen auf, wobei diesen jeweils die Signal-Nummer vorausgeht. Der Anfang der Ausgabe sieht in etwa so aus:

   1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL
   5) SIGTRAP      6) SIGABRT      7) SIGBUS       8) SIGFPE
   9) SIGKILL     10) SIGUSR1     11) SIGSEGV     12) SIGUSR2
   ...

Wenn hinter -l weitere Argumente folgen, müssen dies entweder Signal-Spezifikationen oder Exit-Status-Werte sein. Für jedes dieser Argumente wird der zugehörige Signal-Name bzw. die korrespondierende Signal-Nummer aufgelistet. Hier ein Beispiel:

  kill -l 1 USR1 5 STOP 138
    # Informationen zu den Signalen 1, SIGUSR1, 5 und SIGSTOP sowie zum
    # Exit-Status 138 ermitteln
    #
    # Ausgabe:
    #   HUP
    #   10
    #   TRAP
    #   19
    #   USR1

Variante 2 von kill liefert den Exit-Status 1, wenn mindestens ein ungültiges Argument angegeben wurde. Andernfalls lautet er 0.

Das Kommando trap hat folgende Syntax:

  trap [-lp] [[Kommando] Signal-Spezifikation ...]

Das Kommando ist der Signal-Handler. Er wird durch trap mit denjenigen Signalen verbunden, die durch die hinter dem Handler angegebenen Signal-Spezifikationen benannt wurden, wobei trap dieselben Signal-Spezifikationen wie kill verwendet. Solange diese Verbindung bzw. Zuordnung gilt, wird die Shell einen nicht leeren Handler aufrufen, nachdem eines der zugehörigen Signale empfangen wurde.

Das Kommando, also der Signal-Handler, muss aus Sicht der Shell ein einzelnes Wort sein. Falls der Handler Worttrenner enthält, sind diese geeignet zu quotieren, wie folgendes Beispiel zeigt:

  trap 'echo SIGUSR1' SIGUSR1

Wenn entweder das Kommando fehlt und nur eine Signal-Spezifikation folgt oder das Kommando dem Zeichen - (Minus) entspricht, dann wird jedes der spezifizierten Signale auf diejenige Einstellung zurückgesetzt, die die Shell von ihrem Eltern-Prozess erbte.

Wenn das Kommando dem leeren String entspricht, dann werden die spezifizierten Signale von der Shell ignoriert. Außerdem ruft die Shell die von ihr gestarteten Kommandos so auf, dass diese die betreffenden Signale ebenfalls standardmäßig ignorieren.

Wenn das Kommando fehlt, aber die Option -p angegeben wurde, dann werden für die als Argumente angegegebenen Signale die aktiven Traps (also Zuordnungen von Kommandos bzw. Handlern zu diesen Signalen) angezeigt. Sofern für ein als Argumente benanntes Signal kein Trap existiert, erfolgt dafür keinerlei Anzeige.

Wenn trap ganz ohne Argumente oder nur mit der Option -p aufgerufen wurde, dann wird die komplette Liste der aktiven Traps angezeigt, also inkl. jener, die ein leeres Kommando enthalten und somit das Signal ignorieren.

Die Option -l von trap bewirkt die Anzeige aller Signal-Namen und zugehörigen Nummern (analog kill -l). Argumente hinter der Option werden hierbei allerdings ignoriert.

trap unterstützt einige spezielle Signal-Spezifikationen:

EXIT Der Handler wird beim Beenden der Shell ausgeführt.
0
wirkt wie EXIT
DEBUG Der Handler wird jeweils vor einem der folgenden Kommandos ausgeführt:
  • einfaches Kommando
  • for
  • case
  • select
  • erstes Kommando einer Shell-Funktion

Anmerkung: Bei der nicht-arithmetischen for-Schleife wird der DEBUG-Trap vor jedem Schleifendurchlauf aktiviert. Bei der arithmetischen Variante der for-Schleife wird der DEBUG-Trap vor jedem arithmetischen Kommando ausgeführt.

ERR Der Handler wird standardmäßig nach jedem einfachen Kommando ausgeführt, dessen Exit-Status ungleich 0 ist, nicht aber, wenn das fehlgeschlagene Kommando hinter while oder until folgt, im Rahmen von if oder einer UND- bzw. ODER-Liste (&& und ||) getestet oder sein Exit-Status mittels ! invertiert wird. Das sind dieselben Bedingungen wie bei der Option errexit.
RETURN Der Handler wird jedesmal dann ausgeführt, wenn ein mit dem Kommando source bzw. . (Punkt) eingelesenes Skript beendet wurde. Laut Manual soll ein RETURN-Trap auch beim Beenden einer Shell-Funktion ausgeführt werden. In praktischen Tests des Autors war dies aber nicht der Fall.

Die speziellen Traps werden im Skript SCRIPTS/spezial_traps.sh demonstriert.

Den Signalen, die beim Start der Shell vom Eltern-Prozess ignoriert wurden, kann man innerhalb der Shell keine anderen Handler zuordnen.

Wenn die Shell einen Kind-Prozess kreiert, reinstalliert sie für alle Signale, deren Behandlung mit trap geändert worden war, die vom Eltern-Prozess der Shell geerbte Einstellung.

Der Exit-Status von trap lautet bei ungültiger Signal-Spezifikation 1, sonst 0.

Die Skripte

zeigen die Vererbung der Signal-Handler-Einstellungen eines Eltern-Prozesses an eine Bash. Der Eltern-Prozess wurde hier als Python- und nicht als Shell-Skript realisiert, um nicht den Restriktionen der Shell zu unterliegen, sondern flexibler die gewünschten Signal-Handler über Unix-Systemrufe installieren zu können. Python-Skripte haben den Vorteil, dass sie relativ leicht lesbar sind und im Gegensatz zum Beispiel zu C-Programmen keine explizite Übersetzung erfordern. Die Kommentare im Python-Skript erläutern jeweils den Sinn der einzelnen Anweisungen.

Als weiteres Beispiel zur Verwendung von trap sollen die Shell-Skripte

dienen. Das erste Skript startet das zweite und sorgt dafür, dass alle Ausgaben des zweiten Skripts sowohl auf die Standard-Ausgabe als auch in eine Log-Datei geschrieben werden. Das zweite Skript soll mit den Signalen SIGINT und SIGQUIT sauber beendet werden können. D.h., diese beiden Signale sind einzufangen und durch einen eigenen Handler gezielt zu behandeln. Das erste Skript muss daher dafür sorgen, dass beide Signale an das zweite Skript weitergeleitet werden.


33. Ausgewählte eingebaute Kommandos (Builtin-Kommandos)

Kommando-Index:

  1. :
  2. builtin
  3. cd
  4. command
  5. declare/typeset
  6. dirs
  7. echo
  8. enable
  9. eval
  10. getopts
  11. hash
  12. help
  13. popd
  14. printf
  15. pushd
  16. read
  17. source
  18. test
  19. type

Kommandos:


34. Flexible Prompt-Anpassung

Eine interaktive Bash zeigt den primären Prompt PS1 an, wenn sie zum Einlesen eines Kommandos bereit ist. Anschließend wartet sie auf die Eingabe. Mit der Umgebungsvariablen TMOUT kann man hierfür einen Timeout festlegen. Durch den sekundären Prompt PS2 signalisiert die Shell, dass das eingegebene Kommando noch unvollständig ist und daher weitere Eingaben benötigt werden.

Die als Werte dieser beiden Prompt-Variablen PS1 und PS2 gespeicherten Prompt-Strings können die nachfolgend aufgeführten Spezialzeichen enthalten, die durch einen vorangestellten Backslash gekennzeichnet werden:

\a ASCII-Zeichen 7 (Alert bzw. Bell)
\d Datum im Format "Wochentag Monat Datum", z.B. So Jan 08
\D{Format} Resultat der Bibliotheks-Funktion strftime(3), der die Angabe Format übergeben wurde

Ein leerer Format-String resultiert in einer Locale-spezifischen Zeitdarstellung, z.B. 17:36:44.
\e Escape-Zeichen (Oktalwert 033)
\h Host-Name bis zum ersten Punkt
\H kompletter Host-Name
\j Anzahl der aktuell von der Shell verwalteten Jobs
\l Basis-Geräte-Name des Terminals der Shell, z.B. 4 beim Gerät /dev/pts/4
\n Newline-Zeichen
\r Carriage-Return-Zeichen
\s Name der Shell, d.h. der Basisname des Wertes des Parameters $0
\t aktuelle Zeit im 24-Stunden-Format HH:MM:SS
\T aktuelle Zeit im 12-Stunden-Format HH:MM:SS
\@ aktuelle Zeit im 12-Stunden-Format HH:MM
\A aktuelle Zeit im 24-Stunden-Format HH:MM
\u Nutzername des aktuellen Benutzers
\v Bash-Version, z.B. 3.00
\V Bash-Version und Patch-Level, z.B. 3.00.16
\w aktuelles Verzeichnis, wobei die Tilde das Home-Verzeichnis abkürzt
\W Basisname des aktuellen Verzeichnisses, wobei die Tilde das Home-Verzeichnis abkürzt
\! History-Nummer des einzugebenden Kommandos
\# Kommando-Nummer des einzugebenden Kommandos
\$ das Zeichen #, wenn die effektive UID den Wert 0 hat, sonst das Zeichen $
\nnn Zeichen mit dem Oktalwert nnn
\\ Backslash
\[ Beginn einer Folge nicht druckbarer Zeichen, die benutzt werden kann, um Terminal-Steuer-Sequenzen in den Prompt einzubetten
\] Ende einer Folge nicht druckbarer Zeichen

Sofern die standardmäßig gesetzte shopt-Option promptvars aktiv ist, unterliegt der Prompt-String nach der Dekodierung der o.g. Spezialzeichen der Parameter-Expandierung, der Kommando-Substitution, der arithmetischen Expandierung sowie der Entfernung der Quotierungs-Zeichen.

Die Kommando- und die History-Nummer eines Kommandos können differieren. Die History-Nummer gibt die Position des Kommandos in der History-Liste an, welche auch Kommandos enthalten kann, die aus einer History-Datei eingelesen wurden und somit aus einer anderen Shell-Sitzung stammen. Dagegen bezieht sich die Kommando-Nummer immer auf den aktuellen Shell-Prozess und wird für jedes neue Kommando um eins erhöht.

Kommando- und die History-Nummer unterscheiden sich auch dann, wenn ein ausgeführtes Kommando auf Grund einer History-Option (z.B. ignoreboth in HISTCONTROL) nicht in die History aufgenommen wird oder nachdem die History vom Nutzer partiell oder komplett gelöscht wurde, weil dadurch die aktuelle History-Nummer verringert werden kann.

Der folgende Auszug aus ~/.bashrc zeigt ein Beispiel für eine Anpassung des primären Prompts (PS1):

  if (($UID))
  then
    # Prompt-Zeichen für Nicht-Root-Nutzer ist >
    pschar='$>'
  else
    # Prompt-Zeichen für root ist #
    pschar='\\$#'
        # Anmerkung: $pschar, also die Zeichenfolge
        #   \\$#
        # wird unten in den Wert von PS1 übernommen. Dabei wird \\ von der
        # Prompt-String-Dekodierung als Backslash gewertet. Nach der
        # Dekodierung steht also die Zeichenfolge
        #   \$#
        # in PS1. Diese wird wegen der gesetzten Option promptvars zu
        #   $#
        # expandiert. Würde der Backslash fehlen, würde $# durch die Anzahl der
        # Positionsparameter ersetzt.
  fi
  if [[ $TERM == xterm* ]]
  then
    # Hinweis: Alle Zeichen, die bei ihrer Ausgabe keinen Platz auf dem
    # Bildschirm einnehmen, sind in \[ und \] einzuschließen, weil sonst die
    # Readline-Bibliothek nicht ordentlich arbeiten kann.
    PS1="\[\e]2;\h:\w\a\e[7m\]\u@\h\[\e[0m\] \! $pschar "
  else
    PS1="\[\e[7m\]\u@\h\[\e[0m\] \W \! $pschar "
  fi

Erläuterung:

Für PS1 werden zwei Einstellungen vorgenommen: die erste für Shells, die in einem XTerm-artigen Terminal-Emulator laufen, die zweite für alle anderen Shells (also z.B. auf der Linux-Text-Konsole).

Beschreibung der Elemente von PS1 für Shells im XTerm-artigen Terminal-Emulator:

\[ Beginn einer Folge nichtdruckbarer Zeichen
\e]2; Zeichenfolge mit 3 Zeichen: ESC (Escape) 2 ;
Bedeutung: Beginn einer Escape-Sequenz zum Setzen der Titelzeile im XTerm-Fenster
\h:\w Host-Name in Kurzform gefolgt von einem Doppelpunkt gefolgt vom kompletten absoluten Pfad zum aktuellen Verzeichnis
\a Ctrl-G (Bell) zum Beenden der Escape-Sequenz, die die XTerm-Titelzeile setzt
\e[7m Zeichenfolge mit 3 Zeichen: ESC 7 m
Bedeutung: inverse Schrift einschalten (Wechsel von Vorder- und Hintergrund-Farbe)
\] Ende einer Folge nichtdruckbarer Zeichen
\u@\h Nutzer-Name gefolgt von @ gefolgt vom Host-Namen in Kurzform
\[\e[0m\] Escape-Sequenz ESC 0 m zum Beenden der inversen Schrift durch Zurückschaltung auf die normale Schrift (wird wieder als Folge nichtdruckbarer Zeichen angegeben)
 \! $pschar Leerzeichen gefolgt von der History-Nummer des aktuellen Kommandos gefolgt vom oben gewählten Prompt-Zeichen (# für root, $ sonst) gefolgt von einem weiteren Leerzeichen

Beschreibung der Elemente von PS1 für Shells, die nicht im XTerm laufen:

\[\e[7m\] inverse Schrift einschalten (s. oben)
\u@\h s. oben
\[\e[0m\] Zurückschaltung auf die normale Schrift (s. oben)
 \W letztes Element des absolutes Pfades zum aktuellen Verzeichnis, wobei das Home-Verzeichnis durch ~ (Tilde) symbolisiert wird
 \! $pschar s. oben

35. Einige Neuerungen in Bash 3.0

Die folgende Liste nennt einige der Neuerungen in Bash 3.0 gegenüber der Bash 2:


36. Bash-Debugger

Unter http://bashdb.sourceforge.net ist der Bash-Debugger von Rocky Bernstein zu finden, der von seiner Handhabung her an den GDB erinnert.


37. Sichere Shell-Skripten

Nachfolgend sind verschiedene Tips aufgeführt, um sichere Shell-Skripten zu schreiben, die also (möglichst) immer das tun, was deren Autor beabsichtigt hat und einem Angreifer keine Chance bieten, die Funktionalität des Skripts zu ändern, um ggf. unautorisierte Manipulationen am System vorzunehmen.

Die Liste erhebt keinen Anspruch auf Vollständigkeit.


38. Beispiel-Skripten

Im Verzeichnis SCRIPTS liegen verschiedene Beispiel-Skripten bzw. -Programme. Die meisten von ihnen sind Bash-Skripte, daneben gibt es aber auch Vertreter, die in den Sprachen AWK, Python und C implementiert wurden. Alle Dateien (mit Ausnahme des aus dem C-Quelltext generierten Maschinen-Codes) enthalten Kommentare, die den Zweck des Skripts/Programms sowie seine Funktionalität erläutern. Auf einige Dateien wird im obigen Text verwiesen, andere stehen zusätzlich hier bereit.


Holger Trapp

letzte Modifikation: 04.05.2014