Bash/Syntax

Aus Foxwiki

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)

  1. Alle nicht-markierten Worte werden expandiert (siehe nachfolgend)
  2. Ein- und Ausgabe-Umleitungen werden vorbereitet (indem die entsprechenden Dateideskriptoren geschlossen/geöffnet werden)
  3. 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.

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

Vorlage:Clear

Datei:Grafik21.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

Datei:Grafik9.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 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