Shell Programmierung

Eine praktische Anleitung

Die Bourne-Shell /bin/sh

erste Auflage

Juli 2003

Sophie Yaëlle Schlichting
Creative Commons License
Für diese Arbeit gelten folgende Lizenzbestimmungen Creative Commons Attribution-NonCommercial-NoDerivs 2.5 License.

Ich vertrete die Auffassung, dass für die Nutzung dieses Textes keine Kommerzielle Verwendung nötig ist. Sie können diesen Text ja lesen und was davon lernen. Das alleine ist ja keine kommerzielle Nutzung.


Inhaltsverzeichnis


Anleitung zur Shell-Programmierung mit der Bourne-Shell /bin/sh

Copyright (C) 2003, 2004, 2005, 2006 Sophie Yaëlle Schlichting
Veröffentlicht durch: Sophie Yaëlle Schlichting
Binderweg 2, D-83084 Reischenhart
Im Sträler 25, CH-8047 Zürich

Für die Richtigkeit, b.z.w. Fehlerfreiheit dieses Kursmaterials übernehme ich keinerlei Garantie.

Vorwort

Im Moment ist dieser Text noch sehr am Anfang. Mit anderen Worten, es werden gerade die ersten Zeilen geschrieben. Das bedeutet natürlich auch, daß dieser Text zunächst unvollständig und in Teilen sicher auch fehlerhaft ist.

Dieser Kurs richtet sich an Shell-Programmierer, die wenig oder keine Erfahrung mit Softwareentwicklung haben. Ich selbst habe sehr lange gebraucht, bis ich die Vorteile der Programmierung mit Shell-Scripte nutzen konnte. Gleichwohl nutze ich Shell-Scripte nach wie vor nur wenig. Wenn Softwareentwicklung ansteht, dann suche ich mir erst mal eine geeignete Sprache. Hierbei müssen viele einzelne Punkte gegeneinander abgewogen werden. Wenn ich dann zur Shell-Programmierung greife, dann aus den folgenden Gründen:

Der eigentliche Vorteil in der Shell-Programmierung liegt darin, daß man Arbeitsschritte automatisieren kann. Viele Anwender verfluchen die Shell geradezu und verlangen mit Nachdruck nach graphischen Benutzerumgebungen. Diese sind durchaus Augenfreundlich, auch wenn sie selten wirklich ergonomisch sind (Aqua und Gnome sind Ausnahmen und bestätigen so die Regel).

Es ist jedoch ungeheuer mühsam viele Male mit der Maus die selbe Aktion auszuführen. Mit einem guten Script kann man solche Tätigkeiten aus der Welt schaffen. Dadurch wird das saure Anwenderleben wesentlich einfacher. Die Aufgabe von Entwicklern und Systemadministratoren ist es schließlich das Leben von Computer-Anwendern einfacher zu machen und nicht die täglichen Probleme mit sinnlosen Gimmicks zu übertünchen.

Es gibt weitere Gründe für die Anwendung von Script-Sprachen. Eine ist die Automatisierung von fehlerträchtigen Aufgaben. Aufgaben, die eine gewisse Komplexität überschritten haben aber nur selten anfallen, werden sehr häufig nur fehlerhaft erledigt oder nehmen sehr viel Zeit in Anspruch. Hier kann ein gutes Shell-Script die Produktivität deutlich erhöhen.

Scripte machen zudem die durchgeführten Arbeitsschritte transparent und wiederholbar. Wer jemals unter Windows mit der Maus 50 mal hintereinander dasselbe gemacht hat, weiß nach dem 10ten Mal nicht mehr genau ob der vorletzte Schritt richtig war. Die Zweifel verlassen einen nie.

Scripte machen stupide Arbeiten überflüssig und schaffen Sicherheit.

Dieser Kurs hangelt sich an dem Thema "Backup" entlang. Er beginnt aber mit einfachen Betrachtungen zur Shell-Programmierung und zur Programmierung im Allgemeinen.

Ich hoffe, daß dieser Kurs nicht allzu trocken daher kommt und Sie viel Spaß beim Durcharbeiten haben. Ich werde diesen Kurs mit möglichst vielen funktionierenden Beispielen ausstatten, so daß Sie am Ende des Kurses über einen gut gefüllten Werkzeugkasten für die Shell-Programmierung verfügen. Die einzelnen Beispiele stammen entweder aus meiner Feder oder aus dem USENET. Ich betrachte die Beispiele und die Art sie zu programmieren als Allgemeingut. Ebenso wie die Inhalte des USENET dienen sie dazu Kolleginnen und Kollegen weiter zu bringen, Ihnen die Arbeit zu erleichtern und Ihnen Lösungen und nicht neue Probleme zu präsentieren.

Ich gehe davon aus, daß alle hier präsentierten Inhalte bereits im USENET veröffentlicht waren. Diese Veröffentlichungen liegen teilweise lange zurück. Ich gehe weiterhin davon aus, daß niemand in der Lage ist aufgrund eigener Arbeiten ein Patent oder ein Copyright auf die hier beschriebenen Techniken, Scripte etc geltend machen kann, da die betroffenen Inhalte bereits seit langem allgemein bekannt sind. Sollte dies von irgend einem Gericht anders gesehen werden, erhebe ich urheberrechtlichen Anspruch auf das Rad, das Evangelium und auf die Brille.

Die Programm-Beispiele, die in diesem Kurs gezeigt werden, wurden unter HP-UX 11.00, FreeBSD und Linux getestet. Zwischen diesen Betriebssystemen b.z.w. den dazugehörigen Rechnerarchitekturen bestehen Unterschiede, die, soweit sie zum Tragen kommen, in diesem Kurs auch behandelt werden.

Weiterhin plane ich die Beispiele sobald wie möglich auch unter IRIX 6.5 und unter MinGW zu testen und den Kurs entsprechend anzupassen.

Einleitung

Shell-Programmierung gehört zunächst zur imperativen Programmierung. Dabei wird ein Shell-Script als Automat betrachtet. Durch das Ausführen von Kommandos ändert dieser Automat seinen Zustand. Diese über die Zeitachse betrachtete Verkettung von Kommandos und Zustandsänderungen bezeichnet man als Zustandsänderungsfolgen.

Eine Shell ist zunächst ein "Command execution Interface"; zu Deutsch, eine Schnittstelle, über die man Kommandos ausführen lassen kann. Für solche Interfaces existiert mit POSIX 1003.2 auch eine Norm. Wer diese Norm einhält, hat gute Chancen, daß sein Shell-Script auf allen möglichen Computern lauffähig sein wird. Unter MS-Windows ist das natürlich mal wieder ein anderes Kapitel. Direkt unter Windows, also auf der DOS-Shell, wird man solche Shell-Scripte nicht zum Laufen kriegen. Mit der kostenlosen MinGW Erweiterung bekommt man MS-Windows soweit POSIX-Compliant und das auch noch in besserer Qualität als mit dem kommerziellen MKS-Toolkit.

Nachdem WINE erfolgreich installiert ist, kann man als nächstes MinGW im gefälschten Windows installieren. Ich mache das so, weil ich bei der Qualität, die man mit WINE geboten bekommt nicht mehr einsehe, warum ich extra Windows installieren soll. Fehlerhaft ist beides. Und nachdem ich inzwischen auch MS-Office, Visio und alle möglichen anderen Programme mit WINE laufen lassen kann, sehe ich nicht mehr ein, warum ich noch Windows betreiben sollte.

In allen Programmiersprachen werden auch Werte gespeichert, die den Zustand des Automaten für einen bestimmten Zeitpunkt beschreiben. Die Speicherung dieser Werte kann auf die unterschiedlichsten Arten erfolgen. In der Shell-Programmierung erfolgt dies überwiegend mit der Hilfe sog. Variablen. Das Wort "Variable" bezeichnet die Relation "Variablen-Name"<-> "zugehörige Speicherstelle" <-> "Art des gespeicherten Wertes". D.h. wir speichern einen Wert immer an einer entsprechenden Speicherstelle im Computer. Hierzu müssen wir uns immer darüber im Klaren sein, was für einen Wert wir so abspeichern. Ist es ein im weiteren Sinne druckbares Zeichen oder ist es eine Zahl? Und wenn es eine Zahl ist, was für eine? Eine natürliche Zahl? Eine ganze Zahl? Eine rationale Zahl oder vielleicht eine relle Zahl? ...

Die Frage nach der Natur der Werte, die wir an unterschiedlichen Stellen speichern ist wesentlich für die Qualität der Programme. Sie ist ebenso wichtig wie Syntax und Semantik. Alle drei Aspekte der Programmierung

sind gleichermaßen wichtig bei der Erstellung von Anwendungen. Der bloße Umstand, daß man für Syntaxfehler eine Fehlermeldung bekommt, bedeutet nicht, daß die Syntax wichtiger sei als die beiden anderen Aspekte. Die Shell b.z.w. ein Interpreter oder ein Compiler sind nun mal nicht in der Lage Sinn und Unsinn voneinander zu unterscheiden. Das macht die semantisch korrekte Entwicklung eben so schwierig.

Erste Schritte

