Praktikum "UNIX"

von Prof. Jürgen Plate

8 Shell-Programmierung

Die Shell dient nicht nur der Kommunikation mit dem Bediener, sondern sie kennt die meisten Konstukte einer Programmiersprache. Es lassen sich Anweisungen in einer Textdatei speichern, die dann wie ein beliebiges anderes UNIX-Kommando aufgerufen werden kann. Solche Dateien nennt man 'Shell-Skript' oder 'shell-script'. Ein Shell-Skript kann auf zwei Arten aufgerufen werden:

Das Shell-Skript wird mit einem Editor erstellt und kann alle Möglichkeiten der Shell nutzen, die auch bei der interaktiven Eingabe möglich sind. Insbesondere kann auch die Umleitung der Ein-/Ausgabe wie bei einem Binärprogramm erfolgen. Selbstverständlich lassen sich auch innerhalb eines Skripts weitere Shell-Skripten aufrufen. Zusätzlich kennt die Shell einige interne Befehle. Dem Shell-Skript können Parameter übergeben werden, es ist damit universeller verwendbar.

8.1 Testen von Shell-Skripts

Die Ersetzungsmechanismen der Shell machen es manchmal nicht leicht, auf Anhieb korrekt funktionierende Skripts zu erstellen. Zum Testen bieten sich daher einige Möglichkeiten an:

Merke: Durch Testen kann nur die Fehlerhaftigkeit von Skripts nachgewiesen, aber nicht deren Korrektheit bewiesen werden.

8.2 Kommentare in Shellskripts

Wie Programme müssen auch Shellskripts kommentiert werden. Kommentare werden durch das Zeichen '#' eingeleitet. Alles was in einer Zeile hinter dem '#' steht, wird als Kommentar betrachtet (Übrigens betrachten auch nahezu alle anderen Unix-Programme das '#' als Kommentarzeichen in Steuer- und Parameterdateien). Leer- und Tabulatorzeichen können normalerweise in beliebiger Anzahl verwendet werden. Da die Shell Strukturen höherer Programmiersprachen enthält, ist durch Einrücken eine übersichtliche Gestaltung der Skripts möglich. Auch eingestreute Leerzeilen sind fast überall erlaubt.

Bei vielen Skripts findet man eine Sonderform der Kommentarzeile zu Beginn, die beispielsweise so aussieht:

#!/bin/sh

Durch diesen Kommentar wird festgelegt, welches Programm für die Ausführung des Skripts verwendet wird. Er wird hauptsächlich bei Skripts verwendet, die allen Benutzern zur Verfügung stehen sollen. Da möglicherweise unterschiedliche Shells verwendet werden, kann es je nach Shell (sh, csh, ksh,...) zu Syntaxfehlern bei der Ausführung des Skripts kommen. Durch die Angabe der ausführenden Shell wird dafür gesorgt, daß nur die "richtige" Shell verwendet wird. Die Festlegung der Shell stellt außerdem einen Sicherheitsmechanismus dar, denn es könnte ja auch ein Benutzer eine modifizierte Shell verwenden. Neben den Shells können auch andere Programme zur Ausführung des Skripts herangezogen werden; häufig sind awk- und perl-Skripts.

8.3 Shell-Variable

8.3.1 Allgemeines

Variable sind frei wählbare Bezeichner (Namen), die beliebige Zeichenketten aufnehmen können. Bestehen die Zeichenketten nur aus Ziffern, werden die von bestimmten Kommandos als Integer-Zahlen interpretiert (z. B. expr). Bei Variablen der Shell sind einige Besonderheiten gegenüber anderen Programmiersprachen zu beachten. Im Umgang mit Variablen lassen sich grundlegend drei Formen unterscheiden:

Beispiele zur Verdeutlichung des Sachverhaltes:

Form Beispiel Ausgabe
Deklaration $ foo=  
Wertzuweisung $ foo=bar  
Wertreferenzierung $ echo $foo bar

Im Allgemeinen werden Variablen in der Shell nicht explizit deklariert. Vielmehr ist der Wertzuweisung die Variablendeklaration implizit enthalten. Wird eine Variable dennoch ohne Wertzuweisung deklariert, so wird bei der Wertreferenzierung ein leerer String ("") zurückgegeben.

Für Variablen gilt allgemein:

Beispiele:

Kommando-Eingaben beginnen mit "$  ".
$ VAR="Hello World!" 
$ echo $VAR 
Hello World! 
$ echo '$VAR' 
$VAR 

$ VAR=`pwd` 
$ echo "Aktuelles Verzeichnis: $VAR" 
Aktuelles Verzeichnis: /home/plate 

$ VAR=`pwd` 
$ echo ${VAR}/bin 
/home/plate/bin 

$ VAR=/usr/tmp/mytmp 
$ ls > $VAR  

Das letzte Beispiel schreibt die Ausgabe von ls in die Datei /usr/tmp/mytmp

Enthält eine Variable ein Kommando, so kann dies Kommando durch Angabe der Variablen ausgeführt werden, z. B.:

$ VAR="ls -la"
$ $VAR

8.3.2 Quoting von Variablen

Es gelte: VAR=abcdef

\ entwertet nachfolgendes Metazeichen
'   ' entwertet alle dazwischenliegenden Metazeichen
echo '$VAR' liefert als Ausgabe $VAR
"   " entwertet alle dazwischenliegenden Metazeichen, aber nicht die Variablen- und Kommandosubstitution
echo "$VAR" liefert abcdef

8.3.3 Vordefinierte Variablen:

Beim Systemstart und beim Aufruf der Dateien /etc/profile (System-Voreinstellungen) und .profile (benutzereigene Voreinstellungen), die ja auch Shellskripts sind, werden bereits einige Variablen definiert. Alle aktuell definierten Variablen können durch das Kommando set aufgelistet werden.
Einige vordefinierte Variablen sind neben anderen:

VariableBedeutung
HOMEHome-Directory (absoluter Pfad)
PATHSuchpfad für Kommandos und Skripts
MANPATHSuchpfad für die Manual-Seiten
MAILMail-Verzeichnis
SHELLName der Shell
LOGNAME
USER
Login-Name des Benutzers
PS1System-Prompt ($ oder #)
PS2Prompt für Anforderung weiterer Eingaben (>)
IFS(internal field separator) Trennzeichen, meist CR, Leerzeichen und Tab)
TZZeitzone (z. B. MEZ)

Folgende spezielle Variablen sind definiert:

Variable Bedeutung Kommandobeispiel
$- gesetzte Shell-Optionen set -xv
$$ PID (Prozeßnummer) der Shell kill -9 $$ (Selbstmord)
$! PID des letzten Hintergrundprozesses kill -9 $! (Kindermord)
$? Exitstatus des letzten Kommandos cat /etc/passwd ; echo $?

8.3.4 Parameterzugriff in Shell-Skripten:

Shell-Skripts können mit Parametern aufgerufen werden, auf die über ihre Positionsnummer zugegriffen werden kann. Die Parameter können zusätzlich mit vordefinierten Werten belegt werden (später mehr). Trennung zweier Parameter durch die in IFS definierten Zeichen.

Positionsparameter Bedeutung
$# Anzahl der Argumente
$0 Name des Kommandos
$1 1. Argument
.
.
.
.
.
.
.
.
$9 9. Argument
$@ alle Argumente (z. B. für Weitergabe an Subshell)
$* alle Argumente konkateniert (--> ein einziger String)

Zur Verdeutlichung soll ein kleines Beispiel-Shell-Skript dienen:

#!/bin/sh
echo "Mein Name ist $0"
echo "Mir wurden $# Parameter übergeben"
echo "1. Parameter = $1"
echo "2. Parameter = $2"
echo "3. Parameter = $3"
echo "Alle Parameter zusammen: $*"
echo "Meine Prozeßnummer PID = $$"

Nachdem dieses Shell-Skript mit einem Editor erstellt wurde, muß es noch ausführbar gemacht werden (chmod u+x foo). Anschließend wird es gestartet und erzeugt die folgenden Ausgaben auf dem Bildschirm:

$ ./foo eins zwei drei vier
Mein Name ist ./foo
Mir wurden 4 Parameter übergeben
1. Parameter = eins
2. Parameter = zwei
3. Parameter = drei
Alle Parameter zusammen: eins zwei drei vier
Meine Prozeßnummer PID = 3212
$

Anmerkung: So, wie Programme und Skripts des UNIX-Systems in Verzeichnissen wie /bin oder /usr/bin zusammengefaßt werden, ist es empfehlenswert, im Home-Directory ein Verzeichnis bin einzurichten, das Programme und Skripts aufnimmt. Die Variable PATH wird dann in der Datei .profile durch die Zuweisung PATH=$PATH:$HOME/bin erweitert. Damit die Variable PATH auch in Subshells (d. h. beim Aufruf von Skripts) auch wirksam wird, muß sie exportiert werden:

export PATH

Alle exportierten Variablen bilden das Environment für die Subshells. Information darüber erhält man mit dem Kommandos:

set: Anzeige von Shellvariablen und Environmentvariablen

oder

env: Anzeige der Environmentvariablen

In Shellskripts kann es sinnvoll sein, die Variablen in Anführungszeichen ("...") zu setzen, um Fehler zu verhindern. Beim Aufruf müssen Parameter, die Sonderzeichen enthalten ebenfalls in Anführungszeichen (am besten '...') gesetzt werden. Dazu ein Beispiel. Das Skript "zeige" enthält folgende Zeile:

grep $1 dat.adr

Der Aufruf zeige 'Hans Meier' liefert nach der Ersetzung das fehlerhafte Kommando grep Hans Meier dat.adr, das nach dem Namen 'Hans' in der Adreßdatei dat.adr und einer (vermutlich nicht vorhandenen) Datei namens 'Meier' sucht. Die Änderung von "zeige":

grep "$1" dat.adr

liefert bei gleichem Parameter die korrekte Version grep "Hans Meier" dat.adr. Die zweite Quoting-Alternative grep '$1' dat.adr ersetzt den Parameter überhaupt nicht und sucht nach der Zeichenkette "$1". Das Skript "zeige" soll nun enthalten:

echo "Die Variable XXX hat den Wert $XXX"

Nun wird eingegeben:

$ XXX=Test
$ zeige

Als Ausgabe erhält man:

Die Variable XXX hat den Wert

Erst wenn die Variable "exportiert" wird, erhält man das gewünschte Ergebnis:

$ XXX=Test
$ export XXX 
$ zeige 
Die Variable XXX hat den Wert Test 

Das Skript "zeige" enthalte nun die beiden Kommandos:

echo "zeige wurde mit $# Parametern aufgerufen:" 
echo "$*" 

Die folgenden Kommandoaufrufe zeigen die Behandlung unterschiedlicher Parameter:

$ zeige
zeige wurde mit 0 Parametern aufgerufen:

$zeige eins zwei 3
zeige wurde mit 3 Parametern aufgerufen:
eins zwei 3

$ zeige eins "Dies ist Parameter 2" drei
zeige wurde mit 3 Parametern aufgerufen:
eins Dies ist Parameter 2 drei

Die Definition von Variablen und Shell-Funktionen (siehe später) kann man mit unsetwieder Rückgängig machen.

8.3.5 Namens- und Parameterersetzung:

Die einfache Parameterersetzung (textuelle Ersetzung durch den Wert) wurde oben gezeigt. Es gibt zusätzlich die Möglichkeit, Voreinstellungen zu vereinbaren und auf fehlende Parameter zu reagieren. Bei den folgenden Substitutionen kann bei manchen Shell-Varianten der Doppelpunkt hinter "variable" auch fehlen.

Am besten läßt sich das am Beispielen zeigen, für die folgende Vorbesetzungen gelten:

einfache Substitution $W1 HELLO
String-Konkatenation ${W1}HaHa HelloHaHa
bedingte Substitution ${W1-"is nich!"}
${W2-"is nich!"}
Hello
is nich!
falls Variable undefiniert ist, nimm Parameter 1 ${W1-$1}
${W2-$1}
Hello
abc
falls Variable undefiniert, nimm $1 und brich Skript ab ${W1?$1}
${W2?$1}
Hello
abc <Abbruch>
falls Variable definiert, nimm $1, sonst nichts ${W1+$1}
${W2+$1}
abc
 

In Kommandodateien können Variablen auch Kommandonamen oder -aufrufe enthalten, da ja die Substitution vor der Ausführung erfolgt.

8.3.6 Bearbeitung einer beliebigen Anzahl von Parametern

Die Positionsparameter $1 bis $9 reichen nicht immer aus. Man denke nur an Skripts, die (ähnlich wie viele Kommandos) beliebig viele Dateinamen auf Parameterposition erlauben sollen. Die Shell-Skripten können mit mehr als neun Parametern versorgt werden - es wird dann mit dem Befehl shift gearbeitet:

shift
Eliminieren von $1, $2 ... $n --> $1 ... $n-1

Die Prozedur "zeige" enthält folgende Befehle:

echo "$# Argumente:" 
echo "$*" 
shift 
echo "Nach shift:" 
echo "$# Argumente:" 
echo "$*" 

Der folgende Aufruf von "zeige" liefert:

$ zeige eins zwei drei 
3 Argumente: 
eins zwei drei 
Nach shift: 
2 Argumente: 
zwei drei 

shift wird jedoch viel häufiger verwendet, wenn die Zahl der Parameter variabel ist. Es wird dann in einer Schleife so lange mit shift gearbeitet, bis die Anzahl der Parameter 0 ist:

