Shellprogrammierung


Die Shell ist eines der ersten Programme, die für Unix-Systeme entwickelt wurden. Sie diente hauptsächlich zum grundlegenden Arbeiten mit dem System. Sie wird seit dem ausgiebig verwendet und kann aufgrund ihrer Leistungsfähigkeit selbst komplexe Aufgaben bewältigen.

Grundlagen

Prozesse

Prozesse sind ausgeführte Programme. Sie beinhalten nicht nur den Programmcode, sondern auch ihre Arbeitsdaten und Prozessattribute.

Zu den Prozessattributen gehören :

Beim Beenden gibt der Prozess einen Exit Code an die Umgebung zurück. Per Konvention zeigt eine 0 an, daß der Prozess erfolgreich war, andere Werte (1-255) einen Mißerfolg.

Spezielle Dateien

Dateien werden über File Descriptoren identifiziert. Diese sind Nummern. Besondere Bedeutung haben hierbei : Sie stellen eine Konvention dar, damit offene Dateien einfach an Prozesse übergeben werden können. Standard Input ist eine zum Lesen geöffnete Datei. Standard Output und Standard Error sind zum Schreiben geöffnete Dateien. Standard Error dient zur Ausgabe von Fehlermeldungen.

Pipelines

Pipelines sind eine Methode, um jeweils eine Ausgabedatei an eine Eingabedatei zu binden. Dadurch kann ein Prozess Daten, die ein anderer Prozess schreibt, lesen. Durch eine Verkettung von Pipelines über mehrere Prozesse können Daten in beliebig vielen Stufen gefiltert werden.

Aufgaben einer Shell

Die Shell dient u. a. dazu, eine Umgebung für Prozesse zu schaffen, d. h., Prozesse zu starten und deren Prozessattribute zu beeinflussen. Sie kann jedoch auch selbst Aufgaben bewältigen, z. B. Dateien verwalten.

Die prinzipielle Arbeitsweise ist die, wiederholt interaktiv Befehle entgegenzunehmen, diese zu interpretieren und auszuführen. Diese Befehle können jedoch auch von einer Datei gelesen werden. Eine solche Datei nennt man dann ein Shellscript. Shellscripts werden nicht-interaktiv ausgeführt.

Neben der ursprünglichen Bourne Shell entstanden im Laufe der Zeit andere Programme, die Aufgaben der Shell übernehmen können :

Die Sprache der Shell

Im Folgenden soll die Sprache, die in Shellscripts für die Bourne Shell oder die Bourne Again Shell verwendet werden kann, beschrieben werden. Man kann sie durchaus als Programmiersprache bezeichnen, denn ihre Fähigkeiten sind mit denen üblicher Programmiersprachen vergleichbar.

Kommentare

Kommentare sind Teile des Shellscripts, die nicht interpretiert werden. In der Shell ist jeder Text zwischen dem Zeichen # am Anfang eines Wortes und der nächten Zeilenschaltung ein Kommentar.

Variablen

Wie in fast jeder Programmiersprache gibt es auch in der Shell Variaben. Es gibt allerdings keine Datentypen. Variablen können beliebige Zeichenketten (Strings) enthalten oder auch leer sein. Sie brauchen im allgemeinen nicht deklariert zu werden.

Variablennamen werden aus Buchstaben (A-Z, a-z), Ziffern (0-9) und dem Underscore (_) gebildet.

Die Zuweisung einer Variablen erfolgt duch den Befehl

    var=value

Den Wert (Inhalt) einer Variablen erhält man durch einen der Ausdrücke :

    $var
    ${var}

Die zweite Form ist interessant, wenn unmittelbar hinter dem Variablennamen Zeichen folgen sollen, die sonst als Bestandteil des Variablennamens betrachtet würden.

Variablen können mit gleichnamigen Environment-Variablen verbunden werden. Dadurch werden Änderungen der Shell-Variablen automatisch in die entsprechenden Environment-Variablen übertragen.