Das Kapitel "Erste Schritte" behandelt die wichtigsten Elemente der Shell-Programmierung. Dies sind zuerst natürlich die Variablen. Gefolgt von Strukturanweisungen für Fallunterscheidungen und für Schleifen.

Nachfolgend wird davon ausgegangen, daß Sie es geschaft haben auf Ihrer graphischen Benutzerumgebung ein Fenster mit einer Terminalemulation zu öffnen. Weiterhin müssen Sie alle folgenden Schritte unter der Umgebung des Programms "sh" ausführen. Zu diesem Zweck geben Sie bitte zunächst das folgende Kommando in der Kommandozeile ein:

$ which sh
/bin/sh
$
Das Kommando which findet den Pfad im Verzeichnisbaum, in dem das Programm sh zu finden ist. Das geht aber nur, wenn die PATH-Variable richtig gesetzt ist. Die PATH-Variable wird üblicherweise mit Hilfe von /etc/profile b.z.w. .profile gesetzt. Bei Bedarf wird sie auch ad hoc auf der Kommandozeile gesetzt. Beim setzen von Variablen wie PATH gehen wir immer so vor, daß wir ihren bisherigen Inhalt nicht löschen. Dies geschieht indem man den bisherigen Inhalt der Variablen mit einem neuen Wert Konkateniert und das Ergebnis in die Variable zurückspeichert. Die einzelnen Werte die zusammen in der PATH-Variablen konkateniert wurden, sind mit dem :-Zeichen als Delimiter (Trennzeichen) voneinander getrennt. Das sieht dann z. B. so aus:

PATH=$PATH:/usr/local/bin:/usr/bin:/usr/X11R6/bin:/bin
PATH=$PATH:/usr/local/pgsql/bin:/opt/dce/bin
export PATH

echo $PATH
/usr/local/bin:/usr/bin:/usr/X11R6/bin:/bin:/usr/local/pgsql/bin:/opt/dce/bin
 

Geben Sie nun das Kommando sh in die Kommandozeile ein:

$ sh
$
Achten Sie dabei darauf, daß die POSIX Umgebung case sensitive ist.Das heißt, daß Sie auf Groß- und Kleinschreibung achten müssen. sh ist also etwas anderes als SH.

Das Starten der Shell sh war noch das kleinere Problem. Jetzt geht's los. Ab diesem Moment werden alle Kommandos, die wir eingeben, von der Bourne-Shell interpretiert. Das ist notwendig, da nach dem Login die Shell ausgeführt wird, die in /etc/passwd für einen Benutzer eingetragen wurde. Diese Shell ist in der Regel eine andere. Sehr häufig werden dort z. B. die Korn-Shell ksh, die C-Shell csh, die Bourne-Again-Shell bash oder andere eingetragen. Diese Shells sind zwar in komfortabler als die Bourne-Shell aber dafür sind sie nicht auf jedem Computer zu finden. Die Korn-Shell ist sogar kommerziell. Für FreeBSD, OpenBSD, NetBSD und Linux bedeutet das, daß der freie Ersatz pdksh eingesetzt werden muß.

Wichtige "Kleinigkeiten"

Wie in jeder Systemumgebung gibt es ein paar Konventionen, mit denen man möglichst früh Bekannt werden sollte. Sonst steht man lange still und ärgert sich über große Startschwierigkeiten deren Ursachen nur sehr klein sind:

Variable

Variable sind Platzhalter für Werte, die sich im Verlauf des Programs ändern können. In vielen Sprachen gibt es sehr fein abgestimmte Variablentypen. Scriptsprachen gehören nicht dazu. Sie sind auch nicht dazu gedacht komplexe Berechnungen oder Datenverarbeitungen durchzuführen. Gleichwohl kann man es auch in der Shell auf die Spitze Treiben. Nur möchte ich hier nochmal eines ausdrücklich erwähnen. Wer mit riesigen Scripten endet, der hat höchstwahrscheinlich die falsche Programmiersprache ausgewählt.

Einfache Variable

In der Bourne-Shell können Variablen ad hoc vereinbart werden. Das heißt, man denkt sich einfach einen Namen für die Variable aus und weist diesem Namen einen Wert zu:

$ my_var=inhalt
$ echo $my_var
inhalt
$
Mit dem Kommando echo wird ein Wert auf die sog. Standardausgabe ausgegeben. Mit dem Zeichen $ wird der Inhalt der Variablen bereitgestellt. Vergißt man das $-Zeichen, dann wird nicht der Inhalt sondern der Variablenname ausgegeben.

Diese Variable ist jetzt in der gegenwärtigen Umgebung bekannt. Um sie in einem größeren Umfang bekannt zu machen, so daß auch andere Programme sie verwenden können, muß die Variable my_var zur Umgebungsvariablen (engl. environment variable) erhoben werden. Dies geschieht folgendermaßen:

$ export my_var=inhalt
$ echo $my_var
inhalt
$
Auf diese Weise wird die Variable auch für sogenannte subprocesses bekannt. Ein Subprocess ist ein Programm, das von dem gegenwärtig laufenden, heraus gestartet wurde. Man kann den gegenwärtig laufenden Prozeß als parent-process und den aus ihm heraus gestarteten Prozeß als Child-Process auffassen. Also Eltern-Prozeß und Kind-Prozeß. Weil jedoch in der Literatur fast ausschließlich von Parent und Child die Rede ist, bleibt es hier bei der Erklärung und im Folgenden werden die Bezeichnungen Parent und Child verwendet.

Es kommt als ein erstes Schlüsselwort zum Einsatz: export

In diesem ersten Beispiel haben wir eine Zeichenkette als Variable verwendet. Wir können nun den Inhalt der Variablen verändern, indem wir z.B. den Inhalt der Variablen ergänzen:

$  export my_var="der "$my_var" ist nicht sehr sinnvoll"
$  echo $my_var
der inhalt ist nicht sehr sinnvoll
Das Aneinanderhängen von Zeichenketten nennt man Concatenieren. Bei dem letzten Beispiel macht es einen Unterschied, wo Leerzeichen sind. Zwischen der Variablen und den Zeichenketten in Anführungszeichen dürfen keine Leerzeichen sein. In den durch Anführungszeichen eingeschlossenen Zeichenketten dagegen sehr wohl.

Es ist immer gut, wenn Sie die Beispiele verändern, um zu sehen, wie sie sich verhalten. Welches Ergebnis wird wohl herauskommen. Das bringt die Erfahrung, die sie benötigen, und die ich in so einem Tutorial nicht vermitteln kann.

Numerische Variablen

Die Wertzuweisung numerischer Variablen geschieht, indem man den Zuweisungsoperator "=" mit einem allem vorangestellten let aufwertet. Das sieht dann so aus:

$ let zahl=5
$ echo $zahl
5
Diese kleine Besonderheit kann man sich zunächst leicht merken. Damit ist das Thema jedoch längst nicht am Ende.

Die Wertzuweisung ist jedesmal notwendig, wenn wir einen neuen Wert und damit auch eine neue numerische Variable einführen und natürlich auch, wenn wir mit diesen Variablen rechnen und das Ergebnis wieder in eine numerische Variable zuweisen. Das sieht dann zum Beispiel so aus:

$ let z1=5
$ let z2=2
$ let erg=0
$ let erg=$z1+$z2
$ echo $erg
7
Daß dieses Beispiel soweit eingängig ist, liegt daran, daß es genau dafür konstruiert wurde eine große Zahl von Problemen zu umgehen. Hier haben wir nur die ganzzahlige Addition behandelt. Das ist recht einfach und jeder kann das auch im Kopf rechnen. Mit der ganzzahligen Substraktion und Multiplikation geht das auch noch gut. Aber mit der ganzzahligen Division ist schon nicht mehr gewährleistet, daß das Ergebnis ebenso ganzzahlig ist. Hier beginnt die Sache etwas komplizierter zu werden.

Shells, insbesondere die POSIX-Shell sind nicht dafür gemacht, daß mit ihnen Gleitkomma-Arithmetik durchgeführt wird. Gleichwohl ist es möglich mit Nachkommastellen zu rechnen. Hierfür muß man jedoch etwas Extraaufwand treiben.

Die folgende Rechnung geht garantiert in die Hose:

$ let z1=5
$ let z2=2
$ let erg=0
$ let erg=$z1/$z2
$ echo $erg
2
In der Shell-Programmierung ist die Verarbeitung von Float-Typen und komplexen Datentypen erst einmal nicht vorgesehen. Dennoch kriegt man es ohne größeren Aufwand hin mit Gleitkommazahlen und Festkommazahlen zu rechnen oder gar dynamische Arrays (Felder) zu verarbeiten.

Rechnen mit Nachkommastellen

Im letzten Beispiel zu numerischen Variablen fällt 0.5 einfach unter den Tisch. An einem etwas komplizierteren Beispiel möchte ich die Lösung hierfür aufzeigen. Dieses Beispiel sei hier nicht auf der Kommandozeile ausgeführt, sondern hierfür ist es am besten, wenn das Ganze mit einen kleinen Shell-Script erledigt wird. Im Einzelfall gehe ich immer so vor, daß ich einzelne Zeilen des Scripts auf der Kommandozeile ausführe und das Ergebnis kontrolliere. Erst dann füge ich diese Zeile in das Script ein.

