Shell Programmierung

ArticleCategory: [Es gibt verschiedene Artikel Kategorien]

UNIX Basics

AuthorImage:[Ein Bild von Dir]

[Photo of the Authors]

TranslationInfo:[Autor und Übersetzer]

original in en Katja and Guido Socher

en to de Katja Socher

AboutTheAuthor:[Eine kleine Biographie über den Autor]

Katja ist die deutsche Redakteurin von LinuxFocus. Sie mag Tux, Film & Fotografie und das Meer. Ihre Homepage findet sich hier.

Guido ist ein langj�hriger Linuxfan und er mag Linux, weil es von ehrlichen und offenen Leuten entwickelt wurde. Dies ist einer der Gr�nde, warum wir es Open Source nennen. Seine Homepage ist auf linuxfocus.org/~guido.

Abstract:[Hier sollte eine kleine Zusammenfassung stehen]

In diesem Artikel erkl�ren wir, wie man kleine Shellskripte schreibt und geben viele Beispiele.

ArticleIllustration:[Das Titelbild des Artikels]

[Illustration]

ArticleBody:[Der eigentliche Artikel. Überschriften innerhalb des Artikels sollten h2 oder h3 sein.]

Warum Shell Programmierung?

Auch wenn es inzwischen verschiedene grafische Oberfl�chen f�r Linux gibt, ist die Shell immer noch ein n�tzliches Werkzeug. Die Shell ist nicht nur eine Sammlung von Befehlen, sondern eine wirklich gute Programmiersprache. Man kann mit ihr viele Aufgaben automatisieren, sie eignet sich sehr gut f�r Systemadministrationsaufgaben, man kann sehr schnell austesten, ob eine Idee funktioniert, was sie sehr n�tzlich f�r einfaches Erstellen von Prototypen macht und sie ist sehr n�tzlich f�r kleine Programme, die relativ einfache Aufgaben ausf�hren, bei denen die Effizienz weniger wichtig ist als die Leichtigkeit der Konfiguration, der Pflege und der Portierbarkeit.
La�t uns jetzt sehen, wie sie funktioniert:

Schreiben eines Skripts

Es gibt viele verschiedene Shells f�r Linux, aber normalerweise wird die bash (bourne again shell) f�r die Shell Programmierung benutzt, da sie frei verf�gbar und einfach zu benutzen ist. Deshalb werden wir f�r alle Skripte in diesem Artikel die bash benutzen (aber meistens laufen sie auch unter der �lteren Schwester, der bourne shell).
Zum Schreiben unserer Shellprogramme benutzen wir einen Texteditor, z.B. nedit, kedit, emacs, vi...wie f�r jede andere Programmiersprache auch.
Das Programm mu� mit der folgenden Zeile beginnen (es mu� die erste Zeile in der Datei sein):
    #!/bin/sh 
   
Die #! Zeichen sagen dem System, da� das erste Argument, das auf der Zeile folgt, das Programm ist, das benutzt werden soll, um diese Datei auszuf�hren. In diesem Fall ist /bin/sh die Shell, die wir benutzen.
Wenn du dein Skript geschrieben und abgespeichert hast, mu�t du es ausf�hrbar machen, um es benutzen zu k�nnen.
Um ein Skript ausf�hrbar zu machen, tippe
chmod +x filename
Dann kannst du dein Skript durch Tippen von ./filename starten

Kommentare

Kommentare bei der Shell Programmierung beginnen mit # und gelten bis zum Ende der Zeile. Wir empfehlen wirklich, Kommentare zu benutzen. Wenn du Kommentare eingef�gt hast und ein bestimmtes Skript einige Zeit nicht benutzt, wei� du immer noch sofort, was es macht und wie es funktioniert.

Variablen

Wie auch in anderen Programmiersprachen kommt man ohne Variablen nicht aus. In der Shellprogrammierung sind alle Variablen vom Datentyp string und brauchen nicht deklariert zu werden. Um einer Variablen einen Wert zuzuweisen, schreibt man:
varname=value
Um den Wert wiederzubekommen, setzt man ein Dollarzeichen vor die Variable:
#!/bin/sh
# assign a value:
a="hello world"
# now print the content of "a":
echo "A is:"
echo $a
Tippe diese Zeilen in deinen Texteditor und speichere ihn z.B. als first. Dann mach das Skript durch Tippen von chmod +x first in der Shell ausf�hrbar und starte es dann durch Eingabe von ./first
Das Skript gibt einfach das folgende aus:
A is:
hello world
Manchmal ist es m�glich, Variablennamen mit dem Rest des Textes zu vermischen:
num=2
echo "this is the $numnd"
Dies druckt nicht "this is the 2nd", sondern "this is the ", weil die Shell nach einer Variablen namens numnd sucht, die keinen Wert hat. Um der Shell zu sagen, da� wir die Variable num meinen, m�ssen wir geschweifte Klammern benutzen:
num=2
echo "this is the ${num}nd"
Dies druckt, was wir wollen: this is the 2nd

