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