Für das Arbeiten mit rationalen und reellen Zahlen muß auf das Programm bc zurückgegriffen werden. bc ist die sog. arbitrary precision calculator language. Mehr dazu finden Sie in der dazugehörigen man-page mit man bc.

Vorab möchte ich nochmal einige Konventionen erklären. Das hab' ich zwar weiter oben schon einmal erklärt, aber ich glaube nicht, daß es schadet, wenn es hier nochmal steht:

Script 1:


#!/bin/sh

let zahl1=4711
let nks=2        # Nachkommastellen
let div=15
>
s_erg=`echo $zahl1"/"$div | bc -l`   # das Ergebnis ist 314.06666666666666666666
t1=`echo $s_erg | cut -d "." -f 1`   # teile die Zeichenkette durch das Trenn-
                                     # zeichen in einzelne Abschnitte auf und
                                     # nehme davon das erste Feld
t2=`echo $s_erg | cut -d "." -f2`    # diesmal das zweite Feld. in "t2" ist
                                     # jetzt der Wert "06666666666666666666"
t3=`echo $t2 | cut -b 1-2`           # schneide die ersten beiden Bytes von "t2"
                                     # ab
s_erg2=$t1$t3                        # concateniere die beiden Felder. Das Erg-
                                     # ebnis ist noch nicht gerundet.
let n_erg2=$s_erg2                   # jetzt ist es eine Zahl
let r_pos=$nks+1                     # die Stelle, nach der wir Runden ist eins
                                     # hinter der letzten Stelle, die wir für
                                     # das Ergebnis berücksichtigen.
# Hierbei ist wichtig anzumerken, daß es sehr wichtig ist, sich vorher darüber
# ein paar Gedanken zu machen, wie man rundet. Je nach Fachgebiet gibt es dafür
# eindeutige Vorschriften. Am besten geht man zu seinem Auftraggeber und fragt
# ihn verbindlich (schriftlich) wie zu runden ist.

let r_fig=`echo $t2 | cut -b $r_pos` # jetzt müßte 6 in r_fig gespeichert # sein if [[ $r_fig -ge 5 ]] ; then let n_erg=$n_erg2+1 fi
end_erg=`echo $n_erg"/10^"$nks | bc -l` echo $end_erg
Naja, nach dieser Aktion ist das Ergebnis natürlich 314.07000000000000000000 und nicht wie zunächst erwartet 314.07. Jetzt sollte aber jeder in der Lage sein aus diesem Ergebnis das gewünschte Format mit zwei Nachkommastellen zu destillieren.

Vektoren und Felder (Arrays)

Vektoren sind Werte-Tupel, die auf ganz bestimmte Weise zueinander in Beziehung stehen. Der Wilkür des Programmierers sind hier keine Grenzen gesetzt. In vielen Programmiersprachen b.z.w. in mathematischen Notationen wird für die Indizierung der einzelnen Werte eine eigene Syntax eingeführt. Wenn man z. B. ein fünfer-Tupel hat, dann möchte man auch irgendwie sagen können, daß man jetzt gerade mit dem vierten Wert aus diesem Tupel arbeitet.

In Shell-Scripten muß man diese Indizierung einführen, indem man die Variablen numeriert und die Index-Nummer zum Namensbestandteil der Variablen macht. Wenn man nun während der Verarbeitung des Scripts auf beliebige Einträge im Vektor oder Array zugreifen will, muß man den Variablennamen "berechnen". Das folgende Beispiel zeigt wie das geht:

#!/bin/sh

# create an array called 'square' cantaining the squares
# with each element containing the square of the array index
for i in  1 2 3 4 5
do
        eval square_$i=`expr $i \* $i`
done

# print the array
for i in  1 2 3 4 5
do
        eval echo $i squared is \$square_$i
done

Ganz einfach oder? Nein, ich mußte das auch erst mal ausprobieren und jedesmal, wenn ich solche Felder mache fange ich klein an und hangle mich vom einfachen zum komplizierten.

Auf jeden Fall kann man mit dieser Vorgehensweise auch mehrdimensionale Felder erzeugen und verarbeiten.

Ein- und Ausgabe

Einfache Ein- und Ausgabe

Viele Funktionen werden in der Shell-Programmierung nicht direkt mit dem nativen Sprachschatz erledigt, sondern durch das Ausführen von kleinen Programmen, die auf der Kommandozeilen-Ebene zur Verfügung stehen. Angefangen mit Programmen wie z.B. ls und seinen vielfältigen Parametern bis hin zu größeren Programmsystemen wie z. B. dump und restore, mit denen man das Backup auf Filesystem-Level erledigen kann.

Solche Programme produzieren verschiedene Ausgaben:

Gleichzeitig lesen viele dieser Programme Eingaben von Die einfachste Form der Ausgabe haben wir bereits kennengelernt. Mit den Kommando echo kann man beliebige Zeichenketten und den Inhalt von Variablen ausgeben. Und man kann diese beiden beliebig miteinander kombinieren.
$ echo "hallo"
hallo
$ echo hallo
hallo
echo gibt hier die Zeichenkette "hallo" aus. Das funktioniert natürlich auch ohne Anführungszeichen. Im Verlauf dieses Kurses werde ich jedoch an der Variante mit Anführungszeichen festhalten.

Und natürlich kann man auch den Inhalt von Variablen ausgeben und mit Zeichenketten kombinieren:

$ let zahl=75
$ echo "zahl = "$zahl
zahl = 75

Umleitung von Ein- und Ausgabe
I/O Re-Direction

Die Standard-Ausgabe eines Programms kann man direkt in die Standard-Eingabe eines anderen Programms umleiten. Daraus kann man eine richtige Kette von Ein-/Ausgabeumleitungen machen. Dabei muß man jedoch immer darauf achten, daß der entwickelte Code einigermaßen übersichtlich bleibt. Man sollte im Kommentar mit wenigen Worten beschreiben können, warum man eine solche Aktion durchführt.

Man könnte z. B. alle tar-Files in einem Verzeichnis suchen und Zählen. Natürlich sind bei dieser Beschreibung alle Dateien mit der Endung .gz erst mal ausgeschlossen. Das geht in einzelnen Schritten erst mal so:

$ ls *.tar
install_flash_player_6_linux.tar  myx.tar         shellforms.tar
l1l8.tar                          plan-1.8.5.tar
Bei Ihnen wird das Ergebnis natürlich anders aussehen. Und wenn Sie keine tar-Files herumliegen haben, dann verwenden Sie eben andere Dateien für diesen Übungsschritt.

Als nächstes müssen die Dateien noch gezählt werden:

$ ls *.tar | wc -l
5
In meinem Fall ergibt das fünf (5). Bei Ihnen mag etwas anderes herauskommen. Wichtig ist jedenfalls, daß mit dem |-Zeichen die Ausgabe des ls-Kommandos in die Eingabe des wc-Kommandos umgeleitet wird. wc wird mit dem Parameter -l dazu veranlaßt die Zeilen zu zählen.

Moment mal! Hier sollten sie stutzig werden:

install_flash_player_6_linux.tar  myx.tar         shellforms.tar
l1l8.tar                          plan-1.8.5.tar
Das sind doch zwei Zeilen!
Im Prinzip schon. Beim genauen Hinsehen sieht man auch, daß diese beiden Zeilen fünf Einträge haben. Die Ausgabe von ls ist zwar formatiert, kommt bei wc aber weiterhin als einzelne Zeilen an. Warum genau das so ist, weiß ich nicht. Vielleicht sollte man diese Frage mal in einer Newsgroup stellen (z. B. comp.unix.shell). Auf jeden Fall zählt wc die einzelnen Einträge in der Liste, die ls ausgibt, so als wären sie in einzelnen Zeilen wie z. B. bei dem Kommando ls -l.

Ein anderes Beispiel für die Umleitung von Aus- und Eingaben kann zum Kopieren größerer Verzeichnisbäume verwendet werden. Gehen Sie dabei davon aus, daß zwei Verzeichnisse existieren. Z. B. Ihr Home-Verzeichnis und ein zweites, leeres, in dem Sie eine Backup-Kopie Ihres Home-Verzeichnisses anlegen möchten. Weiterhin nehmen wir an, daß dieses Backupverzeichnis /backup heißt.

$ cd
$ tar cf - . | (cd /backup; tar xf -)
In diesem Beispiel wechseln wir mit dem Kommando cd erst mal in unser Home-Verzeichnis. Das Kommando tar erstellt von allem, was es in .(Punkt, also dem aktuellen Verzeichnis) findet ein sog. tape-archive und gibt dieses tape-archive als Zeichenstrom auf die Standard-Ausgabe (stdout) aus. Verantwortlich dafür ist der Parameter f -. f sorgt für die Ausgabe in die gleich dahinter angegebene Datei. Diese Datei ist aber mit dem Zeichen "-" als stdout angegeben. Der Parameter c bedeutet create und sagt dem tar-Kommando, daß es ein tape-archive erstellen soll.

tape-archive heißt also nicht zwingend, daß die Daten auf ein tape b.z.w. ein Band geschrieben werden müssen. tape-archive bedeutet nur, daß die Daten in einem bestimmten Format zu einer großen Archiv-Datei zusammengefaßt werden.