Es gibt eine Anzahl von Variablen, die immer automatisch gesetzt werden. Wir diskutieren sie weiter unten, wenn wir sie zum ersten Mal benutzen.

Wenn du mathematische Ausdr�cke berechnen mu�t, mu�t du Programme wie expr (siehe Tabelle unten) benutzen.
Au�er den normalen Shellvariablen, die nur innerhalb des Shellprogramms g�ltig sind, gibt es auch noch Umgebungsvariablen. Eine Variable, der das Schl�sselwort export vorausgeht, ist eine Umgebungsvariable. Wir sprechen hier nicht weiter �ber sie, da sie normalerweise nur in Login-Skripten benutzt werden.

Shellbefehele und Steuerungsstrukturen

Es gibt drei Kategorien von Befehlen, die in Shellskripten benutzt werden:

1)Unixbefehle:
Obwohl ein Shellskript jeden Unixbefehl benutzen kann, listen wir hier einige Befehle auf, die �fter benutzt werden als andere. Diese Befehle k�nnen allgemein als Befehle f�r Datei- und Textmanipulation charakterisiert werden.

Befehlssyntax Zweck
echo "some text" schreibt some text auf den Bildschirm
ls listet Dateien auf
wc -l file
wc -w file
wc -c file
z�hlt die Zeilen in einer Datei oder
z�hlt die W�rter in einer Datei oder
z�hlt die Anzahl der Buchstaben
cp sourcefile destfile kopiert sourcefile nach destfile
mv oldname newname benennt eine Datei um bzw. verschiebt sie
rm file l�scht eine Datei
grep 'pattern' file sucht nach Zeichenketten in einer Datei
Beispiel: grep 'searchstring' file.txt
cut -b colnum file holt Daten aus Textspalten mit fester Breite
Beispiel: hol die Zeichenpositionen 5 bis 9 heraus
cut -b5-9 file.txt
Verwechsle diesen Befehl nicht mit "cat", der etwas ganz anderes macht
cat file.txt schreibt file.txt nach stdout (deinen Bildschirm)
file somefile beschreibt, von welchem Typ die Datei somefile ist
read var fordert den Benutzer zur Eingabe aus und schreibt sie in eine Variable (var)
sort file.txt sortiert Zeilen in file.txt
uniq l�scht doppelte Zeilen, wird in Kombination mit sort benutzt, da uniq nur aufeinander folgende doppelte Zeilen entfernt
Beispiel: sort file.txt | uniq
expr rechnen mit der Shell
Beispiel: addiere 2 und 3
expr 2 "+" 3
find sucht nach Dateinamen
Beispiel: suche nach Namen:
find . -name filename -print
Dieser Befehl hat viele verschiedene M�glichkeiten und Optionen. Es ist leider zu viel, um sie alle in diesem Artikel zu erkl�ren.
tee schreibt Daten nach stdout (deinen Bildschirm) und in eine Datei
Normalerweise wie folgt benutzt:
somecommand | tee outfile
Es schreibt die Ausgabe von somecommand auf den Bildschirm und in die Datei outfile
basename file gibt nur den Dateinamen eines gegebenen Namens zur�ck und schneidet den Verzeichnispfad ab
Beispiel: basename /bin/tux
gibt nur tux zur�ck
dirname file gibt nur das Verzeichnis eines gegebenen Namens zur�ck und schneidet den tats�chlichen Dateinamen ab
Beispiel: dirname /bin/tux
gibt nur /bin zur�ck
head file druckt einige Zeilen vom Dateianfang
tail file druckt einige Zeilen vom Dateiende
sed sed ist grunds�tzlich ein finde und ersetze Programm. Es liest Text von der Standardeingabe (z.B. von einer pipe) und schreibt das Ergebnis in stdout (normalerweise der Bildschirm). Das Suchmuster ist ein regul�rer Ausdruck (siehe Referenzen). Diese Suchmuster d�rfen nicht mit der Shellwildcardsyntax verwechselt werden. Um die Zeichenkette linuxfocus mit LinuxFocus in einer Textdatei zu ersetzen, benutze:
cat text.file | sed 's/linuxfocus/LinuxFocus/' > newtext.file
Dies ersetzt das erste Auftreten der Zeichenkette linuxfocus in jeder Zeile mit LinuxFocus. Wenn es Zeilen gibt, in denen linuxfocus mehrmals vorkommt und man alle ersetzen will, schreibt man:
cat text.file | sed 's/linuxfocus/LinuxFocus/g' > newtext.file
awk Meistens wird awk benutzt, um Felder aus einer Textzeile herauszuziehen. Der Standard-Feldtrenner ist ein Leerzeichen. Um einen davon verschiedenen zu spezifizieren, benutzt man die Option -F.
 cat file.txt | awk -F, '{print $1 "," $3 }' 