Die Verbindung erfolgt durch den export-Befehl :

    export var

Beim Aufruf der Shell stehen alle übernommenen Environment-Variablen als Shell-Variablen zur Verfügung. Diese sind jedoch nicht exportiert.

Es gibt auch Variablennamen, die aus einem Sonderzeichen bestehen. Diese haben eine besondere Bedeutung und werden in den folgenden Kapiteln erklärt.

Prozessaufruf

Ein Prozess (Programm) wird einfach durch die Angabe seines Namens aufgerufen. Die entsprechende Programmdatei wird über die Shell-Variable PATH in verschiedenen Directories gesucht.

    pwd

Das Programm wird im "Vordergrund" aufgerufen. Die Shell wartet daraufhin bis zur Beendigung des Prozesses. Anschließend steht der Exit Code des Prozesses in der speziellen Variablen ? zur Verfügung.

Es können mehrere Befehle durch ; getrennt in einer Zeile angegeben werden :

    date; pwd; ls

Dann werden die einzelnen Befehle hintereinander ausgeführt.

Soll die Shell nicht warten, wird das Programm durch Angabe des Zeichens & im "Hintergrund" aufgerufen :

    xterm &

Natürlich steht der Exit Code hiernach nicht zur Verfügung, weil der Prozess ja i. a. noch läuft. Stattdessen steht in der speziellen Variablen ! die Prozessnummer zur Verfügung.

Auch die Shell selber hat natürlich eine Prozessnummer. Sie läßt sich über die spezielle Variable $ ermitteln.

Parameter werden durch Spaces getrennt hinter den Programmnamen gesetzt :

    ls -l mydir
    xterm -bg blue &

Die aufgerufenen Prozesse bekommen alle Prozessattribute der Shell übergeben, insbesondere die Environment-Variablen und offene Dateien.

Die Parameter, die einem Shellscript übergeben wurden, können darin über spezielle Variablen ermittelt werden. Die Variable # gibt die Anzahl der Parameter an. Die Parameter selbst stehen in den Variablen 1, 2, usw. und der Name des Shellscriptes in der Variablen 0.

Auch Shellscripts können wie Programme über ihren Namen aufgerufen werden, indem sie mit Ausführungsberechtigung versehen werden und in den Suchpfad (PATH) gestellt werden.

Es gibt eine Reihe in der Shell eingebaute Befehle, die nicht als Programme aufgerufen werden, sondern von der Shell selber interpretiert werden.

Eines dieser Befehle ist echo, das alle Parameter einfach über Standard Output ausgibt. Dies kann für kurze Nachrichten oder Tests verwendet werden.

Ein anderer eingebauter Befehl ist wait, der auf die Beendigung eines vorher im Hintergrund gestarteten Programmes wartet und dessen Exit Code verfügbar macht :

    wait $!
    echo $?

Ein weiterer Befehl ist exec. Dieser bewirkt, daß der Shellprozeß durch den als Parameter angegebenen Prozeß ersetzt wird :

    exec echo All work done.

Die Beendigung eines Shellscripts erfolgt implizit durch das Erreichen des Endes oder explizit durch den Befehl exit, ggf. unter Angabe des Exit-Codes :

    exit 1

Steuerung der Eingaben und Ausgaben

Alle offenen Dateien, insbesondere die Standard-Dateien, können auf verschiedene Weise umgeleitet werden. Hierzu dienen die Befehlszusätze

    fileno<filename
    fileno>filename
    fileno>>filename
    <filename
    >filename
    >>filename

Sie leiten die angegebene offene Datei als Eingabe (<) bzw. als Ausgabe (>) auf die Datei des angegebenen Namens um. Sinnvollerweise sind die Default-Filenummern (fileno) die des Standard Input bzw. des Standard Output. >> bewirkt ein Anhängen der Ausgabe an die angegebene Datei.

    ls -l 1>list.txt 2>error.txt
    sort +2 <data.txt >result.txt
    date >>logfile