Der Zeichenstrom, der vom ersten tar-Kommando auf die Standard-Ausgabe ausgegeben wird wird dann mit dem |-Zeichen, der Pipe, in die Standard-Eingabe einer sog. Sub-Shell umgeleitet.

Die Prozesse der Sub-Shell laufen parallel zum ersten tar-Kommando ab. Bevor das erste tar-Kommando irgendetwas ausgegeben hat, hat die Sub-Shell das Verzeichniss nach /backup gewechelt. Die Ausgabe von tar cf - . wird dann von dem Kommando tar xf - aufgenommen. Der Parameter x bedeutet extract und sagt dem tar-Kommando, es soll die Datei auspacken. Der Parameter f - bedeutet dem tar-Kommando, es soll die Datei stdin auspacken. Wenn statt dem "-"Zeichen ein richtiger Dateiname angegeben ist, dann versucht das tar-Kommando, je nachdem ob es aus- oder einpacken soll, aus dieser Datei zu lesen oder in sie zu schreiben.

Umleitung von Ausgaben in eine Datei
I/O Re-Direction

Natürlich kann man eine Ausgabe auch in eine Datei umleiten. Das wird sogar sehr häufig gemacht, um ein Ergebnis, und sei es ein Zwischenergebnis, zunächst einmal zu speichern. Diese Art der Umleitung eines Zeichenstromes ist sehr einfach:
$ ls -l > /tmp/dateiliste
Es erscheint zunächst keine Ausgabe auf dem Bildschirm. Das, was wir ohne Umleitung (re-direction) gesehen hätten, ist in die Datei /tmp/dateiliste geschrieben worden. Mit dem Zeichen > haben wir die Richgung, in die die Daten geschickt wurden, vorgegeben. Mit dem Kommando
$ cat /tmp/dateiliste
-rw-r--r--   1 r0v18  staff     18448 Jun 17 08:52 Makefile.dist
-rw-r--r--   1 r0v18  staff  47677440 Jun 19 20:36 Wine-20030618.tar
-rw-r--r--   1 r0v18  staff       265 Jun 21 07:35 cryptout.pl
-rw-r--r--   1 r0v18  staff   2760222 Jun 20 18:55 cvs-1.11.6.tar.gz
-rw-r--r--   1 r0v18  staff    138386 Jun 17 16:12 lightning_in_a_bottle2.jpg
-rw-r--r--   1 r0v18  staff    103620 Jun 17 16:10 lightning_in_a_bottle2_1280x1024.jpg
-rw-r--r--   1 r0v18  staff   1867371 Jun 21 09:52 myx.tar
-rw-r--r--   1 r0v18  staff    604160 Jun 21 09:50 ooc.tar
-rw-r--r--   1 r0v18  staff   2800199 Jun 21 09:47 tda.tgz
drwxr-xr-x   3 r0v18  staff       512 Jun 21 20:17 test
drwxr-xr-x  24 r0v18  staff      1024 Jun 18 22:40 wine-20030618
-rw-rw-rw-   1 root   staff      1895 Jun 27 05:02 yp_setup.txt
findet dieser Inhalt jetzt doch noch auf den Bildschirm.

Umleitung von Ausgaben aus einer Datei
I/O Re-Direction

Umgekehrt ergibt es sich fast von alleine, daß man mit den entgegengerichteten Zeichen < den Inhalt einer Datei in die Standardeingabe eines Programms umleiten kann.

$ wc -l < /tmp/dateiliste
13
Diese Schreibweise ist genausogut wie
$ cat /tmp/dateiliste | wc -l
13
Nur, kann man mit dem cat noch ein paar paar zusätzliche Funktionen erzielen. cat -b numeriert die ausgegebenen nicht-leeren Zeilen. (mehr dazu unter man cat)

Ablaufkontrolle

Fallunterscheidungen mit if

Während der Bearbeitung einer Aufgabe gilt es immer unterschiedliche Fälle zu berücksichtigen. Mal muß man feststellen, ob eine Eingabe einem bestimmten Wert gleicht, und bei Übereinstimmung bestimmte Aktionen ausführen b.z.w. im anderen Fall eben andere Schritte einleiten. Ein einfaches Beispiel ist z.B. die Existenz eines Verzeichnisses. Falls es nicht existiert, muß man es erstellen, bevor man darin Dateien anlegt. Als Shell-Script würde das dann so aussehen:

$ tdir="test_dir"
$ if [[ -d $t_dir ]] ; then
>     echo "Das Verheichnis "$t_dir" existiert bereits."
> else
>     echo "Das Verzeichnis "$t_dir" wird jetzt erzeugt."
>     mkdir $t_dir
> fi
$
Die Fallunterscheidung mit if läßt sich in Pseudo-Code etwa so formulieren:

WENN [ die Bedingung in Klammern wahr ist ] DANN
   führe die erste Aktion aus
SONST
   führe eine alternative Aktion aus
ENDE

Die alternative Aktion wird häufig weggelassen, wenn eine Aktion nur unter bestimmten Bedingungen ausgeführt werden soll, und sonst nichts zu tun ist. In der if-Anweisung gibt es dann keine else-Klausel. Nach der vollständigen ersten Aktion, die durchaus mehrere Zeilen umfassen kann, folgt dann unmittelbar der Abschluß mit fi.

Im Pseudo-Code wurde es bereits klar gemacht. Die Bedingung in der if-Klausel muß wahr sein, damit die erste Alternative der Fallunterscheidung ausgeführt wird. Wahre Aussagen lassen sich aber auf die Unterschiedlichsten Arten erzeugen.

Diese Aussagen können auch mit dem Kommando test gemacht werden. Lesen Sie hierzu bitte die man-page von test.

Programmieren lernen funktioniert am einfachsten mit Beispielen, die man auch nachvollziehen kann. Daher kommt als nächstes eine ewig lange Liste mit kleinen Beispielen, mit denen Sie die einzelnen Aussagen in der Bedingungskausel probieren können. Auch wenn es stereotyp erscheint, habe ich Wert auf Vollständigkeit gelegt. Jeder bleibt an einem anderen "Problem" hängen. Manchmal sind es triviale Dinge, die einem das Leben schwer machen. Wer also meint, das sei zuviel, der sollte lieber die zweite Begründung für diese epische Breite lesen. Die ist noch viel wichtiger.

Viele der Beispiele sind mehr oder weniger doppelt. Einmal so herum und dann noch mal anders herum. Einfach nur der Vergleichsoperator umgedreht. Was macht denn das für einen Sinn? Sehr viel Sinn sogar. Denn Programm-Quelltexte sind ohnehin schon schwer genug lesbar. Wenn dann eine aussage künstlich verdreht wird, weil man zu träge ist den richtigen Vergleichsoperator zu verwenden, dann geht die Semantik über den Jordan. Und das ohne Not. Es ist eine wesentliche Aufgabe von Softwareentwicklern, den Code so zu formulieren, daß die eigentliche Absicht möglichst auf Anhieb erkennbar wird. Bei sog. inverser Logik gelingt das natürlich nicht. Diese Form der Programmierung ist ähnlich sinnvoll wie eine doppelte Verneinung. Deshalb schadet es überhaupt nicht sich auch mal durch einen auf diese Weise langatmig geratenen Text durchzukämpfen. Wenn man dann noch nachdenkt bevor man programmiert, dann kommt wahrscheinlich was raus.

Die erste Aussage testet die Existenz einer Datei. Als Ausgangsbedingung wird davon ausgegangen, daß im "working directory" keine Datei mit dem Namen testdatei existiert. Es kann also erwartet werden, daß die Aussage den Wert FALSE liefert. Somit wird die zweite Alternative ausgeführt. Vor dem zweiten Durchgang wird die Datei testdatei erzeugt. Jetzt muß die Aussage in der if-Klausel TRUE liefern. Daher muß in der zweiten Fallunterscheidung die erste Alternative ausgeführt werden.

Bevor wir jedoch in eine ganze Reihe kleiner Shell-Scripte einsteigen, hier noch eine kleine Erklärung zur ersten Zeile der Scripte.

Diese Scripte beginnen alle mit der Zeile


#!/bin/sh

Das veranlaßt die aktuelle Shell die Folgenden Programmzeilen durch as Programm /bin/sh ausführen zu lassen. Wer also z.B. sonst lieber mit der Korn-Shell arbeitet sorgt so dafür, daß seine Shell-Scripte von der Bourne-Shell interpretiert werden. Das macht durchaus Sinn. Denn je nach persönlichem Geschmack sind für die direkte Anwendung durch den Benutzer die Korn-Shell, die Bourne-Again-Shell, die TC-Shell oder andere viel komfortabler. Und die Bourne-Shell ist dagegen eher etwas unhandlicher. Aber die Bourne-Shell ist halt portabel. Und das ist auch der Grund für dieses Tutorial.

Beispiel-Scripte zu Fallunterscheidungen mit if

Prüfen von Dateiattributen

Prüfen ob eine Datei existiert


#!/bin/sh