Hier benutzen wir ein Komma (,) als Feldtrenner und drucken die erste und dritte ($1 $3) Spalte. Wenn file.txt Zeilen hat, wie die folgenden:
Adam Bor, 34, India
Kerry Miller, 22, USA

dann ergibt dies:
Adam Bor, India
Kerry Miller, USA

Es gibt sehr viel mehr, was man mit awk tun kann, aber dies ist eine sehr oft vorkommende Anwendung.


2) Konzepte: Pipes, redirection und backtick
Sie sind keine echten Befehle, aber sehr wichtige Konzepte.

pipes (|) schicken die Ausgabe (stdout) eines Programms als Eingabe (stdin) zu einem anderen Programm.
    grep "hello" file.txt | wc -l
findet die Zeilen mit der Zeichenkette hello in file.txt und z�hlt dann die Zeilen.
Die Ausgabe des grep Befehls wird als Eingabe f�r den wc Befehl benutzt. Man kann (innerhalb vern�nftiger Grenzen) beliebig viele Befehle auf diese Weise miteinander verbinden.

redirection: schreibt die Ausgabe eines Befehls in eine Datei oder h�ngt Daten an eine Datei an
> schreibt die Ausgabe in eine Datei und �berschreibt die alte Datei, falls diese existiert
>> h�ngt Daten an eine Datei an (oder erstellt eine neue, wenn diese noch nicht existiert hat, aber �berschreibt nie irgendetwas).

Backtick
Die Ausgabe eines Befehls kann als Kommandozeilenargument (nicht stdin wie oben; Kommandozeilenargumente sind alle Zeichenketten, die nach dem Befehl spezifiziert werden wie Dateinamen und Optionen) f�r einen anderen Befehl benutzt werden. Man kann auch die Ausgabe eines Befehls einer Variablen zuweisen.
Der Befehl
find . -mtime -1 -type f -print
findet alle Dateien, die innerhalb der letzen 24 Stunden modifiziert wurden (-mtime -2 w�ren 48 Stunden). Wenn du alle diese Dateien in ein tar Archiv packen willst (file.tar), w�rde die Syntax daf�r so aussehen:
tar xvf file.tar infile1 infile2 ...
Statt alle Dateien einzutippen, kann man die beiden Befehle durch Benutzen von backticks kombinieren (find und tar). Tar packt dann alle Dateien ein, die find gedruckt hat:
#!/bin/sh
# The ticks are backticks (`)  not normal quotes ('):
tar -zcvf lastmod.tar.gz `find . -mtime -1 -type f -print`

3) Steuerungsstrukturen

Die "if" Anweisung testet, ob die Bedingung wahr ist (der Exitstatus ist 0, Erfolg). Wenn das so ist, wird der "then" Teil ausgef�hrt:
if ....; then
   ....
elif ....; then
   ....
else
   ....
fi
Meistens wird ein sehr spezieller Befehl namens test innerhalb der if-Anweisung benutzt. Er kann benutzt werden, um Zeichenketten miteinander zu vergleichen oder zu pr�fen, ob eine Datei existiert, lesbar ist etc...
Der "test" Befehl wird in eckigen Klammern " [ ] " geschrieben. Beachte, da� die Leerzeichen hier von Bedeutung sind: Stelle sicher, da� du immer ein Leerzeichen um die eckigen Klammern hast. Beispiele:
[ -f "somefile" ]  : Test if somefile is a file.
[ -x "/bin/ls" ]   : Test if /bin/ls exists and is executable.
[ -n "$var" ]      : Test if the variable $var contains something
[ "$a" = "$b" ]    : Test if the variables "$a" and  "$b" are equal
La� den Befehl "man test" laufen und du bekommst eine lange Liste mit allen m�glichen Testoperatoren f�r Vergleiche und Dateien.
Dies in einem Shellskript zu benutzen, ist einfach:
#!/bin/sh
if [ "$SHELL" = "/bin/bash" ]; then
  echo "your login shell is the bash (bourne again shell)"