Es kann auch im Shellscript eine Eingabe angegeben werden :

    cat <<EOF >testfile
    These are lines
    that all go into
    the output file.
    The shell received $# parameters.
    Our home is at $HOME and not where the heart is.
    EOF

Die Angabe hinter << dient hierbei als Kennzeichen für das Ende der Eingabe.

Statt eines Dateinamens kann auch eine (andere) Dateinummer angegeben werden :

    fileno1<&fileno2
    fileno1>&fileno2
    <&fileno2
    >&fileno2

Dies bewirkt die Umleitung einer offenen Datei fileno1 auf eine andere offene Datei fileno2. Auch hier kann vereinfachend Standard Input oder Standard Output verwendet werden. Hierdurch kann man offene Dateien "kopieren".

Wird statt fileno2 - angegeben, so wird die Datei stattdessen geschlossen.

In folgendem Beispiel werden Standard Output und Standard Error auf die selbe Datei umgeleitet :

    find /tmp -user $USER -print 1>list.txt 2>&1

Über den Befehl exec kann eine permanente Umleitung erfolgen :

    exec 2>error.log
    rmdir /tmp

Eine Datei kann dadurch auch für eine Passage umgeleitet werden :

    exec 3>&1 1>status.txt
    date
    pwd
    ls -l
    exec 1>&3 3>&-

Pipelines werden erzeugt, indem mehrere Befehle hintereinander durch | angegeben werden :

    ls -l | sort +4n

Für jedes | wird eine Pipeline erzeugt und jeweils der Standard Output des linken Befehls und der Standard Input des rechten Befehls auf diese Pipeline umgeleitet.

Auch Kombinationen sind möglich :

    find /tmp -name test -print 2>&1 | xargs ls -l | tee logfile
    ls -al | fgrep testuser | sort +4n >testfiles
    cpio -it <data.cpio | less

Der jeweils ganz rechts stehende Befehl wird dabei im Vordergrund, alle anderen im Hintergrund ausgeführt. Anschließend steht der Exit Code des rechten Befehls zur Verfügung.

Befehlskonstrukte

Befehle können in Gruppen zusammengefaßt werden :

    {
      date
      pwd
      ls -l
    } >status.txt

Die Shell kann auch geteilt werden, um ein Teil-Shellscript auszuführen :

    (
      cd subdir
      ls -t
    ) >subfiles.txt

Da hier ein neuer Shellprozess erzeugt wird, kann dieser seine Prozessattribute ändern, ohne daß sich dies auf die "umliegende" Shell auswirkt.

Eine einfache Unterscheidung erfolgt mit if :

    if expr
      then
        command1
      else
        command2
    fi

Zunächst wird expr als Befehl ausgeführt. Ist sein Exit-Code 0, so wird command1 ausgeführt, sonst command2. Für command können auch jeweils mehrere Befehle angegeben werden. Der else-Teil kann auch entfallen.

Eine verkürzte Syntax ermöglichen && und || :

    command1 && command2
    command3 || command4

command2 wird nur dann ausgeführt, wenn command1 einen Exit-Code 0 liefert und command4 nur dann, wenn command3 einen Exit-Code ungleich 0 liefert.

Eine Unterscheidung nach Textmustern geschieht durch case :

    case string in
      pattern1)
        command1 ;;
      pattern2)
        command2 ;;
      pattern3)
        command3 ;;
    esac

string ist ein beliebiger Text, der meistens aus einer Variablen stammt. Er wird der Reihe nach auf alle Muster pattern geprüft. Paßt eines, so wird der entsprechende Befehl ausgeführt und das Konstrukt beendet. Es wird also höchstens einer der Teile ausgeführt. Es können beliebig viele Teile angegeben werden. Auch hier können für command jeweils mehrere Befehle angegeben werden.

pattern ist ein beliebiger Text. Bestimmte Zeichen und Zeichenfolgen haben darin jedoch eine besondere Bedeutung :