#
# prüfen ob eine Datei existiert
#
if [[ -a testdatei ]] ; then
    echo "das hätte nicht passieren dürfen."
    echo "Zu diesem Zeitpunkt darf diese Datei noch nicht existieren"
else
    echo "so ist es richtig, die \"testdatei\" existiert nicht"
fi

touch testdatei # Auf manchen UNIX-Systemen ist das Erzeugen von Dateien mit
                # dem > Operator nicht das richtige Mittel.
                # touch funktioniert immer


if [[ -a testdatei ]] ; then
    echo "so ist es richtig, die \"testdatei\" existiert!"
else
    echo "das hätte nicht passieren dürfen."
    echo "Zu diesem Zeitpunkt hätte die Datei vorhanden sein müssen"
fi

Prüfen ob eine Datei ein Verzeichnis ist

Im zweiten Fall wird geprürt ob testdir existiert und ob es ein Verzeichnis ist. testdir existiert gar nicht. Daher liefert die Aussage in der if-Klausel den Wert FALS . Im zweiten Durchgang wird eine Datei mit dem Namen testdir erzeugt. Auch das ist kein Verzeichnis und führt daher zu einer falschen Aussage in der if-Klausel. Erst der dritte Versuch führt zu einer wahren Aussage und die erste Alternat wird ausgeführt.


#!/bin/sh

#
# prüfen ob ein Verzeichnis existiert
#
if [[ -d testdir ]] ; then
    echo "das hätte nicht passieren dürfen."
    echo "Zu diesem Zeitpunkt darf dieses Verzeichnis noch nicht existieren"
else
    echo "so ist es richtig, das \"testdir\" existiert nicht"
fi

touch testdir

if [[ -d testdir ]] ; then
    echo "das hätte nicht passieren dürfen."
    echo "Zu diesem Zeitpunkt darf dieses Verzeichnis noch nicht existieren"
else
    echo "so ist es richtig, das \"testdir\" existiert nicht"
fi

mkdir testdir

if [[ -d testdir ]] ; then
    echo "so ist es richtig, das \"testdir\" existiert!"
else
    echo "das hätte nicht passieren dürfen."
    echo "Zu diesem Zeitpunkt hätte das Verzeichnis vorhanden sein müssen"
fi

Prüfen ob eine Datei eine reguläre Datei ist

Im folgenden Beispiel wird geprüft, ob es sich bei testdatei um eine reguläre Datei handelt. Also um eine ganz normale Datei mit Text, Daten oder ausführbarem Code. Nicht-reguläre Dateien sind demnach Verzeichnisse, FIFOs, Gerätedateien (devices)... Für ein Beispiel nach dem gleichen Strickmuster wie oben wird zunächst einmal die Datei testdatei gelöscht und durch ein FIFO ersetzt. Dies geschieht durch das Kommando mknod testdatei p wobei der Parameter p dafür sorgt, daß mknod ein FIFO anlegt. (siehe man mknod. Weiterführende Informationen finden Sie auch in Handbüchern zur Systemadministration). Im zweiten Durchgang wird das FIFO wieder entfernt und durch eine reguläre Datei ersetzt. Jetzt ist die Aussage in der if-Klausel wahr und es wird die erste Alternative ausgeführt.


#!/bin/sh

#
# prüfen ob eine Datei eine reguläre Datei ist
#

rm testdatei
mknod testdatei p # das hier ergibt einen FIFO und keine reguläre Datei.
                  # test -f muß false liefern

if [[ -f testdatei ]] ; then
    echo "das hätte nicht passieren dürfen."
    echo "Der Test hätte keine reguläre testdatei finden dürfen"
else
    echo "so ist es richtig, die \"testdatei\" ist keine reguläre Datei"
fi

rm testdatei
touch testdatei

if [[ -f testdatei ]] ; then
    echo "so ist es richtig, die \"testdatei\" existiert!"
else
    echo "das hätte nicht passieren dürfen."
    echo "Zu diesem Zeitpunkt hätte eine reguläre Datei gefunden werden müssen"
fi

Prüfen ob eine Datei lesbar ist

Als nächstes wird geprürt ob für eine Datei Lese-Rechte vorhanden sind. Diese rechte lassen werden mit dem Kommando chmod gesetzt. Mehr zur Anwendung von chmod finden Sie wieder in der man page. Zuerst werden die Zugriffsrechte so gesetzt, daß keine Lese-Rechte bestehen. Somit ist die Aussage falsch und es wird die zweite Alternative ausgeführt. Im zweiten Anlauf werden die Lese-Rechte für den owner gesetzt und die Aussage ist wahr.


#!/bin/sh

#
# prüfen ob man lese-Rechte für eine Datei hat
#

touch testdatei
chmod 200 testdatei

if [[ -r testdatei ]] ; then
    echo "das hätte nicht passieren dürfen."
    echo "Zu diesem Zeitpunkt darf diese Datei nicht lesbar sein"
else
    echo "so ist es richtig, die \"testdatei\" ist nicht lesbar"
fi

chmod 400 testdatei

if [[ -r testdatei ]] ; then
    echo "so ist es richtig, die \"testdatei\" ist lesbar!"
else
    echo "das hätte nicht passieren dürfen."
    echo "Zu diesem Zeitpunkt müßte die Datei lesbar sein."
fi

Prüfen ob eine Datei schreibbar ist

Das folgende Beispiel ist analog zum vorigen. Nur, daß dieses mal die Schreibrechte behandelt werden.

#!/bin/sh

#
# prüfen ob man schreib-Rechte für eine Datei hat
#

touch testdatei
chmod 400 testdatei

if [[ -w testdatei ]] ; then
    echo "das hätte nicht passieren dürfen."
    echo "Zu diesem Zeitpunkt darf diese Datei nicht schreibbar sein"
else
    echo "so ist es richtig, die \"testdatei\" ist nicht schreibbar"
fi

chmod 200 testdatei

if [[ -w testdatei ]] ; then
    echo "so ist es richtig, die \"testdatei\" ist schreibbar!"
else
    echo "das hätte nicht passieren dürfen."
    echo "Zu diesem Zeitpunkt darf in diese Datei nicht geschrieben werden"
fi


Prüfen ob eine Datei ausführbar ist

Dieses Beispiel ist ebenfalls analog zu den beiden vorangegangenen. Diesmal werden wird überprüft ob eine Datei ausführbar ist.

#!/bin/sh

#
# prüfen ob die Datei ausführbar ist
#

touch testdatei
chmod 400 testdatei

if [[ -x testdatei ]] ; then
    echo "das hätte nicht passieren dürfen."
    echo "Zu diesem Zeitpunkt darf diese Datei nicht ausführbar sein"
else
    echo "so ist es richtig, die \"testdatei\" ist nicht ausführbar"
fi

chmod 100 testdatei

if [[ -x testdatei ]] ; then
    echo "so ist es richtig, die \"testdatei\" ist ausführbar!"
else
    echo "das hätte nicht passieren dürfen."
    echo "Zu diesem Zeitpunkt darf in diese Datei nicht ausführbar sein"
fi

Prüfen ob eine Datei dem aktuellen Benutzer gehört

Das folgende Beispiel ist ein klein wenig komplizierter als die vorangegangenen. Hier wird geprüft ob eine Datei, dem selben Benutzer gehört, der auch das Script ausführt. Damit die gleiche Vorgehensweise wie bisher funktioniert, muß man erst mal eine Datei testen, die einem sicher nicht gehört. Systemprogramme sind da gute Kandidaten. Für dieses Beispiel habe ich daher das Kommando who gewählt. Das Kommando which liefert zunächst den absoluten Pfad an dem who gespeichert ist. Weiterhin wird der Benutzername bestimmt um eine passende Meldung in den jeweiligen Alternativen der Fallunterscheidungen ausgeben zu können. Das passiert mit dem Kommando id -n -u. Siehe dazu auch die zugehörigen man pages. Zunächst wird also geprüft ob who dem gegenwärtigen Benutzer gehört der auch das Script ausführt. Das geht nur schief, wenn Sie auf die Schnapsidee kommen und das Script als root ausführten. Im zweiten Durchgang wird eine andere Datei getestet. Die "testdatei" nämlich, die wir bisher auch strapaziert haben. Die haben wir selber gemacht. Daher sollte sie uns auch gehören.


#!/bin/sh

#
# prüfen ob die Datei dem gehört, der das Script ausführt
#

testdatei=`which who`
myid=`id -n -u`

if [[ -O $testdatei ]] ; then
    echo "das hätte nicht passieren dürfen."
    echo $testdatei" darf nicht dem Benutzer "$myid" gehören"
else
    echo "so ist es richtig, "$testdatei" gehört nicht dem Benutzer "$myid
fi

if [[ -O testdatei ]] ; then
    echo "so ist es richtig, die \"testdatei\" gehört dem Benutzer "$myid
else
    echo "das hätte nicht passieren dürfen."
    echo "Die Datei \"testdatei\" muß dem Benutzer "$myid" gehören"
fi

Prüfen ob eine Datei zur selben Gruppe wie der aktuelle Benutzer gehört

