
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.
if
if
case
if oder case etc, sind darin aber nicht enthalten.
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.
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:
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.
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
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ß.
#! wird die aktuelle Shell#!/bin/sh
"=" inusername=`id -n -u`echo $username erhält man dann den Inhalt der Variablen username
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.
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 sinnvollDas 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.
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 5Diese 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 7Daß 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 2In 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.
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:
#! wird die aktuelle Shell#!/bin/sh
"=" inusername=`id -n -u`echo $username erhält man dann den Inhalt der Variablen username
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.
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.
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:
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
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.tarBei 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 5In 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.tarDas sind doch zwei Zeilen!
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.
$ ls -l > /tmp/dateilisteEs 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.txtfindet dieser Inhalt jetzt doch noch auf den Bildschirm.
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 13Diese Schreibweise ist genausogut wie
$ cat /tmp/dateiliste | wc -l 13Nur, 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)
ifWä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 ENDEDie 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.
cd-Kommando ausgeführt werden. cd liefert 0 als Rückgabewert, wenn die Aktion erfolgreich war, und einen Wert größer 0, falls das Kommando fehl schlug.
| Test-Operator | Die Aussage ist wahr wenn |
| -e Datei | die Datei existiert |
| -d Datei | die Datei existiert und ist ein Verzeichnis |
| -f Datei | die Datei existiert und ist eine reguläre Datei (d.h. kein Verzeichnis und keine spezielle Datei wie z. B. ein Link oder ein Fifo) |
| -r Datei | man hat Lese-Rechte für diese Datei |
| -s Datei | die Datei ist nicht leer |
| -w Datei | man hat Schreib-Rechte für diese Datei |
| -x Datei | diese Datei ist ausführbar b.z.w. für ein Verzeichnis gibt es das Recht, es zu durchsuchen (-> find-Kommando) |
| -O Datei | Die Datei gehört Ihnen |
| -G Datei | Die Datei trägt denselben Gruppen-ID wie Sie |
| Datei-1 -nt Datei-2 | Datei-1 ist jünger als Datei-2 (newer than) |
| Datei-1 -ot Datei-2 | Datei-1 ist älter als Datei-2 (older than) |
| Vergleichs-Operator | Die Aussage ist wahr wenn |
| Zeichenkette = Vorlage | Zeichenkette entspricht der Vorlage |
| Zeichenkette != Vorlage | Zeichenkette entspricht nicht der Vorlage |
| Zeichenkette-1 > Zeichenkette-2 | die Zeichenkette-1 ist kleiner als die Zeichenkette-2 |
| Zeichenkette-1 < Zeichenkette-2 | die Zeichenkette-1 ist größer als die Zeichenkette-2 |
| -n Zeichenkette | die Zeichenkette ist nicht leer und hat eine Länge größer Null Bytes |
| -z Zeichenkette | die Zeichenkette ist leer und hat eine Länge von Null Bytes |
| Vergleichs-Operator | Art des Vergleiches |
| -lt | kleiner als (less than) |
| -le | kleiner oder gleich als (less than or equal) |
| -eq | gleich (equal) |
| -ge | größer oder gleich als (greater than or equal) |
| -gt | größer als (greater than) |
| -ne | ungleich (not equal) |
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/shDas 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 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
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
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
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
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
Bei dem folgenden Beispiel muß die sog. Locality beachtet werden. Diese kann mit der Umgebungsvariablen LANG gesetzt b. z. w. abgefragt werden. Auf dem System, auf dem ich diese Beispiele entwickelt habe war die LANG-Variable auf den Wert en_US gesetzt. Das führt dazu, das dieses Beispiel auf Rechnern, bei denen diese Variable einen anderen Wert hat nicht funktionieren würde. Daher wird die LANG-Variable explizit gesetzt.
#!/bin/sh
LANG=en_US
case `date +%a` in
Mon) echo "Montag"
;;
Tue) echo "Dienstag"
;;
Wed) echo "Mittwoch"
;;
Thu) echo "Donnerstag"
;;
Fri) echo "Freitag"
;;
Sat) echo "Samstag"
;;
Sun) echo "Sonntag"
;;
*) echo "Fehler"
exit 1
esac
exit
Das war auch schon alles, was man zu der Fallunterscheidung mit case sagen kann. Anstelle der echo Statements können freilich auch kompliziertere Befehlsfolgen stehen. Und anstelle von `date +%a` könnte auch eine Variable oder ein beliebiger anderer Ausdruck ausgewertet werden.
Beispiel:
Schneide solange Scheiben vom Brotlaib ab, bis Du sechs scheiben hast. Oder
Spühle solange schmutzige Teller, bis keine schmutzigen Teller mehr da sind.
Beispiel:
#!/bin/sh
for i in 1 2 3 4 5 6 7 8 9; do
man -S$i stat
done
Auf den ersten Blick erscheint diese Schleife als würde sie von eins bis neun durchzählen. Dies trifft aber nicht zu. Vielmehr geben wir der Iteration eine Menge mit den Elementen
.
Anmerkung:
Das Kommando man ist nicht auf allen Systemen gleich implementiert. Die Option "-S" für die Auswahl der Section kommt so auf Linux-Systemen vor. Auf FreeBSD gibt man nur die Nummer für die Section an. Gegebenenfalls muß man einfach die man-page zu man durchlesen.
Beispiel:
#!/bin/sh
let i=1
while [[ i -lt 10 ]]; do
man -S$i stat
let i=i+1
done
Eine andere Möglichkeit, das hinzuschreiben ist zeigt das folgende Beispiel:
#!/bin/sh
let i=1
while (( i < 10 )); do
man -S$i stat
let i=i+1
done
Im Grunde prüfen alle Schleifen ob eine Bedingung wahr ist. Dies tritt je nach Konstruktion der Schleife mehr oder weniger deutlich in Erscheinung.
while-Schleife mit einer Abbruchbedingung die so gestrickt ist:
#!/bin/sh
stop=false
while [[ $stop != true ]]; do
read -p "enter \"true\" to stop: " stop
echo $stop
done
Solange man irgend etwas anderes als true eingibt läuft die Schleife immer weiter. Sobald man das Wort true eingibt, wird die Schleife beendet und das Program hört auf zu "arbeiten".
#!/bin/sh echo "\$0="$0 echo "\$1="$1 echo "\$2="$2 echo "\$3="$3Und das Ergebnis sieht dann so aus:
# sh alist.sh bla blub hallo $0=alist.sh $1=bla $2=blub $3=hallo #
#!/bin/sh
let var_cnt=1
weiter=true
while [[ $weiter = true ]]; do
argv=\$$var_cnt
wert=`eval echo $argv`
if [[ -z $wert ]]; then
weiter=false
else
echo "-->"$wert
fi
let var_cnt=$var_cnt+1
done
In diesem Beispiel werden die Variablen von $1 bis $<n> durgezählt, wie das im Abschnitt Vektoren und Felder (Arrays) vorgestellt wurde.
getopts
Einzelne Parameter werden mit einem Präfix übergeben. Dieser Präfix besteht aus einem Minus-Zeichen "-" und einem charakteristischen Buchstaben. Diesem Buchstaben wird dann der Parameter zugeordnet. Der Systemaufruf getopts liefert dann einen Parameter nach dem anderen.
Die Parameterverarbeitung mit getopts erlaubt die Parameterübergabe flexibel zu gestalten. So ist es nicht notwendig immer alle Parameter mitzugeben. Wenn einer Fehlt, wird er von getopts einfach nicht verarbeitet.
Beispiel:
#!/bin/sh
while getopts s:g:b: value; do
echo "value = " $value
echo "optarg =" $OPTARG
case $value in
s) rflag=1
surname="$OPTARG"
;;
g) wflag=1
given_name="$OPTARG"
;;
b) sflag=1
dt_of_birth="$OPTARG"
;;
?) printf "\n\nUsage: %s: -s -g -v \n\n" $0
exit 1
;;
esac
done
echo "surname = " $surname
echo "given name = " $given_name
echo "date of birth = " $dt_of_birth
Der Aufruf des Programms führt dann zum folgenden Ergebnis:
# sh gtopt.sh -s Huber -g Hugo -b 05.07.1988 value = s optarg = Huber value = g optarg = Hugo value = b optarg = 05.07.1988 surname = Huber given name = Hugo date of birth = 05.07.1988Wenn man's aber falsch aufruft, dann erhält man auch ein unerwünschtes Ergebnis:
# sh gtopt.sh -s Huber Maier -g Hugo -b 05.07.1988 value = s optarg = Huber surname = Huber given name = date of birth =Diesen Fehler vermeidet man indem man Zeichenketten, die von "whitespace", also einem Leerzeichen oder einem TAB, unterbrochen ist, mit Anführungszeichen einfaßt. Das folgende Beispiel zeigt, wie das gemeint ist:
# sh gtopt.sh -s "Huber Maier" -g Hugo -b 05.07.1988 value = s optarg = Huber Maier value = g optarg = Hugo value = b optarg = 05.07.1988 surname = Huber Maier given name = Hugo date of birth = 05.07.1988
#!/bin/sh while read line ; do echo "---> "$line done < /etc/fstabFür viele kleine Problemchen ist dieser Dreizeiler sicher die Lösung. Aber man kann das noch verfeinern. Man könnte zum Beispiel jede Zeile Spalte für Spalte zerlegen:
#!/bin/sh df | while read line do # /dev/hda11 1040568 735396 305172 71% /data device=`echo $line | cut -d " " -f1` usage=`echo $line | cut -d " " -f5` mnt_pt=`echo $line | cut -d " " -f6` echo -n "Das device "$device" ist am Mount-Point "$mnt_pt echo " eingehängt. Es ist zu "$usage" gefüllt." doneIn Verbindung mit dynamischen Arrrays könnte man so zum Beispiel eine interne Tabelle aufbauen, die man frei indizieren kann.
ls > my_fileUnd dann können wir diese Datei auch verarbeiten:
#!/bin/sh
for i in $(cat my_file); do
echo "---> "$i
done
Es geht aber auch anders. Im Grunde ist es ja egal wo der Zeichenstrom herkommt. Wichtig ist nur, daß die einzelnen Worte durch sog. Whitespaces, also Leerzeichen oder TAB getrennt sind. Zwischen den runden Klammern mit dem führenden $-Zeichen geht es einzig darum einzelne Worte auszugeben. Und so könnte die for-Schleife auch so aussehen:
#!/bin/sh
for i in $(ls); do
echo "---> "$i
done
Zwischen diesen beiden Klammern kann man nun allerhand treiben. Hier nur eine kleine Einstimmung auf das, was möglich ist:
#!/bin/sh
for i in $(df | cut -b 57-75); do
echo "---> "$i
done
#!/bin/sh
#
# zunächst den stdin in dem file-descriptor 3 sichern
#
exec 3<&0
#
# jetzt lassen wir den file-descriptor 0 auf /etc/fstab zeigen
#
exec 0</etc/fstab
while read line; do
echo "---> "$line
done
#
# und zum Abschluß stellen wir bei den file-descriptoren den Orginalzustand
# wieder her. Das nennt man Aufräumen!
#
exec 0<&3
Das folgende Beispiel habe ich aus den Usenet News von dem Kollegen Dan Mercer. Um es besser zu verstehen sollten wir damit ein wenig herumbasteln:
#!/bin/sh exec 3>/tmp/t$$ exec 4</tmp/t$$ exec 5</tmp/t$$ exec 6</tmp/t$$ exec 7</tmp/t$$ exec 8</tmp/t$$ exec 9</tmp/t$$ rm -f /tmp/t$$ echo "Katze" >&3 echo "Hase" >&3 echo "Gockel" >&3 echo "Hund">&3 echo "Fuchs" >&3 cat /tmp/t$$ sort <&4 echo "====================================" sort <&5 echo "====================================" sort <&6 echo "====================================" sort <&7 echo "====================================" sort <&8 echo "====================================" sort <&9Was passiert da oben? Schon ein bisserl schräg!?! Es wird also eine Datei erzeugt und der File-Descriptor der dazu verwendet wird hat die Nummer 3. 0, 1 und 2 sind schon von stdin, stdout und stderr belegt. Danach öffenen wir die Datei gleich sechs mal zum Lesen. Und sogleich löschen wir die Datei wieder aus dem File-System. Die File-Descriptoren sind aber noch da. Sie sind alle mit den virtuellen File-System verbandelt und dort werden die Daten gepuffert obwohl die Datei aus dem File-System bereits entfernt ist. Pro File-Descriptor können wir einmal bis EOF lesen. Das können wir genau einmal so machen.
Am billigsten ist das wenn ein Programm seine eigene PID in eine pid-Datei schreibt. In vielen Fällen erzeugen Programme ihre pid-Dateien in dem Verzeichnis /var/run. Es besteht aber kein Zwang, das genauso zu machen. Die Datenbank PostgreSQL schreibt seine pid-Datei in das Verzeichnis, in dem die Datenbank liegt. I der pid-Datei steht nur die PID, mehr nicht.
Wenn ein Programm diese pid-Datei aber nicht von selbst anlegt, dann müssen wir nachhelfen. Dazu müssen wir das Programm von einem Shell-Script aus starten und danach die PID bestimmen und in eine Datei schreiben. Das könnte dann so aussehen:
#!/bin/sh /usr/X11R6/bin/xclock & let xclpid=`ps -ef | grep 1185 | grep xclock | egrep -v grep | cut -b 8-14 | sed -e 's/ //g'` echo $xclpid > xclock.pidDanach steht in der Datei xclock.pid die PID des Programms. Mit den folgenden Zeilen kriegen wir die xclock wieder los:
#!/bin/sh xclpid=`cat xclock.pid` kill -1 $xclpidUnd jetzt wäre es noch ganz angenehm, wenn wir das in einem Script zum Starten und Stoppen richtig verpacken könnten. Das geht etwa so:
#!/bin/sh
XCLOCK_BIN=/usr/X11R6/bin/xclock
PROG_BNM=`/bin/basename $XCLOCK_BIN`
OS_TYPE=`uname -s`
case $OS_TYPE in
Linux | HP-UX | AIX )
PS_FLAG=-ef
;;
BSD )
PS_FLAG=agux
;;
esac
case $1 in
start )
exec $XCLOCK_BIN &
let xclpid=`ps $PS_FLAG | grep $$ | grep $PROG_BNM | egrep -v grep | cut -b 8-14 | sed -e 's/ //g'`
echo $xclpid > xclock.pid
;;
stop )
xclpid=`cat xclock.pid`
kill -1 $xclpid
;;
* ) echo
echo "usage: " $0 "[start|stop]"
echo
;;
esac
exit
Und was bedeutet das jetzt? Das meiste können Sie sich zusammenreimen. Nur die Zeile mit dem ps-Kommando bedarf wohl ein wenig Erklärung:
let xclpid=`ps $PS_FLAG | grep $$ | grep $PROG_BNM | egrep -v grep | cut -b 8-14 | sed -e 's/ //g'`Ist eine Kaskade aus einzelnen Kommandos. Eines übergibt seine Ausgabe in die Eingabe des nächsten. Zuerst kommt das
ps-Kommando. Das gibt eine Liste der laufenden Programme auf der Standardausgabe aus. Je nachdem welches UNIX-Derivat auf Ihrem Rechner läuft unterscheidet sich die Parameterliste von ps-Kommando. Bei System V UNIX-Derivaten verwenden wir ps -ef und bei BSD UNIX-Derivaten verwenden wir ps agux (ohne Minus-Zeichen (-)). Als Ergebnis erhalten wir eine Liste der laufenden Prozesse die wie eine Tabelle aussieht. In der zweiten Spalte dieser Tabelle ist die PID aufgeführt. Damit wir diese eine PID unseres gerade gestarteten Programms erhalten müssen wir erst mal die eine Zeile mit dem Eintrag unseres Programms heraus filtern. Wir gehen dabei zunächst immer davon aus, daß unser Programm wirklich nur einmal läuft und zwar nachdem es von uns gestartet wurde. Dieses Herausfiltern erledigen wir mit dem grep-Kommando. Grep filtert alle Ereignisse, die dem pattern entsprechen. grep filtert entweder eine Datei, deren Namen wir auch noch angeben. Wenn kein Dateiname angegeben ist, dann wartet grep auf einen Zeichenstrom von der Standardeingabe stdin. Mit einer Dateiangabe sieht ein einfacher Aufruf von grep so aus
grep PATH /etc/profile
grep filtert alle Vorkommnisse der Zeichenkette PATH aus der Datei /etc/profile.
Um mit grep einen Zeichenstrom vom stdin zu filtern geht man so vor:
cat /etc/profile | grep PATHDas Ergebnis ist das gleiche. In unserem Beispiel von oben wird die Liste, die
ps ausgibt gefiltert. Das sieht eben so aus:
ps $PS_FLAG | grep $$ |Die Shell löst erst mal die Variablen auf und ersetzt sie durch die eigentlichen Werte. Bei einem UNIX System V sähe es dann so aus:
ps -ef | grep 1234 # vorausgesetzt die PID des gerade laufenden Prozesses ist 1234.
$$ ist die PID des aktuellen Prozesses. Und unser gerade laufender Prozess hat natürlich "child-Prozesse"
Wer schon öfter mit UNIX-Systemen gearbeitet hat, der weiß wie Signale verschickt werden können. Mit dem Kommando kill versendet man sie. Das ist aber nicht alles. kill benötigt mindestens zwei Parameter. Der erste Parameter, den wir angeben ist, welches Signal wir verschicken. Eine Liste der Signale finden wir in der Datei signal.h. Und das sieht etwa so aus:
|
Signal |
Zahlenwert |
Signal |
Zahlenwert |
Signal |
Zahlenwert |
|---|---|---|---|---|---|
|
SIGHUP |
1 |
SIGSEGV |
11 |
SIGTTOU |
22 |
|
SIGINT |
2 |
SIGUSR2 |
12 |
SIGURG |
23 |
|
SIGQUIT |
3 |
SIGPIPE |
13 |
SIGXCPU |
24 |
|
SIGILL |
4 |
SIGALRM |
14 |
SIGXFSZ |
25 |
|
SIGTRAP |
5 |
SIGTERM |
15 |
SIGVTALRM |
26 |
|
SIGABRT |
6 |
SIGSTKFLT |
16 |
SIGPROF |
27 |
|
SIGIOT |
6 |
SIGCHLD |
17 |
SIGWINCH |
28 |
|
SIGBUS |
7 |
SIGCONT |
18 |
SIGIO |
29 |
|
SIGFPE |
8 |
SIGSTOP |
19 |
SIGPWR |
30 |
|
SIGKILL |
9 |
SIGTSTP |
20 |
SIGSYS |
31 |
|
SIGUSR1 |
10 |
SIGTTIN |
21 |
SIGUNUSED |
31 |
signal.h aus. Das kann sich zwischen unterschiedlichen UNIX-Derivaten in hinteren Teil der Tabelle etwas unterscheiden. Die Signale, die wir verschicken können, sind nur wenige. Am besten wir beschränken uns auf SIGHUP, SIGUSR1 und SIGUSR2. ps ermitteln. Das Script weiß seine PID aber auch selber. Wir müssen sie nur ausgeben. Die nehmen wir dann und schicken an den Prozeß mit dieser PID das Signal. Jetzt aber das Script:
#!/bin/sh
servus()
{
echo "hello world"
exit
}
trap servus 1
stop=false
while [[ $stop != true ]]; do
sleep 1
echo "sleep - "$$
done
Mit $$ erhalten wir die PID des laufenden Scripts. Und an diese PID schicken wir dann das Signal. Z. B.:
kill -1Wobei wir
PID durch die wirkliche PID ersetzen.
An dieser Stelle ist also ein Minimum an "Software Engineering" notwendig. Sonst wird uns das Script aus dem Ruder laufen. Ausserdem ereignet es sich vermehrt, dass wir Funktionen immer wieder benötigen. Warum also sollten wir eine Funktion dreimal hinschreiben, wenn wir sie dreimal brauchen??? Jetzt ist es soweit, dass wir Funktionen einsetzen werden.
Eine Funktion beim Programmieren funktioniert praktisch genauso. Die Werte, die auf der rechten Seite des Gleichheitszeichens (Zuweisungsoperator) verarbeitet werden, nennt man Parameter. Hier kommt es oft zur Begriffsverwirrung. Sehr häufig wird hier von Argumenten gesprochen. Um Argumente handelt es sich aber nur solange wir nicht zur Tat schreiten. Solange wir also die Funktion nur definieren und die Werte nur beschreiben, bestenfalls hypothetisch besetzen sprechen wir von den Platzhaltern für diese Werte von Argumenten. In dem Moment, in dem wir aber die Funktion tatsächlich aufrufen und wirkliche Werte zum Rechnen übergeben, sind das die Parameter.
Im einfachsten Fall haben Funktionen genau einen Parameter. Sie liefern immer genau ein Ergebnis.
Einige Parameter werden nicht direkt übergeben. Es können alle sein, es können aber auch nur einige sein, die nicht direkt, also mit Call-By-Value übergeben werden. Die Übergabe solcher Parameter erfolgt indem man der Prozedur sagt wo der Wert, um den es geht, gespeichert ist. Man übergibt also eine Referenz. Und daher nennt man diese Art der Parameterübergabe auch Call-By-Reference.
Ganz einfach!
Die habe hier eigentlich gar nichts zu suchen und werden nur der Vollständigkeit halber behandelt.
Wenn man ein Objekt machen will, dann behandelt man eine Datenstruktur und die Funktionen und Prozeduren, die man über diese Datenstruktur anwendet als eine Einheit. Durch diese Vereinheitlichung macht Funktionen und Prozeduren zur Methode. Jetzt ist noch die Frage, wie diese Vereinheitlichung stattfindet. In der Shell-Programmierung findet sie gar nicht statt!
Und mal ehrlich. Ziemlich oft macht das objekt orientierte Paradigma auch gar keinen Sinn. Deshalb müssen wir ein bisserl was an den Haaren herbeiziehen, damit wir endlich die Anatomie einer Klasse und eines Objektes beschreiben können und letztlich zu der Erkenntniss gelangen, wie aus einer Funktion eine Methode wird.
Dazu nehmen wir erst mal die Programmiersprache C. Egal welches, K&R oder ANSI. Egal ob ANSI 89 oder ANSI99. Völlig wurscht.
Erst mal denken wir und ein "Ding" das wir mit Hilfe einer Datenstruktur beschreiben möchten. Wenn wir mal in modernen Strukturen denken, dann nehmen wir doch die "human resource". Also dieses Ding, die "human resource" nämlich hat ein paar Eigenschaften. Sie hat
Und sie hat was weiss ich noch. Im nächsten Schritt müssen wir uns noch darüber Gedanken machen, wie wir diese Eigenschaften genau beschreiben. Diese Eigenschaften verfügen über eine "gewisse" Natur. Und diese natur müssen wir so beschreiben, dass wir den richtigen Datentyp dafür auswählen.
Soweit die für's Erste die Datenstruktur. Als nächstes kommen die Funktionen und Prozeduren, deren Anwendung im Zusammenhang mit der "human resource" Sinn machen.
Vielleicht interessiert uns die Effizienz. Die können wir aber errechnen indem wir den Quotienten aus Gehalt und Leistung. Das Gehalt kriegen wir sicher über den Umweg über die tatsächliche Person, die diese "human Resource" ausmacht.
Brrrrr, ich weiss, das ist alles ziemlich zynisch.
Wenn ich also nun in C einen Zeiger auf eine Funktion mache, dann kann ich die Funktion aufrufen indem ich den Zeiger de-referenziere. Und wenn ich diesen Zeiger zum Bestandteil meiner Datenstruktur mache wird aus der Funktion eine Methode. Tatsächlich ist die Funktion immer noch eine Funktion, und zwar per Definition.
Aber Methode hört sich doch viel schöner an, oder?
Am Ende ist Objektorientierung nichts anderes als eine bestimmte Sichtweise. Ob ich nun den Zeiger auf die Funktion zum Element meiner Datenstruktur mache ist reine Haarespalterei. Und diese Haarespalterrei wird umso energischer betrieben je schlechter die Programmierer sind. Die meinen sie müssten ersatzweise zeigen wieviel Fremdworte aus diesem Gebiet sie auswendig wissen und möglichst gleichzeitig dem Gesprächspartner um die Ohren hauen können.
Und jetzt wissen wir also auch noch was eine Methode ist.
Aber wir wissen immer noch nicht wie wir eine Funktion in einem Shell-Programm hinschreiben. Damit das ganze nicht so zwanghaft wird, werde ich die folgenden Kapitel ein wenig durchmischen. Es ist nämlich immer wieder das Problem das Ganze mit einem halbwegs nachvollziehbarem Beispiel zu unterfüttern.
Als erstes machen wir einen kleinen Rahmen in unser Terminal. Egal ob wir ein altes DEC VT220 oder ein X-Term auf vor uns haben. Unser Programm sollte ungefähr gleich aussehen. Daher machen wir um alles, was wir in so einem Programm veranstalten einen Rahmen.
Bevor wir irgendwas auf den Bildschirm zeichnen, machen wir den Bildschirm sauber. Das geschieht so:
clear
Und der Rahmen sieht dann so aus:
clear
echo "+------------------------------------------------------------------------------+"
echo "|+----------------------------------------------------------------------------+|"
let count=0
while (( count < 19 )); do
echo "|| ||"
let count=count+1
done
echo "|+----------------------------------------------------------------------------+|"
echo "+------------------------------------------------------------------------------+"
Als nächstes sehen, dass wir diesen Rahmen vielleicht ziemlich oft brauchen. Daher verpacken wir ihn in eine Funktion und rufen fortan nur noch diese Funktion.
big_frm()
{
clear
echo "+------------------------------------------------------------------------------+"
echo "|+----------------------------------------------------------------------------+|"
let count=0
while (( count < 19 )); do
echo "|| ||"
let count=count+1
done
echo "|+----------------------------------------------------------------------------+|"
echo "+------------------------------------------------------------------------------+"
}
Funktionen wie diese stellen wir ans obere Ende unseres Quelltextes. Das "main", also das Hauptprogramm steht vorzugsweise am untersten Ende unseres Quelltextes. Ich mache das immer so, dass ich ganzganzganz unten das Wort main hinschreibe und gleich darüber eine Funktion mit dem Namen main
In dieser main-Funktion führe ich dann die wichtigsten Dinge aus und steuere dort den zentralen Flow of Control
Ein Shell-Script mit Funktionen besteht aus einem Haupt-Programm und eben den Funktionen. Die Befehle des Haupt-Programmes stehen ganzganz unten. Am obersten Ende des Quelltextes steht das she-bang, dann die "includes", dann die Funktionen und dann, zu allerletzt die Aufrufe des eigentlichen Shell-Scripts. Bei mir ist das nur ein einziges Kommando. Nämlich der Aufruf der Funktion main.
Im Laufe der folgenden drei Kapitel werden die Funktionen immer aufwendiger. Ich möchte in diesem Kapitel keinen undurchsichtigen Exzess hinschreiben und den Sie im folgenden auf sich alleine gestellt lassen. Lieber werde ich in den folgenden Kapiteln immer wieder auf das Thema Funktionen b.z.w. IO oder sonstwas zurückblenden, so dass immer wieder der Zusammenhang klar wird.
#!/bin/sh
big_frm()
{
#
# loescht den Bildschirm
#
clear
#
# Jetzt wird ein Rahmen auf den Bildschirm b.z.w. das xterm gepinselt.
#
echo "+------------------------------------------------------------------------------+"
echo "|+----------------------------------------------------------------------------+|"
let count=0
while (( count < 19 )); do
echo "|| ||"
let count=count+1
done
echo "|+----------------------------------------------------------------------------+|"
echo "+------------------------------------------------------------------------------+"
}
abfrage()
{
#
# Also erst mal den Rahmen auf einen sauberen Bildschirm pinseln
#
big_frm
#
# Innerhalb des Rahmens sollte alles an den richtigen Platz geschrieben
# werden. Um den Cursor zu positionieren verwenden wir "tput cup x y"
# Danach koennen wir schreiben.
#
tput cup 8 20
echo "Waehlen Sie eine Frucht aus"
done=false
while [[ $done = false ]]; do
done=true
tput cup 10 20
echo "1) Apfel"
tput cup 11 20
echo "2) Birne"
tput cup 12 20
echo "3) Pflaume"
tput cup 13 20
echo "4) Mirabelle"
tput cup 15 20
echo "Ihre Auswahl: "
tput cup 15 34
read ANTWORT
tput cup 23 0
case $ANTWORT in
1 ) ;;
2 ) ;;
3 ) ;;
4 ) ;;
* ) echo "ungueltige Eingabe"
done=false
;;
esac
done
return $ANTWORT # Wir koennen leider nur numerische Werte zurückgeben.
}
main()
{
TTYSAVE=`stty -g` # alle Terminal-Settings abfragen und in der Variablen
# TTYSAVE speichern
abfrage
#
# Der Umstand, dass wir nur numerische Variablen mit Return zurückliefern
# können macht zunächst diese erneute Fallunterscheidung nötig.
#
case $? in
1 ) FRUCHT=Apfel
;;
2 ) FRUCHT=Birne
;;
3 ) FRUCHT=Pflaume
;;
4 ) FRUCHT=Mirabelle
;;
esac
echo $FRUCHT
stty $TTYSAVE # alle ursprünglichen Terminal-Settings wiederherstellen
}
main
Die Quellen von using_read.sh
Funktionen wie diese stellen wir ans obere Ende unseres Quelltextes. Das "main", also das Hauptprogramm steht vorzugsweise am untersten Ende unseres Quelltextes. Ich mache das immer so, dass ich ganzganzganz unten das Wort main hinschreibe und gleich darüber eine Funktion mit dem Namen main
An dieser Stelle stelle ich Ihnen vor wie man mit Shell-Scripten Benutzerschnittstellen baut, die ähnlich wie "curses" aussehen.
Der Schlüssel zu solchen Benutzerschnittstellen ist das Kommando tput. In diesen Beispielen verwende ich nur einen kleinen Teil der Möglichkeiten von tput, weil es sonst wirklich den Rahmen sprengen wird, wenn ich jedes dieser Kommandos bis ins Detail bearbeite.
Die hier abgebildete Benutzerschnittstelle läuft in einem X-Term das folgendermassen aufgerufen wird:
xterm -fg yellow -bg blue -cr red
Der Cursor steht also auf dem ersten Auswahlfeld. Daher ist das Feld links neben der Auswahl "English" auch rot.
Die Invertierung des Textes, also blaue Schrift auf gelbem Grund erreicht mit dem Kommando
tput smso
Und wenn man wieder normal weiterschreiben will, also in diesem Fall gelbe Schrift auf blauem Grund dann erreichen wir das mit dem Kommando
tput rmso
Wenn man also solche Eingabefelder von Vorne herein richtig darstellen will, dann muss man die Benutzerschnittstelle hinmalen bevor man mit dem Benutzer wieder in Interaktion tritt. Dazu führt man den Cursor an die gewünschte Stelle und zeichnet das Feld. Das geht z. B. so:
#!/bin/sh
clear
#
# Cursor an die gewünschte Stelle plazieren
#
tput cup 10 10
#
# Eingabefeld begrenzen. Das geschient üblicherweise mit eckigen Klammern
#
echo "[ ]"
#
# Den Cursor in die Begrenzung plazieren
#
tput cup 10 11
#
# Die Farbe wechseln
#
tput smso
#
# das Feld malen
#
echo " "
#
# Die Farbe zurücksetzen
#
tput rmso
#
# den Cursor da plazieren, wo er hingehört
#
tput cup 23 0
Naja, das war so ein Anfang. Aber irgendwo muss man ja anfangen. Jetzt haben wir also ein Feld in das wir was einfügen könnten, aber der Cursor steht ganz wo anders. Jetzt müssen wir allmählich das ganze Programmierportfolio, das wir oben gelernt haben, auspacken.
Zunächst also wieder ein main. Dann die main-Funktion und dann geht's los. Hier wollen wir auch gleich mit der konsequenten Modularisierung unserer Quellen anfangen. Es wird also auch Quell-Moduln geben, die wir am Besten in einem Verzeichnis ./lib verstauen.
Zunächst verwenden wir hierfür die folgenden Dateien:
Die Bibliothek gadget.sh
Die Bibliothek io.sh
Ich möchte mich hier gleich für die äussere Form dieser Dateien entschuldigen. Zum jetzigen Zeitpunkt stammen sie so wie sie sind aus meinem Fundus und wurden noch nicht für dieses Tutorial aufbereitet. Im Laufe der Zeit werde ich die Quelltexte noch überarbeiten.
Aber wie kriegen wir diese Bibliotheken in unser Programm? Das ist so einfach, dass es schon fast peinlich ist:
#!/bin/sh
init_this()
{
#
# Natuerlich findet in dieser init-Funktion noch viel mehr statt. Aber um
# zu zeigen, wie man Bibliotheken anzieht stehen eben nur diese zwei Zeilen
# da.
# Es wird vorausgesetzt, dass Sie die Bibliotheken in einem lokalen
# Verzeichnis mit dem Namen "lib" gespeichert haben.
#
. lib/gadget.sh
. lib/io.sh
}
main()
{
TTYSAVE=`stty -g` # alle Terminal-Settings abfragen und in der Variablen
# TTYSAVE speichern
init_this
stty $TTYSAVE # alle ursprünglichen Terminal-Settings wiederherstellen
}
main
Das Heranziehen solcher Dateien wie z. B. dieser Bibliotheken nennt man "sourcing". Und in bestem "denglish" würde man dan wohl sagen, die Bibliotheken würden (jetzt kommt's) "gesourced" :-)))
So haben wir jetzt zwar gesehen, wie diese Bibliotheken eingebunden werden. Tatsächlich handelt es sich hier um eine "textuelle Ersetzung" analog zu den #include Präprozessoranweisungen in der Programmiersprache "C"
Den Quelltext für lang01.sh finden Sie hier
Den Cursor können wir ja schon positionieren. Wenn wir den Cursor jetzt in einem Eingabefeld mitführen wollen, dann müssen wir die Position des Cursors berechnen. Das geht eigentlich recht einfach. Wir müssen nur wissen wo unser Ausgangspunkt ist. Von da an wird nur noch nach rechts jeweils eine Stelle pro eingegebenes Zeichen addiert.
Solange wir für ein Eingabefeld Zeichen lesen, müssen wir jedes eingegebene zeichen einzeln lesen. Dazu müssen wir das Gerät /dev/tty anzapfen. Hierzu müssen wir uns einmal Gedanken machen, wie das unter UNIX eigentlich gedacht ist.
Unter UNIX ist alles eine Datei. Dieser Satz hat eine sehr sehr große Tragweite. Denn auch Geräte (devices) sind nichts weiter als Dateien. Wir können diese Dateien öffnen um daraus zu lesen oder in sie zu schreiben. Es spielt keine Rolle ob wir eine normale Datei verwenden, ob wir einen FIFO verwenden oder eben ein Device. Wir müssen nur eines im Auge behalten. Wenn wir die Eingabe wirklich kontrollieren möchten, dann müssen wir Zeichen für Zeichen lesen. Und das geht so:
stty raw
readchar=`dd if=/dev/tty bs=1 count=1 2>/dev/null`
stty -raw
Das bedeutet folgendes:
Das Script sieht dann so aus:
Als nächstes wollen wir das für 15 Zeichen machen. Zumindest in unserem Beispiel geht das 15 Zeichen lang so. 15 Zeichen b.z.w. bis der Anwender auf [RETURN] klopft. Inzwischen haben wir schon alles zusammen, was wir als Parameter an die Funktion übergeben, mit der wir das Eingabefeld auslesen:
Und dann sieht der Aufruf der Funktion so aus:
getentry <Anzahl d. Zeichen> <num|char|all> <X> <Y>
Die Funktion getentry() ruft solange die Funktion getpc() auf wie Zeichen erwartet werden. Und das geht solange, wie eben noch nicht die angegebene Anzahl der Zeichen eingegeben wurde oder [RETURN] gedrückt wurde. Oder anders. getentry() liest bis [RETURN] gedrückt wird oder bis genausoviele Zeichen eingegeben wurden wie im ersten Parameter angegeben wurde.
Jedes Zeichen wird dann behandelt. In der vorliegenden Version ist diese Behandlung recht einfach und sicherlich auch unzureichend. Erst einmal wird geprüft, ob es sich um einen carriage-return handelt und wenn ja, dann ist die Funktion ja fertig. Wenn nein, dann muss immer noch geprüft werden, ob die Anzahl der zu lesenden Zeichen erreicht wurde.
Und dann gibt's da noch einige Sonderfälle:
Den Quelltext für entry_fld.sh finden Sie hier
stty -echoUnd schon wird das, was wir tippen nicht mehr angezeigt. Und wenn wir es wieder sichtbar haben wollen, dann brauchen wir nur folgendes einzugeben:
stty echoUnd schon ist alles wieder da.
Das ist zwar praktisch und in vielen Fällen auch völlig ausreichend. Aber manchmal haben wir es eben nicht mit Systemadministratoren, sondern mit normalen Anwendern zu tun. Und normale Anwender brauchen Führung. Das heißt: Der Systemadministrator hat eine Bringschuld. Er ist gefordert sein Hirn einzuschalten. Vom durchAnwender (auch vom Power-User) dürfen wir das nicht verlangen. Da haben wir die Bringschuld. Wir müssen die Benutzerführung weitgehend wasserdicht machen, sonst kommt es eben zu skurilen Anwendungsfehlern. Und an denen sind immer die Entwickler schuld und nie die Anwender. Das ist einfach so.
Der Anwender braucht eine Rückmeldung für das was er tut. Im Falle der Passworteingabe ist es notwendig, daß wir ihm für jeden Tastendruck ein Zeichen auf dem Bildschirm präsentieren. Im Falle von normalen Eingabefeldern ist das eben das zeichen, das er gerade eingegeben hat und im Falle von Passwortfeldern ist das der übliche "Stern" (asterisk).
Der Weg dahin ist sehr sehr einfach. Wir haben ja bereits das Eingabefeld. Dort müssen wir einfach nur anstelle des gerade eingegebenen Zeichens eben ein Sternchen ausgeben. Die entsprechende Stelle finden Sie in den Kommentaren der Quelltexte. Dazu habe ich zunächst einfach nur die Funktion getentry() abgewandelt, wie ich das oben beschrieben habe. Natürlich könnte man auch einen zusätzlichen Parameter einführen. Z. B. ein "c" für "cleartext" und ein "h" für "hidden", oder wie auch immer. In der Funktion müßte man dann unterscheiden ob man Klartext oder nur den Asterisk ausgibt.
In diesem Beispiel gibt es also zwei fast identische Funktionen. getentry() und getpass(). Und die Anwendung ruft eben die Funktion getpass() um die Eingabe abzugreifen ohne daß diese auf dem Bildschirm sichtbar wird. Das sieht dann so aus:
Den Quelltext für pass_fld.sh finden Sie hier
Stellen Sie sich vor, sie benötigen die Möglichkeit einen Anwender das Passwort ändern zu lassen ohne ihn durch eine komplizierte Kaskade von Menüs zu führen. Gehen wir mal davon aus, daß wir noch mit ganz einfachen Mitteln administrieren. Kein NIS, kein LDAP, kein Kerberos. Das Passwort ist lokal in /etc/shadow gespeichert.
Die Voraussetzungen für diese kleine Anwendung sind also eher einfach. Aber stellen Sie sich vor, wir stellen einem einzelnen Anwender einen PC mit Linux nach Hause. Dort will er nun sein Passwort wechseln. Und da nimmt das Unheil seinen Lauf. Sie erklären ihm, daß eine Shell aufmachen muß und das Kommando /usr/bin/passwd aufrufen muß. Stellen Sie sich nur vor was da alles passieren kann. Der Anwender ruft sicher bald wieder bei Ihnen an und ruft um Hilfe.
Hier ist eine brauchbare Anwendung für das folgende Script. Wir haben die folgenden Anforderungen an das Script:
Freilich kann man das auch mit Tcl/Tk machen. Man kann das mit allem möglichem Machen. Von mir aus schreiben Sie die Anwendung mit X11/Motif in C oder sonstwie. Aber hier machen wir's eben mit der Bourne-Shell.
Diesmal benötigen wir nicht ein Eingabefeld sondern drei. Eines für das alte Passwort und zwei für das neue. Wie üblich muß der Anwender das neue Passwort zweimal eingeben, damit er sich nicht aus versehen vertippt. Und dann sollten wir dem Benutzer noch die Fehlermeldungen von /usr/bin/passwd zur Kenntnis bringen.
Weiter gehen wir davon aus, daß unser Anwender mit einer X11-basierten graphischen Benutzeroberfläche arbeitet. Denn in diesem Beispiel werden wir das Script in einem "X-Term" laufen lassen.
Erst mal basteln wir einen Dummy, um zu sehen, wie die Benutzerschnittstelle aussehen könnte.
Den Quelltext für chg_pass_dummy.sh finden Sie hier
Dieses Script macht freilich noch gar nichts ausser die einzelnen Felder der Benutzerschnittstelle hinzumalen.
In einem zweiten Schritt wird das Script etwas verfeinert und tut auch schon etwas. Wir verdrahten die einzelnen Felder mit der Funktion getpass() und weisen den Wert den wir aus dem Eingabefeld abgreifen in eine jeweils eigene Variable zu. Zur Kontrolle geben wir zum Schlus in der Funktion main() diese drei Werte aus.
Zwischenzeitlich habe ich eingesehen, dass es nicht sehr hübsch aussieht, wie das Script chg_pass_dummy.sh die Benutzerschnittstelle zeichnet. Daher baut chg_pass_dummy2.sh die Benutzerschnittstelle sukzessive auf.
Den Quelltext für chg_pass_dummy2.sh finden Sie hier
Jetzt wo wir die Passworte in Variablen speichern können tauchen einige Fragen auf:
So nach und nach verdrahten wir diese zusätzlichen Problemchen. Als erstes müssen wir wohl überprüfen, ob der Anwender bei der Abfrage des alten Kennwortes überhaupt etwas eingegeben hat. Falls er einfach nur auf [RETURN] gedrückt hat, sollten wir ihn fragen, ob er das absichtlich gemacht hat, ob er abbrechen will oder ob er weitermachen will.
In diesem Beispiel schlage ich vor dieses Problem mit einem zusätzlichen Dialog-Fenster zu erledigen. Dadurch stören wir nicht den Bildaufbau im Hauptfenster.
Zunächst jedoch müssen wir überprüfen, ob das Kennwort korrekt ist. Dazu benötigen wir das Programm /usr/bin/passwd. Bevor wir uns darin verfangen müssen wir mit /usr/bin/passwd erst ein wenig herumprobieren. Wie kriegen wir eine brauchbare Meldung von /usr/bin/passwd??? Am besten wir probieren das einfach auf der Shell aus. Also wir tippen passwd und werden aufgefordert unser altes Kennwort einzugeben. Und hier geben wir irgendwelchen Non-Sense ein. Nur nicht das richtige Kennwort.
# passwd
Changing password for toni
Old Password:
passwd: Authentication failure
Als nächstes geben wir das richtige Passwort ein. Und dann?
# passwd
Changing password for toni
Old Password:
New Password:
Und jetzt??? Jetzt hängt das Ding und wartet auf das neue Passwort. Das kann's ja nicht sein. Das Programm passwd gehört dem User root und wir haben nicht mal das Recht ihm ein Signal zu schicken. Aber es gibt Hilfe. Wenn wir hier einfach [CTRL-D] eingeben, dann bricht passwd ab. Und dann kriegen wir zwei völlig unterschiedliche Fehlermeldungen. Das eine mal eben "Authentication failure" und das andere mal "Password change aborted".
Aber wie kriegen wir [CTRL-D] in den Prompt von passwd?
Ein wenig wissen über die Funktionsweise einer Shell (ich meiner eine UNIX-Shell) führt uns weiter. Eine Shell arbeitet in einer Endlosschleife. Darin werden alle zeichen verarbeitet. Die Schleife funktioniert nach dem Muster
while (getc()==TRUE) do{
...
}
Und wenn eben eine Eingabe FALSE ist, dann bricht die Schleife ab. [CTRL-D] hat eben genau den Wert 0 (Null) und der entspricht FALSE. Offenbar arbeitet passwd die Eingaben der Anwender genauso ab.
In einem nächsten Schritt wollen wir das in unser Script einbauen. Aber wie?
Dabei hilft uns die Sub-shell Ein einfacher Aufruf von Passwd könnte dann etwa so aussehen:
altes_passwort=blabla
(echo $altes_passwort) | passwd
passwd: Authentication failure
Und nun müssen wir noch die Fehlermeldung abgreifen:
fehlermeldung=`(echo $altes_passwort) | passwd 2>&1`
auth_err=`echo $fehlermeldung | grep "Authentication failure"`
if [[ -z $auth_err ]]; then
echo "Erfolg"
else
echo "Fehler"
fi
Und so müssen wir das nun in user Script einbauen. Freilich nicht genau so. Statt der Ausgabe der Worte "Fehler" b.z.w. "Erfolg" müssen wir eine Fehlerbehandlung einbauen und einen entsprechenden Rückgabewert liefern.
Freilich einen numerischen Rückgabewert. Und dann? Dann müssen wir was damit anfangen. Ich schlage vor, dass die Funktion chk_pass() bei Erfolg eine Null (0) liefert und sonst etwas größeres. Und in der Funktion read_pass() machen wir eine Fehlerbehandlung, wenn der Rückgabewert größer Null (0) ist.
if [[ $? -gt 0 ]]; then
init_pass_UI
done=false
fi
Hier ist es mal wieder Zeit, daß wir einen funktionsfähigen Quelltext absaugen. Den finden hinter dem folgenden Link:
Den Quelltext für chg_pass_dummy3.sh finden Sie hier
Jetzt muß die Fehlerbehandlung etwas Substanz kriegen. Wir sollten dem Benutzer sagen, daß er etwas falsch gemacht hat und daß er leider nochmal von Vorne anfangen muß. Dazu brauchen wir eine weitere Funktion, die das unserem "user" klarmachen wird. Jetzt sind ein paar Umbauarbeiten fällig. Wir bemerken, dass die Funktion init_pass_UI() irgendwie an der falschen Stelle ist. Sie gehört in die erste Schleife. Denn da wird sie sowieso aufgerufen bevor etwas passiert und sie wird auch wieder aufgerufen, wenn wir das Fenster mit der Fehlermeldung verunstaltet haben:
#
# altes Passwort abfragen
#
done=false
while [[ $done = false ]]; do
init_pass_UI
done=true
tput cup 7 31 # Cursor positionieren
tput smso
getpass 15 all 7 31 # Eingabe lesen. Cursorposition relativ zu
# diesen Koordinaten nachfuehren
# !! Nur "char" also Buchstaben!!
tput rmso
if [[ $? -eq 1 ]]; then # bei Eingabefehlern, nochmal von Vorne.
tput cup 21 7
echo "You entered an invalid character!"
tput cup 22 7
echo "Please only use characters."
done=false
fi
altes_passwort=$entry # Bei erfolg, Wert zuweisen und Loop beenden
chk_pass $altes_passwort
if [[ $? -gt 0 ]]; then
altes_pw_falsch
init_pass_UI
done=false
fi
done
In die Fehlerbehandlung habe ich jetzt den Aufruf der Funktion altes_pw_falsch()
Das folgende Projekt habe ich nicht aus dem Blauen heraus entwickelt. Vielmehr sind die meisten Bestandteile in einem Projekt entstanden, in dem ich für eine UNIX-Netzwerk eine Anwendung entwickeln mußte aber keinen C-Compiler zur Verfügung hatte. Es gab ausreichend Gründe auf den Rechnern nichts zu installieren, was irgendwie systemnah sein würde. Und so mußte ich mit dem auskommen, was da war. Da wäre ein K&R-C-Compiler ohne viel Bibliotheken. Er diente nur zum Übersetzen des Kernels und eben die Shell.
Trotz dieser spartanischen Ausstattung war es notwendig eine möglichst komfortable Benutzerführung zu entwickeln. Da notwendigerweise alles im Terminal, hier im xterm, stattfand sollte die Benutzerführung wenigstens wie Curses aussehen. Und damit fangen die Probleme an.
Hier die Problemlösung:
TTYSAVE=`stty -g`Und unser Programm hört auf mit den Worten:
stty $TTYSAVEDiese beiden Zeilen machen unser Terminal wieder Benutzbar, nachdem wir während des Programmierens möglicherweise wichtige Einstellungen geändert haben.
Als nächstes müssen wir uns der Einsicht beugen, daß UNIX-Systeme unterschiedlich sind. Unser Ziel ist es aber, daß sich unser Shell-Programm überall gleich verhält. Daher müssen wir gegebenenfalls eine Fallunterscheidung einbauen, die es uns ermöglicht auf die Eigenheiten der unterschiedlichen UNIX-Derivate einzugehen. Dies wird insbesondere notwedig werden, wenn wir einzelne Zeichen von der Tastatur lesen und ihre Verarbeitung selbst in die Hand nehmen.
os_type=`uname -s`
if [[ $os_type = "Linux" ]]; then
let r_char=10
elif [[ $os_type = "HP-UX" ]]; then
let r_char=13
fi
Das hier war nur ein kleiner Ausblick auf das was uns in Sachen Terminalsteuerung noch bevorsteht.
xterm hat 24 Zeilen und 80 Spalten, wenn man es ohne weitere Parameter startet. Danach richten wir uns in diesem Beispiel:
big_frm()
{
clear
echo "+------------------------------------------------------------------------------+"
echo "|+----------------------------------------------------------------------------+|"
let count=0
while (( count < 19 )); do
echo "|| ||"
let count=count+1
done
echo "|+----------------------------------------------------------------------------+|"
echo "+------------------------------------------------------------------------------+"
}
Mit big_frm() kriegen wir immer wieder den Bildschirm in Form. So geben wir ihm das richtige Aussehen. Und innerhalb des so gezeichneten Rahmens entsteht dann die eigentliche Benutzerschnittstelle.