Vorlesung "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-Script' oder 'shell-Script'. Ein Shell-Script kann auf zwei Arten aufgerufen werden:

Das Shell-Script 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 Scripts weitere Shell-Scripten aufrufen. Zusätzlich kennt die Shell einige interne Befehle. Dem Shell-Script können Parameter übergeben werden, es ist damit universeller verwendbar.

8.1 Testen von Shell-Scripts

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

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

8.2 Kommentare in Shellscripts

Wie Programme müssen auch Shellscripts 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 Scripts möglich. Auch eingestreute Leerzeilen sind fast überall erlaubt.

Bei vielen Scripts 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 Scripts verwendet wird. Er wird hauptsächlich bei Scripts 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 Scripts 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 Scripts herangezogen werden; häufig sind awk- und perl-Scripts.

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 Shellscripts 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 Scripts
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-Scripten:

Shell-Scripts 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-Script 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-Script 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 Scripts 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 Scripts 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 Scripts) 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 Shellscripts 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 Script "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 Script "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 Script "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 Script 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 Scripts, die (ähnlich wie viele Kommandos) beliebig viele Dateinamen auf Parameterposition erlauben sollen. Die Shell-Scripten 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 (Shellscript) 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-Script in der aktuellen Shell und nicht in einer Subshell ausführt:

. (Dot) Script ausführen

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

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

export PATH

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

8.4 Interaktive Eingaben in Shellscripts

Es können auch Shellscripts 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-Script die Eingabe mit < aus einer Datei gelesen, bearbeitet read die Datei zeilenweise.

Anmerkung: Da das Shell-Script in einer Sub-Shell läuft, kann IFS im Script 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-Script 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-Script "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

Die Variablenersetzung oder andere Substitutionen im Text finden nur statt, wenn das Wort hinter dem Umleitungsoperator keine Quotation (einfache oder doppelte Anführungszeichen, Backslash) enthält. Text und Endmarkierung lassen sich leider nicht einrücken: Führende Leer- oder Tabulatorzeichen würden Bestandteil des Textes, und eine eingerückte Endmarkierung erkennt die Shell nicht. Ohne Einrückungen hingegen sind Scripte oft schlechter lesbar. Die Boume-Shell verfügt deshalb über den zweiten Operator <<-, der Tabulatorzeichen am Zeilenanfang ignoriert.

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-Scripts gemeinsam umzuleiten, wenn das Script 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 Script-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 Scripts 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 Scripts:

!/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

Oder auch rückwärts:

#!/bin/sh
F=60
tput clear
while [ $X -gt 0 ]
do
  X=`expr $X - 1`
  tput clear
  banner $X
  tput bel
  sleep 1
