![]() |
Vorlesung "UNIX"von Prof. Jürgen Plate |
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.
Merke: Durch Testen kann nur die Fehlerhaftigkeit von Scripts nachgewiesen, aber nicht deren Korrektheit bewiesen werden.
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.
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:
VAR=Wert | Wert ist ein String (ohne Leerzeichen und Sonderzeichen) |
VAR="Wert" | Wert ist ein String (Ersetzung eingeschränkt, Leerzeichen und Sonderzeichen dürfen enthalten sein) |
VAR=`kommando` VAR=$(kommando) |
Wert der Variablen ist die Ausgabe des Kommandos (Newline → Leerzeichen) |
$ 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 > $VARDas 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
\ | 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 |
Variable | Bedeutung |
HOME | Home-Directory (absoluter Pfad) |
PATH | Suchpfad für Kommandos und Scripts |
MANPATH | Suchpfad für die Manual-Seiten |
Mail-Verzeichnis | |
SHELL | Name der Shell |
LOGNAME USER | Login-Name des Benutzers |
PS1 | System-Prompt ($ oder #) |
PS2 | Prompt für Anforderung weiterer Eingaben (>) |
IFS | (internal field separator) Trennzeichen, meist CR, Leerzeichen und Tab) |
TZ | Zeitzone (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 $? |
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.
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.
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
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.
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
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.
kommando1 ; kommando2; kommando3
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 |
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 |
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.
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.
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 |
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> |
if kommandoliste then kommandos fi
if kommandoliste then kommandos else kommandos fi
if kommandoliste1 then kommandos elif kommandoliste2 then kommandos elif ... ... fi
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 {} ";"
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.
case selector in Muster1) Kommandofolge1 ;; Muster2 | Muster3) Kommandofolge2 ;; *) Kommandofolge3 ;; esac
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
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 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
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
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
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
break | Schleife verlassen |
continue | Sprung zum Schleifenanfang |
echo | Ausgabe |
eval | Mehrstufige Ersetzung |
exec | Überlagerung der Shell durch ein Kommando |
exit | Shell beenden |
export | Variablen für Subshells bekannt machen |
read | Einlesen einer Variablen |
shift | Parameterliste verschieben |
trap | Behandlung von Signalen |
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 UhrAber 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 fooDa 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
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 | expr2 | oder |
expr1 & expr2 | und |
expr1 < expr2 | kleiner |
expr1 <= expr2 | kleiner oder gleich |
expr1 > expr2 | größer |
expr1 >= expr2 | größer oder gleich |
expr1 = expr2 | gleich |
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
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.
$ 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 nuetzlichJetzt 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 = zMit der eval-Anweisung ist das gwünschte Ziel erreicht. $a wird ausgewertet, es ergibt sich a=$Buchstabe, was schliesslich "z" ergibt.
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:
0 | SIGKILL | Terminate (beim Beenden der shell) |
1 | SIGHUP | Hangup (beim Beenden der Verbindung zum Terminal oder Modem) |
2 | SIGINT | Interrupt (wie Ctrl-C-Taste am Terminal) |
3 | SIGQUIT | Abbrechen (Beenden von der Tastatur aus) |
9 | SIGKILL | Kann nicht abgefangen werden - Beendet immer den empfangenden Prozeß |
15 | SIGTERM | 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
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.
dialog bietet im einzelnen folgende Interaktionsmöglichkeiten:
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:
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:
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 $$ doneDas 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 $$ doneStartet 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 ...
#!/bin/sh du -ks * | sort -rn | head -11 # total reverse # kilobyte numerisch # summarize
#!/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
( cat file1 ; echo "$SHELLVAR" ) > file2
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
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
# 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
# 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
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
#!/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
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 ""
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"
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
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 fiLä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 ttyZusammen 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.
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:
,-. ,-. ,-. | | | | | | _|_|_ | | | | |_____| | | | | |_______| | | | | |_________| | | | | |___________| | | | | | | | | | | +--------------------------------------------------------------+ | 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 doneWill 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=$SEEDStatt 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
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.
case=Wert1 # Problem! 23foo=Wert2 # Problem! _23foo=Wert3 # O. K. _=Wert4; echo $_ # Problem: $_ liefert immer den letzten # Parameter des letzten KommandosJedoch 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
foo = 42wird interpretiert als "Führe das Kommando 'foo' mit dem Parametern '=' und '42' aus. Richtig ist also:
foo=42Das 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!
a=" 4096" if [ "$a" = 4096 ] # Stringvergleich liefert FALSCH if [ "$a" -eq 4096 ] # Zahlenvergleich liefert WAHR
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 weiterVergessen Sie auch nicht, dass alles, was Leerzeichen oder irgendwelche Sonderzeichen enthält, am Besten in Gänsefüsschen eingeschlossen wird.
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 = cccIn 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 = threeDie 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 }
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
# 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 ---"
# 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
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.
#!/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
#!/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"
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 : $*"
#!/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
#!/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
#!/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"
#!/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
while true ; do echo "`pwd`:$PS1\c" read KDO eval $KDO done
#!/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
who | while read USR REST ; do # für alle aktiven User banner "Teatime!" | write $USR done
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
#!/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
#!/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
#!/bin/sh # # prune: Shorten textfiles listetd in $FLIST. # files are shortened to a certain number of lines at # their end. In $FLIST are lines containing filename # (full path) and number of remaining lines. E. g.: # /var/adm/messages 500 # /var/adm/debug 100 # # /var/adm/wtmp will also be shortened # a.out, core, *.o and tmp-files will be deleted after 8 days # HOSTNAME=/bin/hostname # Pfad hostname-Programm FLIST=/etc/prune_list # Liste zu loeschender Dateien TMPF=/tmp/prune.$$ # Temporaerdatei { while read FILE LEN do if [ -n $FILE -a -n $LEN ] ; then if [ `wc -l $FILE` -lt $LEN ] ; then echo "prune: ${FILE} nothing to do" else tail -$LEN $FILE >$TMPF cp $TMPF $FILE echo "prune: ${FILE} shortened to $LEN lines" fi else echo "prune: error in $FLIST, FILE or LEN missing" fi done < $FLIST rm $TMPF cd /var/adm [ -f wtmp.3 ] && cp wtmp.3 wtmp.4 [ -f wtmp.2 ] && cp wtmp.2 wtmp.3 [ -f wtmp.1 ] && cp wtmp.1 wtmp.2 cp wtmp wtmp.1 cp /dev/null wtmp chmod 664 wtmp echo "wtmp shortened" [ -f wtmpx.3 ] && cp wtmpx.3 wtmpx.4 [ -f wtmpx.2 ] && cp wtmpx.2 wtmpx.3 [ -f wtmpx.1 ] && cp wtmpx.1 wtmpx.2 cp wtmpx wtmpx.1 cp /dev/null wtmpx chmod 664 wtmpx echo "wtmpx shortened" # clean up /tmp and /usr/tmp /usr/bin/find /tmp -type f -atime +7 -exec /bin/rm -f {} \; /usr/bin/find /var/tmp -type f -atime +7 -exec /bin/rm -f {} \; /usr/bin/find /usr/tmp -type f -atime +7 -exec /bin/rm -f {} \; /usr/bin/find / \( -name a.out -name core -name '*.o' \) -atime +7 \ -exec /bin/rm -f {} \; } | mailx -s "Output from PRUNE `$HOSTNAME`" root 2>&1
#!/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
#!/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
#!/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
#!/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 eofMit "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".
#!/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
![]() |
![]() |