SelfLinux Programmierung Shellprogrammierung Abschnitt 5 | SelfLinux-0.13.0

| Shellprogrammierung | Autor Formatierung GFDL

#### 5 Anhang A: Beispiele

#### 5.1 Schleifen und Rückgabewerte

Man kann mit einer until- bzw. mit einer while-Schleife schnell kleine aber sehr nützliche Tools schreiben, die einem lästige Aufgaben abnehmen.

#### 5.1.1 Schleife, bis ein Kommando erfolgreich war

Angenommen, bei der Benutzung eines Rechners tritt ein Problem auf, bei dem nur der Administrator helfen kann. Dann möchte man informiert werden, sobald dieser an seinem Arbeitsplatz ist. Man kann jetzt in regelmäßigen Abständen das Kommando who ausführen, und dann in der Ausgabe nach dem Eintrag root suchen. Das ist aber lästig.

Einfacher geht es, wenn wir uns ein kurzes Skript schreiben, das alle 30 Sekunden automatisch überprüft, ob der Admin angemeldet ist. Wir erreichen das mit dem folgenden Code:

auf-root-warten.sh

    #!/bin/sh
    until who | grep "^root "
          do sleep 30
    done
    echo Big Brother is watching you!

Das Skript führt also so lange das Kommando aus, bis die Ausführung erfolgreich war. Dabei wird die Ausgabe von who mit einer Pipe in das grep-Kommando umgeleitet. Dieses sucht darin nach einem Auftreten von root am Zeilenanfang. Der Rückgabewert von grep ist 0 wenn das Muster gefunden wird, 1 wenn es nicht gefunden wird und 2 wenn ein Fehler auftrat. Damit der Rechner nicht die ganze Zeit mit dieser Schleife beschäftigt ist, wird im Schleifenkörper ein sleep 30 ausgeführt, um den Prozeß für 30 Sekunden schlafen zu schicken. Sobald der Admin sich eingeloggt hat, wird eine entsprechende Meldung ausgegeben.

#### 5.1.2 Schleife, bis ein Kommando nicht erfolgreich war

Analog zum vorhergehenden Beispiel kann man auch ein Skript schreiben, das meldet, sobald sich ein Benutzer abgemeldet hat. Dazu ersetzen wir nur die until-Schleife durch eine entsprechende while-Schleife:

warten-bis-root-verschwindet.sh

    #!/bin/sh
    while who | grep "^root "
          do sleep 30
    done
    echo Die Katze ist aus dem Haus, Zeit, dass die Mäuse tanzen!

Die Schleife wird nämlich dann so lange ausgeführt, bis grep einen Fehler (bzw. eine erfolglose Suche) zurückmeldet.

#### 5.2 Ein typisches Init-Skript

Dieses Skript dient dazu, den Apache HTTP-Server zu starten. Es wird während des Bootvorgangs gestartet, wenn der dazugehörige Runlevel initialisiert wird.

Das Skript muss mit einem Parameter aufgerufen werden. Möglich sind hier start, stop, status, restart und reload. Wenn falsche Parameter übergeben wurden, wird eine entsprechende Meldung angezeigt.

Das Ergebnis der Ausführung wird mit Funktionen dargestellt, die aus der Datei /etc/rc.d/init.d/functions stammen. Ebenfalls in dieser Datei sind Funktionen, die einen Dienst starten oder stoppen.

Zunächst wird festgelegt, dass dieses Skript in der Bourne-Shell ausgeführt werden soll ( Auswahl der Shell).

beispiel.sh

    #!/bin/sh

Dann folgen Kommentare, die den Sinn des Skriptes erläutern.

beispiel.sh (Fortsetzung)

    ## Startup script for the Apache Web Server
    #
    # chkconfig: 345 85 15
    # description: Apache is a World Wide Web server. It is \
    #              used to serve HTML files and CGI
    #
    # processname: httpd
    # pidfile: /var/run/httpd.pid
    # config: /etc/httpd/conf/access.conf
    # config: /etc/httpd/conf/httpd.conf
    # config: /etc/httpd/conf/srm.conf

Jetzt wird die Datei mit den Funktionen eingebunden.

beispiel.sh (Fortsetzung)

    # Source function library.
    /etc/rc.d/init.d/functions

Hier werden die Aufrufparameter ausgewertet.

beispiel.sh (Fortsetzung)

    # See how we were called.
    case "$1" in
         start)
            echo -n "Starting httpd: "

Nachdem eine Meldung über den auszuführenden Vorgang ausgegeben wurde, wird die Funktion daemon aus der Funktionsbibliothek ausgeführt. Diese Funktion startet das Programm, dessen Name hier als Parameter übergeben wird. Dann gibt sie eine Meldung über den Erfolg aus.

beispiel.sh (Fortsetzung)

            daemon httpd
            echo