while [ $# -gt 0 ]
  do
  tuwas mit $1
  shift
  done

8.3.7 Gültigkeit von Kommandos und Variablen

Jeder Kommandoaufruf und somit auch der Aufruf einer Befehlsdatei (Shellskript) hat einen neuen Prozeß zur Folge. Wie wir wissen, wird zwar das Environment des Elternprozesses "nach unten" weitergereicht, jedoch gibt es keine umgekehrten Weg. Auch der Effekt der Kommandos (z. B. Verzeichniswechsel) ist nur innerhalb des Kindprozesses gültig. Im Elternprozeß bleibt alles beim alten. Das gilt natürlich auch für Zuweisungen an Variablen.

Die Kommunikation mit dem Elternprozeß kann aber z. B. mit Dateien erfolgen. Bei kleinen Dateien spielt sich fast immer alles im Cache, also im Arbeitsspeicher ab und ist somit nicht so ineffizient, wie es zunächst den Anschein hat. Außerdem liefert jedes Programm einen Rückgabewert, der vom übergeordneten Prozeß ausgewertet werden kann.

Es gibt außerdem ein Kommando, das ein Shell-Skript in der aktuellen Shell und nicht in einer Subshell ausführt:

. (Dot) Skript ausführen

Das Dot-Kommando erlaubt die Ausführung eines Skripts in der aktuellen Shell-Umgebung, z. B. das Setzen von Variablen usw.

Damit die Variable auch in Subshells (d. h. beim Aufruf von Skripts auch wirksam wird, muß sie exportiert werden:

export PATH

Alle exportierten Variablen bilden das Environment für die Subshells.

8.4 Interaktive Eingaben in Shellskripts

Es können auch Shellskripts mit interaktiver Eingabe geschrieben werden, indem das read-Kommando verwendet wird.

read variable [variable ...]

read liest eine Zeile von der Standardeingabe und weist die einzelnen Felder den angegebenen Variablen zu. Feldtrenner sind die in IFS definierten Zeichen. Sind mehr Variablen als Eingabefelder definiert, werden die überzähligen Felder mit Leerstrings besetzt. Umgekehrt nimmt die letzte Variable den Rest der Zeile auf. Wird im Shell-Skript die Eingabe mit < aus einer Datei gelesen, bearbeitet read die Datei zeilenweise.

Anmerkung: Da das Shell-Skript in einer Sub-Shell läuft, kann IFS im Skript umdefiniert werden, ohne daß es nachher restauriert werden muß. Die Prozedur "zeige" enthält beispielsweise folgende Befehle:

IFS=',' 
echo "Bitte drei Parameter, getrennt durch Komma eingeben:" 
read A B C 
echo Eingabe war: $A $B $C 

Aufruf (Eingabe kursiv):

$ zeige 
Bitte drei Parameter, getrennt durch Komma eingeben: 
eins,zwei,drei 
Eingabe war: eins zwei drei 

8.5 Hier-Dokumente

Die Shell bietet die Möglichkeit, Eingaben für Programme direkt in das Shell-Skript mit aufzunehmen - womit die Möglichkeit einer zusätzlichen, externen Datei wegfällt.

Eingeleitet werden Hier-Dokumente mit << und anschließend einer Zeichenfolge, die das Ende des Hier-Dokuments anzeigt. Diese Zeichenfolge steht dann alleine am Anfang einer neuen Zeile (und gehört nicht mehr zum Hier-Dokument). Bei Quoting der Ende-Zeichenfolge (eingeschlossen in "...", '...'), werden die Datenzeilen von den üblichen Ersetzungsmechanismen ausgeschlossen. Dazu ein Beispiel:

Die Shell-Skript "hier" enthält folgende Zeilen:

cat << EOT 
Dieser Text wird ausgegeben, als ob er von 
einer externen Datei kaeme - na ja, nicht ganz so. 
Die letzte Zeile enthaelt nur das EOT und wird 
nicht mit ausgegeben. Die folgende Zeile wuerde
bei der Eingabe aus einer Datei nicht ersetzt.
Parameter: $* 
EOT 

Aufruf:

$ hier eins zwei 
Dieser Text wird ausgegeben, als ob er von 
einer externen Datei kaeme - na ja, nicht ganz so. 
Die letzte Zeile enthaelt nur das EOT und wird 
nicht mit ausgegeben.  Die folgende Zeile wuerde
bei der Eingabe aus einer Datei nicht ersetzt.
Parameter: eins zwei 

Außerden wäre bei der Eingabe aus einer Datei die Ersetzung von '$*' durch die aktuellen Parameter nicht möglich. Hier-Dokumente bieten also weitere Vorteile. Diese Vorteile lassen sich besonders gut ausspielen, wenn man eine Datei mit ed bearbeitet und dabei die Editor-Kommandos als Hier-Dokument mitgibt. Durch in die Kommandos eingestreute Variablen wird die ganze Dateibearbeitung auch variabel steuerbar.

Noch ein Beispiel, diesmal die Simulation des Kommandos 'wall'.

for  X in `who | cut -d' ' -f1` 
do 
write $X << TEXTENDE
Hallo Leute,
das Wetter ist schoen. Wollen wir da nicht um 17 Uhr Schluss machen
und in den Biergarten gehen?
TEXTENDE 
done 

8.6 Verkettung und Zusammenfassung von Kommandos

Hintereinanderausführung

Will man mehrere Kommandos ausführen lassen, braucht man nicht jedes Kommando einzeln einzugeben und mit der Eingabe des nächsten Kommandos auf die Beendigung des vorhergehenden warten. Die Kommandos werden, getrennt durch Strichpunkt, hintereinander geschrieben:

kommando1 ; kommando2; kommando3

Sequentielles UND

Das zweite Kommando wird nur dann ausgeführt, wenn das erste erfolgreich war.

Kommando1 && Kommando2

Für die Bewertung der Abarbeitung wird folgende Wahrheitstabelle verwendet:

Exitstatus Kommando 1 Exitstatus Kommando 2 &&-Verkettung
Exitstatus gleich 0 --> ok Exitstatus gleich 0 --> ok Exitstatus gleich 0 --> ok
Exitstatus gleich 0 --> ok Exitstatus ungleich 0 --> nicht ok Exitstatus ungleich 0 --> nicht ok
Exitstatus ungleich 0 --> nicht ok wird nicht gestartet! Exitstatus ungleich 0 --> nicht ok

Sequentielles EXOR

(EXOR gleich Exklusives ODER) Das zweite Kommando wird nur dann ausgeführt, wenn das erste erfolglos war.

Kommando1 || Kommando2

Für die Bewertung der Abarbeitung wird folgende Wahrheitstabelle verwendet:

Exitstatus Kommando 1 Exitstatus Kommando 2 ||-Verkettung
Exitstatus gleich 0 --> ok wird nicht gestartet! Exitstatus gleich 0 --> ok
Exitstatus ungleich 0 --> nicht ok Exitstatus gleich 0 --> ok Exitstatus gleich 0 --> ok
Exitstatus ungleich 0 --> nicht ok Exitstatus ungleich 0 --> nicht ok Exitstatus ungleich 0 --> nicht ok

Zusammenfassung von Kommandos

Kommandofolgen lassen sich - analog der Blockstruktur höherer Sprachen - logisch klammern. Das Problem der normalen Hintereinander-Ausführung mit Trennung durch ";" ist die Umleitung von Standardeingabe und Standardausgabe, z. B.:

pwd > out ; who >> out ; ls >> out

Die Umleitung läßt sich auch auf die Fehlerausgabe erweitern

echo "Fehler!" # geht nach stdout
echo "Fehler!" 1>&2 # geht nach stderr

Kommandos lassen sich zur gemeinsamen E/A-Umleitung mit {...} klammern:

{ Kommando1 ; Kommando2 ; Kommando3 ; ... ; }

Wichtig: Die geschweiften Klammern müssen von Leerzeichen eingeschlossen werden!

Die Ausführung der Kommandos erfolgt nacheinander innerhalb der aktuellen Shell, die Ausgabe kann gemeinsam umgelenkt werden, z. B.:

{ pwd ; who ; ls ; } > out

Die schließende Klammer muß entweder durch einen Strichpunkt vom letzen Kommando getrennt werden oder am Beginn einer neuen Zeile stehen. Die geschweiften Klammern sind ein ideales Mittel, die Ausgabe aller Programme eines Shell-Skripts gemeinsam umzuleiten, wenn das Skript beispielsweise mit 'nohup' oder über cron gestartet wird. Man fügt lediglich am Anfang eine Zeile mit der öffnenden Klammer ein und am Schluß die gewünschte Umleitung, z. B.:

{  
  ... 
  ... 
  ... 
  ... 
} | mailx -s "Output from Foo" $LOGNAME 

Eine Folge von Kommandos kann aber auch in einer eigenen Subshell ausgeführt werden:

( Kommando1 ; Kommando2 ; Kommando3 ; ... )

Das Ergebnis des letzten ausgeführten Kommados wird als Ergebnis der Klammer zurückgegeben. Auch hier kann die Umleitung der Standardhandles gemeinsam erfolgen. Auch dazu ein Beispiel. Bei unbewachten Terminals "bastle" ich gerne an der Datei '.profile' des Users. Eine Zeile

( sleep 300 ; audioplay /home/local/sounds/telefon.au ) &

ist schnell eingebaut. Fünf Minuten nach dem Login rennt dann jemand zum Telefon (geht natürlich nur, wenn der Computer auch soundfähig ist). Noch gemeiner wäre

( sleep 300 ; kill -9 0 ) &

Abschließend vielleicht noch etwas Nützliches. Wenn Sie feststellen daß eine Plattenpartition zu klein geworden ist, müssen Sie nach Einbau und Formatierung einer neuen Platte oftmals ganze Verzeichnisbäme von der alten Platte auf die neue kopieren. Auch hier hilft die Kommandoverkettung zusammen mit dem Programm tar (Tape ARchive), das es nicht nur erlaubt, einen kompletten Verzeichnisbaum auf ein Gerät, etwa einen Streamer, zu kopieren, sondern auch in eine Datei oder auf die Standardausgabe. Wir verknüpfen einfach zwei tar-Prozesse, von denen der erste das Verzeichnis archiviert und der zweite über eine Pipe das Archiv wieder auspackt. Der Trick am ganzen ist, das beide Prozesse in verschiedenen Verzeichnissen arbeiten. Angenommen wir wollen das Verzeichnis /usr/local nach /mnt kopieren:

( cd /usr/local ; tar cf - . ) | ( cd /mnt ; tar xvf - )

Der Parameter "f" weist tar an, auf eine Datei zu schreiben oder von einer Datei zu lesen. Hat die Datei wie oben den Namen "-", handelt es sich um stdout bzw. stdin.

8.7 Strukturen der Shell

In diesem Abschnitt werden die Programmstrukturen (Bedingungen, Schleifen, etc.) besprochen. Zusammen mit den Shell-Variablen und den E/A-Funktionen 'echo', 'cat' und 'read' hat man nahezu die Funktionalität einer Programmiersprache. Es fehlen lediglich strukturierte Elemente wie z. B. Arrays und Records, die teilweise in anderen Shells (z. B. Korn-Shell) oder auch in Skript-Sprachen realisiert sind.

8.7.1 Bedingungen testen

Das wichtigste Kommando ist 'test', mit dem man mannigfache Bedingungen testen kann.

test Argument

Dieses Kommando prüft eine Bedingung und liefert 'true' (0), falls die Bedingung erfüllt ist und 'false' (1), falls die Bedingung nicht erfüllt ist. Der Fehlerwert 2 wird zurückgegeben, wenn das Argument syntaktisch falsch ist (meist durch Ersetzung hervorgerufen). Es lassen sich Dateien, Zeichenketten und Integer-Zahlen (16 Bit, bei Linux 32 Bit) überprüfen.

Das Argument von Test besteht aus einer Testoption und einem Operanden, der ein Dateiname oder eine Shell-Variable (Inhalt: String oder Zahl) sein kann. In bestimmten Fällen können auf der rechten Seite eines Vergleichs aus Strings oder Zahlen stehen - bei der Ersetzung von leeren Variablen kann es aber zu Syntaxfehlern kommen. Weiterhin lassen sich mehrere Argumente logisch verknüpfen (UND, ODER, NICHT). Beispiel:

test -w /etc/passwd

mit der Kommandoverkettung lassen sich so schon logische Entscheidungen treffen, z. B.:

test -w /etc/passwd && echo "Du bist ROOT"

Normalerweise kann statt 'test' das Argument auch in eckigen Klammern gesetzt werden. Die Klammern müssen von Leerzeichen umschlossen werden:

[ -w /etc/passwd ]

Die folgenden Operationen können bei 'test' bzw. [ ... ] verwendet werden.

Eigenschaften von Dateien

Ausdruck Bedeutung
-e < datei > datei existiert
-r < datei > datei existiert und Leserecht
-w <datei> datei existiert und Schreibrecht
-x <datei> datei existiert und Ausführungsrecht
-f <datei> datei existiert und ist einfache Datei
-d <datei> datei existiert und ist Verzeichnis
-h <datei> datei existiert und ist symbolisches Link
-c <datei> datei existiert und ist zeichenor. Gerät
-b <datei> datei existiert und ist blockor. Gerät
-p <datei> datei existiert und ist benannte Pipe
-u <datei> datei existiert und für Eigentümer s-Bit gesetzt
-g <datei> datei existiert und für Gruppe s-Bit gesetzt
-k <datei> datei existiert und t- oder sticky-Bit gesetzt
-s <datei> datei existiert und ist nicht leer
-L <datei> datei ist symbolisches Link
-t <dateikennzahl> dateikennzahl ist einem Terminal zugeordnet

Vergleiche und logische Verknüpfungen

Vergleich von Zeichenketten
Ausdruck Bedeutung
-n <String> wahr, wenn String nicht leer
-z <String> wahr, wenn String leer ist
<String1> = <String2> wahr, wenn die Zeichenketten gleich sind
<String1> != <String2> wahr, wenn Zeichenketten verschieden sind
Algebraische Vergleiche ganzer Zahlen
Operator Bedeutung
-eq equal - gleich
-ne not equal - ungleich
-ge greater than or equal - größer gleich
-gt greater than - größer
-le less than or equal - kleiner gleich
-lt less than - kleiner
Logische Verknüpfung zweier Argumente
UND <bedingung1> -a <bedingung2>
ODER <bedingung1> -o <bedingung2>
Klammern \( <ausdruck> \)
Negation ! <ausdruck>

8.7.2 Bedingte Anweisung (if - then - else)

Wichtig: Als Bedingung kann nicht nur der test-Befehl, sondern eine beliebige Folge von Kommados verwendet werden. Jedes Kommando liefert einen Errorcode zurück, der bei erfolgreicher Ausführung gleich Null (true) und bei einem Fehler oder Abbruch ungleich Null (false) ist. Zum Testen einer Bedingung dient die if-Anweisung. Jede Anweisung muß entweder in einer eigenen Zeile stehen oder durch einen Strichpunkt von den anderen Anweisungen getrennt werden. Trotzdem verhät sich eine bedingte Anweisung - oder die Schleifenkonstrukte, die weiter unten behandelt werden - wie eine einzige Anweisung. Somit ergibt sich eine starke Ähnlichkeit mit der Blockstruktur von C oder Pascal. Man kann dies ausprobieren, indem man eine if- oder while-Anweisung interaktiv eingibt. Solange nicht 'fi' bzw. 'done' eingetippt wurde, erhält man den PS2-Prompt ('>').

einseitiges if:

if kommandoliste 
then 
      kommandos 
fi 

zweiseitiges if:

if kommandoliste 
then 
      kommandos 
else 
      kommandos 
fi 

mehrstufiges if:

if kommandoliste1 
then 
      kommandos
elif kommandoliste2 
  then 
        kommandos 
elif ... 
          ... 
fi 

Beispiele für die if-Anweisung:

Es soll eine Meldung ausgegeben werden, falls mehr als 5 Benutzer eingeloggt sind:

USERS=`who | wc -l`  # Zeilen der who-Ausgabe zählen
if test $USERS -gt 5 
then 
  echo "Mehr als 5 Benutzer am Geraet" 
fi 

Das geht natürlich auch kürzer und ohne Backtics:

if [ $(who | wc -l) -gt 5 ] ; then 
  echo "Mehr als 5 Benutzer am Geraet" 
fi 

Man sollte bei der Entwicklung von Skripts aber ruhig mit der Langfassung beginnen und sich erst der Kurzfassung zuwenden, wenn man mehr Übung hat und die Langfassungen auf Anhieb funktionieren. Ein weiteres Beispiel zeigt eine Fehlerprüfung:

if test $# -eq 0 
then 
  echo "usage: sortiere filename" >&2 
else 
  sort +1 -2 $1 | lp 
fi 

Das nächste Beispiel zeigt eine mehr oder weniger intelligente Anzeige für Dateien und Verzeichnisse. 'show' zeigt bei Dateien den Inhalt mit 'less' an und Verzeichnisse werden mit 'ls' präsentiert. Fehlt der Parameter, wird interaktiv nachgefragt:

if [ $# -eq 0 ]                          # falls keine Angabe 
then                                     # interaktiv erfragen 
   echo -n "Bitte Namen eingeben: "
   read DATEI 
else  
   DATEI=$1 
fi
if  [-f $DATEI ]                         # wenn normale Datei 
then                                     # dann ausgeben 
   less $DATEI  
elif [ -d $DATEI ]                       # wenn aber Verzeichnis 
  then                                   # dann Dateien zeigen 
     ls -CF $DATEI 
   else                                  # sonst Fehlermeldung 
     echo "cannot show $DATEI"  
fi

Das nächste Beispiel hängt eine Datei an eine andere Datei an; vorher erfolgt eine Prüfung der Zugriffsberechtigungen: append Datei1 Datei2

if [ -r $1 -a -w $2 ] 
then 
   cat $1 >> $2 
else 
    echo "cannot append" 
fi 

Beim Vergleich von Zeichenketten sollten möglichst die Anführungszeichen (" ... ") verwendet werden, da sonst bei der Ersetzung durch die Shell unvollständige Test-Kommandos entstehen können. Dazu ein Beispiel:

if [ ! -n $1 ] ; then 
   echo "Kein Parameter" 
fi 

Ist $1 wirklich nicht angegeben, wird das Kommando reduziert zu:

if [ ! -n ] ; then .... 
Es ist also unvollständig und es erfolgt eine Fehlermeldung. Dagegen liefert

if [ ! -n "$1" ] ; then 
    echo "Kein Parameter" 
fi 

bei fehlendem Parameter den korrekten Befehl if [ ! -n "" ]

Bei fehlenden Anführungszeichen werden auch führende Leerzeichen der Variablenwerte oder Parameter eliminiert.

Noch ein Beispiel: Es kommt ab und zu vor, daß eine Userid wechselt oder daß die Gruppenzugehörigkeit von Dateien geändert werden muß. In solchen fällen helfen die beiden folgenden Skripts:

!/bin/sh
# Change user-id
#
if [ $# -ne 2 ] ; then
  echo "usage `basename $0` <old id> <new id>"
  exit
fi
find / -user $1 -exec chown $2 {} ";"


#!/bin/sh
# Change group-id
#
if [ $# -ne 2 ] ; then
  echo "usage `basename $0` <old id> <new id>"
  exit
fi
find / -group $1 -exec chgrp $2 {} ";"

8.7.3 case-Anweisung

Diese Anweisung erlaubt eine Mehrfachauswahl. Sie wird auch gerne deshalb verwendet, weil sie Muster mit Jokerzeichen und mehrere Muster für eine Auswahl erlauben

case selector in 
     Muster-1) Kommandofolge 1 ;; 
     Muster-2) Kommandofolge 2 ;; 
             .... 

     Muster-n) Kommandofolge n ;; 
esac 

Die Variable selector (String) wird der Reihe nach mit den Mustern "Muster-1" bis "Muster-n" verglichen. Bei Gleichheit wird die nachfolgende Kommandofolge ausgeführt und dann nach der case-Anweisung (also hinter dem esac) fortgefahren.

Beispiel 1: Automatische Bearbeitung von Quell- und Objekt-Dateien. Der Aufruf erfolgt mit 'compile Datei'.

 
case $1 in 
   *.s) as $1 ;;                       # Assembler aufrufen 
   *.c) cc -c $1 ;;                    # C-Compiler aufrufen 
   *.o) cc $1 -o prog ;;               # C-Compiler als Linker 
     *) echo "invalid parameter: $1";; 
