minpic01.jpg

Dr. Oliver Diedrich

Mit flinken Fingern

Die Power der Kommandozeile

Auch mit einem grafischen Desktop wie Gnome oder KDE bleibt die Tastatur das wichtigste Werkzeug des Linuxers. Dabei setzt eine Shell die Eingaben des Benutzers in entsprechende Aktionen um. Neben zahllosen Komfortfunktionen enthalten Linux-Shells auch eine vollständige Programmiersprache.

Jeder Linux-Benutzer kommt irgendwann mit der Shell in Berührung - sei es auf der Konsole, sei es im xterm oder einem anderen Terminalprogramm unter X11. Und jeder Linux-Fan schätzt die kleinen Freuden der Arbeit mit der Shell, etwa die Kommando-History oder die wundervolle Tab-Completion: Aus

xem TAB /u TAB d TAB h TAB HO TAB 
etwa macht die Shell die Zeile `xemacs /usr/doc/howto/HOWTO-INDEX´. Schneller bewegt man sich mit keinem Dateimanager durch den Verzeichnisbaum.

Auch die Kommando-History bietet mehr als das altbekannte Blättern durch die zuletzt eingegebenen Kommandos mit Hilfe der Cursortasten: Mit der Tastenkombination Ctrl-R kann man inkrementell nach Einträgen in der History suchen. Die Eingabe `Ctrl-R less´ etwa zeigt das letzte eingegebene less-Kommando ein; weiteres Drücken von Ctrl-R blättert rückwärts durch alle in der History gespeicherten Kommandos, die den String `less´ enthalten.

Eine für alles

Standard-Shell unter Linux ist die bash, eine Erweiterung der klassischen Bourne-Shell sh. Weitere verbreitete Shells sind die C-Shell tcsh und die Korn-Shell pdksh. Leider unterscheiden sich die verschiedenen Shells sowohl in Bedienung und Befehlen als auch in der Skriptsprache; daher ist man im allgemeinen gut beraten, bei der bash zu bleiben. Auch im folgenden wird es um die bash gehen.

Wahre Power entfaltet die Shell erst zusammen mit den zahllosen Hilfsprogrammen für die Kommandozeile - die Shell kann kaum eine Aufgabe selbst erledigen. Unter Unix-artigen Betriebssystemen ist die Logik solcher Tools immer sehr ähnlich. Die Programme kommunizieren über drei Kanäle mit der Außenwelt: Sie lesen Daten von der Standardeingabe (STDIN) oder aus einer Datei; die Programmausgaben landen auf der Standardausgabe (STDOUT), Fehlermeldungen werden auf die Fehlerausgabe (STDERR) geschrieben. Zum Umleiten von Ein- und Ausgaben stehen < und > zur Verfügung; dabei werden STDOUT (1) und STDERR (2) getrennt behandelt:

Befehl < Eingabe > Ausgabe 2> Fehler 
Auf diese Art läßt sich der Output eines Kommandos in eine Datei umleiten, die man beispielsweise als Eingabe eines anderen Programms verwenden kann. >> hängt die Ausgabe an eine bestehende Datei an.

Das Pipe-Zeichen | leitet die Ausgabe eines Kommandos direkt an ein anderes Programm weiter und erspart so den Umweg über eine Ausgabeumleitung in eine temporäre Datei. Mit Ein- und Ausgabeumleitungen sowie Piping lassen sich mehrere einfache Tools zu mächtigen Werkzeugen kombinieren:

ls|grep x|sort|tee x-files|more 
filtert aus der Liste der Dateien im aktuellen Verzeichnis alle Dateinamen mit `x´ aus und sortiert sie alphabetisch. tee schreibt diese Liste in die Datei `x-files´ und läßt sie gleichzeitig auf dem Bildschirm erscheinen, wo der Pager more für eine seitenweise Ausgabe sorgt.

Allerdings verbindet | nur STDOUT und STDIN; der Fehlerkanal bleibt außen vor. Pager wie less oder more, über | an ein Kommando angehängt, arbeiten daher nur auf der Standardausgabe. Bei einem Programm wie dem C-Compiler, der Fehler im Quelltext auf STDERR ausgibt, kann das sehr lästig sein, wenn Hunderte von Fehlermeldungen über den Bildschirm huschen, ohne daß sie sich durch einen Pager bändigen lassen. Doch auch hier ist Abhilfe möglich:

cc quelle.c 2>&1|more 
leitet zunächst STDERR auf STDOUT um und schickt die Standardausgabe dann an den Pager.

Optionen, denen unter Linux ein - vorangestellt ist, beeinflussen die Arbeitsweise der Kommandozeilenprogramme: wc beispielsweise zählt die Anzahl der Wörter, wc -l die Anzahl der Zeilen in der Eingabe. Dabei lassen sich mehrere solche Schalter zusammenfassen: In

sort -br unsortiert > sortiert 
bewirkt die Option `-b´, daß sort führende Leerzeichen beim Sortieren ignoriert; `-r´ läßt das Programm in umgekehrter Reihenfolge sortieren.

Muster gesucht

Über `pattern matching´ hilft die Shell dabei, mehrere Dateien zur Bearbeitung durch ein Kommando auszuwählen. Wichtig für DOS-Umsteiger: Der . ist unter Linux ein ganz normaler Bestandteil von Dateinamen und hat keine besondere Bedeutung. rm * löscht alle Dateien - egal, ob mit oder ohne Punkt und `Extension´ im Dateinamen.

Während * eine beliebige Zeichenfolge ersetzt, steht ? für ein einzelnes Zeichen: `*x?´ paßt auf alle Dateien, deren vorletzter Buchstabe ein `x´ ist. In eckigen Klammern kann man alternative Zeichen oder einen Zeichenbereich angeben:

ls *[tg]if 
listet alle tif- und gif-Dateien;

rm [0-9]*.html
löscht alle HTML-Files, die mit einer Zahl beginnen.

Unter Linux nimmt die Shell das `pattern matching´ selbst vor und ersetzt beispielsweise einen * durch die Namen aller Dateien im aktuellen Verzeichnis. Daher muß man derartige Sonderzeichen `quoten´, wenn man sie als Argument an ein Programm übergeben möchte. Einzelne Sonderzeichen lassen sich einen vorangestellten Backslash vor der Interpretation durch die Shell schützen; bei mehreren Sonderzeichen in einem String schließt man den ganzen String in doppelte Anführungszeichen ein. Um beispielsweise nach * in einer Datei zu suchen, verwendet man

grep \* dateiname
Die doppelten Anführungszeichen (") sorgen auch dafür, daß Strings mit Leerzeichen als ein Argument aufgefaßt werden; üblicherweise betrachtet die Shell Leerzeichen als Trennzeichen zwischen mehreren Argumenten:

grep eins zwei drei
sucht in den Dateien zwei und drei nach dem String `eins´,