Jetzt wird ein Lock-File angelegt. (Ein Lock-File signalisiert anderen Prozessen, dass ein bestimmter Prozeß bereits gestartet ist. So kann ein zweiter Aufruf verhindert werden.)

beispiel.sh (Fortsetzung)

            touch /var/lock/subsys/httpd
            ;;
          stop)
            echo -n "Shutting down http: "

Hier passiert im Prinzip das gleiche wie oben, nur dass mit der Funktion killproc der Daemon angehalten wird.

beispiel.sh (Fortsetzung)

            killproc httpd
            echo

Danach werden Lock-File und PID-File gelöscht. (In einem sogenannten PID-File hinterlegen einige Prozesse ihre Prozeß-ID, um anderen Programmen den Zugriff zu erleichtern, z.B. um den Prozeß anzuhalten etc.)

beispiel.sh (Fortsetzung)

            rm -f /var/lock/subsys/httpd
            rm -f /var/run/httpd.pid
            ;;
        status)

Die Funktion status stellt fest, ob der entsprechende Daemon bereits läuft, und gibt das Ergebnis aus.

beispiel.sh (Fortsetzung)

            status httpd
            ;;
       restart)

Bei Aufruf mit dem Parameter restart ruft sich das Skript zwei mal selbst auf (in $0 steht der Aufrufname des laufenden Programms). Einmal, um den Daemon zu stoppen, dann, um ihn wieder zu starten.

beispiel.sh (Fortsetzung)

            $0 stop
            $0 start
            ;;
        reload)
            echo -n "Reloading httpd: "

Hier sendet die killproc-Funktion dem Daemon ein Signal das ihm sagt, dass er seine Konfiguration neu einlesen soll.

beispiel.sh (Fortsetzung)

            killproc httpd -HUP
            echo
            ;;
             *)
            echo "Usage: $0 {start|stop|restart|reload|status}"

Bei Aufruf mit einem beliebigen anderen Parameter wird eine Kurzhilfe ausgegeben. Dann wird dafür gesorgt, dass das Skript mit dem Exit-Code 1 beendet wird. So kann festgestellt werden, ob das Skript ordnungsgemäß beendet wurde ( exit).

beispiel.sh (Fortsetzung)

            exit 1
    esac
    exit 0

#### 5.3 Parameterübergabe in der Praxis

Es kommt in der Praxis sehr oft vor, dass man ein Skript schreibt, dem der Anwender Parameter übergeben soll. Wenn das nur eine Kleinigkeit ist (zum Beispiel ein Dateiname), dann fragt man einfach die entsprechenden vordefinierten Variablen ab. Sollen aber richtige Parameter eingesetzt werden, die sich so einsetzen lassen wie man es von vielen Kommandozeilentools gewohnt ist, dann benutzt man das Hilfsprogramm getopt. Dieses Programm parst die originalen Parameter und gibt sie in standardisierter Form zurück.

Das soll an folgendem Skript verdeutlicht werden. Das Skript kennt die Optionen -a und -b. Letzterer Option muss ein zusätzlicher Wert mitgegeben werden. Alle anderen Parameter werden als Dateinamen interpretiert.

getopt.sh

    #!/bin/sh
    set -- `getopt "ab:" "$@"` || {

Das set-Kommando belegt den Inhalt der vordefinierten Variablen neu, so dass es aussieht, als ob dem Skript die Rückgabewerte von getopt übergeben wurden. Man muss die beiden Minuszeichen angeben, da sie dafür sorgen, dass die Aufrufparameter an getopt und nicht an die Shell selbst übergeben werden. Die originalen Parameter werden von getopt untersucht und modifiziert zurückgegeben: a und b werden als Parameter Markiert, b sogar mit der Möglichkeit einer zusätzlichen Angabe.

Wenn dieses Kommando fehlschlägt ist das ein Zeichen dafür, dass falsche Parameter übergeben wurden. Also wird nach einer entsprechenden Meldung das Programm mit Exit-Code 1 verlassen.

getopt.sh (Fortsetzung)

           echo "Anwendung: `basename $0` [-a] [-b Name] Dateien" 1>&2
           exit 1
    }
    echo "Momentan steht in der Kommandozeile folgendes: $*"
    aflag=0 name=NONE
    while :
    do

In einer Endlos-Schleife, die man mit Hilfe des Null-Befehls (:) baut, werden die neuen Parameter der Reihe nach untersucht. Wenn ein -a vorkommt, wird die Variable aflag gesetzt. Bei einem -b werden per shift alle Parameter nach Links verschoben, dann wird der Inhalt des nächsten Parameters in der Variablen name gesichert.

getopt.sh (Fortsetzung)

           case "$1" in
                -a) aflag=1 ;;
                -b) shift; name="$1" ;;
                --) break ;;

Wenn ein -- erscheint, ist das ein Hinweis darauf, dass die Liste der Parameter abgearbeitet ist. Dann wird per break) die Endlosschleife unterbrochen. Die Aufrufparameter enthalten jetzt nur noch die eventuell angegebenen Dateinamen, die von dem restlichen Skript wie gewohnt weiterverarbeitet werden können.