esac

Beispiel 2: Menü mit interaktiver Eingabe:

while :  # Endlosschleife (s. später) 
do 
tput clear  # Schirm löschen und Menütext ausgeben 
   echo " +---------------------------------+"  
   echo " | 0 --> Ende                      |" 
   echo " | 1 --> Datum und Uhrzeit         |" 
   echo " | 2 --> aktuelles Verzeichnis     |" 
   echo " | 3 --> Inhaltsverzeichnis        |" 
   echo " | 4 --> Mail                      |" 
   echo "+----------------------------------+" 
   echo "Eingabe: \c"  # kein Zeilenvorschub 
   read ANTW 
   case $ANTW in 
    0) kill -9 0 ;; # und tschuess 
    1) date ;;  
    2) pwd ;; 
    3) ls -CF ;; 
    4) elm ;; 
    *) echo "Falsche Eingabe!" ;; 
   esac 
done

8.7.4 for-Anweisung

Diese Schleifenanweisung hat zwei Ausprägungen, mit einer Liste der zu bearbeitenden Elemente oder mit den Kommandozeilenparametern.

for-Schleife mit Liste:

for selector in liste 
  do 
  Kommandofolge 
done 

Die Selektor-Variable wird nacheinander durch die Elemente der Liste ersetzt und die Schleife mit der Selektor-Variablen ausgeführt. Beispiele:

for X in hans heinz karl luise # vier Listenelemente do echo $X done Das Programm hat folgende Ausgabe:

 
hans
heinz 
karl 
luise

for FILE in *.txt # drucke alle Textdateien 
  do              # im aktuellen Verzeichnis
  lpr $FILE
done  

for XX in $VAR    # geht auch mit 
  do
  echo $XX  
done  

for-Schleife mit Kommandozeilen-Parametern

for selector 
  do  
  Kommandofolge
done

Die Selektor-Variable wird nacheinander durch die Parameter $1 bis $n ersetzt und mit diesen Werten die Schleife durchlaufen. Es gibt also $# Schleifendurchläufe. Beispiel:

Die Prozedur 'makebak' erzeugt für die in der Parameterliste angegebenen Dateien eine .bak-Datei.

for FF 
  do 
  cp $FF ${FF}.bak 
done 

8.7.5 Abweisende Wiederholungsanweisung (while)

Als Bedingung kann nicht nur eine "klassische" Bedingung (test oder [ ]) sondern selbverständlich auch der Ergebniswert eines Kommandos oder einer Kommandofolge verwendet werden.
while Bedingung
  do 
  Kommandofolge 
  done

Solange der Bedingungsausdruck den Wert 'true' liefert, wird die Schleife ausgeführt. Beispiele:

Warten auf eine Datei (z. B. vom Hintergrundprozeß)

while [ ! -f foo ] 
  do 
  sleep 10  # Wichtig damit die Prozesslast nicht zu hoch wird 
done 

Pausenfüller für das Terminal
Abbruch mit DEL-Taste

while : 
  do 
  tput clear  # BS löschen 
  echo "\n\n\n\n\n" # 5 Leerzeilen 
  banner $(date '+ %T ') # Uhrzeit groß 
  sleep 10  # 10s Pause
done 

Umbenennen von Dateien durch Anhängen eines Suffix