Das Muster * paßt auf alle Texte und kann somit sinnvoll als letztes Muster verwendet werden.

Zur Bildung von Schleifen gibt es while :

    while expr
      do
        command
      done

Zunächst wird expr als Befehl ausgeführt. Ist sein Exit-Code 0, so wird command ausgeführt und der Vorgang wiederholt, sonst wird das Konstrukt beendet. Für command können auch hier mehrere Befehle angegeben werden.

Eine aufzählende Schleife wird mit for gebildet :

    for var in wordlist
      do
        command
      done

wordlist ist hier eine Liste von Texten (Wörter), durch Spaces getrennt. Für jedes dieser Wörter wird der Variablen var das jeweilige Wort zugewiesen und dann der Befehl command ausgeführt.

Wird in wordlist ausgelassen, so werden alle dem Shellscript übergebenen Parameter verwendet.

Filename Generation

Befehle und Parameterlisten werden aus Wörtern gebildet, die durch Spaces getrennt sind. Mit Hilfe besonderer Zeichen und Zeichenfolgen lassen sich auch aus Dateinamen Wortlisten bilden.

Enthält ein angegebenes Wort eines dieser Zeichen, so wird es als Muster betrachtet und durch eine Liste aller Dateinamen ersetzt, die diesem Muster entsprechen.

Die besonderen Zeichen und Zeichenfolgen sind :

Die Zeichen * und ? umfassen nur dann einen Punkt, wenn sie weder am Anfang eines Musters noch hinter einem / stehen.

Alle diese Angaben lassen sich innerhalb eines Musters mehrfach und an beliebigen Stellen verwenden. Das Zeichen / bedeutet wie in allen Dateinamen das Trennzeichen für Directories und läßt sich entsprechend in Mustern verwenden :