In diesem Beispiel w geprüft ob eine Datei zur gleichen Gruppe geört wie der aktuelle Benutzer. Die Vorgehensweise ist im Grunde dieselbe wie bei der überprüfung ob die Datei dem aktuellen Benutzer gehört. Bei diesem Beispiel wurden aus Platzgründen die Zeichenketten bei der Ausgabe mit einem sog. backslash umgebrochen, so daß eine Ausgabezeile im Code des Shell-Script über mehrere Zeilen gehen kann. Die Ausgabe erfolgt aber "einzeilig". Einzeilig ist jedoch wieder abhängig von der Terminalbreite. Wenn wenn die Länge der Zeichenkette die Terminalbreite (Anzahl d. Spalten (columns)) überschreitet, wird der Text bei der Ausgabe doch umgebrochen.

#!/bin/sh


#
# prüfen ob die Datei denselben Gruppen-ID hat wie der aktuelle Benutzer
#

testdatei=`which who`
myid=`id -n -u`

if [[ -G $testdatei ]] ; then
    echo "das hätte nicht passieren dürfen."
    echo "Zu diesem Zeitpunkt darf die Datei "$testdatei\
         " zur gleichen Gruppe wie der Benutzer "$myid" gehören"
else
    echo "so ist es richtig, die "$testdatei" gehört nicht" \
         " zur gleichen Gruppe wie der Benutzer "$myid
fi

if [[ -G testdatei ]] ; then
    echo "so ist es richtig, die \"testdatei\""\
         " gehört zur gleichen Gruppe wie der Benutzer "$myid
else
    echo "das hätte nicht passieren dürfen."
    echo "Die Datei \"testdatei\" muß zur gleichen"\
         " Gruppe wie der Benutzer "$myid" gehören"
fi

Prüfen ob eine Datei jünger ist als eine andere Datei

Im folgenden Beispiel wird geprüft welche von zwei Dateien jünger ist. Natürlich sorge ich mit einem Ausgibigen sleep dafür, daß die erste Datei hinreichend älter ist als die zweite. Danach verläuft alles nach Schema-F.

#!/bin/sh

#
# prüfen ob die Datei welche von zwei Dateien jünger ist
# nt steht für newer than
#

touch testdatei_1
sleep 2
touch testdatei_2

if [[ testdatei_1 -nt testdatei_2 ]] ; then
    echo "das hätte nicht passieren dürfen."
    echo "Zu diesem Zeitpunkt darf die testdatei_1 nicht jünger "\
         "sein als testdatei_2."
else
    echo "so ist es richtig, die testdatei_2 ist jünger als testdatei_1"
fi

if [[ testdatei_2 -nt testdatei_1 ]] ; then
    echo "so ist es richtig, die testdatei_2 ist jünger als testdatei_1"
else
    echo "das hätte nicht passieren dürfen."
    echo "Zu diesem Zeitpunkt darf die testdatei_1 nicht jünger "\
         "sein als testdatei_2."