else
  echo "your login shell is not bash but $SHELL"
fi
Die Variable $SHELL enth�lt den Namen der Loginshell und das ist, was wir hier durch Vergleich mit der Zeichenkette "/bin/bash" testen.

Shortcut Operatoren
Leute, die sich mit C auskennen, werden den folgenden Ausdruck begr��en:
[ -f "/etc/shadow" ] && echo "This computer uses shadow passwors"
Das && kann als eine kurze if-Anweisung benutzt werden. Die rechte Seite wird ausgef�hrt, wenn die linke wahr ist. Du kannst dies als UND lesen. Daher ist ein Beispiel: "Die Datei /etc/shadow existiert UND der Befehl echo wird ausgef�hrt". Der ODER Operator (||) ist ebenfalls verf�gbar. Hier ein Beispiel:
#!/bin/sh
mailfolder=/var/spool/mail/james
[ -r "$mailfolder" ] || { echo "Can not read $mailfolder" ; exit 1; }
echo "$mailfolder has mail from:"
grep "^From " $mailfolder
Das Skript �berpr�ft zuerst, ob es einen gegebenen Mailfolder lesen kann. Wenn ja, dann druckt es die "From" Zeilen in dem Folder. Wenn es die Datei $mailfolder nicht lesen kann, dann kommt der ODER Operator ins Spiel. In klarem deutsch liest sich der Code als "Mailfolder lesbar oder verlasse das Programm". Das Problem ist hier, da� man genau einen Befehl hinter dem OR haben mu�, wir aber zwei brauchen:
-gib eine Fehlermeldung aus
-beende das Programm
Um sie als einen Befehl zu handhaben, k�nnen wir sie in einer anonymen Funktion durch Benutzen von geschweiften Klammern zusammen gruppieren. Funktionen im allgemeinen werden weiter unten erkl�rt.
Man kann alles auch ohne ANDs und ORs durch Benutzen von if-Anweisungen machen, aber manchmal sind die Shortcuts AND und OR einfach g�nstiger.

Die case Anweisung kann benutzt werden, um eine gegebene Zeichenkette mit einer Anzahl von M�glichkeiten zu vergleichen (durch Benutzen von shell wildcards wie * und ?).
case ... in
...) do something here;;
esac
La�t uns ein Beispiel anschauen. Der Befehl file kann pr�fen, um welchen Dateityp es sich bei einer gegebenen Datei handelt:
file lf.gz

ergibt:

lf.gz: gzip compressed data, deflated, original filename, 
last modified: Mon Aug 27 23:09:18 2001, os: Unix
Wir benutzen dies nun, um ein Skript namens smartzip zu schreiben, das bzip2, gzip and zip komprimierte Dateien automatisch dekomprimieren kann:
#!/bin/sh
ftype=`file "$1"`
case "$ftype" in
"$1: Zip archive"*)
    unzip "$1" ;;
"$1: gzip compressed"*)
    gunzip "$1" ;;
"$1: bzip2 compressed"*)
    bunzip2 "$1" ;;
*) error "File $1 can not be uncompressed with smartzip";;
esac

Hier bemerkst du, da� wir eine neue spezielle Variable namens $1 benutzt haben. Diese Variable enth�lt das erste Argument, da� einem Programm gegeben wurde. Sagen wir, wir lassen
smartzip articles.zip
laufen, dann enth�lt $1 die Zeichenkette articles.zip

Die select Anweisung ist eine bash spezifische Erweiterung und ist sehr gut f�r interaktive Benutzung geeignet. Der Benutzer kann aus einer Liste mit verschiedenen Werten eine Wahl treffen:
select var in ... ; do
  break
done
.... now $var can be used ....
Hier ist ein Beispiel:
#!/bin/sh
echo "What is your favourite OS?"
select var in "Linux" "Gnu Hurd" "Free BSD" "Other"; do
        break
done
echo "You have selected $var"
Hier ist, was das Skript macht:
What is your favourite OS?
1) Linux
2) Gnu Hurd
3) Free BSD
4) Other
#? 1
You have selected Linux
In der Shell stehen die folgenden Schleifen zur Verf�gung:
while ...; do
 ....