# Aufruf change suffix datei(en) 
if [ $# -lt 2 ] ; then 
   echo "Usage: `basename $0` suffix file(s)"  
else  
   SUFF=$1                 # Suffix speichern  
   shift  
   while [ $# -ne 0 ]     # solange Parameter da sind
     do    
     mv $1 ${1}.$SUFF      # umbenennen 
     shift 
   done 
fi 

Umbenennen von Dateien durch Anhängen eines Suffix
Variante 2 mit for

# Aufruf change suffix datei(en) 
if [ $# -lt 2 ] ; then 
   echo "Usage: `basename $0` suffix file(s)"  
else  
   SUFF=$1                       # Suffix speichern    
   shift
   for FILE
     do    
     mv $FILE ${FILE}.$SUFF      # umbenennen 
     shift 
   done 
fi 

8.7.6 until-Anweisung

Diese Anweisung ist identisch zu einer while-Schleife mit negierter Bedingung. Als Bedingung kann nicht nur eine "klassische" Bedingung (test oder [ ]) sondern selbverständlich auch der Ergebniswert eines Kommandos oder einer Kommandofolge verwendet werden.

until Bedingung
  do 
  Kommandofolge 
done 

Die Schleife wird solange abgearbeitet, bis Bedingungsausdruck einen Wert ungleich Null liefert. Beispiele:

# warten auf Datei foo 
until [ -f foo ] 
  do 
  sleep 10 
done 

oder Warten auf einen Benutzer:

# warten, bis sich der Benutzer hans eingeloggt hat 
TT=`who | grep -c "hans"` 
until [ $TT -gt 0 ] 
  do 
  sleep 10 
  TT=`who | grep -c "hans"` 
done 

# warten, bis sich der Benutzer hans eingeloggt hat 
# Variante 2 - kuerzer
until [ `who | grep -c "hans"` -gt 0 ] 
  do 
  sleep 10 
done 

8.7.7 select-Anweisung

select VAR in Wortliste
  do 
  Kommandofolge
done 

Die Select-Kontrollstruktur bietet eine Kombination aus menügesteuerter Verzweigung und Schleife. Die Wortliste wird als numerierte Liste (Menü) auf dem Standardfehlerkanal ausgegeben. Mit dem PS3-Prompt wird daraufhin eine Eingabe von der Tastatur angefordert. Eine leere Eingabe führt zu einer erneuten Anzeige des Menüs.
Wenn ein Wort aus der Wortliste durch die Eingabe seiner Nummer bestimmt wird, führt die Shell die Kommandofolge aus und stellt dabei das ausgewählte Wort in der Variablen VAR und die die Eingabezeile ist aber in der Variablen REPLY zur Verfügung. Wird in der Eingabezeile keine passende Zahl übergeben, ist VAR leer.

Menüteil und Ausführung der Liste werden so lange wiederholt, bis die Schleife mit break oder return verlassen wird. Es ist möglich, mit Ctrl-D das Menü unmittelbar zu verlassen. Wenn die Wortliste fehlt (nur die Zeile select VAR), werden stattdessen die Positionsparameter $0 ... $9 verwendet. Beispiel:

export PS3="Ihre Wahl: "
select EING in eins zwei drei fertig
  do
  echo "EING=\"$EING\" REPLY=\"$REPLY\""
  if [ "$EING" = "fertig" ] ; then
    break
  fi
done

8.7.8 Weitere Anweisungen

exit

Wie schon bei der interaktiven Shell kann auch eine Shell-Skript mit exit abgebrochen werden. Vom Terminal aus kann mit der DEL-Taste abgebrochen werden, sofern das Signal nicht abgefangen wird (siehe trap).

break [n]

Verlassen von n umfassenden Schleifen. Voreinstellung für n ist 1.

continue [n]

Beginn des nächsten Durchgangs der n-ten umfassenden Schleife, d. h. der Rest der Schleife(n) wird nicht mehr ausgeführt. Voreinstellung für n ist 1.

Interne Kommandos

Etliche der besprochenen Shell-Kommandos starten nicht, wie sonst üblich, einen eigenen Prozeß, sondern sie werden direkt von der Shell interpretiert und ausgeführt. Teilweise ist keine E/A-Umleitung möglich. Etliche Kommandos der folgenden Auswahl wurden schon besprochen. Andere werden weiter unten behandelt. Zum Teil gibt es interne und externe Versionen, z. B. 'echo' (intern) und '/bin/echo' (extern).

breakSchleife verlassen
continueSprung zum Schleifenanfang
echoAusgabe
evalMehrstufige Ersetzung
execÜberlagerung der Shell durch ein Kommando
exitShell beenden
exportVariablen für Subshells bekannt machen
readEinlesen einer Variablen
shiftParameterliste verschieben
trapBehandlung von Signalen

set

set [Optionen] [Parameterliste]
Setzen von Shell-Optionen und Positionsparametern ($1 ... $n). Einige Optionen:

Der Aufruf von set ohne Parameter liefert die aktuelle Belegung der Shell-Variablen. Außerdem kann set verwendet werden, um die Positionsparameter zu besetzen.

set eins zwei drei vier besetzt die Parameter mit $1=eins, $2=zwei, $3=drei und $4=vier. Da dabei auch Leerzeichen, Tabs, Zeilenwechsel und anderes "ausgefiltert" wird (genauer alles, was in der Variablen IFS steht), ist set manchmal einfacher zu verwenden, als die Zerlegung einer Zeile mit cut. Die Belegung der Parameter kann auch aus einer Variablen (z. B. set $VAR) oder aus dem Ergebnis eines Kommandoaufrufs erfolgen. Beispiel:

set `date`                # $1=Fri $2=Apr $3=28 $4=10:44:16 $5=MEZ $6=1999 
echo "Es ist $4 Uhr" 
Es ist 10:44:16 Uhr 
Aber es gibt Fallstricke. Wenn man beispielsweise den Output von "ls" bearbeiten möchte, gibt es zunächst unerklärliche Fehlermeldungen (set: unknown option):
ls -l > foo
echo "Dateiname Laenge"
while read LINE
  do
  set $LINE
  echo $9 $5
done < foo
rm foo
Da die Zeile mit dem Dateityp und den Zugriffsrechten beginnt, und für normale Dateien ein "-" am Zeilenbeginn steht, erkennt set eine falsche Option (z. B. "-rwxr-xr-x"). Abhilfe schafft das Voranstelle eines Buchstabens:
ls -l > foo
echo "Dateiname Laenge"
while read LINE
  do
  set Z$LINE
  echo $9 $5
done < foo
rm foo

Weitere Beispiele: Wenn ein Benutzer eingeloggt ist, wird ausgegeben seit wann. Sonst erfolgt eine Fehlermeldung.

if HELP=`who | grep $1`
then 
  echo -n "$1 ist seit "
  set $HELP
  echo "$5 Uhr eingeloggt."
else 
  echo "$1 ist nicht auffindbar"
fi

Ersetzen der englischen Tagesbezeichung durch die deutsche:

set `date`
case $1 in
  Tue) tag=Die;;
  Wed) tag=Mit;;
  Thu) tag=Don;;
  Sat) tag=Sam;;
  Sun) tag=Son;;
  *)   tag=$1;;
esac
echo $tag $3.$2 $4 $6 $5

8.7.9 Arithmetik in Skripts

Die expr-Anweisung erlaubt das Auswerten von arithmetischen Ausdrücken. Das Ergebnis wird in die Standardausgabe geschrieben. Als Zahlen können 16-Bit-Integerzahlen (beim Ur-UNIX) oder 32-Bit-Integerzahlen (bei LINUX) verwendet werden (bei manchen Systemen auch noch längere Zahlen mit 64 Bit).

Die Bash wurde auch an dieser Stelle erweitert. Mit der doppelten Klammerung $((Ausdruck)) kann man rechnen, ohne ein externes Programm aufzurufen. expr Ausdruck und $((Ausdruck)) beherrschen die vier Grundrechenarten:

+Addition
-Subtraktion
*Multiplikation
/Division
%Divisionsrest (Modulo-Operator)

Die Priorität "Punkt vor Strich" gilt auch hier. Außerdem können Klammern gesetzt werden. Da die Klammern und der Stern auch von der Shell verwendet werden, müssen diese Operationszeichen immer durch den Backslash geschützt werden: '\*', '\(', '\)' . Damit die Operatoren von der Shell erkannt werden, müssen sie von Leerzeichen eingeschlossen werden. Zum Beispiel eine Zuweisung der Summe von A und B an X durch:

X=`expr $A + $B`

oder

X=$((expr $A + $B))

(Backquotes beachten!) Außerdem sind logische Operationen implementiert, die den Wert 0 für 'wahr' und den Wert 1 für 'falsch' liefern.

expr1 | expr2oder
expr1 & expr2 und
expr1 < expr2kleiner
expr1 <= expr2 kleiner oder gleich
expr1 > expr2größer
expr1 >= expr2größer oder gleich
expr1 = expr2gleich
expr1 != expr2 ungleich

Beispiele:

# Mittelwert der positiven Zahlen, die von stdin gelesen werden 
SUM=0 
COUNT=0 
while read $WERT          # lesen, bis zum ^D 
  do 
  COUNT=`expr $COUNT + 1` 
  SUM=`expr $SUM + $WERT` 
done 
AVINT=`expr $SUM / $COUNT` 
echo "Summe: $SUM    Mittelwert: $AVINT" 

#Nimm-Spiel, interaktiv 
ANZ=0 
if test $# -ne 1 
then 
  echo "Usage: $0  Startzahl" 
else 
  echo "NIM-Spiel als Shell-Skript" 
  echo "Jeder Spieler nimmt abwechselnd 1, 2 oder 3 Hoelzer" 
  echo "von einem Haufen, dessen Anfangszahl beim Aufruf fest-" 
  echo "gelegt wird. Wer das letzte Holz nimmt, hat verloren." 
  echo 
  ANZ=$1 
  while [ $ANZ -gt 1 ]                 # bis nur noch 1 Holz
    do                                  # da ist wiederholen
    echo "\nNoch $ANZ Stueck. Du nimmst (1 - 3): \c # Benutzer
    read N
    if [ $N -lt 1 -o $N -gt 3 ] ; then # Strafe bei Fehleingabe
      N=1
    fi
    ANZ=`expr $ANZ - $N`                # Benutzer nimmt N weg
    if [ $ANZ -eq 1 ] ; then           # Computer muß letztes Holz nehmen
      echo "\nGratuliere, Du hast gewonnen"
      exit                              # Prozedur verlassen
    else
      C=`expr \( $ANZ + 3 \) % 4        # Computerzug berechnen
      if [ $C -eq 0 ] ; then
        C=1                             # Wenn 0 Verlustposition
      fi
      echo "Es bleiben $ANZ Stueck. Ich nehme ${C}.\c"
      ANZ=`expr $ANZ - $C`              # Computerzug abziehen 
      echo " Rest $ANZ"
    fi
  done                                  # Dem Benutzer bleibt
  echo "\nIch habe gewonnen"            # das letzte Holz
fi

8.7.10 exec [Kommandozeile]

Ähnlich wie beim Dot-Kommando wird keine Subshell erzeugt, sondern die Kommandozeile in der aktuellen Umgebung ausgeführt. Eine erste Anwendung liegt darin, das aktuelle Programm durch ein anderes zu überlagern. Wenn Sie z. B. die Bourne-Shell als Login-Shell haben, aber lieber mit der C-Shell arbeiten, können sie die Bourne-Shell durch die Kommandozeile

exec /bin/csh

als letzte Zeile in der .profile-Datei durch die C-shell ersetzen (Wenn Sie die C-Shell nur Aufrufen, müssen Sie beide Shells beenden, um sich auszuloggen). Das Kommando entspricht also dem Systemcall exec(). Wird jedoch kein Kommando angegeben, kann die E/A der aktuellen Shell dauerhaft umgeleitet werden. Beispiel:

exec 2>fehler

leitet alle folgenden Fehlerausgaben in die Datei "fehler" um, bis die Umleitung explizit durch

exec 2>-

zurückgenommen wird. Es können bei exec auch andere Dateideskriptoren verwendet werden. Ebenso kann auch die Dateiumleitung einer Eingabedatei erfolgen, z. B.:

exec 3< datei

Danach kann mit read <&3 von dieser Datei gelesen werden, bis die Umleitung mit exec 3<- wieder zurückgenommen wird. Man kann also in Shellskripts durch das Einfügen einer exec-Anweisung die Standardausgabe/-eingabe global für das gesamte Skript umleiten, ohne weitere Änderungen vornehmen zu müssen (eine andere Möglichkeit wäre die oben beschriebene Verwendung von { }).

8.7.11 eval [Argumente]

Das Kommando eval liest seine Argumente, wobei die üblichen Ersetzungen stattfinden, und führt die resultierende Zeichenkette als Kommando aus. Die Argumente der Kommandozeile werden von der Shell gelesen, wobei Variablen- und Kommandoersetzungen sowie Dateinamenersetzung durchgeführt werden. Die sich ergebende Zeichenkette wird anschließend erneut von der Shell gelesen, wobei wiederum die oben genannten Ersetzungen durchgeführt werden. Schließlich wird das resultierende Kommando ausgeführt. Beispiel (Ausgaben fett):

$ A="Hello world!" 
$ X='$A'
$ echo $X
$A
$ eval echo $X
Hello world!

Der Rückgabestatus von eval ist der Rückgabestatus des ausgeführten Kommandos oder 0, wenn keine Argumente angegeben wurden. Ein weiteres Beispiel:

$ cat /etc/passwd | wc -l
76
$ foo='cat /etc/passwd'
$ bar=`| wc -l'
$ $foo $bar                    # Fehler: $bar ist Argument von cmdl
cat: | : No such file or directory
cat: wc: No such file or directory
cat: -l: No such file or directory
$ eval $foo $bar 
76

In diesem Beispiel wird zunächst ein einfaches Kommando gestartet, das die Anzahl der Zeilen der Datei /etc/passwd bestimmt. Anschließend werden die beiden Teile des gesamten Kommandos in die zwei Shell-Variablen foo und bar aufgeteilt. Der erste Aufrufversuch $foo $bar bringt nicht das gewünschte Ergebnis, sondern lediglich einige Fehlermeldungen, da in diesem Fall der Wert von bar als Argument für foo interpretiert wird ('cat' wird mit den Dateien '/etc/passwd', '|', 'wc' und '-l' aufgerufen). Wird jedoch das Kommando eval auf die Argumente $foo und $bar angewendet, werden diese zunächst zur Zeichenkette "cat /etc/passwd | wc -l" ersetzt. Diese Zeichenkette wird dann durch das Kommando eval erneut von der Shell gelesen, die jetzt das Zeichen "|" in der Kommandozeile als Pipesymbol erkennt und das Kommando ausführt. Das Kommando eval wird üblicherweise dazu eingesetzt, eine Zeichenkette als Kommando zu interpretieren, wobei zweifach Ersetzungen in den Argumenten der Kommandozeile vorgenommen werden.
Eine andere Anwendung ist beispielsweise die Auswahl des letzen Parameters der Kommandozeile. Mit \$$# erhält man die Parameterangabe (bei fünf Parametern --> $5). Das erste Dollarzeichen wird von der Shell ignoriert (wegen des '\'), $# hingegen ausgewertet. Durch eval wird der Ausdruck nochmals ausgewertet, man erhält so den Wert des letzten Parameters:

eval \$$#

Aber Vorsicht, das klappt nur bei 1 - 9 Parametern, denn z. B. der 12. Parameter führt zu $12 --> ${1}2. Es lassen sich mit eval sogar Pointer realisieren. Falls die Variable PTR den Namen einer anderen Variablen, z. B. XYZ, enthält, kann auf den Wert von XYZ durch eval $PTR zurückgegriffen werden, z. B. durch eval echo \$$PTR.

8.7.12 trap 'Kommandoliste' Signale

Ausführen der Kommandoliste, wenn eins der angegebenen Signale an den Prozeß (= Shell) gesendet wird. Die Signale werden in Form der Signalnummern oder über ihre Namen (SIGKILL, SIGHUP, ...), getrennt durch Leerzeichen aufgeführt.

Ist die Kommandoliste leer, werden die entsprechenden Signale abgeschaltet. Bei einfachen Kommandos reichen oft auch die Anführungszeichen, um die Shell-Ersetzung zu verhindern.

Signale sind eine Möglichkeit, wie verschiedenen Prozesse, also gerade laufende Programme, miteinander kommunizieren können. Ein Prozeß kann einem anderen Prozeß ein Signal senden (der Betriebssystemkern spielt dabei den Postboten). Der Empfängerprozeß reagiert auf das Signal, z. B. dadurch, daß er sich beendet. Der Prozeß kann das Signal auch ignorieren. Das ist beispielsweise nützlich, wenn ein Shellskript nicht durch den Benutzer von der Tastatur aus abgebrochen werden soll. Mit dem trap-Kommando kann man festlegen, mit welchen Kommandos auf ein Signal reagiert werden soll bzw. ob überhaupt reagiert werden soll.