grep "eins zwei" drei
sucht nach der Zeichenkette `eins zwei´ in der Datei drei. Mittels Quoting lassen sich auch Dateien behandeln, deren Namen Sonderzeichen oder einen Blank enthalten.

Wörtlich genommen

Noch stärker ist die `Quoting´-Wirkung der einfachen Hochkommata ('): Zwischen zwei ' bleiben selbst Variablennamen erhalten, die die Shell ansonsten (auch zwischen Anführungszeichen) durch den Wert der jeweiligen Variable ersetzt. Die Anweisung

echo $PWD; echo "$PWD"; echo ´$PWD´
gibt zweimal das aktuelle Verzeichnis aus, gefolgt von dem String $PWD. Der Strichpunkt trennt mehrere Anweisungen auf einer Eingabezeile.

minpic02.jpg

Quoting erlaubt den Umgang mit Dateien, deren Namen Sonderzeichen enthalten.

Die Backticks (`) haben eine ganz andere Bedeutung: Die Shell ersetzt ein Kommando zwischen zwei ` durch die Ausgabe dieses Kommandos.

cat `which startx` .
zeigt den Inhalt der Datei startx an, da `which startx` durch die Ausgabe dieses Kommandos - nämlich den kompletten Pfad von startx - ersetzt wird. Ohne die Backticks erhielte man lediglich zweimal die Fehlermeldung `No such file or directory´ - einmal für which, einmal für startx.

Auf diese Art lassen sich komplexe Aufgaben recht einfach erledigen: Angenommen, Sie wollen alle Dateien mit der Endung `.txt´ in den verschiedenen Verzeichnissen unterhalb Ihres Home-Verzeichnisses in eine Archiv-Datei sichern. find erstellt eine Liste dieser Dateien:

find ~ -name "*.txt"
find ist eines der wenigen Programme, das selbst mit Wildcards als Parametern umgehen kann; daher muß man durch Quoting verhindern, daß die Shell das `*.txt´ selbst interpretiert. Diese Liste kann man nun, komfortablerweise noch alphabetisch sortiert, an das tar-Kommando übergeben:

tar cf txt.tar `find ~ -name "*.txt" | sort`
Möchte man hingegen mit jeder Datei der find-Liste einzeln die gleiche Aktion ausführen, bietet find dazu die Option `-exec´:

find ~ -name "*.txt" -exec cp "{}" /save \;
kopiert alle txt-Dateien in das Verzeichnis /save. Der Ausdruck {} wird dabei durch den jeweils aktuellen Dateinamen ersetzt; der Strichpunkt beendet das Kommando des exec-Arguments. Da Strichpunkt und geschweifte Klammern für die Shell eine besondere Bedeutung haben, muß man sie innerhalb des find-Kommandos quoten.

Vorarbeiter

Eine wichtige Aufgabe der Shell ist die `job control´. Die Shell kann nicht nur Programme starten, sondern auch Prozesse in den Hintergrund schicken, wieder `nach vorne´ bringen, ihnen Signale schicken oder sie beenden.

Um ein Programm als Hintergrundprozeß zu starten, muß man lediglich ein & an den Programmnamen anhängen; schon steht die Shell nach dem Start des Programmes für andere Aufgaben zur Verfügung. Sinn macht das natürlich nur bei Programmen, die nicht von STDIN lesen und nach STDOUT schreiben (etwa X11-Programme), oder wenn man beim Aufruf selbst für eine Input- und Output-Umleitung sorgt.

Natürlich kann man Hintergrundprozesse auch wieder loswerden: ps zeigt die laufenden Prozesse mit ihrer Prozeßnummer (PID) an. kill PID beendet einen Prozeß, indem es ihm das TERM-Signal schickt; ein kill -9 PID sollte auch renitente Programme mit dem KILL-Signal `abschießen´, die nicht auf das TERM-Signal reagieren - etwa, weil sie abgestürzt sind. Allerdings sollte man auch nur dann zu kill -9 greifen, da dieses Signal den Programmen keine Möglichkeit läßt, sich regulär zu beenden und etwa Daten zu sichern oder Temporärfiles zu beseitigen.

Mit kill lassen sich auch andere Signale an Hintergrund-Prozesse schicken (`man 7 signal´ zeigt alle Signale an): kill -STOP beispielsweise unterbricht ein Programm, kill -CONT läßt es wieder weiterlaufen. Bei anderen Signalen bleibt es dem jeweiligen Programm überlassen, was es damit anfängt. Viele Daemons lassen sich so über Signale steuern: Der Printserver lpd etwa liest nach einem HUP-Signal die Konfigurationsdatei /etc/printcap neu ein.

Mit der Tastenkombination Ctrl-Z kann man das im Vordergrund laufende Programm anhalten und in den Hintergrund schicken. Die Shell steht nun für andere Aufgaben bereit; das Kommando fg holt den schlafenden Prozeß wieder in den Vordergrund. bg weist einen über Ctrl-Z angehaltenen Prozeß an, im Hintergrund weiterzulaufen. Ist die Shell etwa durch ein versehentlich ohne angehängtes & gestartetes Programm blockiert, hält Ctrl-Z den Prozeß an und macht die Shell frei; ein anschließendes bg sorgt dann dafür, daß das Programm im Hintergrund weiterläuft.

Variabel

Umgebungsvariablen bestimmen eine Vielzahl von Eigenschaften der bash. Das Kommando env zeigt die Namen und Werte aller gesetzten Variablen an. Mit $VARIABLE greift man auf den Inhalt einer Variablen zu.

echo $PS1 
beispielsweise gibt den Kommandoprompt aus; mit

export PS1="\u@\h [\w] $ "
erhält man einen Prompt der Form `username@hostname [aktueller Pfad] $ ´. export weist die Shell an, die deklarierte Variable auch an Kind-Prozesse - etwa weitere Shells - weiterzugeben; ohne export gilt die Variable nur in der aktuellen Shell. Diese Unterscheidung ist vor allem bei Shell-Skripten von Bedeutung: Variablen, die in einem Skript lediglich deklariert, aber nicht exportiert werden, sind auch nur in diesem Skript gültig.

Gerade, wenn man in einem Skript Umgebungsvariablen setzen möchte, stolpert man auch leicht in eine andere Falle. Skripte werden üblicherweise in einer eigenen Shell ausgeführt; die Zeile

#!/bin/sh
zu Beginn eines Skriptes legt fest, welches Programm (hier /bin/sh, unter Linux normalerweise ein Link auf /bin/bash) die nachfolgenden Zeilen ausführen soll. Steht dort beispielsweise `#!/usr/bin/perl´, wird das Skript an den Perl-Interpreter zur Bearbeitung weitergereicht.

Variablen, die ein Shell-Skript exportiert, gelten nur für die ausführende Shell und deren Kindprozesse, nicht aber für die `höhere Instanz´ der Shell, die das Skript startet. Soll die Shell, aus der das Skript gestartet wird, auch die Anweisungen des Skriptes ausführen, muß man das Skript durch einen vorangestellten Punkt `sourcen´:

$ cat script
#!/bin/sh
export X="Satz mit x"
echo $X
$ ./script
Satz mit x
$ echo $X
$ . ./script
Satz mit x
$ echo $X
Satz mit x
Vordefinierte Variablen erlauben den Zugriff auf die Parameter, mit denen ein Skript aufgerufen wurde. $0 ist der Name des Skriptes selbst, $# die Anzahl der Argumente; $1, $2 und so weiter bezeichnen die einzelnen Argumente, $* enthält eine Liste aller Argumente. Über $$ läßt sich auf die PID zugreifen - nützlich beispielsweise zur eindeutigen Benennung von Temporärdateien. Das folgende Skript gibt in der for-Schleife alle Argumente aus:

#!/bin/sh
echo "$0 (PID $$) mit $# Argumenten:"
for I in $* ; do echo $I; done
Mit dieser `for I in $*´-Konstruktion lassen sich Skripte erstellen, die alle beim Aufruf eines Skriptes als Argumente angegebenen Dateien bearbeiten. Das funktioniert auch bei einem Skript-Aufruf mit Wildcards: Schließlich kümmert sich die Shell darum, ein Wildcard-Muster durch die Namen aller passenden Dateien zu ersetzen.

Unter falschem Namen

Für kleinere Aufgaben braucht man allerdings gar keine Skripte zu programmieren, sondern kann auf Aliases und Funktionen zurückgreifen:

alias mv=´mv -i´
beispielsweise sorgt dafür, daß jedes mv mit dem Parameter `-i´ aufgerufen wird. So erfolgt jedesmal eine Rückfrage, wenn man eine Datei umbenennen oder verschieben will und bereits eine Datei mit dem neuen Namen existiert - ohne `-i´ würde mv eine solche Datei ohne Warnung überschreiben. alias ohne Argumente zeigt die bereits definierten Aliases an.

Während ein Alias einfach einen String auf der Kommandozeile durch einen anderen ersetzt, können Funktionen auch Argumente entgegennehmen und so dieselben Aufgaben wie ein kleines Shell-Skript übernehmen: Nach

function find_tgz { tar tzf $1|grep $2 ; }
überprüft `find_tgz archiv.tgz datei´, ob das tgz-Archiv eine bestimmte Datei enthält. Es bietet sich an, Aliases und Funktionen in der Datei /etc/profile (für alle Anwender des Systems) oder in der eigenen ~/.bashrc oder ~/.profile zu definieren.

Mehrere Kommandos lassen sich auf verschiedene Art miteinander verbinden. Sollen die einzelnen Programme nacheinander ausgeführt werden, trennt man die Aufrufe durch einen Strichpunkt. Verbindet man zwei Befehle mit logischem Und (&&), wird der zweite nur ausgeführt, wenn der erste erfolgreich war - in der Unix-Welt bedeutet das einen Exit-Status von 0. || wirkt als exklusives Oder; der zweite Befehl wird nur ausgeführt, wenn der erste über einen Exit-Status ungleich null einen Fehler bei der Ausführung meldet. Diese Verbindungen funktionieren auch auf der Kommandozeile:

$ test -e foo && echo "ok"
gibt `ok´ aus, wenn die Datei foo existiert. Bei || statt && wird echo nur ausgeführt, wenn foo nicht existiert;

$ test -e foo ; echo "ok"
gibt immer `ok´ aus, egal, wie der Test ausfällt. Bei der Deklaration einer Funktion ist es nicht nötig, mehrere Programmaufrufe in eine lange Zeile mit vielen Strichpunkten zu packen: Die Shell erkennt, wenn man Return drückt, ohne die Funktionsdefinition abgeschlossen zu haben, und fordert mit einem Fortsetzungsprompt ($PS2) weitere Zeilen an. Auch hier funktionieren die Editierhilfen der Shell wie Command-History oder Tab-Completion.

Streng getestet

test kann nicht nur mit `-e´ auf Existenz einer Datei testen, sondern auch, ob es sich um eine normale Datei (-f), ein Verzeichnis (-d) oder einen symbolischen Link handelt (-L). Ebenso läßt sich überprüfen, ob eine Datei ausführbar (-x), lesbar (-r) und beschreibbar (-w) ist oder ob sich das Datum von zwei Dateien unterscheidet. Die folgende Anweisung (via Fortsetzungsprompt über mehrere Zeilen eingegeben) kopiert /etc/profile nur dann nach /save, wenn in /save keine neuere Datei mit dem Namen profile existiert:

$ if test /save/profile -nt /etc/profile
> then echo "/save/profile ist neuer!"
> else cp /etc/profile /save/
> fi
Das Schlüsselwort `fi´ (einfach `if´ rückwärts) beendet eine `if´-Konstruktion; weitere Tests lassen sich mit `elif´ (für `else if´) einfügen. Statt `if test ...; then ...; fi´ kann man auch eine einfachere Formulierung wählen, die sich vor allem für Skripte anbietet: Die Zeile

[  -r foo  ] || exit 1
beendet ein Skript mit einem Fehlercode, falls die Datei foo - etwa die Konfigurationsdatei eines zu startenden Programms - nicht lesbar ist.

minpic03.jpg

Der Fortsetzungsprompt erleichtert die Eingabe komplexer Anweisungen.

Außerdem kann test zwei Strings auf Gleichheit (`test str1 = str2´) und Ungleichheit testen (`test str1 != str2´) und numerische Argumente über die Operatoren -eq (gleich), -ne (ungleich), -gt (größer als) und -lt (kleiner als) vergleichen. Weitere Optionen zeigt die man-page zu test.

Wie in jeder ordentlichen Programmiersprache steht natürlich auch eine `case´-Anweisung bereit, um sich nicht in ewig langen `if-elif´-Konstruktionen zu verlieren:

case $VAR in
start) echo "wir starten ..."
start-it
;;
stop) echo "... und Schluß"
stop-it
;;
*) echo "ungültiges Argument $VAR"
;;
esac
Je nach dem Inhalt von $VAR startet das Skript eines der Programme `start-it´ und `stop-it´ oder gibt eine Fehlermeldung aus.

Zählen und rechnen

Zahlen und numerische Variablen liegen Shell-Skripten nicht so - die Skriptsprache ist eher auf den Umgang mit Strings angelegt. Möchte man beispielsweise mehrere hundert .gif-Dateien in einem Verzeichnis nach `001.gif´, `002.gif´ und so weiter umbenennen, sind kleinere Klimmzüge nötig, um die dreistelligen Namen mit führenden Nullen zu erzeugen:

I=1
for FILE in *.gif
do
  if test $I -ge 100
    then mv $FILE $I.gif
  elif test $I -ge 10
    then mv $FILE 0$I.gif
  else mv $FILE 00$I.gif
  fi
  I=$((I+1))
done
`Numerische´ Schleifen der Art (for i=0; i<n; i++) sind in Shell-Skripten gar nicht möglich; statt dessen muß man sich mit einer while-Schleife behelfen:

i=0
while [ $i -lt $n ] ; do
...
i=$((i+1))
done
Wer die Möglichkeiten der Shell-Programmierung im Detail kennenlernen möchte, findet in der man-page zur bash eine systematische Einführung. Hier sind auch eine Reihe weiterer eingebauter Shell-Kommandos beschrieben, die den Umgang mit der Shell erleichtern. Für Shell-Programmierer besonders interessant ist das Kommando `set -o Option´, mit dem man die Shell anweist, das Skript nicht auszuführen, sondern nur auf Syntaxfehler zu checken (Option `noexec´) oder das gerade ausgeführte Kommando auszugeben (`verbose´ und `xtrace´).

Ebenfalls hilfreich ist das Studium fremder Skripte - sowohl die Konfigurations- und Installations-Skripte von Softwarepaketen als auch die Skripte der eigenen Distribution (etwa die Init-Skripte) verraten eine ganze Menge Tricks und zeigen, was mit Shell-Skripten alles möglich ist. Man darf sich nur nicht durch die kryptische Skript-Syntax abschrecken lassen ... (odi)