getopt.sh (Fortsetzung)

           esac
           shift
    done
    shift

Am Ende werden die Feststellungen ausgegeben.

getopt.sh (Fortsetzung)

    echo "aflag=$aflag / Name = $name / Die Dateien sind $*"

#### 5.4 Fallensteller: Auf Traps reagieren

Ein laufendes Shell-Skript kann durch Druck auf die Interrupt-Taste (normalerweise [ `CTRL+``C` ]) unterbrochen werden. Durch Druck auf diese Taste wird ein Signal an den entsprechenden Prozeß gesandt, das ihn bittet sich zu beenden. Dieses Signal heißt SIGINT (für SIGnal INTerrupt) und trägt die Nummer 2. Das kann ein kleines Problem darstellen, wenn das Skript sich temporäre Dateien angelegt hat, da diese nach der Ausführung nur noch unnötig Platz verbrauchen und eigentlich gelöscht werden sollten. Man kann sich sicher auch noch wichtigere Fälle vorstellen, in denen ein Skript bestimmte Aufgaben auf jeden Fall erledigen muss, bevor es sich beendet.

Es gibt eine Reihe weiterer Signale, auf die ein Skript reagieren kann. Alle sind in der Man-Page von signal beschrieben. Hier die wichtigsten:

Nummer | Name | Bedeutung
--- | --- | ---
0 | Normal Exit | Wird durch das exit-Kommando ausgelöst.
1 | SIGHUP | Wenn die Verbindung abbricht (z.B. wenn das Terminal geschlossen wird).
2 | SIGINT | Zeigt einen Interrupt an ([ `CTRL+``C` ]).
15 | SIGTERM | Wird vom kill-Kommando gesendet.

Wie löst man jetzt dieses Problem? Glücklicherweise verfügt die Shell über das trap-Kommando, mit dessen Hilfe man auf diese Signale reagieren kann. Die Anwendung soll in folgendem Skript beispielhaft dargestellt werden.

Das Skript soll eine komprimierte Textdatei mittels zcat in ein temporäres File entpacken, dieses mit pg seitenweise anzeigen und nachher wieder löschen.

zeige-komprimierte-datei.sh

    #!/bin/sh
    stat=1
    temp=/tmp/zeige$$

Zunächst werden zwei Variablen belegt, die im weiteren Verlauf benutzt werden sollen. In stat wird der Wert abgelegt, den das Skript Falle eines Abbruchs als Exit-Status zurückliefern soll. Die Variable temp enthält den Namen für eine temporäre Datei. Dieser setzt sich zusammen aus /tmp/zeige und der Prozeßnummer des laufenden Skripts. So soll sichergestellt werden, dass noch keine Datei mit diesem Namen existiert.

zeige-komprimierte-datei.sh (Fortsetzung)

    trap 'rm -f $temp; exit $stat' 0
    trap 'echo "`basename $0`: Ooops..." 1>&2' 1 2 15

Hier werden die Traps definiert. Bei Signal 0 wird die temporäre Datei gelöscht und der Wert aus der Variable stat als Exit-Code zurückgegeben. Dabei wird dem rm-Kommando der Parameter -f mitgegeben, damit keine Fehlermeldung ausgegeben wird, falls die Datei (noch) nicht existiert. Dieser Fall tritt bei jedem Beenden des Skriptes auf, also sowohl bei einem normalen Ende, als auch beim Exit-Kommando, bei einem Interrupt oder bei einem Kill. Der zweite Trap reagiert auf die Signale 1, 2 und 15. Das heißt, er wird bei jedem unnormalen Ende ausgeführt. Er gibt eine entsprechende Meldung auf die Standard-Fehler-Ausgabe aus. Danach wird das Skript beendet, und der erste Trap wird ausgeführt.

zeige-komprimierte-datei.sh (Fortsetzung)

    case $# in
        1) zcat "$1" > $temp
           pg $temp
           stat=0
           ;;

Jetzt kommt die eigentliche Funktionalität des Skriptes: Das case-Kommando ( case) testet die Anzahl der übergebenen Parameter. Wenn genau ein Parameter übergeben wurde, entpackt zcat die Datei, die im ersten Parameter angegeben wurde, in die temporäre Datei. Dann folgt die seitenweise Ausgabe mittels pg. Nach Beendigung der Ausgabe wird der Status in der Variablen auf 0 gesetzt, damit beim Skriptende der korrekte Exit-Code zurückgegeben wird.

zeige-komprimierte-datei.sh (Fortsetzung)

        *) echo "Anwendung: `basename $0` Dateiname" 1gt;&2
    esac

Wenn case eine andere Parameterzahl feststellt, wird eine Meldung mit der Aufrufsyntax auf die Standard-Fehlerausgabe geschrieben.