done
Die while-Schleife l�uft, solange der Ausdruck, den wir testen, wahr ist. Das Schl�sselwort "break" kann benutzt werden, um die Schleife jeder Zeit zu verlassen. Mit dem Schl�sselwort "continue" f�hrt die Schleife mit der n�chsten Wiederholung fort und l��t den Rest des Schleifenk�rpers aus.

Die for-Schleife nimmt eine Liste von Zeichenketten (Zeichenketten getrennt durch Leerzeichen) und weist sie einer Variablen zu:
for var in ....; do
  ....
done
Das folgende gibt z.B. die Buchstaben A-C auf dem Bildschirm aus:
#!/bin/sh
for var in A B C ; do
  echo "var is $var"
done
Ein n�tzlicheres Beispielskript, genannt showrpm, gibt eine Zusammenfassung des Inhalts einer Anzahl von RPM-Paketen aus:
#!/bin/sh
# list a content summary of a number of RPM packages
# USAGE: showrpm rpmfile1 rpmfile2 ...
# EXAMPLE: showrpm /cdrom/RedHat/RPMS/*.rpm
for rpmpackage in $*; do
  if [ -r "$rpmpackage" ];then
    echo "=============== $rpmpackage =============="
    rpm -qi -p $rpmpackage
  else
    echo "ERROR: cannot read file $rpmpackage"
  fi
done
Oben kannst du die n�chste spezielle Variable $* sehen, die alle Kommandozeilenargumente enth�lt. Wenn du
showrpm openssh.rpm w3m.rpm webgrep.rpm
laufen l��t, dann enth�lt $* die drei Zeichenketten openssh.rpm, w3m.rpm und webgrep.rpm.

Die GNU bash kennt auch until-Schleifen, aber im allgemeinen sind while und for Schleifen ausreichend.

