Bash/Syntax
Syntax der Bash
Datei:Grafik4.png
Definitionen
Metazeichen | Ein Zeichen, das einzelne Worte trennt: | & ; () < > Leerzeichen Tabulator |
Name | Zeichenkette, bestehend aus alphanumerischen Zeichen und dem Unterstrich, wobei das erste Zeichen keine Ziffer ist. |
Kontrolloperator | Ein Token, das eine spezielle Kontrollfunktion auslöst, die Symbole sind: | & && ; ;; () Zeilenumbruch |
Token | Eine Zeichenfolge, die von der Shell als eine Einheit betrachtet wird. |
Whitespace | Steht für Leerzeichen und Tabulatoren und ggf. dem Zeilenumbruch. |
Einfache Kommandos
Ein einfaches Kommando ist ein Ausschnitt der Kommandozeile, den die Bash in einem Zusammenhang betrachtet. Der Begriff des »einfachen Kommandos« definiert einen wichtigen Aspekt der Semantik der Bash.
Dazu untersucht die Shell die Eingabe und extrahiert alle enthaltenen »einfachen Kommandos«, für die sie nachfolgend definierte Substitutionen vollzieht.
Ein einfaches Kommando wird stets durch einen Kontrolloperator abgeschlossen.
Es besteht aus einer Sequenz optionaler Variablenzuweisungen, von durch Whitespaces getrennten Worten und Ein-/Ausgabe-Umleitungen. Das erste Wort, das keine Variablenzuweisung ist, wertet die Bash als Kommandoname, alle weiteren Worte sind die Argumente des Kommandos.
Man kann sich als einfaches Kommando den Abschnitt der Kommandozeile vorstellen, der innerhalb ein und desselben Prozesses ausgeführt wird.
Die einzelnen Substitutionsschritte, die die Bash für jedes einfache Kommando vornimmt, sind: # Variablenzuweisungen und Ein-/Ausgabe-Umleitungen werden markiert (damit sie nicht im folgenden Schritt betrachtet werden)
- Alle nicht-markierten Worte werden expandiert (siehe nachfolgend)
- Ein- und Ausgabe-Umleitungen werden vorbereitet (indem die entsprechenden Dateideskriptoren geschlossen/geöffnet werden)
- Die Variablenzuweisungen werden vorgenommen (inklusive vorheriger Expansionen)
Rückgabewerte
Mit Beendigung eines Kommandos liefert dieses einen Statuswert an seinen Elternprozess.
Dieser Wert kann eine Zahl zwischen 0 und 255 beinhalten und gibt Auskunft über den Erfolg (Rückgabewert »0«) und Misserfolg (Rückgabewert »> 0«) der Ausführung des Kommandos.
Viele Kommandos kodieren im Wert die vermutliche Ursache des Scheiterns, wobei Werte zwischen 0 und 127 empfohlen sind.
Wird ein Kommando »von außen« beendet (kill), so ist der Rückgabewert die Signalnummer + 128. Die Shell speichert den Rückgabestatus des letzten Kommandos in der speziellen Variable $?.
grep root /etc/passwd
root:x:0:0:root:/root:/bin/bash echo $? 0 grep bla /etc/passwd echo $? 1 grep root /ETC/passwd grep: /ETC/passwd: Datei oder Verzeichnis nicht gefunden echo $? 2
Shell-Sonderzeichen und Maskierung
Shell-Sonderzeichen
In der Bash gibt es verschiedene Zeichen, die eine besondere Bedeutung haben.
Maskieren (Quoting) dient dazu, diesen Shell-Sonderzeichen vor der Shell zu 'verstecken', ihnen ihre besondere Bedeutung zu nehmen, um z. B. zu verhindern, dass diese expandiert (ersetzt) werden.
Viele Zeichen haben eine besondere Bedeutung je nach Kontext.
Die wichtigsten Shell-Sonderzeichen
; | Befehls-Trennzeichen |
& | Hintergrund-Verarbeitung |
( ) | Befehls-Gruppierung |
{ } | Klammersubstitution |
| | Pipe |
< > & | Umlenkungssymbole |
* ? [ ] ~ + - @ ! | Meta-Zeichen für Dateinamen |
` ` | (Backticks) Befehls-Substitution |
$ | Variablen-Substitution |
[newline] [space] [tab] | Wort-Trennzeichen (IFS) |
Maskierung (quoting)
Einzelne Zeichen maskieren (escape characters)
Escape characters werden benutzt, um einem einzelnen Zeichen seine spezielle Bedeutung zu nehmen. Dazu wird in der Bash ein nicht maskierter Backslash \ benutzt.
Es erhält den literalen Wert des folgenden Zeichens mit Ausnahme von newline.
Wenn ein newline Zeichen direkt nach einem Backslash auftaucht, markiert es die Fortsetzung des Befehls in der nächsten Kommandozeile; der Backslash wird aus dem input stream entfernt und tatsächlich ignoriert.
Beispiel
echo $USER
dirkwagner
echo \$USER
$USER
Keinerlei Substitutionen (Single quotes)
Einfache Anführungszeichen (' ') werden genutzt um den literalen Wert aller eingeschlossenen Zeichen zu erhalten.
Die eingeschlossene Zeichenkette darf kein einfaches Anführungszeichen enthalten, selbst wenn es mit einem Backslash maskiert wird.
Beispiel
echo 'Weder * noch $(pwd) werden ersetzt'
Weder * noch $(pwd) werden ersetzt
Selektive Substitutionen (Double quotes)
Bei der Benutzung doppelter Anführungszeichen behalten alle Zeichen ihre literale Bedeutung mit Ausnahme des Dollarzeichens, der backticks (backward single quotes, ``) und dem Backslash. Sie behalten Ihre spezielle Bedeutung.
Der Backslash behält seine Bedeutung nur, wenn ein Dollarzeichen, ein Backtick doppelte Anführungszeichen, ein Backslash oder newline folgen. Bei doppelten Anführungszeichen wird der Backslash aus dem Eingabedatenstrom entfernt, wenn eines dieser Zeichen folgt.
Backslashes preceding characters that don't have a special meaning are left unmodified for processing by the shell interpreter.
Doppelte Anführungszeichen können innerhalb von doppelten Anführungszeichen durch einen Backslash maskiert werden
echo "$date" 20021226
echo "`date`" Sun Apr 20 11:22:06 CEST 2003
echo "I'd say: \"Go for it!\"" I'd say: "Go for it!"
echo "\" More input>"
echo "\\" \
Keine Ersetzung der Metazeichen, jedoch Variablen- und Befehlssubstitution
echo Der * wird hier durch alle Dateinamen ersetzt echo "Der * wird hier nicht ersetzt"
ANSI-C quoting
Ausdrücke der Art "$'STRING'" werden auf eine spezielle Weise behandelt.
Der Ausdruck expandiert zu einer Zeichenkette mit Backslash-maskierten Zeichen wie es im ANSI-C -Standard festgelet ist. Backslash-maskierte Sequenzen können der Bash-Dokumentation entnommen werden.
Locales
Eine Zeichenkette in doppelten mit vorangestelltem Dollarzeichen wird in die aktuelle Landeseinstellung (locale) übersetzt.
Wenn die aktuelle Landeseinstellung „C“ oder „POSIX“ ist, wird das Dollarzeichen ignoriert. Wurde die Zeichenkette übersetzt und ersetzt, ist die Ersetzung mit doppelten Anführungszeichen maskiert.
Kommentare
Normalerweise leitet das Doppelkreuz # in der Bash einen Kommentar ein.
Alles, was diesem Zeichen folgt, wird von der Bash ignoriert bis zum nächsten Zeilenumbruch. Dies gilt sowohl für die interaktive als auch für die nicht-interaktive Bash.
Eine Ausnahme sind Shellskripte, in denen die erste Zeile mit #!... beginnt. Für die Bash ist es die Anweisung, die nachfolgenden Zeilen mit dem hinter #! angegebenen Interpreter (eine Shell, Perl, awk,...) auszuführen.
Die Bedeutung von # kann entweder durch maskieren oder durch die Shelloption "interactive_comments" aufgehoben werden.
cat \#script
#!bin/sh echo "sinnlos" #script # Wirkung des Doppelkreuzes aufheben: shopt -u interactive_comments #script sinnlos # Wirkung des Doppelkreuzes aktivieren: shopt -s interactive_comments
Kommentare einfügen
Kommentare sind wichtig und man sollte regen Gebrauch davon machen.
An schwierigen Passagen sollte man kleine Notizen hinterlegen um festzuhalten, was man sich dabei gedacht hat und wie das Konstrukt funktioniert.
So erleichtert man sich selbst, z. B. nach längeren Pausen, oder aber auch Anderen, welche das Skript bearbeiten möchten, das Verständnis.
Zudem lassen sich Kommentare auch gut als strukturgebendes Element einsetzen, was eine höhere Übersichtlichkeit ermöglicht.
Einfügen lassen sich Kommentare durch das Hash-Zeichen (#). Dies kann überall in einer Zeile stehen; die Bash ignoriert alles, was dahinter steht. Im Allgemeinen sieht das dann so aus.
- Dies ist ein Kommentar
kein Kommentar # Dies ist ein Kommentar. ### Auf diese Weise kann man beispielsweise Abschnitte trennen ###
Ein- und Ausgabeumleitung
Die drei dem Terminal zugeordneten Dateikanäle stdin, stdout und stderr können jederzeit auf Dateien umgeleitet werden.
Den drei Standarddateien sind die Filehandles 0 (stdin), 1 (stdout) und 2 (stderr) zugeordnet. |
Datei:Grafik2.png |
Eingabeumleitung
Das Programm liest nun nicht mehr von der Tastatur (stdin), sondern aus einer Datei, bis das Dateiende erreicht ist.
Die Eingabeumleitung erfolgt durch das Zeichen "<", gefolgt von einem Dateinamen. |
Datei:Grafik32.png |
Kommando < Dateiname
Statt z. B. beim write-Kommando den Text direkt einzugeben, kann auch einen Datei an ein anderes Terminal gesendet werden:
mail tux@melmac.all -s Wichtig < Message.txt
Ausgabeumleitung
Die Ausgabe des Programms wird nicht auf dem Bildschirm (stdout) ausgegeben, sondern in einen Datei geschrieben. Die Ausgabeumleitung erfolgt durch das Zeichen ">", gefolgt von einem Dateinamen.
Falls die Datei noch nicht vorhanden war, wird sie automatisch angelegt. Falls die Datei schon vorhanden ist, wird sie überschrieben, d. h. es wird immer ab dem Dateianfang geschrieben. |
Datei:Grafik10.png |
Kommando > Dateiname
Fehlermeldungen (stderr) erscheinen nach wie vor auf dem Bildschirm. Beispiel |
Datei:Grafik22.png |
ls -l > info
# Umleitung der Ausgabe von "ls -lt" in eine Datei ls -lt > time_sorted # Provozieren eines Fehlers touch /etc/passwd touch: /etc/passwd: Keine Berechtigung # Umleitung des Fehlers touch /etc/passwd 2> /dev/null
Überschreiben existierender Dateien verhindern
Die Shelloption "noclobber" verhindert das Überschreiben existierender Dateien:
set -o noclobber
ls -lt > time_sorted bash: time_sorted: cannot overwrite existing file
"noclobber" zurüsetzen
set +o noclobber
Umlenkung der Fehlerausgabe (stderr)
Die Umleitung der Fehlerausgabe erfolgt genauso, wie die Ausgabeumleitung, jedoch wird hier die Zeichenfolge "2>" verwendet, da stderr die Handlenummer 2 hat.
Kommando 2> Fehlerdatei Die Umleitung der Standardausgabe ist nur die Kurzform von Kommando 1> Dateiname. Natürlich ist eine beliebige Kombination von Ein- und Ausgabeumleitung möglich: |
Datei:Grafik3.png |
Kommando < Eingabedatei > Ausgabedatei 2> Fehlerdatei
Unterdrückung der Ausgabe
Durch Umleitung von Ein- und Ausgabe lässt sich auch unterdrücken. Für die Ausgabe schreibt man
Kommando > /dev/null
oder für die Fehlerausgabe
Kommando 2> /dev/null
Beides lässt sich auch kombinieren:
Kommando > Ergebnisdatei 2> /dev/null
Will man ein Programm mit einem beliebigen Eingabedatenstrom versorgen, schreibt man
Kommando < /dev/zero
Anhängen von Daten an eine Datei
Es ist auch möglich, die Ausgabe des Programms an eine bereits vorhandene Datei anzuhängen. Dazu wird des ">" doppelt geschrieben.
Kommando >> Sammeldatei
- Beispiele
Dateiliste und aktive Benutzer in einen Datei schreiben:
ls -l > liste
who >> liste
Zusammenfassung von Datenströmen
Die Umleitung von stout und stderr in dieselbe Datei würde prinzipiell eine zweimalige Angabe der Datei (eventuell mit einem langen Pfad) erfordern. Für die Standarddateien werden in solchen Fällen spezielle Platzhalter verwendet:
&0 | Standardeingabe |
&1 | Standardausgabe |
&2 | Standard-Fehlerausgabe |
Beispiel
Kommando > ausgabe 2>&1
Pipelines
In einer Pipeline werden mehrere Kommandos mit dem Pipesymbols | verknüpft.
Jedes Kommando wird in einer eigenen Prozessumgebung ausgeführt und über einen temporären Puffer mit dem Nächsten verbunden. Das Kommando links eines Pipesymbols schreibt seine Ergebnisse in die Pipe (»Röhre«) anstatt auf die Standardausgabe. |
Datei:Grafik11.png |
Ein rechts des Symbols stehendes Kommando bezieht seine Eingabedaten aus der Pipe.
Die Pipeline ist somit eine wichtige Unterstützung des Unix-Werkzeugkasten-Prinzips durch die Bash, womit durch geschickte Kombination der scheinbar simplen Kommandos komplexe Aufgaben lösbar werden.
Kommando 1 | Kommando 2
Es können auch mehrere Kommandos hintereinander durch Pipes verbunden werden:
Kommando 1 | Kommando 2 | Kommando 3 | Kommando 4 | ...
Solche Kommandofolgen werden auch als Filter bezeichnet. Einige nützliche Filter sind in jedem UNIX-System verfügbar.
Beispiel
Ausgabe der Dateien eines Verzeichnisses mit der Möglichkeit, zu blättern:
ls -Rl / | less
Eine komplexere Anwendung ist die nächste Verknüpfung zweier tar-Befehle, womit ein ganzes Verzeichnis bei Beibehaltung der Eigentümer kopiert wird:
tar cf -./bla | (cd /zielpfad; tar xf -)
Filter
head
head [-n] [datei(en)]
Ausgabe der ersten n Zeilen aus den angegebenen Dateien.
Voreinstellung ist 10 Zeilen. Wird keine Datei angegeben, liest head von der Standardeingabe.
tail
tail [-/+n] [bc[f|r]] [datei]
Optionen
+n | ab der n. Zeile ausgeben |
-n | die letzten n Zeilen ausgeben Wird hinter die Zahl n ein 'b' gesetzt (z. B. -15b), werden nicht n Zeilen, sondern n Blöcke ausgegeben. Wird hinter die Zahl n ein 'c' gesetzt (z. B. -200c), werden nicht n Zeilen, sondern n Zeichen (characters) ausgegeben. |
-r | Zeilen in umgekehrter Reihenfolge ausgeben (letzte zuerst). Geht nicht bei GNU-tail - stattdessen kann man das Programm toc verwenden. |
-f | tail am Dateiende nicht beenden, sondern auf weitere Zeilen warten. (Ende des Kommandos mit der Strg-C-Taste). Damit kann man z. B. Logfiles beobachten, die ständig wachsen. |
tee
tee liest von der Standardeingabe und verzweigt die Ausgabe auf die Standardausgabe und Datei.
Wird auf eine existierende Datei verzweigt, so wird sie überschrieben, anderenfalls wird sie angelegt. |
Datei:Grafik14.png |
tee [-ai] [-append] [-ignore-interrupts] [Datei ...]
Beispiel
make -k 2>&1 | tee make.out
Optionen
-a | die Datei wird nicht überschrieben, sondern die Ausgabe daran angehängt |
-i | ignoriert Interrupt Signale |
wc
wc [-lwc] [Datei(en)]
Dieses Kommando zählt Zeilen, Worte oder Zeichen in einer Datei. Wird kein Dateiname angegeben, liest wc von der Standardeingabe. Normalerweise zählt man damit in Skripten irgendwelche Ergebnisse.
Optionen
-l | Zähle Zeilen |
-w | Zähle Worte |
-c | Zähle Zeichen |
Weitere Filter sind ...
Kommandoverkettungen
Die bash unterstützt mehrere Möglichkeiten, mehrere Kommandos nacheinander und in Abhängigkeit voneinander zu starten:
ls; date
Die Kommandofolge führt zunächst das Kommando ls aus und zeigt dann das aktuelle Datum an.
ls; date > datei
In datei steht das aktuelle Datum.
(ls; date) > datei
In datei stehen nun der Verzeichnisinhalt und das aktuelle Datum. Die Klammerung bewirkt die Ausführung der eingeschlossenen Kommandos in derselben Shell, sodass diese ein Ergebnis zurückliefern.
Übersicht
komm1; komm2 | Führt die Kommandos nacheinander aus | |
komm1 && komm2 | Führt komm2 nur aus, wenn komm1 erfolgreich war | |
komm1 | komm2 | Führt komm2 nur aus, wenn komm1 einen Fehler liefert |
komm1 & | Führt Kommando als Hintergrundprozess aus | |
komm1 & komm2 | Startet komm1 im Hintergrund, komm2 im Vordergrund | |
(komm1; komm2) | Startet beide Kommandos in einer Shell |
Kommando_1; Kommando_2
Führt die Kommandos nacheinander aus. Kommandos, die unabhängig voneinander arbeiten, lassen sich somit bequem durch Semikola getrennt angeben. Das jeweils nächste Kommando wird unverzüglich gestartet, sobald das vorherige terminierte.
Kommando_1 && Kommando_2
Führt komm2 nur aus, wenn komm1 erfolgreich war.
Kommando_1 || Kommando_2
Führt Kommando_2 nur aus, wenn Kommando_1 erfolgreich (&&) bzw. nicht erfolgreich (||) war.
Insbesondere in Shellskripten wünscht man oft die bedingte Ausführung, d.h. ein Kommando soll in Abhängigkeit vom (Miss)Erfolg eines anderen Kommandos gestartet werden.
Als Beispiel soll eine Datei mit dem Kommando rm gelöscht werden.
Damit rm allerdings keine Fehlermeldung erzeugt, soll es nur ausgeführt werden, wenn wir die Berechtigung dafür besitzen und die Datei auch existiert.
Eine Erfolgsmeldung kann auch nicht schaden... Der Aufruf könnte wie folgt formuliert werden:
test -w bla && rm bla && echo "Datei entfernt"
Vielleicht sollte die Datei»bla«zur Demonstration des Beispiels angelegt werden? Allerdings nur, wenn sie noch nicht existiert:
test -e bla || touch bla
test -w bla && rm bla && echo "Datei entfernt" Datei entfernt
Kommando &
Führt das Kommando als Hintergrundprozess aus.
Schickt man ein Kommando in den Hintergrund (indem ihm ein & nachgestellt wird), so wartet die Shell nicht mehr aktiv auf dessen Terminierung, sondern schreibt sofort wieder das Prompt aus und nimmt neue Eingaben entgegen.
Das im Hintergrund tätige Kommando ist von der Standardeingabe abgeschnitten. Einzig die Ausgaben landen weiterhin auf dem Bildschirm.
sleep 100 &
[1] 956
Die Ausgabe betrifft die Job- und die Prozessnummer des Hintergrundprozesses (1 bzw. 956).
Irgendwann wird der Prozess seine Arbeit erledigt haben, dann erscheint mit der nächsten Eingabe folgende Ausschrift:
"beliebige Eingabe" [Enter]
[1]+ Done sleep 100
Folgt dem & ein weiteres Kommando, wird dieses sofort gestartet.
Somit lassen sich Kommandos quasi parallel ausführen (zur Demonstration greifen wir auf die Verwendung von Subshells zurück):
(sleep 6; echo -n "[1] "; date) & (echo -n "[2] "; date)
[1] 14914 [2] Die Okt 17 13:58:45 MEST 2000 [1] Die Okt 17 13:58:51 MEST 2000
{ Kommando;}
Startet das Kommando innerhalb der aktuellen Shell. Sinn dieses Konstrukts ist die Gruppierung mehrerer Kommandos, sodass sie eine gemeinsame Ausgabe erzeugen:
ls -l | head -1; date > bla
insgesamt 763 cat bla Die Okt 17 18:40:40 CEST 2000 { ls -l | head -1; date;} > bla cat bla insgesamt 763 Die Okt 17 18:40:40 CEST 2000
Die erste Kommandozeile leitet nur die Ausgabe von date in die Datei »bla« um, deshalb landet die Ausgabe von »ls -l | head -1« auf der Standardausgabe. Die zweite Zeile gruppiert beide Kommandos, sodass die Ausgabe komplett in die Datei umgeleitet wird.
(Kommando)
Startet das Kommandos innerhalb einer neuen Shell. Die letzte Variante zur Eingabe einer Kommandosequenz betrifft deren Ausführung in einer Subshell.
D.h. die Bash startet als Kindprozess eine weitere Shell und übergibt dieser die auszuführenden Kommandos. Das Verhalten ähnelt stark der Gruppierung mittels geschweifter Klammern und tatsächlich lassen sich dieselben Wirkungen erzielen:
pwd; date > output
/home/user cat output Don Jun 8 09:38:23 MEST 2000 (pwd; date) > output cat output /home/user Don Jun 8 09:38:27 MEST 2000
Aus Effizienzgründen sollte, wann immer es möglich ist, auf die Subshell verzichtet werden (Erzeugen eines Prozesses kostet immer Zeit).
Notwendig kann sie allerdings werden, wenn verschiedenen Kommandos auf dieselben lokalen Variablen zugreifen. Durch Start in einer eigenen Shell sind Wechselwirkungen ausgeschlossen.
Der Rückgabewert einer Subshell ist immer »0« und die in ihr bearbeiteten Kommandos haben keinen Einfluss auf den Elternprozess.
So kann eine Subshell verwendet werden, um ein Kommando in einem anderen Verzeichnis auszuführen, ohne dass das Verzeichnis in der aktuellen Shell gewechselt wird:
pwd
/home/user (cd /usr/X11R6/bin; pwd) /usr/X11R6/bin pwd /home/user
Variablen
Im Sinne der Syntax werden skalare und Feldvariablen unterschieden.
Erzeugen von Variablen
variablen_name=wert
Variablen löschen
Hierzu dient das builtin-Kommando unset.
unset a
bash: unset: a: cannot unset: readonly variable unset b declare -p b bash: declare: b: not found
Erzeugen einer Feldvariable (Array)
feld_var_name[index]=wert
# oder feld_var_name=(wert1 wert2 wert3)
Den Inhalt einer Variable betrachten Sie mit Hilfe des Kommandos echo :
echo $variablen_name
wert echo $feld_var_name wert1 echo $feld_var_name[2] wert1[2] echo ${feld_var_name[2]} wert3
Wenn Sie auf den Inhalt einer Feldvariable zugreifen, müssen Sie durch Klammerung klar stellen, dass Sie den Inhalt eines konkreten Elementes meinen.
Ohne explizites Setzen der Klammern wird als Index implizit »0« angenommen; daraus resultiert die Ausgabe von »echo $feld_var_name[2]«.
Variable-Attribut
Hierzu verwenden Sie declare oder typeset. Beide Kommandos besitzen die selben Optionen und Wirkungen.
Vorsicht : Entgegen aller Logik setzt »-« ein Attribut und »+« löscht es!
-a | Setzt das Feldattribut einer Variable (wird einer Variable ein Feld zugewiesen, wird das Attribut automatisch gesetzt) |
[-/+] f | Zeigt eine Funktionsdefinition an/zeigt sie nicht an:
first_func() {echo "erste Funktion";} first_func erste Funktion declare -f first_func first_func() { echo "erste Funktion" } |
[-/+] i | Setzt/Löscht das Integer-Attribut. Für so deklarierte Variablen ist eine einfachere Syntax für Arithmetik erlaubt.
declare -i a=3 b=3 a=a+b b=a+b echo $a " " $b 6 a+b |
-p | Zeigt die Attribute und den Inhalt einer Variablen an.
declare -p a declare -i a="6" declare -p SHELL declare -x SHELL="/bin/bash" |
[-/+] r | Setzt das Leseattribut/verwirrt den Leser. Ist es aktiv, kann der Wert einer Variablen nicht verändert werden. Die Variable kann weder gelöscht, noch kann ein Lese-Attribut entfernt werden:
declare -r a a=nix bash: a: readonly variable declare +r a bash: declare: a: readonly variable |
[-/+] x | Setzt/Löscht das export-Attribut. Eine exportierte Variable ist auch in den Nachfahren des die Shell ausführenden Prozesses sichtbar (Siehe unter Shellvariablen) |
Gültigkeitsdauer von Variablen
Globale Shell-Variablen
Wie wir im einführenden Beispiel gesehen haben, dienen Shellvariablen dazu, das Verhalten bestimmter Programme zu beeinflussen.
Eine globale Variable ist ab der Shell ihrer Einführung sichtbar, also auch in allen daraus abgeleiteten Shells und in allen darin gestarteten Programmen. Bereits beim Systemstart werden einige Variablen global belegt. So zum Beispiel PATH, die die Suchpfade der Programme enthält oder WINDOWMANAGER, die Variable, die den Default-Windowmanager setzt. |
Datei:Grafik13.png |
Manchmal entsprechen die vom Administrator getroffenen Einstellungen nicht den Vorstellungen des Nutzers. So wird dieser die Variablen überschreiben.
Um die Variablen auch außerhalb der aktiven Shell sichtbar zu machen, muss diese möglichst in der ersten aktiven Shell gesetzt werden (da alle Programme/Shells Nachfahren der Login-Shell sind). Um den Windowmanager zu ändern und das HOME-Verzeichnis in den Suchpfad aufzunehmen, sind solche Variablen zu exportieren:
export WINDOWMANAGER=/usr/X11R6/bin/fvwm2
export PATH=$PATH:$HOME
Möchte man solche Einstellungen für alle zukünftigen Sitzungen geltend machen, trägt man obige Zeilen in eine Datei .profile in seinem Home-Verzeichnis ein.
Lokale Shell-Variablen
Für die jeweils aktive Shell lassen sich lokale Variablen vereinbaren. Solche Variablen sind einzig in dieser Shell und in den darin ausgeführten Programmen sichtbar:
var=10
echo $var 10 string="abc efg" echo $string abc efg bash echo $var exit echo $var 10
Lokale wie auch globale Shell-Variablen werden mittels unset <variable>gelöscht.
Vordefinierte Variablen
Beim Systemstart und beim Aufruf der Dateien /etc/profile (System-Voreinstellungen) und .profile (benutzereigene Voreinstellungen), die ja auch Shellskripts sind, werden bereits einige Variablen definiert.
Alle aktuell definierten Variablen können durch das Kommando set aufgelistet werden. Einige vordefinierte Variablen sind neben anderen:
Variable | Bedeutung |
HOME | Home-Directory (absoluter Pfad) |
PATH | Suchpfad für Kommandos und Skripts |
MANPATH | Suchpfad für die Manual-Seiten |
Mail-Verzeichnis | |
SHELL | Name der Shell |
LOGNAMEUSER | Login-Name des Benutzers |
PS1 | System-Prompt ($ oder #) |
PS2 | Prompt für Anforderung weiterer Eingaben (>) |
IFS | (internal field separator) Trennzeichen, meist CR, Leerzeichen und Tab) |
TZ | Zeitzone (z. B. MEZ) |
Es gibt eine Reihe von vordefinierten Variablen, deren Benutzung ein wesentlicher Bestandteil des Shell-Programmierens ist. Die wichtigsten eingebauten Shell-Variablen sind:
$! | Prozessnummer des letzten Hintergrundprozesses | kill -9 $! (Kindermord) |
$# | Anzahl der Aufrufparameter | |
$$ | Prozessnummer der aktiven Shell | kill -9 $$ (Selbstmord) |
$* | Alle Aufrufparameter | |
$? | Rückgabewert des letzten Kommandos | cat /etc/passwd ; echo $? |
$@ | Alle Aufrufparameter | |
$0 | Name des ausgeführten Shellskripts | |
$1 | 1. Element einer Liste | |
$9 | 9. Element einer Liste | |
$n | Aufrufparameter mit der Nummer n, n <= 9 | |
$- | gesetzte Shell-Optionen | echo $- |
ERRNO | Fehlernummer des letzten fehlgeschlagenen Systemaufrufs | |
OLDPWD | Vorheriges Verzeichnis (wird durch cd gesetzt) | |
PWD | Aktuelles Verzeichnis (wird durch cd gesetzt) |
- Beispiele
echo $0
-bash set {a,b,c}{1,2} echo $* a1 a2 b1 b2 c1 c2 echo $# 6 echo $4 b2
Bedingte Ausführung
Die auch als Flusskontrolle bekannten Mechanismen ermöglichen eine kontrollierte Beeinflussung des Programmablaufs. Die Bash stellt die if...fi und case...esac -Konstrukte zur Verfügung.
Erstere Form wird meist zur Unterscheidung einiger weniger Fälle (meist 2) verwendet. Die Syntax lautet:
if Liste von Kommandos; then
Liste von Kommandos [elif Liste von Kommandos; then Liste von Kommandos] [else Liste von Kommandos] fi
Von den angegebenen Zweigen werden die Kommandos höchstens eines Zweiges ausgeführt. Entweder die des ersten Zweiges, dessen Bedingung erfüllt ist oder der optionale "else"-Zweig, falls keine Bedingung erfüllt wurde.
Die Bedingung selbst ist der Rückgabewert der Liste der Kommandos (meist also der Rückgabewert des letzten Kommandos der Liste).
Das "case"-Konstrukt wird bei einer höheren Anzahl an Auswahlkriterien bevorzugt. Prinzipiell kann mittels "case" jede "if"-Bedingung abgebildet werden.
Ein wesentlicher Unterschied ist die mögliche Abarbeitung mehrerer Fälle, da alle Anweisungen ab der ersten zutreffenden Bedingung bis zu einem expliziten Verlassen des Konstrukts ausgeführt werden (d.h. ist eine Bedingung erfüllt, werden die nachfolgenden ignoriert).
case Bedingung in
Muster [ | Muster ]) Liste von Kommandos [;;] [Muster [ | Muster ]) Liste von Kommandos [;;]] esac
Die Bedingung muss ein Token sein. Die Muster unterliegen denselben Expansionen wie Pfadnamen und dürfen somit Metazeichen enthalten.
Stimmt ein Muster mit der Bedingung überein, werden alle nachfolgenden Kommandos bis zum Verlassen des Konstrukts mittels ";;" oder bis zum abschließenden "esac" ausgeführt.
Der typische Anwendungsbereich für "if"- und "case"-Konstrukte ist die Shellprogrammierung und in diesem Zusammenhang werden Ihnen noch genügend Beispiele zur Benutzung begegnen.
if test $(id | awk -F'[=(]' '{print $2}';) -eq "0"; then echo Superuser; else echo Normaler User; fi
Normaler User su - Password: root@lincln01> if test $(id | awk -F'[=(]' '{print $2}';) -eq "0"; then echo Superuser; else echo Normaler User; fi Superuser
Das (zugegeben... etwas konstruierte) Beispiel entscheidet, ob der aufrufende Benutzer als Root oder als "normaler" Nutzer arbeitet.
Die Verwendung des builtin-Kommandos test ist typisch für Bedingungen.
Schleifen
Die Bash bietet vier Typen der wiederholten Kommandoausführung. Die "for"-Schleife wird verwendet, um eine Kommandosequenz n-mal auszuführen, wobei die Anzahl vorab fest steht.
Im Unterschied dazu wiederholt die "while"-Schleife die Liste der Kommandos nur so oft, solange die angegebene Bedingung erfüllt ist. "until"-Schleifen sind genau genommen nichts anderes als "while"-Schleifen, wobei die Bedingung negiert wurde, d.h. sie wird solange durchlaufen, bis die Bedingung wahr wird. Schließlich helfen die "select"-Schleifen beim Erzeugen von Auswahlmenüs für Benutzereingaben.
Innerhalb jeder Schleife kann diese durch den Aufruf von break verlassen werden. Ein enthaltenes continue veranlasst das Überspringen nachfolgender Befehle und Fortfahren mit dem nächsten Schleifendurchlauf.
for Variable [ in Wortliste ]; do
Liste von Kommandos done
Die for-Schleife wird genau so oft durchlaufen, wie Einträge in der Wortliste stehen. Im ersten Durchlauf wird der erste Eintrag der Wortliste an Variable zugewiesen, im zweiten der zweite Eintrag usw.:
for i in a b c; do echo $i; done
a b c
Die Wortliste wird zunächst expandiert (dieselben Mechanismen wie bei einfachen Kommandos). Fehlt die Wortliste, so wird die Liste der Positionsparameter verwendet (innerhalb von Shellskripten ist dies die Liste der Kommandozeilenargumente; auf der Kommandozeile selbst ist es eine vom Kommando set erzeugte Liste):
set a b c
for i; do echo $i; done a b c
Mit Bash-Version 2.0.4 wurde die for- -Schleife um eine an die Programmiersprache C angelehnte Syntaxvariante erweitert:
for ((Ausdruck_1; Ausdruck_2; Ausdruck_3)); do...done
Bei den Ausdrücken handelt es sich um arithmische Substitutionen.* Ausdruck_1 wird üblicherweise die Zuweisung eines Anfangswertes an eine Schleifenvariable beinhalten;
- Ausdruck_2 dient als Abbruchbedingung und
- Ausdruck_3 zum Weiterschalten der Schleifenvariablen.
Ausdruck_1 wird nur einmal beim Eintritt in die Schleife ausgewertet; Ausdruck_2 wird vor jedem und Ausdruck_3 nach jedem Schleifendurchlauf neu berechnet.
for ((i=0; i<10; i++)); do echo -n "$i "; done
0 1 2 3 4 5 6 7 8 9
Den Einsatz dieser Form der for-Schleife sollten Sie nur in Betracht ziehen, wenn das Skript nicht portable sein muss oder sicher gestellt ist, dass auf jeder Zielmaschine die Bash in der aktuellsten Version installiert ist.
Beispiel 1
Beim Übersetzen von Softwarepaketen bricht der Vorgang häufig mit einer Fehlermeldung wie "_itoa.o(.text+0x50): undefined reference to `__umoddi3`" ab.
Ursache ist die Verwendung von "__umoddi3". Vermutlich wurde vergessen, eine bestimmte Bibliothek, die das Symbol enthält, hinzuzulinken.
Nun gilt es herauszufinden, welche Bibliothek notwendig ist. Die folgende Kommandozeile findet diese, sofern sie existiert:
for i in $(find /usr -name "*.a" 2>/dev/null); do nm $i 2>/dev/null | grep -sq "T __umoddi3" && echo $i; done
/usr/lib/gcc-lib/i486-linux/egcs-2.91.66/libgcc.a /usr/lib/gcc-lib/i486-linux/2.7.2.3/libgcc.a /usr/i486-glibc20-linux/lib/gcc-lib/i486-glibc20- linux/egcs-2.91.66/libgcc.a
Das Beispiel beschränkt die Suche auf statische Bibliotheken unterhalb von "/usr". nm liest aus jeder Bibliothek die enthaltenen Symbole ("nm" vermag auch die Symbole von Object-Dateien lesen), mit grep suchen wir nach dem Symbol.
Uns interessieren allerdings nur Bibliotheken, wo das Symbol definiert ist (und nicht solche, die es nur verwenden), deshalb die Suche nach "T __umoddi3" (Rufen Sie mal "nm /usr/lib/libc.a" auf und durchforsten die Ausgabe, um den Zweck zu verstehen).
Beispiel 2
Der Windows-Anwender mag die Möglichkeit vermissen, mehrere Dateien mit einer bestimmten Dateikennung zu kopieren und die Dateikennung gleichzeitig zu verändern ("copy *.bat *.bak").
Hat der Leser die Mechanismen einer Unix-Shell verstanden, sollte ihm geläufig sein, warum ein solcher Aufruf unter Unix nicht das erwartete Resultat erzielt.
Mit Hilfe einer "for-Schleife" lässt sich das COMMAND.COM-Verhalten einfach simulieren:
for i in $(ls *.bat); do cp $i ${i%.*}.bak; done
Die Generierung des Ziel-Dateinamens enthält eine Parametersubstitution.
while und until-Schleife
Die while-Schleife evaluiert die ihr unmittelbar folgenden Kommandos (zwischen "while" und "; do") und führt die Kommandos des Schleifenkörpers solange aus, wie der Rückgabestatus des letzten Kommandos der Liste gleich Null ist.
Die until-Schleife quittiert dagegen ihren Dienst, sobald das letzte Kommando der Liste den Wert Null zurück liefert:
while Bedingung do
Liste von Kommandos done until Bedingung do Liste von Kommandos done
Beispiel 1
Die nachfolgende Schleife berechnet die Werte 2n für n=1..8:
declare -i i=1; z=2; while [ $i -le 8 ]; do echo "2^$i=$z"; i=i+1; z=$((2*$z)); done
2^1=2 2^2=4 2^3=8 2^4=16 2^5=32 2^6=64 2^7=128 2^8=256
Beispiel 2
Die folgende while-Schleife zählt, wie viele Benutzer die Gruppe "100" als default-Gruppe besitzen:
(exec < /etc/passwd; IFS=":"; declare -i users=0; while read line; do set $line; test $4 -eq "100" && users=users+1 ; done; echo "Gruppe users hat $users Default-Mitglieder")
Da die Standardeingabe mittels "exec" auf die Datei /etc/passwd umgeleitet wurde, ist die letzte Eingabe, die aus dieser Quelle kommt, das EndOfFile (EOF).
Die Bash unter Linux ist allerdings (meist) so konfiguriert, dass EOF auch zum Beenden der Shell führt ([Strg]+ [D]). Um die aktuelle Shell nicht unbeabsichtigt ins Nirwana zu delegieren, wurde die Ausführung in einer Subshell vorgenommen.
Der Internal Field Separator (IFS) beinhaltet die Trennzeichen, anhand derer die Bash die Eingaben in einzelne Worte zerlegt. Normalerweise sind diese die Whitespaces; wir benötigen aber den Doppelpunkt, da jener die Felder der Passwortdatei aufteilt.
Innerhalb der "while-Schleife" lesen wir jeweils eine Zeile in "line" ein (mittels read) und erzeugen eine Parameterliste (set $line).
In dieser interessiert der vierte Parameter (Gruppeneintrag). Ist dieser "100", wird die Variable "users" hoch gezählt.
Näheres zu "IFS", "read" und "set" im Abschnitt Initialisierungen.
Die Komplexität obiger Anwendungen von "while" zeigt auf, dass die Verwendung von Schleifen weniger auf der Kommandozeile verbreitet ist, sondern zu wichtigen Bestandteilen in Shellskripten zählt.
select
select hat mit dem klassischen Schleifenkonzept nichts gemein; sie schreibt in einer Endlosschleife eine Liste von Auswahlalternativen auf die Standardfehlerausgabe und wartet auf eine Interaktion mit dem Benutzer.
select Name [ in Liste ] do
Liste von Kommandos done
Das Prinzip lässt sich am einfachsten anhand eines Beispiels demonstrieren. In einem Menü sollen die Dateien des aktuellen Verzeichnisses präsentiert werden.
Zur vom Benutzer ausgewählten Datei werden weitere Informationen ausgegeben; außerdem soll die Beendigung der Schleife angeboten werden:
select file in * Ende; do
> if test -e $file; then > ls -l $file; > else > break; > fi; >done 1)./index.html 5)./help.txt 9)./symbol9.html 13)./pfad.gif 2)./symbol6.html 6)./symbol5.html 10)./symbol2.html 14) Ende 3)./foo.tgz 7)./symbol3.html 11)./symbol4.html 4)./symbol8.html 8)./symbol7.html 12)./symbol1.html #? 3 -rw-r--r-- 1 user users 3102 Aug 17 10:49 foo.tgz 1)./index.html 5)./help.txt 9)./symbol9.html 13)./pfad.gif 2)./symbol6.html 6)./symbol5.html 10)./symbol2.html 14) Ende 3)./foo.tgz 7)./symbol3.html 11)./symbol4.html 4)./symbol8.html 8)./symbol7.html 12)./symbol1.html #? 14
Die Liste in "select" wird allen Expansionen (wie bei einfachen Kommandos) unterzogen. Diese Liste wird allerdings nur beim ersten Eintritt in die Schleife generiert. Eine Änderung dieser wird also im Menü nicht sichtbar.
Verschachtelte Schleifen
Angenommen, das letzte Beispiel sollte dahin gehend modifiziert werden, dass die erwählte Datei gelöscht werden soll. Das Problem ist nun, dass diese
Datei im nachfolgenden Menüaufrufs noch immer gelistet wird. Eine Lösung wäre die Kapselung des select-Aufrufs in einer umgebenden "while-Schleife".
Die "select"-Auswahl wird nun nach jedem Durchlauf verlassen, sodass das Menü erneut aufgebaut wird. Um nun beide Schleifen zu verlassen, ist die zu verlassenden Schleifentiefe dem "break"-Aufruf mitzugeben:
while :; do
> select file in * Ende; do > if test -e $file; then > rm $file; > break; > else > break 2; > fi; > done > done 1)./index.html 5)./help.txt 9)./symbol9.html 13)./pfad.gif 2)./symbol6.html 6)./symbol5.html 10)./symbol2.html 14) Ende 3)./foo.tgz 7)./symbol3.html 11)./symbol4.html 4)./symbol8.html 8)./symbol7.html 12)./symbol1.html #? 3 1)./index.html 5)./symbol5.html 9)./symbol2.html 13) Ende 2)./symbol6.html 6)./symbol3.html 10)./symbol4.html 3)./symbol8.html 7)./symbol7.html 11)./symbol1.html 4)./symbol8.html 8)./symbol9.html 12)./pfad.gif #? 13