fi


  • Prüfen ob eine Datei älter ist als eine andere Datei

    Dieser Test ist analot zum Vorigen. Nur wird hier geprüft ob eine Datei älter ist als die andere

    #!/bin/sh
    
    #
    # prüfen ob die Datei welche von zwei Dateien älter ist
    # ot steht für older than
    #
    
    touch testdatei_1
    sleep 2
    touch testdatei_2
    
    if [[ testdatei_2 -ot testdatei_1 ]] ; then
        echo "das hätte nicht passieren dürfen."
        echo "Die testdatei_2 darf nicht älter "\
             "sein als testdatei_1."
    else
        echo "so ist es richtig, die testdatei_1 ist älter als testdatei_2"
    fi
    
    if [[ testdatei_1 -ot testdatei_2 ]] ; then
        echo "so ist es richtig, die testdatei_1 ist älter als testdatei_2"
    else
        echo "das hätte nicht passieren dürfen."
        echo "Die testdatei_2 darf nicht älter "\
             "sein als testdatei_1."
    fi
    
    

    Vergleiche von Zeichenketten

    Prüfen ob eine Zeichenkette einem Muster gleicht

    In diesem Beispiel werden zwei Zeichenketten miteinander verglichen. Im ersten Durchgang sind sie ungleich, damit das bisherige Schema eingehalten wird. Im Zweiten Durchgang wird eine wahre Aussage in der if-Klausel erzeugt. Und so wird dann auch wieder die erste Alternative ausgeführt. Das =-Zeichen wird hier als Vergleichsoperator eingesetzt.

    #!/bin/sh
    
    #
    # prüfen ob eine Zeichenkette einem Muster gleicht
    #
    
    str1="Das Restaurant am Ende des Universums"
    str2="Machts gut und danke für den Fisch"
    
    if [[ $str1 = $str2 ]] ; then
        echo "das hätte nicht passieren dürfen."
        echo "die beiden Zeichenketten sind doch ungleich."
    else
        echo "Das paßt. Die Zeichenketten sind ungleich"
    fi
    
    str2="Das Restaurant am Ende des Universums"
    
    if [[ $str1 = $str2 ]] ; then
        echo "Das paßt. Die Zeichenketten sind gleich"
    else
        echo "das hätte nicht passieren dürfen."
        echo "die beiden Zeichenketten sind doch gleich."
    fi
    
    

    Prüfen ob eine Zeichenkette sich vom Muster unterscheidet

    Dieses Beispiel ist völlig analog zum vorigen. Nur die Zeichenketten sind anders gesetzt und der Vergleichsoperator ist diesmal die Zeichenkombination !=. Diese Zeichenkombination ergibt sich daraus, daß das Ausrufezeichen ! bei vielen Programmiersprachen mit der Bedeutung NOT befrachtet ist. Es negiert also die darauffolgende Aussage. Da die Aussage $str1 = $str2 wahr wäre, wird sie durch den logischen Operator ! zu einer falsche Aussage.

    #!/bin/sh
    
    
    #
    # prüfen ob eine Zeichenkette sich vom Muster unterscheidet
    #
    
    str1="Das Restaurant am Ende des Universums"
    str2="Das Restaurant am Ende des Universums"
    
    if [[ $str1 != $str2 ]] ; then
        echo "das hätte nicht passieren dürfen."
        echo "die beiden Zeichenketten sind doch gleich."
    else
        echo "Das paßt. Die Zeichenketten sind gleich"
    fi
    
    str2="Machts gut und danke für den Fisch"
    
    if [[ $str1 != $str2 ]] ; then
        echo "Das paßt. Die Zeichenketten sind ungleich"
    else
        echo "das hätte nicht passieren dürfen."
        echo "die beiden Zeichenketten sind doch ungleich."
    fi
    
    

    Prüfen ob eine Zeichenkette "kleiner" ist als eine andere

    Dieses Beispiel zeigt wie zwei Zeichenketten darauf verglichen werden ob eine kleiner ist. Dies mutet zunächst etwas komisch an. Tatsächlich wird nicht verglichen ob eine Zeichenkette länger ist als die andere sondern es werden Position für Position die jeweiligen Zeichen in diesen Zeichenketten numerisch verglichen. Ein Zeichen wird im Computer ja nur als Zeichen interpretiert. Intern wird es als Zahl behandelt. Für die korrekte Umsetzung von Zahl zu Zeichen werden Tabellen verwendet. Die ASCII-Tabelle ist so eine. Für Shell-Scripte ist dies auch die weitaus am häufigsten benutzte Tabelle. Im ersten Durchgang sind die beiden Zeichenketten absolut gleich. Im zweiten Durchgang wurde in str1 ein kleines 'a' durch ein großes 'A' ersetzt. Das reicht, der Vergleichsoperator < sieht einen unterschied und liefert eine wahre Aussage.

    #!/bin/sh
    
    
    #
    # prüfen ob eine Zeichenkette kleiner ist als die andere
    #
    
    str1="Das Restaurant am Ende des Universums"
    str2="Das Restaurant am Ende des Universums"
    
    if [[ $str1 < $str2 ]] ; then
        echo "das hätte nicht passieren dürfen."
        echo "die beiden Zeichenketten sind doch gleich."
    else
        echo "Das paßt. Die Zeichenketten sind gleich"
    fi
    
    str1="Das Restaurant Am Ende des Universums"
    
    if [[ $str1 < $str2 ]] ; then
        echo "Das paßt. Die erste Zeichenkette ist \"kleiner\""
    else
        echo "das hätte nicht passieren dürfen."
        echo "die beiden Zeichenketten sind doch ungleich."
    fi
    
    

    Prüfen ob eine Zeichenkette "größer" ist als eine andere

    Dieses Beispiel ist analog zum vorigen. Nur der Vergleichsoperator ist umgekehrt.

    #!/bin/sh
    
    #
    # prüfen ob eine Zeichenkette größer ist als die andere
    #
    
    str1="Das Restaurant am Ende des Universums"
    str2="Das Restaurant am Ende des Universums"
    
    if [[ $str1 > $str2 ]] ; then
        echo "das hätte nicht passieren dürfen."
        echo "die beiden Zeichenketten sind doch gleich."
    else
        echo "Das paßt. Die Zeichenketten sind gleich"
    fi
    
    str2="Das Restaurant Am Ende des Universums"
    
    if [[ $str1 > $str2 ]] ; then
        echo "Das paßt. Die erste Zeichenkette ist \"größer\""
    else
        echo "das hätte nicht passieren dürfen."
        echo "Die beiden Zeichenketten sind doch ungleich."
    fi
    
    

    Prüfen ob eine Zeichenkette nicht leer ist

    Dieses Beispiel zeigt wie man überprüft ob in einer Zeichen-Variablen ein Wert gespeichert ist. Wenn nichts gespeichert ist, dann liefert dieser Vergleich eine falsche Aussage. Der Vergleichsoperator orientiert sich an der Länge der Zeichenkette. Wenn nichts gespeichert ist, dann ist die Länge der Zeichenkette Null (0).

    #!/bin/sh
    
    
    #
    # prüfen ob eine Zeichenkette größer ist als die andere
    #
    
    str1=""
    
    if [[ -n $str1 ]] ; then
        echo "das hätte nicht passieren dürfen."
        echo "die Zeichenkette ist doch leer"
    else
        echo "Das paßt. Die Zeichenkette ist leer"
    fi
    
    str1="Das Restaurant am Ende des Universums"
    
    if [[ -n $str1 ]] ; then
        echo "Das paßt. Die Zeichenkette hat jetzt einen Inhalt"
    else
        echo "das hätte nicht passieren dürfen."
        echo "Die Zeichenketten hat doch einen Inhalt"
    fi
    
    

    Prüfen ob eine Zeichenkette leer ist

    Dieses Beispiel ist analog zu dem vorigen Beispiel. Mit -z wird diesmal nur getestet ob die Zeichenkette leer ist. -z liefert bei einer leeren Zeichenkette eine wahre Aussage.

    #!/bin/sh
    
    #
    # prüfen ob eine Zeichenkette größer ist als die andere
    #
    
    str1="Das Restaurant am Ende des Universums"
    
    if [[ -z $str1 ]] ; then
        echo "das hätte nicht passieren dürfen."
        echo "die Zeichenkette ist doch nicht leer"
    else
        echo "Das paßt. Die Zeichenkette ist nicht leer"
    fi
    
    str1=""
    
    if [[ -z $str1 ]] ; then
        echo "Das paßt. Die Zeichenkette ist jetzt leer"
    else
        echo "das hätte nicht passieren dürfen."
        echo "Die Zeichenketten ist doch leer"
    fi
    
    

    Vergleiche von Zahlen

    Vergleich ob eine Zahl kleiner ist als eine andere

    In diesem Beispiel werden also zwei Zahlen miteinander verglichen. Operator, der vergleicht ob die linke Zahl kleiner ist als die rechte heißt -lt und bedeutet less than. Ansonsten ist alles wie in den vorigen Beispielen. Wenn die Aussage wahr ist, dann wird die erste Alternative ausgeführt sonst wird die zweite Alternative ausgeführt.

    #!/bin/sh
    
    #
    # prüfen ob eine Zahl kleiner ist als eine andere Zahl
    #
    
    let z1=5
    let z2=3
    
    if [[ $z1 -lt $z2 ]] ; then
        echo "Das hätte nicht passieren dürfen."
        echo "z1 ist doch größer als z2."
    else
        echo "Das paßt. z1 ist größer als z2."
    fi
    
    let z2=7
    
    if [[ $z1 -lt $z2 ]] ; then
        echo "Das paßt. z1 ist kleiner als z2."
    else
        echo "Das hätte nicht passieren dürfen."
        echo "z1 ist doch kleiner als z2."
    fi
    
    

    Vergleich ob eine Zahl kleiner oder gleiche ist als eine andere

    Auch in diesem Beispiel werden zwei Zahlen miteinander verglichen. Diesmal lautet der Vergleich -lt und bedeutet less or equal. Dieses Beispiel besteht aus drei Abschnitten, weil die if-Klausel unter zwei Bedingungen eine wahre Aussage liefert. Hier ist also ausdrücklich ein Beisplei für den Fall, daß z1 kleiner also z2 ist und eines für den Fall, daß z1 gleich z2 ist.

    #!/bin/sh
    
    #
    # prüfen ob eine Zahl kleiner oder gleich ist als eine andere Zahl
    #
    
    let z1=5
    let z2=3
    
    if [[ $z1 -le $z2 ]] ; then
        echo "Das hätte nicht passieren dürfen."
        echo "z1 ist doch größer als z2."
    else
        echo "Das paßt. z1 ist größer als z2."
    fi
    
    let z2=5
    
    if [[ $z1 -le $z2 ]] ; then
        echo "Das paßt. z1 ist gleich z2."
    else
        echo "Das hätte nicht passieren dürfen."
        echo "z1 ist doch gleich z2."
    fi
    
    let z2=7
    
    if [[ $z1 -le $z2 ]] ; then
        echo "Das paßt. z1 ist kleiner als z2."
    else
        echo "Das hätte nicht passieren dürfen."
        echo "z1 ist doch kleiner als z2."
    fi
    
    

    Vergleich ob zwei Zahlen gleich sind

    Das folgende Beispiel entspricht weitgehend dem Vorangegangenen. Der Vergleichsoperator ist jetzt -eq und bedeutet equal.

    #!/bin/sh
    
    
    #
    # prüfen ob eine Zahl gleich einer anderen Zahl ist
    #
    
    let z1=5
    let z2=3
    
    if [[ $z1 -eq $z2 ]] ; then
        echo "Das hätte nicht passieren dürfen."
        echo "z1 ist doch größer als z2."
    else
        echo "Das paßt. z1 ist größer als z2."
    fi
    
    let z2=5
    
    if [[ $z1 -eq $z2 ]] ; then
        echo "Das paßt. z1 ist gleich z2."
    else
        echo "Das hätte nicht passieren dürfen."
        echo "z1 ist doch gleich z2."
    fi
    
    

    Vergleich ob eine Zahl größer oder gleich ist als eine andere

    Das folgende Beispiel entspricht weitgehend dem Vorangegangenen. Der Vergleichsoperator ist jetzt -ge und bedeutet greater or equal. Weil die if-Klausel wieder unter zwei verschiedenen Voraussetzungen eine wahre Aussage liefert besteht dieses Beispiel auch wieder aus drei Abschnitten.

    #!/bin/sh
    
    #
    # prüfen ob eine Zahl größer oder gleich einer anderen Zahl ist
    #
    
    let z1=3
    let z2=5
    
    if [[ $z1 -ge $z2 ]] ; then
        echo "Das hätte nicht passieren dürfen."
        echo "z1 ist doch kleiner als z2."
    else
        echo "Das paßt. z1 ist kleiner als z2."
    fi
    
    let z1=5
    
    if [[ $z1 -ge $z2 ]] ; then
        echo "Das paßt. z1 ist gleich z2."
    else
        echo "Das hätte nicht passieren dürfen."
        echo "z1 ist doch gleich z2."
    fi
    
    let z2=3
    
    if [[ $z1 -ge $z2 ]] ; then
        echo "Das paßt. z1 ist größer z2."
    else
        echo "Das hätte nicht passieren dürfen."
        echo "z1 ist doch größer als z2."
    fi
    
    

    Vergleich ob eine Zahl größer ist als eine andere

    Dieses Beispiel besteht ausnahmsweise mal aus drei Abschnitte. Ist aber ansonsten analog zu den vorangegangenen. Es zeigt, daß der Vergleichsoperator -gt eben auch bei Gleichheit eine falsche Aussage liefert. Das ist ja auch beabsichtigt.-gt steht für greater than.

    #!/bin/sh
    
    #
    # prüfen ob eine Zahl größer als eine andere Zahl ist
    #
    
    let z1=3
    let z2=5
    
    if [[ $z1 -gt $z2 ]] ; then
        echo "Das hätte nicht passieren dürfen."
        echo "z1 ist doch kleiner als z2."
    else
        echo "Das paßt. z1 ist kleiner als z2."
    fi
    
    let z1=5
    
    if [[ $z1 -gt $z2 ]] ; then
        echo "Das hätte nicht passieren dürfen."
        echo "z1 ist doch gleich z2."
    else
        echo "Das paßt. z1 ist gleich z2."
    fi
    
    let z2=3
    
    if [[ $z1 -gt $z2 ]] ; then
        echo "Das paßt. z1 ist größer z2."
    else
        echo "Das hätte nicht passieren dürfen."
        echo "z1 ist doch größer als z2."
    fi
    
    

    Vergleich ob zwei Zahlen ungleich sind

    Und dieses ist das letzte Beispiel. Es zeigt wie man zwei Zahlen auf Ungleichheit prüft. Hierzu wird der Operator -ne verwendet. -ne bedeutet -not equal.

    #!/bin/sh
    
    #
    # prüfen ob eine Zahl ungleich einer anderen Zahl ist
    #
    
    let z1=5
    let z2=5
    
    if [[ $z1 -ne $z2 ]] ; then
        echo "Das hätte nicht passieren dürfen."
        echo "z1 ist doch gleich z2."
    else
        echo "Das paßt. z1 ist gleich z2."
    fi
    
    let z2=3
    
    if [[ $z1 -gt $z2 ]] ; then
        echo "Das paßt. z1 ist ungleich z2."
    else
        echo "Das hätte nicht passieren dürfen."
        echo "z1 ist doch ungleich z2."
    fi
    
    
    Es ist zweifellos richtig, daß diese lange Liste an Beispielen in jeder Hinsicht erschöpfend ist. Wenn Sie's bis zum Ende durchgehalten haben, darf ich Ihnen an dieser Stelle versichern, daß auch der Autor seine Mühe dabei hatte diese stereotypen Beispiele alle hinzuschreiben und zu testen, damit sie auch wirklich funktionieren. Das ist einfach Mühsam. Aber nur wenn man alles mal gesehen und ausprobiert hat, kann man die Unterschiede verstehen. Natürlich wird hier manch einer protestieren. Schließlich war von vorne herein alles klar. Aber das geht nicht jedem so. Und wie oben schon erwähnt bleibt man oft an trivialen Dingen unverhältnismäßig lange hängen.

    Ich selbst habe es schon mehrmals erlebt, daß jemand sinngemäß nach dem Motto gehandelt hat: "Das erkläre ich nicht, daß ist doch trivial!". Mit dem Ergebnis, daß viele aus der Vorlesung zwar diese Haltung übernehmen aber das eigentliche Thema nicht verstanden haben. Wenn schon was so trivial ist, daß man es nicht behandeln muß, dann muß man die Kursteilnehmer am Ende mit solchen Trivialitäten auch gnadenlos rausprüfen. Genau das geschieht aber nicht. Daher verfolge ich hier eine andere Vorgehensweise, auch wenn's länglich wird. Sie dürfen sich also jetzt schon auf die nächste Abhandlung freuen. Und wem das noch nicht reicht, dem sei das Buch "A suitable boy" von Vikram Seth empfohlen.

    Wer sich jetzt schon gefreut hat, daß die Fallunterscheidungen mit if hier ein Ende gefunden hätten, der hat sich leider zu früh gefreut. Nach diesen vielen unterschiedlichen Vergleichsoperationen sind jetzt noch Aussagen mit implizitem Aufruf von Kommandos fällig.

    Auswerten der Returnwerte von Kommandos

    Auswerten eines einfachen Kommandos

    Man kann auch ganz einfach eine Fallunterscheidung mit dem Erfolg oder Miserfolg eines Shell-Kommandos b.z.w. eines Programms verbinden. Wie im Nachfolgenden Beispiel reicht es das Programm einfach ohne eckige Klammern in die Aussage der if-Klausel zu stellen. Vorsicht ist jedoch bei Programmen geboten, die Ausgaben produzieren. Im Zweiten Abschnitt dieses Beispiels sehen Sie, daß die Ausgabe von einem grep-Kommando nach /dev/null umgeleitet wird. Auf diese Weise wird die Ausgabe unterdrückt.

    #!/bin/sh
    
    #
    # auswerten des Rückgabewertes eines Kommandos
    # erst mal einfach
    #
    
    suchname="billgates"
    suchdatei="/etc/passwd"
    
    if grep $suchname $suchdatei ; then
        echo "o mei, den lösch ich sofort"
    else
        echo "hier geht noch alles mit rechten Dingen zu"
    fi
    
    suchname="daemon"
    
    if grep $suchname $suchdatei > /dev/null ; then
        echo "schrecklich, geistert es jetzt auf meinem Rechner?"
    else
        echo "Schlimmer noch. Die Kiste ist von allen guten Geistern verlassen."
    fi
    
    

    Auswerten verketteter Kommandos

    In diesem Beispiel werden mehrere Kommandos miteinander verknüpft, indem die Ausgabe des einen in die Eingabe des nächsten umgeleitet wird. Damit das für die if-Klausel wie ein Kommando aussieht, wird es in Hochkomma gestellt. Aber nicht irgendwelche Hochkomma. Bitte beachten Sie genau welche Sie verwenden. Auf der Deutschen Tastatur finden Sie diese Zeichen auf der zweiten Belegung (SHIFT) auf der Taste zwischen dem scharfen S(ß) und der -Taste (sagt mir mal jemand wie das Ding auf deutsch heißt)

    #!/bin/sh
    
    #
    # und jetzt mit mehreren Kommandos in einer Reihe
    #
    
    os_type=`uname -s`
    
    if `echo $os_type | grep 'Irix' > /dev/null`; then
        echo "Das hätte nicht passieren sollen"
    else
        echo "Paßt, ist ja auch nur Linux"
    fi
    
    if `echo $os_type | grep 'Linux' > /dev/null`; then
        echo "Paßt, HP-UX wäre mir aber lieber"
    else
        echo "Das hätte nicht passieren sollen."
    fi
    
    
    Ein weiteres Beispiel für die Verknüpfung verschiedener Shell-Kommandos in einer if-Klausel wird nachfolgend gezeigt. Dabei kommt es darauf an, daß mehrere Kommandos verknüpft werden und die Ergebnisse dieser Kommandos mit dem UND-Operator && verknüpft werden. Auf diese Weise wird die gesamte Aussage falsch, wenn auch nur eine Teilaussage falsch ist. Auf diese Weise können die unterschiedlichsten Aussagen gezimmert werden. Es können auch mehrere && und || miteinander kombiniert werden.

    In diesem Beispiel wird zunächst nachgesehen ob der Benutzer "hugo" in der Datei /etc/passwd eingetragen ist. Falls ja liefert diese Teilaussage schon mal ein wahres Ergebnis. Die Zeichenkette, die diese Operation liefert wird in der Variablen zeile gespeichert. Der Inhalt von zeile wird dann mit cut -d ":" -f 4 weiterverarbeitet. Das Ergebnis dieser Aussage ist immer wahr. Die Ausgabe von cut wird wieder in der Variablen zeile gespeichert. cut teilt in diesem Prozeßschritt die Zeile aus /etc/passwd in einzelne Spalten ein. Diese Spalten sind durch das Colon(:) getrennt. Ein Zeichen, das diese Trennfunktion erfüllt, nennt man Delimiter. Von den so eingeteilten Spalten nimmt cut die vierte und gibt deren Inhalt aus. Im letzten Schritt wird die Ausgabe von cut, die jetzt in der Variablen zeile gespeichert ist nach der Zeichenkette "20" durchsucht. Diese drei Prozeßschritte sind gleichzeitig Teilaussagen der if-Klausel. Die Rückgabewerte ( return-values) der einzelnen Schritte werden als Aussagen mit dem &&-Operator verknüpft. Nur wenn alle wahr sind, ist die gesamte Aussage wahr. Wenn eine einzige falsch ist, dann ist die gesamte Aussage falsch. Das ist das Wesen einer &&-Verknüpfung. Anders sieht's natürlich aus, wenn Teilaussagen mit || (ODER) verknüpft werden.

    Damit nicht alles in eine Zeile geschrieben werden muß wird in diesem Beispiel die if-Klausel mit dem backslash (\) umgebrochen. Ich selbst bevorzute es wenn die UND- und ODER-Operatoren schön in Reih und Glied unter dem if-Schlüsselwort stehen. Man sucht nicht lange wie die Verknüpfung aufgebaut ist. Wenn dagegen alles gut gemischt ist, dann kann man leicht was übersehen.

    #!/bin/sh
    
    if zeile=`grep hugo /etc/passwd` \
    && zeile=`echo $zeile | cut -d ":" -f 4` \
    && `echo $zeile | grep 20 > /dev/null 2>&1` ; then
        echo "hallo"
    else
        echo "ollah"
    fi
    
    

    Fallunterscheidungen mit case

    Fallunterscheidungen mit case bieten eine Einfache Möglichkeit zwischen vielen Alternativen zu unterscheiden. Sie sind einfach strukturiert und daher leicht lesbar. Ihr wesentlicher Nachteil ist, daß sie keine komplexen Ausdrücke wie die if-Konstruktion zulassen. Glücklicherweise wird die aber gar nicht so oft benötigt. Und so hat jede der beiden Strukturanweisungen ihren Platz und keine hat ihn zu Unrecht. Die beiden sind genau so gemacht wie sie gebraucht werden. Für die meisten Programmierer ist das nur eine Frage der Übung.

    Wie immer gibt es jedoch ein paar Kleinigkeiten, die sind keine Frage der übung sondern eine Frage der Sorgfalt. Eine dieser Kleinigkeiten ist die äußere Form und im Zusammenhang mit Fallunterscheidungen ist es auch immer die Frage was geschen soll, wenn die gestellte Bedingung nicht erfüllt ist. Bei if Statements ist das die Frage nach dem dangling else und bei case Statemente ist das die Frage nach dem Default. Strenge Programmiersprachen zwingen den Programmierer an dieser Stelle vollständigen Code zu schreiben. Sprachen wie Shell-Script oder C tun dies nicht. Dadurch sind es noch lange keine schlechten Programmiersprachen. Es gibt keine schlechten Programmiersprachen. Es gibt nur die falsche Auswahl der Programmiersprache und es gibt schlampige und schlechte Programmierer.

    Die Syntax von case sieht so aus:

    case Ausdruck in
        Muster1 )
            Befehle
            ;;
        Muster2 )
            Befehle
            ;;
        Wildcard )
            Befehle
            ;;
    esac
    

    Beispiel-Script zu Fallunterscheidungen mit case