Quotierung
Bevor irgendwelche Argumente an ein Programm weitergegeben werden, versucht die Shell, wildcards und Variablen zu ersetzen und mit ihren Werten auszuf�llen. Dies bedeutet, da� die Wildcard (z.B.*) durch die passenden Dateinamen ersetzt wird, oder da� eine Variable durch ihren Wert ersetzt wird. Um dieses Verhalten zu �ndern, werden Anf�hrungszeichen benutzt. Sagen wir, wir haben eine Anzahl von Dateien in unserem aktuellen Verzeichnis. Zwei davon sind jpg-Dateien, mail.jpg und tux.jpg.
#!/bin/sh
echo *.jpg
Dies gibt "mail.jpg tux.jpg" aus.
Anf�hrungszeichen (quotes) (einfache und doppelte) verhindern dieses Ersetzen der Wildcards:
#!/bin/sh
echo "*.jpg"
echo '*.jpg'
Dies gibt zweimal "*.jpg" aus.
Einfache Anf�hrungsstriche sind am striktesten. Sie verhindern auch die Variablenersetzung. Doppelte Anf�hrungsstriche verhindern Wildcardersetzung, erlauben aber die Variablenersetzung:
#!/bin/sh
echo $SHELL
echo "$SHELL"
echo '$SHELL'
Dies gibt aus:
/bin/bash
/bin/bash
$SHELL
Schlie�lich gibt es die M�glichkeit, die spezielle Bedeutung eines einzelnen Zeichens durch das Voranstellen eines Backslashs wegzunehmen:
echo \*.jpg
echo \$SHELL
Dies gibt aus :
*.jpg
$SHELL
Here documents
Here documents sind eine nette M�glichkeit, einige Zeilen an Text an einen Befehl zu schicken. Es ist ganz n�tzlich, einen Hilfetext in ein Skript zu schreiben, ohne echo zu Beginn jeder Zeile schreiben zu m�ssen. Ein "Here document" beginnt mit << gefolgt von einer Zeichenkette, die auch wieder am Ende des here documents erscheinen mu�. Hier ist ein Beispielskript, genannt ren, das mehrere Dateien umbenennt und ein here document f�r seinen Hilfetext benutzt:
#!/bin/sh
# we have less than 3 arguments. Print the help text:
if [ $# -lt 3 ] ; then
cat <<HELP
ren -- renames a number of files using sed regular expressions

USAGE: ren 'regexp' 'replacement' files...

EXAMPLE: rename all *.HTM files in *.html:
  ren 'HTM$' 'html' *.HTM

HELP
  exit 0
fi
OLD="$1"
NEW="$2"
# The shift command removes one argument from the list of
# command line arguments.
shift
shift
# $* contains now all the files:
for file in $*; do
    if [ -f "$file" ] ; then
      newfile=`echo "$file" | sed "s/${OLD}/${NEW}/g"`
      if [ -f "$newfile" ]; then
        echo "ERROR: $newfile exists already"
      else
        echo "renaming $file to $newfile ..."
        mv "$file" "$newfile"
      fi
    fi
done
Dies ist bisher das komplexeste Skript. La�t es uns ein bi�chen diskutieren. Die erste if-Anweisung testet, ob wir mindestens drei Kommandozeilenparameter angegeben haben. (Die spezielle Variable $# enth�lt die Anzahl der Argumente). Wenn nicht, wird der Hilfetext zu dem Befehl cat geschickt, der ihn wiederum auf dem Bildschirm ausgibt. Nach der Ausgabe des Hilfetextes wird das Programm beendet. Wenn es drei oder mehr Argumente sind, bekommt die Variable OLD den Wert des ersten Arguments zugewiesen und die Varibale NEW den Wert des zweiten. Als n�chstes verschieben wir die Kommandozeilenparameter zweimal, um das dritte Argument in die erste Position von $* zu bekommen. Mit $* gehen wir dann in die for Schleife. Jedes Argument in $* wird nun eines nach dem anderen der Variablen $file zugewiesen. Hier testen wir zuerst, ob die Datei wirklich existiert und bilden dann den neuen Dateinamen durch Benutzen von Finden und Ersetzen mit sed. Die backticks werden benutzt, um das Ergebnis der Variable newfile zuzuweisen. Jetzt haben wir alles, was wir brauchen: Den alten Dateinamen und den neuen. Dies wird dann zusammen mit dem Befehl mv zum Umbennen der Dateien benutzt.

Funktionen
Sobald du ein etwas komplexeres Programm hast, wirst du bemerken, da� du denselben Code an mehreren Stellen brauchst und es zudem hilfreich finden, dem ganzen etwas Struktur zu geben. Eine Funktion sieht folgenderma�en aus:
functionname()
{
 # inside the body $1 is the first argument given to the function
 # $2 the second ...
 body
}
Man mu� Funktionen am Anfang des Skripts "deklarieren", bevor man sie benutzt.

Hier ist ein Skript namens xtitlebar , das man dazu benutzen kann, um den Namen eines Terminalfensters zu ver�ndern. Wenn du mehrere von ihnen offen hast, ist es so einfacher, sie zu finden. Das Skript schickt eine escape Sequenz, die vom Terminal interpretiert wird und es dazu veranla�t, den Namen in der Titelbar zu �ndern. Das Skript benutzt eine Funktion namens help. Wie man sehen kann, wird die Funktion einmal definiert und dann zweimal benutzt:
#!/bin/sh
# vim: set sw=4 ts=4 et:

help()
{
    cat <<HELP
xtitlebar -- change the name of an xterm, gnome-terminal or kde konsole

USAGE: xtitlebar [-h] "string_for_titelbar"

OPTIONS: -h help text

EXAMPLE: xtitlebar "cvs"

HELP
    exit 0
}

# in case of error or if -h is given we call the function help:
[ -z "$1" ] && help
[ "$1" = "-h" ] && help

# send the escape sequence to change the xterm titelbar:
echo -e "\033]0;$1\007"
#
Es ist eine gute Angewohnheit, immer ausf�hrlichen Hilfetext im Skript zu haben. Dies macht es f�r andere (und dich) m�glich, da� Skript zu benutzen und zu verstehen.

Kommandozeilenargumente
Wir haben gesehen, da� $* und $1, $2 ... $9 die Argumente (die Zeichenketten, die hinter dem Programmnamen stehen) enth�lt, die der Benutzer auf der Kommandozeile spezifiziert hat. Bisher hatten wir nur sehr wenig oder eher einfache Kommandozeilensyntax (ein paar obligatorische Argumente und die Option -h f�r Hilfe. Aber bald wirst du feststellen, da� du eine Art Parser (Analysierer) f�r komplexere Programme brauchst, in denen du deine eigenen Optionen definierst. Die Konvention ist, da� alle optionalen Parameter ein vorangestelltes Minuszeichen besitzen und vor allen anderen Parametern (wie z.B. Dateinamen) stehen.

Es gibt viele M�glichkeiten, einen Parser zu implementieren. Die folgende while-Schleife kombiniert mit einer case-Anweisung ist eine sehr gute L�sung f�r einen allgemeinen Parser:
#!/bin/sh
help()
{
  cat <<HELP
This is a generic command line parser demo.
USAGE EXAMPLE: cmdparser -l hello -f -- -somefile1 somefile2
HELP
  exit 0
}

while [ -n "$1" ]; do
case $1 in
    -h) help;shift 1;; # function help is called
    -f) opt_f=1;shift 1;; # variable opt_f is set
    -l) opt_l=$2;shift 2;; # -l takes an argument -> shift by 2
    --) shift;break;; # end of options
    -*) echo "error: no such option $1. -h for help";exit 1;;
    *)  break;;