Neben anderen können folgende Signalnummern verwendet werden:

0SIGKILL Terminate (beim Beenden der shell)
1SIGHUP Hangup (beim Beenden der Verbindung zum Terminal oder Modem)
2SIGINT Interrupt (wie Ctrl-C-Taste am Terminal)
3SIGQUIT Abbrechen (Beenden von der Tastatur aus)
9SIGKILL Kann nicht abgefangen werden - Beendet immer den empfangenden Prozeß
15SIGTERM Terminate (Software-Terminate, Voreinstellung)

Die Datei /usr/include/Signal.h enthält eine Liste aller Signale.

Beispiele:

# Skript sperren gegen Benutzerunterbrechung: 
trap "" 2 3 

oder auch

# Skript sauber beenden 
trap 'rm tmpfile; cp foo fileb; exit' 0 2 3 15 

Bitte nicht das exit-Kommando am Schluss vergessen, sonst wird das Skript nicht beendet. Wiedereinschalten der Signale erfolgt durch trap [Signale]. Ein letztes Beispiel zu trap:

# Automatisches Ausführen des Shellskripts .logoff beim  
# Ausloggen durch den folgenden Eintrag in .profile: 
trap .logoff 0 

8.7.13 xargs

Das xargs-Programm fällt etwas aus dem Rahmen der übrigen in diesem Kapitel behandelten Programme. xargs übergibt alle aus der Standardeingabe gelesenen Daten einem Programm als zusätzliche Argumente. Der Programmaufruf wird einfach als Parameter von xargs angegeben.

xargs Programm [Parameter]

Ein Beispiel soll die Funktionsweise klarmachen:

$ Is *.txt | xargs echo Textdateien:

Hier erzeugt ls eine Liste aller Dateien mit der Endung .txt im aktuellen Verzeichnis. Das Ergebnis wird über die Pipe an xargs weitergereicht. xargs ruft echo mit den Dateinamen von ls als zusätzliche Parameter auf. Der Output ist dann:

Textdateien: kap1.txt kap2.txt kap3.txt h.txt

Durch Optionen ist es möglich, die Art der Umwandlung der Eingabe in Argumente durch xargs zu beeinflussen. Mit der Option -n <Nummer> wird eingestellt, mit wievielen Parametern das angegebene Programm aufgerufen werden soll. Fehlt der Parameter, nimmt xargs die maximal mögliche Zahl von Parametern. Je nach Anzahl der Parameter ruft xargs das angegebene Programm einmal oder mehrmal auf. Dazu ein Beispiel. Vergleich einer Reihe von Dateien nacheinander mit einer vorgegebenen Datei:

ls *.dat | xargs -n1 cmp compare Muster

Die Vergleichsdatei "Muster" wird der Reihe nach mitels cmp mit allen Dateien verglichen, die auf ".dat" enden. Die Option -n1 veranlaßt xargs, je Aufruf immer nur einen Dateinamen als zusätzliches Argument bei cmp anzufügen.
Mit der Option - i <Zeichen> ist es möglich, an einer beliebigen Stelle im Programmaufruf, auch mehrmals, anzugeben, wo die eingelesenen Argumente einzusetzen sind. In diesem Modus liest xargs jeweils ein Argument aus der Standardeingabe, ersetzt im Programmaufruf jedes Vorkommen des hinter - i angegebenen Zeichens durch dieses Argument und startet das Programm. In dem folgenden Beispiel wird das benutzt, um alle Dateien mit der Endung ".txt" in ".bak" umzubenennen.

ls *.txt | cut -d. f1 | xargs -iP mv P.txt P.bak

Das Ganze funktioniert allerdings nur, wenn die Dateien nicht noch weitere Punkte im Dateinamen haben.

xargs ist überall dann besonders notwendig, wenn die Zahl der Argumente recht groß werden kann. Obwohl Linux elend lange Kommandozeilen zuläßt, ist die Länge doch begrenzt. xargs nimmt immer soviele Daten aus dem Eingabestrom, wie in eine Kommandozeile passen und führt dann das gewünschte Kommando mit diesen Parametern aus. Liegen weitere Daten vor, wird das Kommando entsprechend oft aufgerufen. Insbesondere mit dem folgenden Kommando sollte xargs verwendet werden, da find immer den gesamten Dateipfad liefert, also schnell recht lange Argumente weitergibt.

8.7.14 dialog

dialog ist ein Programm, welches innerhalb eines Shellskriptes Interaktionen mit dem Benutzer ermöglicht. Unter einer einheitlichen Oberfläche bietet dialog eine Vielzahl von Interaktionsmöglichkeiten wie z. B.:

dialog bietet im einzelnen folgende Interaktionsmöglichkeiten:

  1. infobox: Übermittlung einer Nachricht an den Benutzer
    Auf der Oberfläche erscheint ein Fenster mit einer Information.
  2. msgbox: Übermittlung einer zu bestätigenden Nachricht an den Benutzer
    Auf der Oberfläche erscheint ein Fenster mit einer Nachricht, die vom Benutzer mit "OK" quittiert werden muß.
  3. yesno: Einfache Benutzerabfrage
    Auf der Oberfläche erscheint ein Fenster mit einer Frage, die vom Benutzer mit "Yes" oder "No" beantwortet werden muß.
  4. menu: Auswahl-Menu für den Benutzer
    Auf der Oberfläche erscheint ein Fenster mit einem Auswahl-Menu, aus dem sich der Benutzer einen Menu-Punkt auswählen kann. Die Auswahl ist auf einen einzigen Menu-Punkt beschränkt.
  5. checklist: Auswahl von mehreren Menu-Punkten
    Auf der Oberfläche erscheint ein Auswahl-Menu, aus dem sich der Benutzer einen oder mehrere Menu-Punkte auswählen kann.
  6. radiolist: Auswahl eines Menu-Punktes mit Vorauswahl
    Auf der Oberfläche erscheint ein Auswahl-Menu, bei dem bereits ein Menu-Punkt ausgewählt ist. Der Benutzer kann den Menu-Punkt akzeptieren oder aber einen anderen Menu-Punkt wählen.
  7. textbox: Anzeige einer Textdatei
    Auf der Oberfläche erscheint ein Fenster mit Scroll-Balken, in dem sich der Benutzer eine Text-Datei ansehen kann.
  8. inputbox: Eingabe von Daten
    Auf der Oberfläche erscheint ein Fenster, in das der Benutzer Daten eingeben kann.

Aufruf:

dialog --title "Fenstertitel" \
       --backtitle "Hintergrundtitel" \
       --[infobox | yesno | menu | ...] \ 
       Fensterinhalt u. -abmaße

aufgerufen. Nach Beendigung von dialog durch den Benuter (Abbruch über Escape-Taste, "OK" / "Yes" bzw. "Cancel" / "No") liefert dialog folgende Informationen zurück:

  1. exit-status von dialog
    Der exit-status von dialog wird an stdout zurückgegeben. Dabei bedeutet ein Rückgabewert von 0 = Beendigung über "OK"/"Yes", 1 = Beendigung über "Cancel"/"No" und 255 = Beendigung über "Escape-Taste"
  2. ausgewählte Menu-Punkte / abgefragte Benutzerdaten
    Die vom Benutzer ausgewählten Menu-Punkte bzw. eingegebenen Benutzerdaten werden auf stderr zurückgegeben.
Beispiel:

dialog --backtitle "$BTITLE" --title "Auswahl-Menu" \
       --menu  "Bitte treffen Sie Ihre Auswahl:" 12 45 3 \
       "Pizza Regina" "heute besonders köstlich zubereitet" \
       "vino chianti" "beschränken wir uns auf das wesentliche" \
       "grappa" "reduced to the max"\
       2 > dialog-dat.tmp

Bei Beendigung von dialog über "OK" wird der ausgewählte Menu-Punkt über den Standard-Fehlerkanal ausgegeben. Da der Standard-Fehlerkanal in die Datei dialog-dat.tmp umgelenkt ist, wird der ausgewählte Punkt demnach in diese Datei geschrieben und kann von dort z. B. mit read AUSWAHL < dialog-dat.tmp gelesen werden.

Beim Einbinden von dialog-Abfragen in Skripte müssen zwei Dinge besonders beachtet werden:

  1. Übergabe von Variablen in Anführungszeichen
    Wenn Titel, Hintergrundtitel bzw. Texte in Form von Variablen übergeben werden, so sind die Variablen in Anführungszeichen zu setzen, z. B.: --title "$TITEL"
  2. Fenstergröße nicht größer als Bildschirmgröße
    Bei jedem Aufruf muß dialog die Fenstergröße mitgeteilt werden: Höhe in Bildschirmzeilen, Breite in Zeichen. Die sich daraus ergebende Fenstergröße darf keinesfalls größer sein als die Bildschirmgröße!

8.8 Beispiele für Shellskripts

Datei verlängern

Weil immer wieder danach gefragt wird: "Wie hänge ich den Inhalt einer Variablen an eine Datei an?":

( cat file1 ; echo "$SHELLVAR" ) > file2

Telefonbuch

Telefonverzeichnis mit Hier-Dokument. Aufruf tel Name [Name ..]:

if [ $# -eq 0 ] 
then 
   echo "Usage: `basename $0` Name [Name ..]" 
   exit 2 
fi 
for SUCH in $* 
do 
    if [ ! -z $SUCH ] ; then 
       grep $SUCH << "EOT" 
       Hans  123456 
       Fritz 234561 
       Karl  345612 
       Egon  456123 
EOT 
   fi 
done  

Kompakte Auflistung des Inhalts eines ganzen Dateibaums

Dies ist ein rekursives Programm. Damit es klappt, muß das folgende Skript dir über PATH erreichbar sein. Aufruf: dir Anfangspfad.

if test $# -ne 1 
then 
    echo "Usage: dir Pfad" 
    exit 2 
fi 
cd $1 
echo 
pwd 
ls -CF 
for I in * 
do 
     if test -d $I 
     then 
        dir $I 
       fi 
done 

Auflisten des Dateibaums in grafischer Form

wobei zur Dateiauswahl alle Optionen des find-Kommandos zur Verfügung stehen. Aufruf z.B. tree . für alle Dateien oder tree . -type d für Directories ab dem aktuellen Verzeichnis.

# find liefert die vollständigen Pfadnamen (temporäre Datei).  
# Mit ed werden die "/"-Zeichen erst zu "|---- " expandiert 
# und dann in den vorderen Spalten aus "|---- " ein Leerfeld  
# "     |" gemacht. 
if test $# -lt 1 
then  
    echo "Usage: tree Pfadname [Pfadname ...] [find-Options]" 
else 
   TMPDAT=$0$$ 
   find $@ -print > $TMPDAT 2>/dev/null 
   ed $TMPDAT << "EOT" >/dev/null 2>/dev/null 
1,$s/[^\/]*\//|---- /g 
1,$s/---- |/     |/g 
w
q
EOT 
   cat $TMPDAT 
    rm $TMPDAT 
   fi 

Mit dem Stream-Editor sed kann das sogar noch kompakter formuliert werden:

if [ $# -lt 1 ] 
then 
    echo "Usage: tree Pfadname [Pfadname ...] [find-Options]" 
else 
    find $@ -print 2>/dev/null | \ 
    sed -e '1,$s/[^\/]*\//|---- /g' -e '1,$s/---- |/     |/g' 
  fi 

Argumente mit J/N-Abfrage ausführen

Das folgende Skript führt alle Argumente nach vorheriger Abfrage aus. Mit "j" wird die Ausführung bestätigt, mit "q" das Skript abgebrochen und mit jedem anderen Buchstaben (in der Regel "n") ohne Ausführung zum nächsten Argument übergegangen. Ein- und Ausgabe erfolgen immer über das Terminal(-fenster), weil /dev/tty angesprochen wird. Das Skript wird anstelle der Argumentenliste bei einem anderen Kommando eingesetzt, z. B. Löschen mit Nachfrage durch rm $(pick *)

# pick - Argumente mit Abfrage liefern 
for I ; do 
   echo "$I (j/n)? \c" > /dev/tty
   read ANTWORT 
   case $ANTWORT in 
     j*|J*) echo $I ;; 
     q*|Q*) break ;; 
   esac 
done </dev/tty  

Sperren des Terminals während man kurz weggeht

Nach Aufruf von lock wird ein Kennwort eingegeben und das Terminal blockiert. Erst erneute Eingabe des Kennwortes beendet die Prozedur. Die Gemeinheit dabei ist, daß sich bei jeder Fehleingabe die Wartezeit verdoppelt.

echo "Bitte Passwort eingeben: \c"
stty -echo  # kein Echo der Zeichen auf dem Schirm
read CODE 
stty echo
tput clear  # BS loeschen 
trap "" 2 3 
banner " Terminal " 
banner " gesperrt " 
MATCH="" 
DELAY=1 
while [ "$MATCH" != "$CODE" ] 
do 
     sleep $DELAY 
     echo "Bitte Passwort eingeben: \c" 
     read MATCH 
     DELAY='expr $DELAY \* 2`  # doppelt so lange wie vorher 
done 
echo 

Dateien im Pfad suchen

Das folgende Skript bildet das Kommando "which" nach. Es sucht im aktuellen Pfad (durch PATH spezifiziert) nach der angegebenen Datei und gibt die Fundstelle aus. An diesem Skript kann man auch eine Sicherheitsmaßnahme sehen. Für den Programmaufruf wird der Pfad neu gesetzt, damit nur auf Programme aus /bin und /usr/bin zugegriffen wird. Bei Skripts, die vom Systemverwalter für die Allgemeinheit erstellt werden, sollte man entweder so verfahren oder alle Programme mit absolutem Pfad aufrufen.