done
reboot   # darf zum Glueck nur root

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-Script 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 Scripts

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=$(($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-Script"
  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

In der Standard-Shell von Linux, der Bash ist die Rechnerei noch einfacher. Hier wird der arithmetische Ausdruck einfach in doppelte Klammern eingeschloseen, z. B. (( ANZ = $ANZ - $C )). Diese bei C entlehnte Aritmetik kann sogar noch mehr, beispielsweise Variablen inkementieren oder dekrementieren:

(( a = 23 ))  #  Wert zu weisen, C-style,
              #  diesmal mit Leerzeichen auf beiden Seiten des "=".
              
echo "a (initial value) = $a"   # 23

(( a++ ))     #  Post-increment 'a', C-style.
echo "a (after a++) = $a"       # 24

(( a-- ))     #  Post-decrement 'a', C-style.
echo "a (after a--) = $a"       # 23

(( ++a ))     #  Pre-increment 'a', C-style.
echo "a (after ++a) = $a"       # 24

(( --a ))     #  Pre-decrement 'a', C-style.
echo "a (after --a) = $a"       # 23

# Achtung: In manchen Faellen gibt es Seiteneffekte:

n=1; (( --n )) && echo "True" || echo "False"  # False
n=1; (( n-- )) && echo "True" || echo "False"  # True

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 DateideScriptoren 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 Shellscripts durch das Einfügen einer exec-Anweisung die Standardausgabe/-eingabe global für das gesamte Script umleiten, ohne weitere Änderungen vornehmen zu müssen (eine andere Möglichkeit wäre die oben beschriebene Verwendung von { }).

exec 3<&1 1<&2 2<&3 3< -

Tauscht die Rollen von Standardausgabe (1) und Standardfehlerausgabe (2) unter Zuhilfenahme eines anonymen Dateihandles (3), das zum Schluss wieder frei gegeben wird.

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.

Variblenreferenzen der Form \$$var, die normalerweise von eval (manchmal auch von echo) verarbeitet werden, nennt man "indirekte Referenzen". Spielen wir etwas damit herum:

# Wertzuweisung
var=23

echo "\$var   = $var"           # $var   = 23
# die Ausgabe ist wie erwartet, probieren wir mal ...

echo "\$\$var  = $$var"         # $$var  = 5678var
# ergibt nicht das Gewuenschte - eigentlich klar:
# $$ liefert die Prozessnummer (5678) und dann kommt
# der Text "var". Probieren wir weiter ...

echo "\\\$\$var = \$$var"       # \$$var = $23
#  das erste Dollarzeichen wird geschuetzt und vor
# den Variableninhalt geklebt - nicht sehr nuetzlich
Jetzt sind wir aber schon auf dem richtigen Weg. Es ist möglich, vor einen Variableninhalt ein Dollarzeichen zu kleben. Das Feature wird im folgenden Beispiel verwendet:
a="Buchstabe"          # Variable "a" enthaelt den Namen einer anderen Variablen
Buchstabe="z"          # die andere Variable

# Direkte Referenz:
echo "a = $a"          # a = Buchstabe

# Indirekte Referenz:
eval a=\$$a
#      ^   erstes $-Zeichen schuetzen ...
echo "a = $a"          # a = z
Mit der eval-Anweisung ist das gwünschte Ziel erreicht. $a wird ausgewertet, es ergibt sich a=$Buchstabe, was schliesslich "z" ergibt.

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 Shellscript 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:

# Script sperren gegen Benutzerunterbrechung:
trap "" 2 3

oder auch

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

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

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

Noch eine Demo für das Kommando: Der Programmabbruch wid erst nach 10 Sekunden zur Kenntnis genommen.

trap "sleep 10; echo 'Und wech'; exit;" 1 2 3 15
while :
do
  echo schnarch
  sleep 1
done

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 Shellscriptes 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 Scripte 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.7.15 Synchronisieren von Scripts

In manchen Fällen benötigt man eine Prozess-Synchronisation auch bei Scripts. Bei nur zwei Beteiligten böte sich eine Datei an, in die geschrieben und aus der gelesen wird. Manchmal reicht sogar das Vorhandensein bzw. Fehlen einer Datei. Etwas komplizierter ist es, wenn in einer Endlosschleife immer überprüft wird, ob eine bestimmte Bedingung erfüllt ist. Je nach Bedingung wird dann ein entsprechendes Script ausgeführt. Neben der Existenz werden oft das Alter, die Größe oder die Zugriffsrechte einer Datei als Bedingung verwendet. Das folgend Beispiel zeigt ein Script, das in einer Endlosschleife überprüft, ob eine Datei lesbar oder schreibbar ist und abhängig davon die entsprechende Aktion ausgeführt (die sleep-Aufrufe dienen nur dazu, den Ablauf besser verfolgen zu können):
FILE=tmpfile.tmp
# Datei ggf. anlegen
touch $FILE
# die uebliche Dauerschleife
while true
  do
  # ist die Datei lesbar
  if [ -r $FILE ]
    then
    echo "Datei wird gelesen ..."
    # mach irgendwas
    read $X < $FILE
    echo $X
    sleep 1
    # Freigeben zum Schreiben
    chmod 0200 $FILE
    fi
  # ist die Datei schreibbar 
  if [ -w $FILE ]
    then
    echo "Datei wird beschrieben ..."
    X=`date`
    echo $X > $FILE
    sleep 1
    # Freigeben zum Lesen
    chmod 0400 $FILE
    fi
  sleep 1
done

Eine recht einfache und zuverlässigere Synchronisation zweier oder auch mehrerer Scripts bieten beispielsweise Signale. Richten Sie hierzu einfach einen Signalhandler mit trap (siehe oben) ein, der auf ein bestimmtes Signal reagiert und ein weiteres Script aufruft. Dazu ein Beispiel:

# Das erste Script: test1
# Trap setzen: Beim Empfang von SIGUSR1 wird test2 aufgerufen 
trap './test2' SIGUSR1
# die uebliche Dauerschleife
while true
  do
  echo "Lese Daten ..."
  sleep 5
  echo "Starte test2 ..."
  # jetzt sendet das Script ein SIGUSR1 an sich selbst
  # damit erfolgt der Prozesswechsel
  kill -SIGUSR1 $$
  done
Das zweite Script sieht fast genauso aus, nur Ziel und Signal sind anders:
# Das zweite Script: test2
# Trap setzen: Beim Empfang von SIGUSR2 wird test1 aufgerufen 
trap './test1' SIGUSR2
# die uebliche Dauerschleife
while true
  do
  echo "Schreibe Daten ..."
  sleep 5
  echo "Starte test1 ..."
  # jetzt sendet das Script ein SIGUSR2 an sich selbst
  # damit erfolgt der Prozesswechsel
  kill -SIGUSR2 $$
  done
Startet man das erste Script, erfolgt die gegenseitige Synchronisation:
./test1

Lese Daten ...
Starte test2 ...
Schreibe Daten ...
Starte test1 ...
Lese Daten ...
Starte test2 ...
Schreibe Daten ...
Starte test1 ...
Lese Daten ...
Starte test2 ...
Schreibe Daten ...

8.8 Beispiele für Shellscripts

Disk usage

Sie erinnern sich noch an das du-Kommando? Mit etwas "Kosmetik" wird die Ausgabe noch aussagekräftiger:
#!/bin/sh
du -ks * | sort -rn | head -11
# total         reverse
# kilobyte      numerisch
# summarize

Nachbildung des Kommandos seq

seq hat zwei Parameter, einen Anfanswert und einen Endwert. Es wird vom Anfangswert bis zum Endwert gezählt.
#!/bin/sh
if [ $# -ne 2 -o $1 -gt $2 ]
then
  echo "usage: seq n1 n2 (n1 < n2)"
  exit 1
fi
I=$1
while [ $I -le $2 ]
do
  echo $I
  I=`expr $I + 1`
done

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 Script 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 Script führt alle Argumente nach vorheriger Abfrage aus. Mit "j" wird die Ausführung bestätigt, mit "q" das Script 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 Script 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. Bei einem Terminalfenster unter X hilft das natürlich nicht.

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 Script bildet das Kommando "which" nach. Es sucht im aktuellen Pfad (durch PATH spezifiziert) nach der angegebenen Datei und gibt die Fundstelle aus. An diesem Script 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 Scripts, 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 Script 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

Umleitung oder nicht?

Manche Kommandos wie ls registrieren, ob ihre Ausgabe auf den Bildschirm geht (Standardausgabe) oder umgeleitet wurde und zeigen ein entsprechendes Verhalten (bei ls z. B. werden die Datenamen auf dem Terminal mehrspaltig ausgegeben, in eine Datei hingegen einspaltig.

Das kann man auch in Shellscripts realisieren, wenn man mit dem Kommando tty die Standardeingabe abfragt. Geht sie auf den Bildschirm, liefert tty das Terminaldevice und den Rückgabewert 0. Im anderen Fall erhält man eine Fehlermeldung und der Rückgabewert ist 1. Das folgende Beispiel zeigt, wie es geht. Der erste Aufruf von tty dient nur der Demonsrtation und gehört später entfernt. Damit man auch bei der Ausgabeumleitung etwas sieht, sind alle Ausgaben auf die Standardfehlerausgabe umgeleitet.

#!/bin/sh
# Output von tty zeigen (nur zur Demonstration)
tty <&1 >&2 ; echo "return: $?" >&2

# hier kommt die eigentliche Abfrage
if tty -s <&1 ; then
  echo "Output ist ein tty" >&2
else
  echo "Output ist KEIN tty" >&2
fi
Lässt man das Script laufen, erhält man folgende Ausgaben:
plate@netzmafia:~$ sh tt.sh
/dev/pts/0
return: 0
Output ist ein tty
plate@netzmafia:~$ sh tt.sh > /dev/null
kein Ausgabegerät
return: 1
Output ist KEIN tty
Zusammen mit tput kann man dann den Textbildschirm "aufmöbeln", wie die folgende Demonstration zeigt:
#!/bin/bash

fg_rot=""     ; fg_gruen="" ; fg_gelb="" ; fg_blau=""
fg_magenta="" ; fg_cyan=""  ; fg_weiss="" 
fett=""       ; invers=""   ; attr_end=""

if tty -s <&1 ; then
  fg_rot=$(tput setaf 1)
  fg_gruen=$(tput setaf 2)
  fg_gelb=$(tput setaf 3)
  fg_blau=$(tput setaf 4)
  fg_magenta=$(tput setaf 5)
  fg_cyan=$(tput setaf 6)
  fg_weiss=$(tput setaf 7)
  fett=$(tput bold)
  invers=$(tput rev)
  untersr=$(tput smul)
  attr_end=$(tput sgr0)
fi

echo "Dies ist ${fg_rot}rot${attr_end}"
echo "Dies ist ${fg_gruen}gruen${attr_end}"
echo "Dies ist ${fg_gelb}gelb${attr_end}"
echo "Dies ist ${fg_blau}blau${attr_end}"
echo "Dies ist ${fg_magenta}magenta${attr_end}"
echo "Dies ist ${fg_cyan}cyan${attr_end}"
echo "Dies ist ${fg_weiss}(fast)weiss${attr_end}"
echo "Dies ist ${fett}fett${attr_end}"
echo "Dies ist ${invers}reverse${attr_end}"
echo "Dies ist ${untersr}unterstrichen${attr_end}"
Weitere Informationen bieten die Manualpages von tput und termcap.

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 Shellscripts, 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 Scripts). Die Parameter werden innerhalb der Funktion genauso, wie beim Aufruf von Shellscripts ü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-Scripts 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 Script verwenden, ginge das nicht, da sich der Wert von ZAHL ja nicht aus dem aufgerufenen Script 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"

Auch Shellfunktionen lassen sich rekursiv aufrufen. Als Beispiel sollen 'die Türme von Hanoi' dienen, die wohl allen Informatikern wohlbekannt sind. Die Aufgabe besteht darin, einen Stapel von Ringen unterschiedlicher Grösse von einem Platz au feinen anderen umzuschichten. Dabei sind folgende Nebenbedingungen zu beachten:

Das Problem bei diesem Verfahren ist, dass sich die Zahl der Gesamtzüge mit jedem zusätzlichen Ring verdoppelt - aber das Verfahren läßt sich rekursiv formulieren, denn um einen Stapel mit n+1 Ringen umzuschichten muss man einen Stapel mit n Ringen auf einen anderen Pfosten umsetzen, dann den letzten Ring auf den Zielpfosten stecken und den Stapel mit n Ringen darüber setzen. Am Anfang liegen alle Ringe auf Pfosten 1:
         ,-.                   ,-.                    ,-.
         | |                   | |                    | |
        _|_|_                  | |                    | |
       |_____|                 | |                    | |
      |_______|                | |                    | |
     |_________|               | |                    | |
    |___________|              | |                    | |
   |             |             | |                    | |
 +--------------------------------------------------------------+
 |         1                    2                      3        |
 +--------------------------------------------------------------+
Die entsprechende Funktion hanoi() hat vier Parameter: zuerst die aktuelle 'Turmhöhe', dann die Nummern der drei zu besetzenden Pfosten. Der Quellcode für die Bash lautet dann:
dohanoi() # $num $pfosten1 $pfosten2  $pfosten3 
  {
  case $1 in
    0) ;;                            # Abbruchbedingung
    *) dohanoi "$(($1-1))" $2 $4 $3  # wegstapeln
        echo move $2 "-->" $3        # Scheibe schieben
        Moves=$(( $Moves + 1 ))      # Zuege zaehlen
        dohanoi "$(($1-1))" $4 $3 $2 # zurueckstapeln
        ;;
    esac
  }
Wer die Standard-Bourne-Shell verwendet, muss die 'Rechnerei' passend umschreiben.

Das Hauptprogramm prüft den Eingabeparameter, der die Turmhöhe angibt und ruft dann die Funktion auf:

#!/bin/bash

dohanoi()     # siehe oben
  { ... }

Moves=0       # Globale Variable fuer die Zahl der Zuege
if [ $# -eq 1 ]
then
  if [ "$1" -gt 0 ]
  then
    dohanoi $1 1 3 2
    echo "Total moves = $Moves"
    exit 0
  else
    echo "Parameter muss groesser 0 sein"
    exit 1
  fi
else
  echo "Parameter fehlt"
  exit 1
fi

Die Bash kann auch Zufallszahlen erzeugen. Da es sich aber nur um Pseudo-Zufallszahlen handelt, sollte man sie nicht für das Erzeugen eines kryptografischen Schlüssels verwenden. Die Zufallsfunktion verbirgt sich hinter einer Variablen namens $RANDOM. Diese Pseudo-Variable liefert jedesmal einen neuen Zufallswert im Bereich zwischen 0 und 32767. Das folgende Beispiel liefert 10 Zufallswerte:

for I in 1 2 3 4 5 6 7 8 9 10
do
  number=$RANDOM
  echo $number
done
Will man den Bereich der Zufallszahlen beschränken, kann man folgendermaßen vorgehen. Das Beispiel liefert Werte zwischen 100 und 200:
MIN=100
MAX=200
number=0
while [ "$number" -le $MIN ]
do
  number=$RANDOM
  number=`expr "$number % $MAX"`
done
echo "Zufallszahl zwischen $MIN und $MAX: $number"
Das folgende Beispiel zeigt noch einen weiteren Shell-Trick: Verwendung von Arrays ohne echte Arrays. Es wird zufällig eine Spielkarte gezogen. Die Werte und Farben werden in Pseudo-Arrays gespeichert, die sich über mehrere Zeilen erstecken (müssen). Deshalb geht folgendes auch wieder nur mit der Bash:
Farben="Karo
Herz
Kreuz
Pik"

Werte="2
3
4
5
6
7
8
9
10
Bube
Dame
Koenig
Ass"

Num_Farben=4
Num_Werte=13

Farbe=($Farben)                # Einlesen in Array-Variablen
Wert=($Werte)

echo  -n "${Farbe[$((RANDOM%Num_Farben))]} "
echo     "${Wert[$((RANDOM%Num_Werte))]} "

Nun noch einige Berechnungen mit Zufallszahlen:

#  Zufallszahlen zwischen 6 und 30.
number=$((RANDOM%25+6))	

#  Dito, aber die Zahl muss durch 3 teilbar sein
number=$(( RANDOM%27/3*3+6 ))

Wird RANDOM ein Wert zugewiesen, setzt man damit einen Startwert für die Erzeugung der Zufallszahlen ("Seed"), nicht für den Zufallswert selbst.

RANDOM=1               # Setzen RANDOM-Seed
make_random_numbers

RANDOM=1               # Dieselbe Seed fuer RANDOM...
make_random_numbers    # ...reproduziert genau dieselbe Zufallsfolge

RANDOM=2               # Eine andere Seed fuer RANDOM...
make_random_numbers    # ...reproduziert eine andere Zufallsfolge

Es stellt sich die Frage, wie man RANDOM etwas "zufälliger" machen kann. Man kann nur versuchen, eine möglichst zufällige Seed zu wählen, indem man zum Beispiel die Prozessnummer ($$) oder time bzw. date verwendet. Eine bessere Quelle ist das Device /dev/urandom, das aber leider Binärzahlen liefert:

 
SEED=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }')
#      ein Wert von urandom | lesbar machen | Nummer extrahieren
RANDOM=$SEED
Statt urandom nur für die Seed zu verwenden, kann man natürlich gleich komplett auf das Device zurückgreifen - man erhät wesentlich "bessere" Zufallsfolgen. Nur muss man die Binärdaten immer noch im lesbare Zahlen umwandeln. Das folgende Kommando liefert beispielsweise eine Folge von XX gleichverteilten Zufallswerten als Datei (die dann mit od oder dergleichen gefiltert werden kann - wie oben gezeigt):
dd if=/dev/urandom of=zieldatei bs=1 count=XX

8.10 Fallen und Stolpersteine

Bei Linux sind 'bash' und 'sh' identisch, beidesmal wird die Bash aufgerufen. Bei anderen Unix-Varianten ist das keineswegs immer so. deshalb sollten Sie entweder das Skript per Shebang #!/bin/bash an die Bash binden oder (bei #!/bin/sh) nur die Befehle und Eigenschaften der Bourne-Shell nutzen.

Ein Script mit DOS/Windows_Zeilenenden (\r\n) wird sich nicht ausführen lassen, weil #!/bin/bash\r\n nicht erkannt wird, denn der Dateiname der Shell lautet hier ja "bash\r" und nicht "bash". Die Script-Datei muss also passend konvertiert werden.

Variablennamen

Es ist keine gute Idee, Befehle der Shell oder reservierte Worte als Variablennamen zu vergeben. In Namen sollten Nur Buchstaben, Ziffern und der Unterstrich auftauchen. Namen müssen mit einem Buchstaben beginnen (Namen, die mit einer Ziffer beginnen sind für die Shell reserviert). Folgende Beispiele funktionieren nicht!
case=Wert1                    # Problem!
23foo=Wert2                   # Problem!
_23foo=Wert3                  # O. K.
_=Wert4; echo $_              # Problem: $_ liefert immer den letzten 
                              # Parameter des letzten Kommandos
Jedoch ist '_' ein gültiger Funktionsname.

Es ist zulässig, für Funktionen und Variablen denselben Namen zu verwenden - das Script wird aber dadurch nahezu unlesbar:

machwas ()
  {
  echo "Diese Funktion macht irgendwas mit \"$1\"."
  }
machwas=machwas
machwas machwas

Wertzuweisung

Mit der beliebteste Anfängerfehler sind Leerzeichen bei der Wertzuweisung (weil ja sonst die Shell überall Leerzeichen braucht). Die Zeile
foo = 42
wird interpretiert als "Führe das Kommando 'foo' mit dem Parametern '=' und '42' aus. Richtig ist also:
foo=42
Das gilt natürlich auch für die Anwendung in Zusammenhang mit Backticks:
foo = `ls -l`        # Geht schief!
foo=`ls -l`          # so muss es sein!

Gerne wird auch das letzte Semikolon bei der Zusammenfassung mit geschweiften Klammern vergessen:

{ ls -l; df; echo "Done." }
# Die Bash gibt eine Fehlermeldung aus: "syntax error: unexpected end of file"

{ ls -l; df; echo "Done."; }
#                        ^  das Semikolon muss sein!

Vergleichsoperatoren

Bei Vergleichen wird auch gerne '=' und '-eq' verwechselt. '=' vergleicht Zeichenketten ('==' gibt es übrigens auch nicht), '-eq' Zahlen. Bedauerlicherweise ist es bei Perl wieder genau andersrum.
a=" 4096"

if [ "$a" = 4096 ]      # Stringvergleich liefert FALSCH
if [ "$a" -eq 4096 ]    # Zahlenvergleich liefert WAHR

Quoting

Denken Sie auch daran, dass bei Vergleichen die Variablen innerhalb der eckigen Klammern sinnvollerweise in Gänsefüsschen stehen sollten, z. B.:
a=""                    # Leerstring

if [ $a = "yes" ]       # Die Bash bricht mit Fehlermeldung ab, denn es 
                        # die Klammer ergibt [  = "yes" ]
                        
if [ "$a" = "yes" ]     # ergibt FALSCH; Ausführung läuft weiter                        
Vergessen Sie auch nicht, dass alles, was Leerzeichen oder irgendwelche Sonderzeichen enthält, am Besten in Gänsefüsschen eingeschlossen wird.

Subshells

Wie wir aus der Biologie wissen, können Eltern Eigenschaften an ein Kind vererben. Der umgekehrte Fall ist unmöglich. Bei Unix ist das genauso, wie folgendes Beispiel zeigt:
outer_variable="Ich bin draussen"
export outer_variable
echo "outer_variable = $outer_variable"    # Ausgabe wie erwartet

(
  # Begin subshell
  echo "outer_variable inside subshell = $outer_variable"
  inner_variable="Ich bin drin"            # Set
  echo "inner_variable inside subshell = $inner_variable"
  outer_variable="Ich bin drin"            # Wird der Wert global geaendert?
  echo "outer_variable inside subshell = $outer_variable"
  # End subshell
)

echo "inner_variable outside subshell = $inner_variable"  # Unbekannt!
echo "outer_variable outside subshell = $outer_variable"  # Unveraendert!
Leitet man Daten per Pipe in ein read-Kommando, kann dies wie das Piping in eine Subshell behandelt werden - mit entsprechend unerwünschten Resultaten. Dazu ein Beispiel:
a=aaa ; b=bbb ; c=ccc

# Versuch, den Variablen neue Werte zu geben:
echo "one two three" | read a b c
# Das ging leider schief:
echo "a = $a"  # a = aaa
echo "b = $b"  # b = bbb
echo "c = $c"  # c = ccc
In diesem Fall könnte man alternativ das set_Kommando verwenden:
set -- one two three
a=$1; b=$2; c=$3
# diesmal klappt es
echo "a = $a"  # a = one
echo "b = $b"  # b = two
echo "c = $c"  # c = three 
Die einfachste Lösung ist es, ein Kommando die Daten in eine Datei schreiben zu lassen, diue dann per Eingabeumleitung von read gelesen wird. Das folgende Beispiel von A. Richardson zeigt mögliche andere Lösungswege.
# Loop piping troubles.
# Example by Anthony Richardson, with addendum by Wilbert Berendsen.

foundone=false
find $HOME -type f -atime +30 -size 100k |
while true
do
   read f
   echo "$f is over 100KB and has not been accessed in over 30 days"
   echo "Consider moving the file to archives."
   foundone=true
   # ------------------------------------
   echo "Subshell level = $BASH_SUBSHELL"
   # Subshell level = 1
   # Yes, we're inside a subshell.
   # ------------------------------------
done
   
#  foundone will always be false here since it is
#  set to true inside a subshell
if [ $foundone = false ]
then
   echo "No files need archiving."
fi

# =====================Now, here is the correct way:=================

foundone=false
for f in $(find $HOME -type f -atime +30 -size 100k)  # No pipe here.
do
   echo "$f is over 100KB and has not been accessed in over 30 days"
   echo "Consider moving the file to archives."
   foundone=true
done
   
if [ $foundone = false ]
then
   echo "No files need archiving."
fi

# ==================And here is another alternative==================

#  Places the part of the script that reads the variables
#  within a code block, so they share the same subshell.
#  Thank you, W.B.

find $HOME -type f -atime +30 -size 100k | {
     foundone=false
     while read f
     do
       echo "$f is over 100KB and has not been accessed in over 30 days"
       echo "Consider moving the file to archives."
       foundone=true
     done

     if ! $foundone
     then
       echo "No files need archiving."
     fi
}

8.11 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-Script zum Eintragen eines neuen Benutzers

Dieses Script soll nur zeigen, was beim Anlegen eines Benutzers alles notwendig ist. Normalerweise exististieren bereits entsprechende Programme oder Scripten (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 Script 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 Script 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 Script ü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 Script "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 Script 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 Script 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 Script 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 Script '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 Shellscripts 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 Shellscripts
# 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!
          fi   ;;
  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 Script 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 Script 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 Script 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 Script 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"-Script

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
rm tmp/file[12].sor

Das folgende Script 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 Script ("! 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 Script 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-Script."
   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 Script 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 Script 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 Script noch alle Dateien aus dem /tmp-Verzeichnis, auf die seit 8 Tagen nicht mehr zugegriffen wurde und auf der gesamten 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 &lt; $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 Script, 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

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 Scripten
# jeweils gepackte Versionen.
# Die Dateien heissen <Verzeichnisname>.tar.gz
#
cd /home/httpd/lbs/Scripten
[ `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/Scripten/.lastpack
} | mailx -s "Scripten-Packer" webmaster

Dateinnamen in Kleinbuchstaben wandeln

Auch folgendes kommt häfig vor. Es werden Webseiten unter Windows erstellt un nach dem Herunterladen werden diverse Dateien nicht gefunden, weil Ihr Name groß geschrieben wurde, die Hyperlinks aber nicht. Unter W. merkt man das nicht, alles funktioniert dort wunderfein. Das folgende Script wandelt alle Dateinamen in Kleinbuchstaben um, achtet aber darauf, dass keine vorhandenen Dateien überschrieben werden. Die Anzahl der bearbeiteten Dateien wird am Bildschirm ausgegeben. Die Anzahl der Misserfolge wird als Rückgabewert geliefert - für eine automatische Weiterbearbeitung.
#!/bin/sh
# tolower - rename files to all lower case

if [ $# -lt 1 ]   # ein Parameter (Dateiname) ist Notwendig
then
  echo "rename files to all lower case"
  echo "usage: tolower file [...]"
  exit 1
fi

Errs=0      # Fehlerzaehler
n=0         # Dateizaehler
for i       # Alle Dateien der Parameterliste bearbeiten
  do
  Lower=`echo "$i" | tr '[A-Z]' '[a-z]'`
  if [ "$i" = "$Lower" ]
  then
    continue			            # name ist bereits in Kleinbuchstaben
  elif [ -r "$Lower" -o -w "$Lower" -o -x "$Lower" ]
  then                        # Zieldatei gibt es schon
    echo "could not rename $i: $Lower exists already"
	  continue
  fi

  if mv "$i" "$Lower"         # umbenennen erfolgreich?
  then
    n=`expr $n + 1`
  else
    Errs=`expr $Errs + 1`     # Sonst Fehlerzaehler erhoehen
	  echo "could not rename $i to $Lower"
  fi
done
echo "$n files renamed"
exit $Errs

Passwort ändern

Das Programm passwd ist so programmiert, daß nur interaktive Eingaben erlaubt sind, eine Umleitung ist nicht möglich. Mit einem weiteren, recht mächtigen Tool kann auch das per Script gesteuert werden. Das Tool heißt "expect". Man kann mit ihm interaktive Dialoge aller Art nachbilden. Es wird im Normalfall in einer Zeile des expect-Scripts ein über die Standardeingabe erwarteter String (expect ...) oder die Reaktion darauf (send ...) angegeben. So lassen sich beliebige Dialoge automatisieren. Eine Einführung in dies Tool kann an dieser Stelle nicht erfolgen, jedoch eine typische Anwendung. Das folgende Script setzt ein neues Passwort für einen User, es wird mit beiden Angaben als Argumente aufgerufen, z.B. "autopass meier Geheim". Es funktioniert nur für den User root:
#!/usr/bin/expect --
set passw [lindex $argv 1]
spawn /usr/bin/passwd [lindex $argv 0]
expect "password:"
send "$passw\r"
expect "password:"
send "$passw\r"
expect eof
Mit "spawn" startet das Script das passwd-Programm ("passwd meier"). Danach wird auf das "Enter ... password:" gewartet und mit der Eingabe des Passworts beantwortet. Das gleiche geschieht mit der Nachfrage ("Reenter ...").

root hat für sochle Aktionen das Kommando usermod, muss sich also nicht "verrenken".

Zufallsauswahl einer Datei

Das folgende Programm wählt unter allen ".wav"-Dateien eines Verzeichnisses zufällig eine zum Abspielen aus:
#!/bin/bash
# Zieldirectory
DIR="/usr/local/sounds"
#Temporaerdatei-Name
TMP="/tmp/"$(date +%s)$$
# Zufallszahl
RND=$RANDOM
cd $DIR
ls *.wav > $TMP
# Maximalzahl der Dateien ermitteln
MAX=`cat $TMP | wc -l`
# NUM ist eine Zufallszahl zwischen 1 und $MAX
NUM=`expr $RND % $MAX + 1`
# erste Datei -> nur head noetig
if [ $NUM -eq 1 ] ; then
  FILE=`head -1 $TMP`
# letzte Datei -> nur tail noetig
elif [ $NUM -eq $MAX ] ; then
  FILE=`tail -1 $TMP`
# sonst nimm die ersten $NUM Dateien und davon die letzte
else
  FILE=`head -$NUM $TMP | tail -1`
fi
# Dateiname ausgeben
echo $FILE
rm $TMP
Zum Inhaltsverzeichnis Zum nächsten Abschnitt
Copyright © Hochschule München, FK 04, Prof. Jürgen Plate
Letzte Aktualisierung: 17. Okt 2012