esac
done

echo "opt_f is $opt_f"
echo "opt_l is $opt_l"
echo "first arg is $1"
echo "2nd arg is $2"
Probier ihn aus. Du kannst ihn z.B. mit dem folgenden laufen lassen:
cmdparser -l hello -f -- -somefile1 somefile2
Dies ergibt
opt_f is 1
opt_l is hello
first arg is -somefile1
2nd arg is somefile2
Wie arbeitet das Programm? Grunds�tzlich l�uft die Schleife durch alle Argumente und vergleicht sie mit der case-Anweisung. Wenn es ein �bereinstimmendes findet, setzt es eine Variable und verschiebt die Kommandozeile um eins. Die Unixkonvention ist, da� Optionen (Dinge, die mit einem Minus anfangen) zuerst kommen m�ssen. Man kann das Ende einer Option durch zwei Minuszeichen anzeigen (--). Dies braucht man z.B. bei grep, um nach einer Zeichenkette zu suchen, die mit einem Minus anf�ngt:
Search for -xx- in file f.txt:
grep -- -xx- f.txt
Unser Optionenparser kann ebenfalls die zwei Minuszeichen handhaben, wie du im obigen Listing sehen kannst.

Beispiele

Ein allgemeines Dummyskript

Jetzt haben wir fast alle Komponenten diskutiert, die man zum Schreiben eines Skriptes braucht. Alle guten Skripte sollten einen Hilfetext enthalten und man kann auch unseren allgemeinen Optionenparser benutzen, selbst wenn das Skript nur eine Option hat. Deshalb ist es eine gute Idee, ein Dummyskript namens framework.sh zu haben, da� du als Rahmen f�r andere Skripte verwenden kannst. Wenn du ein neues Skript schreiben willst, machst du einfach nur eine Kopie:
cp framework.sh myscript
und f�gst dann die aktuelle Funktionalit�t in "myscript" ein.

La�t uns jetzt noch zwei weitere Beispiele anschauen:

Ein Bin�r-nach-Dezimal-Umwandler

Das Skript b2d konvertiert eine bin�re Zahl (z.B. 1101) in ihr dezimales �quivalent. Es ist ein Beispiel, das zeigt, da� man einfache Mathematik mit expr machen kann:
#!/bin/sh
# vim: set sw=4 ts=4 et:
help()
{
  cat <<HELP
b2h -- convert binary to decimal

USAGE: b2h [-h] binarynum

OPTIONS: -h help text

EXAMPLE: b2h 111010
will return 58
HELP
  exit 0
}

error()
{
    # print an error and exit
    echo "$1"
    exit 1
}

lastchar()
{
    # return the last character of a string in $rval
    if [ -z "$1" ]; then
        # empty string
        rval=""
        return
    fi
    # wc puts some space behind the output this is why we need sed:
    numofchar=`echo -n "$1" | wc -c | sed 's/ //g' `
    # now cut out the last char
    rval=`echo -n "$1" | cut -b $numofchar`
}

chop()
{
    # remove the last character in string and return it in $rval
    if [ -z "$1" ]; then
        # empty string
        rval=""
        return
    fi
    # wc puts some space behind the output this is why we need sed:
    numofchar=`echo -n "$1" | wc -c | sed 's/ //g' `
    if [ "$numofchar" = "1" ]; then
        # only one char in string
        rval=""
        return
    fi
    numofcharminus1=`expr $numofchar "-" 1` 
    # now cut all but the last char:
    rval=`echo -n "$1" | cut -b 0-${numofcharminus1}`
}
    

while [ -n "$1" ]; do
case $1 in
    -h) help;shift 1;; # function help is called
    --) shift;break;; # end of options
    -*) error "error: no such option $1. -h for help";;
    *)  break;;
esac
done

# The main program
sum=0
weight=1
# one arg must be given:
[ -z "$1" ] && help
binnum="$1"
binnumorig="$1"

while [ -n "$binnum" ]; do
    lastchar "$binnum"
    if [ "$rval" = "1" ]; then
        sum=`expr "$weight" "+" "$sum"`
    fi
    # remove the last position in $binnum
    chop "$binnum"
    binnum="$rval"
    weight=`expr "$weight" "*" 2`