#!/bin/sh 
# Suchen im Pfad nach einer Kommando-Datei 
OPATH=$PATH 
PATH=/bin:/usr/bin 
if [ $# -eq 0 ] ; then 
   echo "Usage: which kommando" ; exit 1 
fi 
for FILE 
do 
   for I in `echo $OPATH | sed -e 's/^:/.:/' -e 's/::/:.:/g \ -e 's/:$/:./'` 
   do 
      if [ -f "$I/$FILE" ] ; then 
        ls -ld "$I/$FILE" 
       fi 
   done 
done 

Berechnung von Primfaktoren einer Zahl:

echo "Zahl eingeben: \c"; read ZAHL 
P=2 
while test `expr $P \* $P` -le $ZAHL; do 
     while test `expr $ZAHL % $P` -ne 0; do 
        if test $P -eq 2; then 
           P=3 
       else 
            P=`expr $P + 2` 
       fi 
    done 
    ZAHL=`expr $ZAHL / $P` 
    echo $P 
done 
if test $ZAHL -gt 1; then 
   echo $ZAHL 
fi 
echo "" 

Berechnung des Osterdatums nach C.F. Gauss

if [ $# -eq 0 ] ; then 
    echo "Osterdatum fuer Jahr: \c"; read JAHR 
else 
   JAHR="$1" 
fi 
G=`expr $JAHR % 19 + 1` 
C=`expr $JAHR / 100 + 1` 
X=`expr \( $C / 4 - 4 \) \* 3` 
Z=`expr \( $C \* 8 + 5 \) / 25 - 5` 
D=`expr $JAHR \* 5 / 4 - $X - 10` 
E=`expr \( $G \* 11 + $Z - $X + 20 \) % 30` 
if test $E -lt 0; then 
   $E=`expr $E + 30` 
fi 
if [ $E -eq 25 -a $G -gt 11 -o $E -eq 24 ] ; then 
   E=`expr $E + 1` 
fi 
TAG=`expr 44 - $E` 
if [ $TAG -lt 21 ] ; then 
   TAG=`expr $TAG + 30` 
fi 
TAG=`expr $TAG + 7 - \( $D + $TAG \) % 7` 
if [ $TAG -gt 31 ] ; then 
   TAG=`expr $TAG - 31` 
   MON=4 
else 
   MON=3 
fi 
echo "Ostern $JAHR ist am ${TAG}.${MON}.\n"  

Statt des expr-Befehls kann bei der Bash auch das Konstrukt $(( ... )) verwendet werden. Das Programm sieht dann so aus:

if [ $# -eq 0 ] ; then 
    echo "Osterdatum fuer Jahr: \c"; read JAHR 
else 
   JAHR="$1" 
fi 
G=$(($JAHR % 19 + 1)) 
C=$(($JAHR / 100 + 1)) 
X=$((\( $C / 4 - 4 \) \* 3)) 
Z=$((\( $C \* 8 + 5 \) / 25 - 5)) 
D=$(($JAHR \* 5 / 4 - $X - 10)) 
E=$((\( $G \* 11 + $Z - $X + 20 \) % 30)) 
if test $E -lt 0; then 
   $E=$(($E + 30)) 
fi 
if [ $E -eq 25 -a $G -gt 11 -o $E -eq 24 ] ; then 
   E=$(($E + 1)) 
fi 
TAG=$((44 - $E)) 
if [ $TAG -lt 21 ] ; then 
   TAG=$(($TAG + 30)) 
fi 
TAG=$(($TAG + 7 - \( $D + $TAG \) % 7)) 
if [ $TAG -gt 31 ] ; then 
   TAG=$(($TAG - 31)) 
   MON=4 
else 
   MON=3 
fi 
echo "Ostern $JAHR ist am ${TAG}.${MON}.\n"  

Wem die Stunde schlägt

Wer im Besitz einer Soundkarte ist, kann sich eine schöne Turmuhr, einen Regulator oder Big Ben basteln. Die folgende "Uhr" hat Stunden- und Viertelstundenschlag. Damit das auch klappt, ist ein Eintrag in der crontab nötig:

0,15,30,45 * * * * /home/sbin/turmuhr

So wird das Skript turmuhr alle Viertelstunden aufgerufen. Es werden zwei Sounddateien verwendet, hour.au für den Stundenschlag und quater.au für den Viertelstundenschlag. Statt des Eigenbau-Programms audioplay kann auch der sox verwendet werden oder man kopiert die Dateien einfach nach /dev/audio. Die Variable VOL steuert die Lautstärke.

#!/bin/sh
BELL=/home/local/sounds/hour.au
BELL1=/home/local/sounds/quater.au
PLAY=/usr/bin/audioplay
VOL=60
DATE=`date +%H:%M`
MINUTE=`echo $DATE | sed -e 's/.*://'`
HOUR=`echo $DATE | sed -e 's/:.*//'`

if [ $MINUTE = 00 ]
then
	COUNT=`expr \( $HOUR % 12 + 11 \) % 12`
	BELLS=$BELL
	while [ $COUNT != 0 ];
	do
		BELLS="$BELLS $BELL"
		COUNT=`expr $COUNT - 1`
	done
	$PLAY -v $VOL -i $BELLS
elif [ $MINUTE = 15 ]
then 	$PLAY -v $VOL -i $BELL1
elif [ $MINUTE = 30 ]
then 	$PLAY -v $VOL -i $BELL1 $BELL1
elif [ $MINUTE = 45 ]
then 	$PLAY -v $VOL -i $BELL1 $BELL1 $BELL1
else
	$PLAY -v $VOL -i $BELL1
fi

8.9 Shell-Funktionen

Ab System V können in der Bourne-Shell auch Funktionen definiert werden - eine weitere Strukturierungsmöglichkeit. Funktionen können in Shellskripts, aber auch interaktiv definiert werden. Sie lassen sich jedoch nicht wie Variablen exportieren. Sie werden nach folgender Syntax definiert:

  Funktionsname () 
    {  
    Kommandofolge 
    } 

Steht die schließende geschweifte Klammer nicht in einer eigenen Zeile, gehört ein Strichpunkt davor. Die runden Klammern hinter dem Funktionsnamen teilen dem Kommandozeileninterpreter der Shell mit, daß nun eine Funktion definiert werden soll (und nicht ein Kommando Funktionsname aufgerufen wird). Es kann keine Parameterliste in den Klammern definiert werden.

Der Aufruf der Shellfunktion erfolgt durch Angabe des Funktionsnamens, gefolgt von Parametern (genauso wie der Aufruf eines Skripts). Die Parameter werden innerhalb der Funktion genauso, wie beim Aufruf von Shellskripts über $1 bis $nn angesprochen. Ein Wert kann mit der Anweisung 'return <Wert>' zurückgegeben werden, er ist über den Parameter $? abfragbar. Beispiel:

isdir () # testet, ob $1 ein Verzeichnis ist 
   { 
   if [ -d $1 ] ; then 
     echo "$1 ist ein Verzeichnis" # Kontrolle zum Test
     return 0
   else
     return 1   
    fi 
   } 

Im Gegensatz zum Aufruf von Shell-Skripts werden Funktionen in der aktuellen Shell ausgeführt und sie können bei der Boune-Shell nicht exportiert werden; die Bash erlaubt dagegen das Exportieren mit export -f. Das folgende Beispiel illustriert die Eigenschaften von Shellfunktionen.
Die folgende Funktion gibt den Eingangsparameter in römischen Zahlen aus. Dabei wird die Zahl Schritt für Schritt in der Variablen ZAHL zusammengesetzt. Würde man der Funktion ZIFF ein Skript verwenden, ginge das nicht, da sich der Wert von ZAHL ja nicht aus dem aufgerufenen Skript heraustransportieren ließe.

#
# Ausgabe des Eingangsparameters $1 in roemischen Ziffern 
#
ZIFF ()  
   # Funktion zur Bearbeitung einer einstelligen Ziffer $1 
   # Einer-, Zehner-, Hunderterstelle unterscheiden sich nur 
   # durch die verw. Zeichen $2: Einer, $3: Fuenfer, $4: Zehner 
   { X=$1 
   if test $X -eq 9; then 
      ZAHL=${ZAHL}$2$4 
   elif test $X -gt 4; then 
      ZAHL=${ZAHL}$3 
      while test $X -ge 6; do 
         ZAHL=${ZAHL}$2 ; X=`expr $X - 1` 
      done 
   elif test $X -eq 4; then 
    ZAHL=${ZAHL}$2$3 
  else 
      while test $X -gt 0; do 
          ZAHL=${ZAHL}$2 ; X=`expr $X - 1` 
      done 
    fi 
    } 
if test $# -eq 0; then 
      echo "Usage: roem Zahl"; exit 
fi 
XX=$1 
while test $XX -gt 999; do 
      ZAHL=${ZAHL}"M"; XX=`expr $XX - 1000` 
done 
ZIFF `expr $XX / 100` C D M 
XX=`expr $XX % 100` 
ZIFF `expr $XX / 10` X L C 
ZIFF `expr $XX % 10` I V X 
echo "$ZAHL \n" 

8.10 Weitere Beispiele

Die folgenden Beispiele stammen fast alle aus der täglichen Arbeit und helfen bei der Administration eines Systems. Bei einigen Beispielen wurden systemspezifische Teile des Originals weggelassen.

Eingabe ohne RETURN-Taste

Soll nur eine Taste zur Bestätigung gedrückt werden, z. B. 'j' oder 'n', läßt sich das mit dem read-Kommando nicht realisiert werden, da die Eingabe immer mit der Return-Taste abgeschlossen werden muß. Um eine Eingabe ohne Return zu realisieren sind zwei Dinge nötig: Das Umschalten des Terminals wird mit stty raw erreicht. Für die Eingabe wird das dd-Kommando (Disk Dump) zweckentfremdet. dd liest von der Standardeingabe und schreibt auf die Standardausgabe. Für die geplante Aktion werden die Parameter count (Anzahl zu lesender Blöcke) und bs (Blocksize) verwendet. count enthält die Anzahl der einzulesenden Zeichen, bs wird auf 1 gesetzt. Der entstehende Programmteil sieht dann so aus:
echo "Alles Loeschen (j/n)\c"
stty raw -echo
INPUT=`dd count=1 bs=1 2> /dev/null`
stty -raw echo
echo $INPUT
case $INPUT in 
  j|J) echo "Jawoll" ;; 
  n|N) echo "Doch nicht" ;; 
  *)   echo "Wat nu?" ;;
esac 

Shell-Skript zum Eintragen eines neuen Benutzers

Dieses Skript soll nur zeigen, was beim Anlegen eines Benutzers alles notwendig ist. Normalerweise exististieren bereits entsprechende Programme oder Skripten (z. b. useradd oder das Administationstool YaST).

# Neuen Benutzer eintragen, Home-Directory erzeugen, 
# .profile kopieren 
PWDF=/etc/passwd 
GRPF=/etc/group 
STDPROF=.profile 
if test $# -ne 3 ; then 
     echo "Usage: newuser Login-Name Gruppe Voller Name" ; exit 1 
fi 
NAME=$1 ; GRUPPE=$2 
HOME=/home/$1 
case $NAME in 
      ?????????*) echo "Name ist zu lang" ; exit 1 ;; 
  *[A-Z]*)    echo "Name enthaelt Grossbuchstaben" ; exit 1 ;; 
esac 
if grep "^$NAME:" $PWDF ; then 
     echo "Name schon vorhanden" ; exit 1 
fi 
UID=`tail -1 $PWDF | cut -f3 -d:` 
UID=`expr $UID + 1` 
if grep ".*:.*:$UID:" $PWDF ; then 
     echo "Passwortdatei ist nicht sortiert" : exit 1 
fi 
if grep ".*:.*:$GRUPPE:" $GRPF ; then 
 : 
else 
     echo "Gruppen-Nummer nicht vorhanden" ; exit 1 
fi 
mkdir $HOME 
cp $STDPROF $HOME 
chown $UID $HOME $HOME/$STDPROF 
chgrp $GRUPPE $HOME $HOME/$STDPROF 
echo $NAME::$UID:$GRUPPE:$3:$HOME:/bin/sh >>$PWDF 
echo $NAME::$UID:$GRUPPE:$3:$HOME:/bin/sh 

Mit useradd geht das ganze viel einfacher, aber hier geht es ja um ein Beispiel. Das folgende Skript vereinfacht die Anwendung von useradd, das relativ viele Parameter besitzt.