*/test
bezeichnet alle existierenden Dateinamen test in allen Subdirectories erster Ebene
test*/*.c
bezeichnet alle existierenden Dateinamen, die mit .c enden, in allen Subdirectories erster Ebene, deren Name mit test beginnt.
*[0-9A-Z_]*
bezeichnet alle existierenden Dateinamen, die eine Ziffer, einen Großbuchstaben oder das Zeichen _ enthalten.
.??*
bezeichnet alle existierenden Dateinamen, die mit einem Punkt beginnen und mindestens zwei nachfolgende Zeichen haben.

Wird zu dem angegebenen Muster kein passender Dateiname gefunden, so bleibt das Muster als Wort unverändert.

Unabhängig von Dateinamen lassen sich auch über einen Vervielfältigungsmechanismus Wortlisten erzeugen. Dazu dienen geschweifte Klammern {} und das Komma.

Enthält ein Wort die Zeichenfolge {wordlist}, wobei wordlist eine durch Komma getrennte Liste von Wörtern ist, so wird dies als ein Listenmuster betrachtet. Dies bewirkt, daß es durch eine Liste von ähnlichen Wörtern ersetzt wird, die entstehen, indem jeweils der umliegende Text des Musters mit einem der Wörter der wordlist kombiniert wird :

    abc{1,22,333}xyz  ->  abc1xyz abc22xyz abc333xyz
    abc{A,B}{1,2}xyz  ->  abcA1xyz abcA2xyz abcB1xyz abcB2xyz

Diese Listenerzeugung erfolgt vor der Filename Generation und läßt sich daher damit kombinieren :

    test*.{c,h,txt}  ->  test*.c test*.h test*.txt

Die Listenerzeugung ist eine Fähigkeit der bash, jedoch nicht der Bourne Shell.

Escaping und Quoting

Oft will man verhindern, daß besondere Zeichen ihre spezifische Wirkung haben. Dies läßt sich mit verschiedenen Methoden erreichen.

Das Escape-Zeichen \ nimmt dem nachfolgenden Zeichen seine besondere Wirkung. Am Zeilenende bewirkt es, daß die nachfolgende Zeile als Fortsetzung der Zeile betrachtet wird. Dadurch lassen sich lange Zeilen aufspalten, ohne ihre Wirkung zu ändern.

    echo Alles ok \?
    echo \<HTML\>
    echo \*\*\* Error !
    echo Dies \\ ist ein Backslash.
    echo \# Dies ist kein Kommentar.
    echo Diese Zeile wird in zwei Zeilen aufgeteilt, \
         weil sie sonst zu lang ist.

Eine andere Möglichkeit für die direkte Angabe von Schriftzeichen ist das Quoting. Der fragliche Text wird dazu innerhalb von Anführungszeichen gesetzt. Es stehen dafür die einfachen (') und die doppelten (") Anführungszeichen zur Verfügung.

Innerhalb von einfachen Anführungszeichen (') verlieren alle Zeichen ihre besondere Wirkung, sogar die Zeilenschaltung. Daher läßt sich das Anführungszeichen selber nicht damit darstellen.

Innerhalb von doppelten Anführungszeichen (") behalten nur folgende Zeichen ihre besondere Wirkung :

Jeder Text innerhalb von Anführungszeichen wird als (höchstens) ein Wort aufgefaßt. Quoting läßt sich jedoch auch auf einen Teil eines Wortes anwenden.

    echo 'Quotes sind '"'"' und "'

Hier wird 3 mal Quoting betrieben :

Das Ergebnis ist ein Wort, da alle Quotings zusammenhängen.

Es gibt zwei spezielle Variablen, die eine Liste der Parameter enthalten. Deren Wirkung unterscheidet sich, je nach dem, ob sie in doppelten Anführungszeichen stehen, oder nicht :

$* und $@
liefern die Parameterliste als Wortliste mit Filename Generation. Leere Parameter werden ausgelassen.
"$*"
liefert die Parameterliste als ein Wort.
"$@"
liefert die Parameterliste als Wortliste. Ohne Parameter wird eine leere Liste geliefert.

Hierzu hilft auch der Befehl shift. Er löscht den ersten Parameter und schiebt alle folgenden einen Platz zurück. Dies kann verwendet werden, um die ersten Parameter gesondert zu behandeln und alle folgenden als Liste.

Eine andere Form des Quotings bietet das Back-Quote (`). Darin wird ein Befehl eingeschlossen. Hierin haben folgende Zeichenfolgen eine besondere Bedeutung :

Der Befehl wird von einer neuen Shell interpretiert (einschließlich Quoting) und ausgeführt und dessen Standard Output wird als Text in die Befehlszeile eingesetzt.

    ls -ld `find /tmp -user $USER -print`
    echo "The time now is: `date`"

Ohne doppelte Anführungszeichen wird die Befehlsausgabe als Wortliste eingesetzt und Filename Generation durchgeführt. Mit Anführungszeichen wird sie als ein Wort eingesetzt.

Ein geschachteltes Beispiel :

    TEXT='Example. "Text"'
    t="`echo \"\$TEXT\" | sed \"s/\\\\./:/g;s/\\\"/'/g\
"`"
    # `echo "$TEXT" | sed "s/\\./:/g;s/\"/'/g"`
    # echo "$TEXT" | sed "s/\./:/g;s/\"/'/g"
    # sed s/\./:/g;s/"/'/g
    echo "$t"
    # Example: 'Text'

Escaping und Quoting läßt sich auch auf die Muster des case-Befehls und für die Dateiumleitung anwenden.

Ähnlich der Back-Quotes führt der in der Shell eingebaute Befehl eval eine Re-interpretation seiner Parameterliste durch und führt den entstehenden Befehl aus. Dies beinhaltet Variablen-Ersetzung, Filename Generation, Escaping und Quoting. Dies kann zur dynamischen Konstruktion beliebiger Befehle verwendet werden.

    echo 'echo "$HOME"' >evaltest
    eval `cat evaltest`

Funktionen

Teile von Shellscripts können als Funktionen definiert werden. Dadurch können mehrfach vorkommende Teile zusammengefaßt werden oder das Shellscript übersichtlicher gestaltet werden.

Funktionen wirken ähnlich wie Befehle. Ihnen können Parameter übergeben werden und sie haben einen Rückgabewert, einem Exit-Code vergleichbar.

    liste()
    {
      echo "Liste #$1: $2"
      ls -l $3
      return 0
    }
    liste 1 "Text-Dateien" "*.txt"
    liste 2 "Bild-Dateien" "*.png *.jpg"
    liste 3 "Dokumentation" "*.doc"

Der return-Befehl gibt den Rückgabewert an. Der Befehl exit kann hierfür nicht verwendet werden, weil dieser auch innerhalb einer Funktion die Shell beendet.

Zur Ausführung einer Funktion wird keine neue Shell erzeugt, daher wirken sich Änderungen der Prozessattribute auch auf die Aufrufumgebung aus. Lediglich die Parameter sind getrennt.

Innerhalb einer Funktion können lokale Variablen deklariert werden :

    local var

Diese Variablen sind nur innerhalb der betreffenden Funktion und aller darin aufgerufenen Funktionen sichtbar.

Ein rekursiver Aufruf von Funktionen ist möglich.

Komplexe Beispiele

Um einen Eindruck zu vermitteln, wie die Sprachelemente verwendet werden können, folgen einige Beispiel-Shellscripts.

Justage der Berechtigungen eines Directory-Baumes :

    find . -type d -print | xargs chmod u+rw,go-rw,a+x
    find . -type f -print | xargs chmod u+rw,go-rw

Umwandeln einer Liste von Dateien :

    ls *.png | sed 's/\.png$//' |
      while read file
        do
          p="$file.png"
          j="$file.jpg"
          if [ ! -f "$j" ]
            then
              echo "$p -> $j"
              cjpeg "$p" >"$j"
          fi
        done

Erstellung einer Backup-Archivdatei :

    BACKUPDIR=/usr/local/data
    SUBDIRS="mydata otherdata"
    BACKUPARCH=data.cpio
    cd $HOME/backup
    [ -f $BACKUPARCH ] && mv $BACKUPARCH $BACKUPARCH.old
    (
      cd $BACKUPDIR
      find $SUBDIRS ! -name '*.tmp' -print | cpio -o
    ) >$BACKUPARCH
    ls -l $BACKUPARCH

Beenden eines Service-Daemons :

    PIDFILE=service.pid
    if [ -s $PIDFILE ]
      then
        p=`cat $PIDFILE`
        kill "$p"
        n=0
        while [ $n -lt 10 ]
          do
            if kill -0 "$p"
              then
                sleep 1
                n=`expr $n + 1`
              else
                >$PIDFILE
                exit 0
            fi
          done
        kill -9 "$p"
        >$PIDFILE
        exit 1
      else
        exit 0
    fi

Bereinigung der Namen eines Directory-Baumes :

    CLEAN='
      {
        d = ""
        a = $0
        if (a ~ "/")
          {
            d = a
            sub("[^/]*$","",d)
            sub("^.*/","",a)
          }
        b = a
        gsub(" ","_",b)
        gsub("&","+",b)
        if (a != b)
          {
            print d a
            print d b
          }
      }'
    find "$@" -depth -print | awk "$CLEAN" |
      while read a
        do
          read b
          mv "$a" "$b"
        done

Rekursive Directoryliste ohne den find-Befehl

    subdir()
    {
      local d f
      d="$1"
      echo "$d"
      ls "$d" |
        while read f
          do
            [ -d "$d/$f" ] && subdir "$d/$f"
          done
    }
    subdir .

Dokumentation

Die Manual Page der bash ist sehr ausführlich und gut strukturiert.


Zusammenfassung


Thomas Conze <thomas.conze@gmx.net>