done

echo "binary $binnumorig is decimal $sum"
#
Der in diesem Skript benutzte Algorithmus nimmt die dezimale Wertigkeit (1,2,4,8,16,..) jeder Ziffer beginnend mit der am weitesten rechts liegenden und addiert sie dann zu der Summe, wenn die Ziffer eine 1 ist. "10" ist daher:
0 * 1 + 1 * 2 = 2
Um die Ziffern aus der Zeichenkette zu bekommen, benutzen wir die Funktion lastchar. Sie benutzt wc -c, um die Anzahl der Zeichen in der Zeichenkette zu z�hlen und dann cut, um das letzte Zeichen abzuschneiden. Die chop Funktion hat dieselbe Logik, l�scht aber das letzte Zeichen, d.h. es schneidet alles vom Beginn her aus bis auf das letzte Zeichen.

Ein Dateirotierungsprogramm
Vielleicht geh�rst du zu denen, die alle rausgehende Mail in einer Datei abspeichern. Nach ein paar Monaten wird diese Datei recht gro� und macht den Zugriff langsam, wenn du es in dein Mailprogramm l�dst. Das folgende Skript rotatefile kann dir vielleicht helfen. Es benennt den Mailfolder, la� ihn uns outmail nennen, in outmail.1 um, wenn es schon ein outmail.1 gibt, dann wird es outmail.2 etc...
#!/bin/sh
# vim: set sw=4 ts=4 et: 
ver="0.1"
help()
{
    cat <<HELP
rotatefile -- rotate the file name 

USAGE: rotatefile [-h]  filename

OPTIONS: -h help text

EXAMPLE: rotatefile out
This will e.g rename out.2 to out.3, out.1 to out.2, out to out.1
and create an empty out-file

The max number is 10

version $ver
HELP
    exit 0
}

error()
{
    echo "$1"
    exit 1
}
while [ -n "$1" ]; do
case $1 in
    -h) help;shift 1;;
    --) break;;
    -*) echo "error: no such option $1. -h for help";exit 1;;
    *)  break;;
esac
done

# input check:
if [ -z "$1" ] ; then
 error "ERROR: you must specify a file, use -h for help" 
fi
filen="$1"
# rename any .1 , .2 etc file:
for n in  9 8 7 6 5 4 3 2 1; do
    if [ -f "$filen.$n" ]; then
        p=`expr $n + 1`
        echo "mv $filen.$n $filen.$p"
        mv $filen.$n $filen.$p
    fi
done
# rename the original file:
if [ -f "$filen" ]; then
    echo "mv $filen $filen.1"
    mv $filen $filen.1
fi
echo touch $filen
touch $filen

Wie arbeitet das Programm? Nach der �berpr�fung, ob der Benutzer einen Dateinamen eingegeben hat, geht es in eine for-Schleife, die von 9 nach 1 runterz�hlt. Datei 9 wird nun in 10, Datei 8 in 9 usw. umbenannt. Nach der Schleife nennen wir die Originaldatei in 1 um und erzeugen eine leere Datei mit dem Namen der Originaldatei.

Fehlersuche

Die einfachste Fehlersuchenhilfe ist nat�rlich der Befehl echo. Man kann ihn benutzen, um spezielle Variablen, um den Bereich herum, wo man den Fehler vermutet, auszudrucken. Dies ist wahrscheinlich das, was die meisten Shellprogrammierer in 80% der F�lle tun, um einen Fehler zu finden. Der Vorteil von einem Shellskript ist, da� es keinerlei erneute Kompilation erfordert und das Einf�gen der echo Anweisung sehr schnell gemacht is.

Die Shell verf�gt auch �ber einen echten Debugmodus. Wenn in deinem Skript "strangescript" ein Fehler ist, kannst du folgenderma�en nach ihm suchen:
sh -x strangescript
Dies f�hrt das Skript aus und zeigt alle Anweisungen, die ausgef�hrt werden mit bereits ersetzen Variablen und Wildcards an.

In der Shell gibt es auch einen Modus, um Syntaxfehler zu suchen, ohne das das Programm tats�chlich ausgef�hrt wird. Um es zu benutzen, la�:
sh -n your_script
laufen. Wenn nichst auf dem Bildschirm ausgegeben wird, ist das Programm frei von Syntaxfehlern.

Wir hoffen, da� du jetzt selber anfangen wirst, Shellskripte zu schreiben. Viel Spa�!

Referenzen