#!/bin/sh
# Shell-Script zum Anlegen eines Benutzers
# Aufruf: newuser username gruppe Bemerkung
# 
if [ $# != 3 ]; then
  echo "Usage: `basename $0` username gruppe Bemerkung"
  exit 2
fi
GRUP=$2
USR=$1
BEM=$3 
GRP=/etc/group
PASS=/etc/passwd
SHAD=/etc/shadow
SKEL=/etc/skel
cp $PASS ${PASS}.bak
cp $SHAD ${SHAD}.bak
if [ "$USR" != "" ] ; then
  echo "--- Anlegen User: $USR, Bemerkung: $BEM ---" 
  if `grep -s $GRUP $GRP >/dev/null` ; then
    :
  else
    echo "--- Gruppe $GRUP unbekannt ---"
    exit 2
  fi
  if `grep -s $USR $PASS` ; then
    echo "+++ User existiert bereits +++" 
    exit 2
  fi
  /usr/sbin/useradd -d /home/${USR} -g $GRUP -s /bin/sh -c "$BEM" -m -k $SKEL $USR
  if [ $? -eq 0 ] ; then
    echo "+++ $USR angelegt +++"
  else
    echo "--- Fehler beim Anlegen von $USR ---"
  fi
  while [ -f /etc/ptmp ]; do
    sleep 1
  done
fi
echo "--- Fertig ---" 

Löschen von Usern en bloc

Zum Löschen der User einer Schulungsgruppe wird anstelle des "Realname" in der Datei /etc/passwd die Kursnummer eingetragen (z. B. "K1234"). Durch das folgende Skript können durch Angabe eines Suchbegriffs die Teilnehmer eines Jahrgangs komplett als User gelöscht werden. Das eigentliche Löschen wird dabei von einen zweiten Skript übernommen.
# rm-all - Löschen User nach Stichwort in der Passwort-Datei 
if [ $# -ne 1 ]; then 
   echo "Aufruf: $0 Suchbegriff" 
   exit 1 
fi 
# Erst mal testweise die Kandidaten fürs Löschen ausgeben 
KANDIDATEN=`grep "$1" /etc/passwd | sed -e '1,$s/:.*//'` 
echo "$KANDIDATEN loeschen (j/n)? \c" 
read ANTWORT 
if [$ANTWORT != "j" ]; then 
   echo "Abgebrochen!" 
   exit 1 
fi 
# jetzt wird wirklich gelöscht 
/bin/rmuser $KANDIDATEN

Löschen von Usern

Das Skript "rmuser" ist etwas aufwendiger, da einige Sicherheitstest notwendig sind. Auf den meisten Systemen gibt es ein Programm "userdel", das den Benutzer aus /etc/passwd, /etc/group und /etc/shadow löscht. Bei anderen Anlagen könnte man das Gleiche mit einem Skript erledigen. Das Löschprotokoll wird per Mail an root geschickt.

# rmuser - löschen user 
if [ $# -lt 1 ]; then 
    echo "Aufruf: $0 user [user ....]" 
    exit 1 
fi  
{
# Sicherheitskopien anlegen 
cp /etc/passwd /etc/passwd.bak 
cp /etc/shadow /etc/shadow.bak 

# Jetzt wird es ernst 
while [ $# -gt 0 ] ; do 
   USR=$1 
   N=`grep -c "$USR" /etc/passwd` 
     if [ $N -ne 1 ]; then 
      echo "$USR nicht vorhanden oder doppelt" 
   else 
     if [ `grep $USR /etc/passwd | cut -d: -f3` -lt 100 ]; then
       echo "$USR hat eine ID kleiner 100, nicht geloescht"
     else
       echo "*** Loeschen User: $USER" 
       # Homedir aus /etc/passwd extrahieren
       HOM=`grep $USR /etc/passwd | cut -f6 -d:`
       rm -rf $HOM 2>&1 
       find / -user $USR -exec rm -f {} ";" 2>&1 
       /usr/sbin/userdel $USR 
       echo "--- $USR erledigt ..." 
       echo "" 
     fi
    fi 
    shift 
 done 
} | mailx -s "User-Loeschung" root 

Man könnte das Skript noch um eine Prüfung auf weitere nicht zu löschende User ergänzen. Außerdem sollte man das Ganze erst starten, wenn sonst kein User mehr im System ist. Da das Skript bei vielen User recht lange dauert, ist es am günstigsten, es als Batch im Hintergrund zu starten.

Staendig kontrollieren, wer sich ein- und ausloggt

#!/bin/sh 
# PATH=/bin:/usr/bin 
NEW=/tmp/WW1.WHO 
OLD=/tmp/WW2.WHO 
>$OLD                              # OLD neu anlegen 
while :                              # Endlosschleife 
do 
   who >$NEW 
   diff $OLD $NEW 
   mv $NEW $OLD 
    sleep 60 
done 

Speicherbelegung

Das Skript 'lsum' berechnet aus dem ls-Kommando den Gesamtspeicherplatz der ausgewählten Dateien. Einfacher geht es aber mit 'du'.

#!/bin/sh 
# Calculate the amount of space used by the specified files 
# Default is the actual directory 
SUM=0 
TMPF=$HOME/$0$$ 
ls -l $* >$TMPF 
while read D1 D2 D3 D4 D5 REST ; do    # lesen aus TMPF
                                        # Feld 5 enthaelt Groesse
     SUM=`expr $SUM + 0$D5 / 1024` 
done < $TMPF 
echo "$SUM KBytes" 
rm $TMPF 

Preisfrage: Warum funktioniert folgende Variante nicht?

#!/bin/sh 
SUM=0 
ls -l | while read D1 D2 D3 D4 D5 REST ; do 
          SUM=`expr $SUM + 0$D5 / 1024` 
        done
echo "$SUM KBytes" 

Optionen ermitteln

Oft ist es wünschenswert, wenn man bei Shellskripts auch Opionen angeben kann (auf dieselbe Weise, wie bei Programmen). Die Optionen bestehen aus einem Buchstaben mit dem '-' davor. Bei manchen Optionen folgt auch eine durch die Option spezifizierte Angabe (z. B. beim 'pr'-Kommando der Header). Das folgende Fragment zeigt, wie sich solche Optionen behandeln lassen. Die einzige Einschränkung besteht darin, daß sich mehrere Optionen nicht zusammenziehen lassen ('-abc' statt '-a -b -c' geht also nicht). Damit alle Optionen über eine Schleife abgehandelt werden können, wird mit 'shift' gearbeitet. Wie üblich, können nach den Optionen noch Dateinamen folgen. Ein Testaufruf könnte lauten:

otest -a -p "Parameter" -c *.txt

#!/bin/sh 
# Bearbeiten von Optionen in Shellskripts 
# Beispiel: -a -b -c  als einfache Optionen 
#           -p <irgend ein Parameter> als "Spezial-Option" 
READOPT=0                     
while [ $READOPT -eq 0 ] ; do     # solange Optionen vorhanden 
  case $1 in 
      -a) echo "Option a"
          shift ;;
      -b) echo "Option b"
          shift ;; 
      -c) echo "Option c"
          shift ;;
      -p) PARAM=$2 ; shift  # Parameter lesen
          echo "Option p: $PARAM"
          shift ;; 
       *) if `echo $1 | grep -s '^-'` ; then # Parm. beginnt mit '-' 
            echo "unknown option $1"
            shift
          else
            READOPT=1  # Ende Optionen, kein shift! 
  esac
done
echo "Restliche Parameter : $*" 

'Rename'-Kommando

So mächtig das 'mv'-Kommando auch ist, es bietet keine Möglichkeit, eine ganze Reihe von Dateien nach gleichem Schema umzubenennen (z. B. 'kap??.txt' --> 'kapitel??.txt'). Das folgende Skript leistet das gewünschte. Die ersten beiden Parameter enthalten den ursprünglichen Namensteil (z. B. 'kap') und den neuen Namensteil (z. B. 'kapitel'). Danach folgt die Angabe der zu bearbeitenden Dateien. Wenn die Zieldatei schon existiert, wird nicht umbenannt, sondern eine Fehlermeldung ausgegeben.

#!/bin/sh 
# Alle Dateien umbennen, die durch $3 - $n spezifiziert werden 
# dabei wird der String $1 im Dateinamen durch $2 ersetzt, 
# wobei auch regulaere Ausdruecke erlaubt sind 
if [ $# -lt 3 ] ; then 
   echo 'Usage: ren <old string> <new string> files' 
   echo 'Example:  ren foo bar *.foo  renames all files' 
   echo '             *.foo ---> *.bar' 
   exit 1 
fi 
S1=$1 ; shift 
S2=$1 ; shift 
while [ $# -gt 0 ]; do 
   for OLDF in $1 ; do 
      NEWF=`echo $OLDF | sed -e "s/${S1}/${S2}/"` 
      if [ -f $NEWF ] ; then 
           echo "$NEWF exists, $OLDF not renamed" 
       else 
        echo "renaming $OLDF to $NEWF" 
        mv $OLDF $NEWF 
      fi 
   done 
   shift 
done 

Löschen von Prozessen

Das Löschen eines Prozesses über die Prozeßnummer ist insofern lästig, als man die Nummer meist erst per 'ps'-Kommando ermitteln muß. Das folgende Skript erlaubt die Angabe eines beliebigen Strings (z. B. User- oder Programmname) und löscht alle Prozesse, die diesen String in der Ausgabe des 'ps'-Kommandos enthalten. Bei jedem Prozeß wird dann interaktiv gefragt, ob er gelöscht werden soll. Vor dem eigentlichen Löschen wird noch einmal nachgesehen, ob der Prozeß noch existiert, denn er könnte ja als Kindprozeß eines vorher gelöschten Prozesses schon gekillt worden sein. Kleiner Schönheitsfehler: Der zap-Prozess taucht auch in der Liste auf. Wie könnte man das beseitigen?

#!/bin/sh 
# Loeschen Prozess durch Angabe eines Musters 
TMPF=$HOME/zap..$$ 
if [ $# -lt 2 ]; then 
   echo "Usage: zap -Signal Muster"; exit 1 
fi 
SIG=$1 
# alle Prozesse nach Stichwort durchsuchen
ps auwx | grep $2 | sed -e '1,$s/  */ /g' > $TMPF 
while read X 
   do 
   set $X 
   echo "$X (j/n)? \c" 
    read ANTWORT </dev/tty 
   case $ANTWORT in 
      j*|J*)  X=`ps auwx | grep -c $2` 
                  if [ $X -ne 0 ]; then 
              kill $SIG $2 
              fi ;; 
    q*|Q*)  break  ;; 
   esac 
done <$TMPF 
rm $TMPF 

Backup

Das folgende Skript zeigt, wie man den Backup eines Verzeichnisses automatisieren kann. Normalerweise wird ein incrementeller Backup erzeugt, d. h. es werden nur die Dateien gesichert, die nach dem letzten Backup geändert oder neu erstellt wurden. Will man einen vollen Backup, muß man den Parameter '-a' (für 'all') angeben. Um festzustellen, welche Dateien neu sind, wird im entsprechenden Verzeichnis eine leere Datei namens '.lastbackup' angelegt bzw. deren Zugriffsdatum aktualisiert. Nach deren Änderungsdatum richtet sich die Auswahl der zu sichernden Dateien. Beim allerersten Backup muß der Parmeter '-a' angegeben werden. Die Angabe des Backup-Devices (/dev/tape) muß eventuell an die lokalen Gegebenheiten angepaßt werden. Die Angabe '-depth' beim find-Kommando sorgt dafür, daß die Dateien "von unten" her bearbeitet werden (nötig für das cpio-Kommando).

#!/bin/sh 
# Taeglicher Backup, als Parameter wird ein 
# Verzeichnis angegeben 
if [ $# -eq 0 ] ; then 
   echo "Aufruf: $0 [-a] <directory>" 
   echo "-a  Alles sichern (sonst incrementell)" 
   exit 
fi 
echo "\nBand einlegen und [RETURN] druecken!" 
read DUMMY 
if [ "$1" = "-a" -o "$1" = "-A" ] ; then 
   if [ -d "$2" ] ; then 
      echo "Komplett-Backup von $2 ..." 
      MARKER=$2/.lastbackup 
      find $2 -depth -print | cpio -ovc >/dev/tape 
      touch $MARKER 
      echo "Fertig!" 
   else 
     echo "$2 ist kein Verzeichnis!" 
     exit 
   fi 
else 
   if [ -d "$1" ] ; then 
      echo "Inkrementeller Backup von $1 ..." 
      MARKER=$1/.lastbackup 
      find $1 -newer $MARKER -print | cpio -ovc >/dev/tape 
      touch $MARKER 
      echo "Fertig!" 
   else 
     echo "$2 ist kein Verzeichnis!" 
     exit 
   fi 
fi 
echo "\nBand herausnehmen\n" 

Zwischen-Backup

Manchmal ist es neben dm "normalen" Backup auf Band auch günstig, einen weiteren Backup auf der Platte anzulegen - z. B. für den Fall, daß jemand irrtümlich einen Datei löscht. Die kann dann mit ein paar Tastenbetätigungen wieder hervorgezaubert werden. Das folgende Skript sichert alle Dateien des WWW-Servers, die seit der letzten Sicherung hinzugekommen sind. Als Zeitmarkierung dient das Datei-Zugriffsdatum einer Datei namens ".lastcheck". Bei jedem Backup wird das Datum der Datei per touch-Befehl aktualisiert. Man sieht auch schön, wie sich das date-Kommando zum erzeugen von einzigartigen Dateinamen verwenden läßt. Statt die Ausgabe zu unterdrücken, könnte man sie auch per Mail an den WWW-Admin schicken (dann sollte tar etwas auskunftsfreudiger gemacht werden).
#!/bin/sh
#
# Inkrementelles Sichern aller Dateien des WWW-Servers
#
{
TMPFILE="/tmp/check.$$"
TIMESTAMP="`date +%y%m%d`"
DIRECTORY="/home/httpd/htdocs"
WWWARCHIVE="/home/wwwarchive"
cd $DIRECTORY
find . -newer .lastcheck -print >$TMPFILE 2>/dev/null
touch .lastcheck
if [ `cat $TMPFILE | wc -l` -gt 0 ]
then
  tar cf /$WWWARCHIVE/backup.$TIMESTAMP.tar $DIRECTORY
  gzip /$WWWARCHIVE/backup.$TIMESTAMP.tar
  chown wwwadm.staff /$WWWARCHIVE/backup.$TIMESTAMP.tar.gz
  chmod 660 /$WWWARCHIVE/backup.$TIMESTAMP.tar.gz
fi
rm $TMPFILE
} > /dev/null 2>&1

eval-Anwendung

Endlich eine Anwendung für das eval-Kommando, auf die jeder schon gewartet hat. Das folgenden Fragment zeigt, wie man in der Bourne-Shell die Ausgabe des aktuellen Verzeichnisses im Prompt realisieren kann. Einziger Nachteil: Zum Logoff muß man Ctrl-C und Ctrl-D drücken.

while true ; do 
  echo "`pwd`:$PS1\c" 
  read KDO 
   eval $KDO 
done 

Ausgaben aus cron- und at-Jobs auf ein Terminal

Wie schicke ich aus einem cron- oder at-Job etwas an den Benutzer, sofern er eingeloggt ist? Das Problem liegt darin, daß der Job nicht wissen kann, an welchem Terminal der User sitzt. Also muß zunächst per 'who'-Kommando festgestellt werden, ob der Adressat eingeloggt ist und an welchem Terminal er sitzt. Dann kann eine Nachricht nach folgendem Schema an den User geschickt werden.

#!/bin/sh 
# Nachricht ($2-$nn) an User ($1) senden, sofern dieser eingeloggt ist
NAM="$1" 
shift
MSG="$@"
if who | grep -q $NAM ; then        # User eingeloggt?
  write $NAM < $MSG
fi 

Rundruf

Nach dem gleichen Schema kann man ein "wall für Arme" realisieren. Es werden jedoch im Gegensatz zum "echten" 'wall' nur die Benutzer erreicht, die Ihren Mitteilungsempfang offen haben.

who | while read USR REST ; do     # für alle aktiven User
  banner"Teatime!" | write $USR
done 

Das "bang"-Skript

Manche Kommandos möchten einen Dateinamen als Argument und nicht die Daten über die Standardeingabe haben. Für solche Kommandos muß man öfter eine temporäre Datei anlegen, die nach dem Kommandoaufruf wieder gelöscht wird. Ein typisches Beispiel dafür ist ein Vergleich zweier Dateien. Das Kommando "comm" benötigt die Namen der beiden Dateien. Wenn die Dateien vor dem Vergleich sortiert werden müssen, sieht der konventionelle Ansatz folgendermaßen aus:

sort file1 >/tmp/file1.sor 
sort file2 >/tmp/file2.sor 
comm /tmp/file1.sor /tmp/file2.sor 
tmp/file[12].sor 

Das folgende Skript namens "!" führt ein Kommando aus, das als Parameter übergeben wird, und liefert den Namen einer temporären Datei zurück, in dem das Ergebnis der Kommandoausführung gespeichert wurde. Das wäre aber noch kein Fortschritt. Der Trick ist, daß die temporäre Datei nach fünf Minuten automatisch gelöscht wird. Unsere Aufgabe läßt sich dann als Einzeiler schreiben:

comm `! sort file1` `! sort file2`

Die Schwierigkeit beim Schreiben von "!" liegt darin, daß die aufrufende Shell ("comm"-Kommando) normalerweise wartet, bis das aufgerufene Skript ("! sort ...") terminiert - aber dieses soll ja fünf Minuten warten und dann die Datei löschen. In diesem Fall wäre aber die temporäre Datei schon wieder leer. Die Lösung zeigt die Auflistung von "!":

#!/bin/sh 
# Kommado ausführen,  
# Ergebnis in temp. Datei,  
# Dateiname zurückgeben 
TEMPDIR=/tmp 
TEMPF=$TEMPDIR/BANG..$$ 
# Trap zum Löschen von TEMPF 
trap 'rm -f $TEMPF; exit' 1 2 15 
# Falls kein Kommando, nur Dateinamen liefern 
if [ $# -eq 0 ] ; then 
  echo "Usage: `basename $0` command [args]" 1>&2 
  echo $TEMPF 
  exit 1 
fi 
# Kommando ausführen, Dateiname liefern 
"$@" > $TEMPF 
echo $TEMPF 
# jetzt kommt der Trick: 
exec >&-  # Standardausgabe schließen, rufende Shell wartet nicht!
( sleep 300 ; rm -f $TEMPF ) &  # Löschauftrag --> Hintergrund
exit 0 

Rekursives Suchen in Dateien

#!/bin/sh 
# Rekursives Skript zum Suchen und Ersetzen von Text-Pattern 
#
PROGNAME=`basename $0` 
TEMPDAT=/tmp/`basename $0.$$` 

if test $# -lt 4; then 
   echo "$PROGNAME : Recursive search-and-replace-skript." 
   echo "usage : $PROGNAME <start-dir> <file-expression> \ 
<search-pattern> <replace-pattern>" 
   echo "example : $PROGNAME . \"*.html\" \"abc\" \"xxx\" " 
   echo "Both patterns use ex/vi-syntax !" 
else 
   find $1 -type f -name "$2" -print > $TEMPDAT 
   for NAME in `cat $TEMPDAT` 
     do 
     echo -n "Processing $NAME.." 
     ex $NAME << EOT > /dev/null 
1,\$ s/$3/$4/g 
wq 
EOT 
     echo "done."
   done 
   rm $TEMPDAT 
fi 

Datei mit Verzeichnisinfo anlegen

Bei FTP-Servern ist es üblich, eine Datei mit einen Verzeichnislisting in den einzelnen Download-Verzeichnissen anzulegen. Interessenten können sich erst einmal diese Datei holen und in Ruhe durchsehen. Das folgende Skript erzeugt zwei VArianten: 'ls-lR' mit der Verzeichnisinfo und dieselbe Datei gepackt ('ls-lR.Z').
#!/bin/sh
# Generieren ls-Rl und ls-RL.Z
# Eingabeparameter: Ausgangsverzeichnis
#
if [ $# -ne 1 ]; then 
    echo "Aufruf: `basename $0` Verzeichnis" 
    exit 1 
fi  
ROOT=$1
LSFILE=$ROOT/ls-lR
TMPFILE=/tmp/mkls.$$
cd $ROOT
echo "$ROOT" > $TMPFILE
# Verzeichnisinfo erstellen, Leerzeilen und Summenangabe raus
ls -lR 2>/dev/null | grep -v "^total" | grep -v "^$" >> $TMPFILE
cp $TMPFILE $LSFILE
cat $LSFILE | compress -f > $TMPFILE
cp $TMPFILE ${LSFILE}.Z
rm $TMPFILE

Ständig wachsende Dateien kürzen

Viele Logfiles wachsen notorisch. Wenn man nicht achtgibt, ist irgendwann die Platte voll. Über ein per crontab regelmäßig aufgerufenes Skript kann man die Dateien klein halten. Nebenbei kann man auch gleich die spezielle Logdatei wtmp mit bearbeiten. Die Datei '/etc/prune_list' enthält die zu überwachenden Dateien in der Form 'dateiname länge'. Für jede Datei existiert eine Zeile, wobei der Dateiname mit vollem Pfad angegeben werden muß.
Schließlich löscht das Skript noch alle Dateien aus dem /tmp-Verzeichnis, auf die seit 8 Tagen nicht mehr zugegriffen wurde und auf der gesmaten Platte alle Dateien mit den Namen 'a.out', 'core' und '*.o' die 8 Tage alt oder älter sind.
#!/bin/sh
#
# prune: Shorten textfiles listetd in $FLIST.
# files are shortened to a certain number of lines at
# their end. In $FLIST are lines containing filename
# (full path) and number of remaining lines. E. g.:
#      /var/adm/messages    500
#      /var/adm/debug       100
#
# /var/adm/wtmp will also be shortened
# a.out, core, *.o and tmp-files will be deleted after 8 days
#
HOSTNAME=/bin/hostname     # Pfad hostname-Programm
FLIST=/etc/prune_list      # Liste zu loeschender Dateien
TMPF=/tmp/prune.$$         # Temporaerdatei
{
  while read FILE LEN
    do
    if [ -n $FILE -a -n $LEN ] ; then
      if [ `wc -l $FILE` -lt $LEN ] ; then
        echo "prune: ${FILE} nothing to do"
      else
        tail -$LEN $FILE >$TMPF 
        cp $TMPF $FILE
        echo "prune: ${FILE} shortened to $LEN lines"
      fi
    else
      echo "prune: error in $FLIST, FILE or LEN missing"
    fi 
    done < $FLIST
  rm $TMPF
  cd /var/adm
  [ -f wtmp.3 ] && cp wtmp.3 wtmp.4
  [ -f wtmp.2 ] && cp wtmp.2 wtmp.3
  [ -f wtmp.1 ] && cp wtmp.1 wtmp.2
  cp wtmp wtmp.1
  cp /dev/null wtmp
  chmod 664 wtmp
  echo "wtmp shortened"
  [ -f wtmpx.3 ] && cp wtmpx.3 wtmpx.4
  [ -f wtmpx.2 ] && cp wtmpx.2 wtmpx.3
  [ -f wtmpx.1 ] && cp wtmpx.1 wtmpx.2
  cp wtmpx wtmpx.1
  cp /dev/null wtmpx
  chmod 664 wtmpx
  echo "wtmpx shortened"
# clean up /tmp and /usr/tmp
/usr/bin/find /tmp -type f -atime +7 -exec /bin/rm -f {} \;
/usr/bin/find /var/tmp -type f -atime +7 -exec /bin/rm -f {} \;
/usr/bin/find /usr/tmp -type f -atime +7 -exec /bin/rm -f {} \;

/usr/bin/find / \( -name a.out -name core -name '*.o' \) -atime +7 \
   -exec /bin/rm -f {} \;

} | mailx -s "Output from PRUNE `$HOSTNAME`" root 2>&1

Kleiner Sicherheitscheck

Im Lauf der Zeit "verlottert" jedes System ein wenig. Es gibt noch Dateien von längst gelöschten Usern, manche User haben kein Passwort und dergleichen mehr. Ein paar Sicherheitschecks macht das folgende Skript, das man nach den eigenen Bedürfnissen erweitern kann.
#!/bin/sh
# Programm to run weekly to check some important items
# must be run by root
#
# find accounts without password
echo ""
echo "Accounts without password"
echo "-------------------------"
/usr/bin/grep '^[^:]*::' /etc/passwd

# find accounts with UID 0 and/or GID 0
echo ""
echo "Accounts with ID 0"
echo "------------------"
/usr/bin/grep ':00*:' /etc/passwd

# Check Permissions
echo ""
echo "Permissions of important files"
echo "------------------------------"
ls -l /etc/passwd /etc/group /etc/hosts /etc/host.equiv /etc/inetd.conf
echo ""

echo "SUID-files"
echo "-----------"
/usr/bin/find / -perm -4000 -type f -exec ls -l {} \; 
echo ""

echo "SGID-files"
echo "----------"
/usr/bin/find / -perm -2000 -type f -exec ls -l {} \;
echo ""

echo "World-writable files"
echo "--------------------"
/usr/bin/find / -perm -2 \( -type f -o -type d \) -exec ls -l {} \;
echo ""

echo "Files without owner"
echo "-------------------"
/usr/bin/find / -nouser -exec ls -l {} \;
echo ""

echo "/var/adm/sulog:"
echo "---------------"
cat /var/adm/sulog
} 2>&1 | mailx -s "Check-Output" root 2>&1

Pack-Automat

Meine Seminarunterlagen auf dem Webserver ändern sich recht oft. Weil ich immer vergesse, auch eine gepackte Version des gesamten Verzeichnisses mit alle Bildern und HTML-Dateien zu erzeugen, läft per cron-Auftrag jede Nacht der Automatik-Packer. Als Merker dienen zwei Dateien, .lastpack, die sich im übergeordneten Verzeichnis befindet und angibt, wann zuletzt gepackt wurde. Gibt es im Verzeichnisbaum darunter keine Dateien, die neuer sind als .lastpack, wird nichts gemacht. In jedem darunterliegenden Verzeichnis gibt es eine Datei .lastupd, die es erlaubt, für das jeweilige Verzeichnis festzustellen, ob neue Dateien vorliegen. Ist das der Fall, wird das Packen gestartet. Das Ergebnis der Gesamtoperation wird per E-Mail an den Webmaster geschickt:
#!/bin/sh
#
# Erzeugt im WWW-Verzeichnis der Skripten
# jeweils gepackte Versionen.
# Die Dateien heissen <Verzeichnisname>.tar.gz
#
cd /home/httpd/lbs/skripten
[ `find . -newer ".lastpack" -print | wc -l` -eq 0 ] && exit
{
ls > ptmp.$$
while read DIR
  do
  if [ -d $DIR ] ; then
    NAME=`basename $DIR`
    cd $NAME
    if [ -f .lastupd ] ; then
      if [ `find . -newer ".lastupd" -print | wc -l` -gt 0 ] ; then
        rm $NAME.tgz
        /root/bin/upd-index-html
        tar cf $NAME.tar *
        gzip -f $NAME.tar
        mv $NAME.tar.gz $NAME.tgz
        touch .lastupd
        echo "$NAME ... DONE!"
      else
        echo "$NAME ... nothing to do"
      fi
    else
      touch .lastupd
    fi
    cd ..
  fi
done < ptmp.$$
rm ptmp.$$
touch /home/httpd/lbs/skripten/.lastpack
} | mailx -s "Skripten-Packer" webmaster

Zum Inhaltsverzeichnis Zum nächsten Abschnitt
Copyright © FH München, FB 04, Prof. Jürgen Plate