Shell-Skripte selbst schreiben (Teil 2)

Aus LinuxUser 03/2017

Shell-Skripte selbst schreiben (Teil 2)

© Liu Liming, 123RF

Verschlungene Pfade

Mit Schleifen, Fallunterscheidungen und Funktionen programmieren Sie komplexe Skripte auf einfache und elegante Weise.

Im ersten Teil des Workshops in der vorigen Ausgabe [1] haben Sie bereits etliche Konstrukte kennengelernt, mit denen Sie einfache Tests und Zuweisungen vornehmen. In diesem Teil lernen Sie, wie Sie entsprechend bestimmten Bedingungen im Shell-Code verzweigen.

Wenn, dann

Das If-Konstrukt kommt zum Einsatz, um etwa den Erfolg respektive Misserfolg eines Befehls oder die Beschaffenheit von Dateien, Verzeichnissen und Variablen auszuwerten und Folgerungen daraus zu ziehen. Eine einfache Variante besteht aus dem einleitenden if, der Folgerung then und dem abschließenden Statement fi.

Bei Bedarf erweitern Sie das Konstrukt um weitere Verzweigungen mit elif [Bedingung]; then. Mit else rufen Sie eine Anweisung auf, die greift, wenn zuvor alle Bedingungen nicht greifen. In diesem Fall entfällt allerdings das abschließende then. Das ermöglicht ein Verschachteln und damit das Entwerfen komplexerer Verzweigungen (Listing 1).

Listing 1

if [ Bedingung1 ]; then
  Anweisung1
elif [ Bedingung2 ]; then
  Anweisung2
elif [ Bedingung3 ]; then
  if [ Bedingung3A ]; then
    Anweisung3A
  fi
[...]
else
  Anweisung4
fi

Beim Skript aus Listing 2 finden Sie in der dritten Zeile eine Befehlsfolge, die prüft, ob das Verzeichnis versuch existiert. Fehlt das Verzeichnis, legt das Skript es an. Die dazu verwendete Befehlsverkettung ist zwar kompakt, aber auch gleichermaßen kryptisch. Eleganter und wesentlich verständlicher fällt das Ganze mithilfe eines If-Konstrukts aus (Listing 3).

Listing 2

#!/bin/bash
# Prüfen:                      Anlegen:
ls versuch > /dev/null 2>&1 || mkdir versuch

Listing 3

#!/bin/sh
if [ ! -d versuch ]; then
  mkdir versuch;
fi

Mehrfach verzweigen

Mit dem Case-Befehl lösen Sie übersichtlich die Aufgabe, mehrere unterschiedliche Werte je einer Aktion zuzuweisen. Damit programmieren Sie etwa ein Menü, das Eingaben mit unterschiedlichen Zeichen ermöglicht. Sie leiten das Konstrukt mit dem Schlüsselwort case ein; mittels der Umkehrung esac beenden Sie es wieder. Listing 4 zeigt die grundsätzliche Form der mehrfachen Verzweigung.

Listing 4

case "$VARIABLE" in
  Werte1)
    Anweisung
    Anweisung
    ;;
  Werte2)
    Anweisung
    ;;
  *)
    Fallback-Anweisung ;;
esac

Die einzelnen Werte trennen Sie mit dem Pipe-Zeichen | ab. In der Praxis könnte der Code für das Abfragen einer Bestätigung beim Benutzer wie folgt aussehen:

[jJ]|[yY]|Z|Ja|ja)
  echo "Zugestimmt" ;;

Dabei dürfen Sie sowohl sogenannte Wildcards verwenden als auch ganze Wörter einsetzen. Bei der letzten Alternative in der Kette setzen Sie die Joker-Zeichen ? und * ein. Das entspricht dann vom Prinzip her der Else-Klausel des If-Konstrukts.

Iterationen

Eine For-Schleife läuft solange, bis ihr die Argumente ausgehen. Die stammen in vielen Fällen aus einer Subshell, in selteneren Fällen aus einer echten Liste von Werten. Die Anweisung do leitet den Anweisungsteil ein, mit done schließen Sie diesen ab (Listing 5).

Listing 5

for Variable in Argumente; do
  Anweisungen
done

Bei der Arbeit mit der Schleife dürfen Sie Positionsparameter verwenden. Diese geben Sie als Variable $@ an (Listing 6). Alternativ geben Sie feste Werte für die Argumente vor. Diese fügen Sie einfach nach dem in an (Listing 7).

Listing 6

#!/bin/sh
for i in "$@"; do
  echo $i
done

Listing 7

#!/bin/bash
for i in 1 2 3; do
  echo $i
done

Eine Subshell liefert solange Argumente, bis sie abgearbeitet wurde. Sie können auf diese Weise die Ausgaben beliebiger Befehle auswerten, müssen aber penibel auf den Feldtrenner (Standard: Leerzeichen) achten. So versteht die Shell die Zeichenkette Hans Schmidt als zwei Werte. Listing 8 demonstriert eine Suche nach allen Dateien, welche den String log im Namen führen.

Listing 8

#!/bin/bash
for i in $(ls *log*); do
  echo $i
done

Die Bash bietet zusätzlich einen Zähler für Schleifen. Damit geben Sie exakt einen numerischen Bereich vor. Der Befehl seq erlaubt es dabei auf einfache Weise, eine solche Reihe zu erstellen. Geben Sie für den Befehl nur den Endwert an, arbeitet er in Einer-Schritten. Geben Sie sowohl Start- als auch Endwert an, erhalten Sie ebenso Einer-Schritte. Nutzen Sie das Kommando in der Form seq Start Schrittweite Endwert, gestalten Sie die Zählweise noch feiner. Sie finden beide Methoden in Listing 9.

Listing 9

#!/bin/bash
echo "Schleife mit Zähler"
for ((i=0; i<5; i++)); do
  echo $i
done
echo "Schleife mit seq"
for i in $(seq 4); do
  echo $i
done

While-Schleife

Eine While-Schleife läuft solange, wie ein Kommando oder ein Block von Anweisungen erfolgreich arbeitet und den Exit-Code 0 liefert. Den Teil mit den Befehlen leiten Sie mit do ein und schließen ihn mit done ab. Steht kein explizites Kommando bereit, nutzen Sie den Befehl true, der stets den Exit-Code 0 ausgibt. Auf diese Weise erzeugen Sie eine Endlosschleife. Die Schleife in Listing 10 läuft, solange der Wert der Variablen $a größer ist als der von $b.

Listing 10

#!/bin/sh
a=3
b=0
while [ $a -gt $b ]; do
  echo $b
  b=$(echo $b + 1 | bc)
done

Until-Schleife

Eine Until-Schleife läuft, solange die Bedingung den Exit-Code 1 liefert. Den Anweisungsteil leiten Sie wieder mit do ein und schließen ihn mit done ab. Diese Schleifenvariante der Schleife entstand aus der Notwendigkeit heraus, dass einige Shells keinen negierten test mit vorangestelltem Ausrufezeichen beherrschen.

In der täglichen Praxis mit der Bash kommt die Until-Schleife eher selten zum Einsatz. Setzen Sie kein explizites Kommando ein, übernimmt false diesen Platz. Dieser Befehl liefert stets den Exit-Code 1 zurück, wodurch im Prinzip eine Endlosschleife entsteht. Listing 11 zeigt eine Schleife, die solange läuft, bis der Wert der Variablen $a gleich dem von $b ist.

Listing 11

#!/bin/sh
a=3
b=0
until [ $a -eq $b ]; do
  echo $b
  b=$(echo $b + 1 | bc)
done

Tritt innerhalb einer Schleife eine Abbruchbedingung auf, benötigen Sie einen entsprechenden Aufruf für die Reaktion. Hierfür stehen exit, break und continue bereit (siehe Tabelle “Sprunganweisungen”).

Anweisung

Wirkung

exit

Beendet das laufende Shell-Skript sofort.

break

Beendet die Schleife. Der Befehl ist ab der Stelle seines Erscheinens im Skript wirksam.

continue

Bricht nur den Durchlauf ab, die Schleife läuft weiter.

Fallen stellen

Einem Shell-Skript können Sie wie jedem anderen Prozess Signale übermitteln. Um es anzusprechen, benötigen Sie die Nummer des zugehörigen Prozesses (PID). Die wichtigsten Signale finden Sie in der Tabelle “Signale”. Der Aufruf trap -l zeigt eine Übersicht über alle vorhandenen Signale.

Signal

Nummer

Aktion

SIGINT

2

Prozess beenden

SIGKILL

9

Prozess sofort beenden

SIGTERM

15

Prozess normal beenden

SIGALM

14

Prozess beenden nach Zeitablauf

SIGSTOP

19

Prozess anhalten

SIGTSTP

20

Prozess anhalten

SIGCONT

18

Prozess fortsetzen

SIGUSR1

10

Benutzerdefiniertes Signal

SIGUSR2

12

Benutzerdefiniertes Signal

Es ist nicht möglich, die Signale 9 und 19 im Skript abzufangen. Bei den anderen Signalen besteht aber die Möglichkeit, sie durch neue Anweisungen für eigene Zwecke zu nutzen.

Um ein Gefühl für diese Technik zu bekommen, starten Sie das Skript aus Listing 12. Das Programm zeigt in einer Endlosschleife den Aufrufnamen des Skripts, dessen Prozess-ID sowie die Prozess-ID des aufrufenden Prozesses an. Von einem anderen Terminal aus senden Sie mittels dem Kill-Befehl nacheinander die Signale 19, 18, 10, 15 und 9. Einige der Signale beenden das Skript. Starten Sie es deshalb wieder, wenn Sie weiter experimentieren wollen.

Listing 12

#!/bin/sh
while true; do
  echo $0 $$ $PPID
  sleep 30
done

Die Trap-Anweisung zum Abfangen von Signalen hat die Form trap Ersatzkommando Signal. Geben Sie kein Kommando als Ersatz an, ignoriert das Skript das Signal lediglich. Listing 13 demonstriert das Abfangen eines Signals per trap in der Praxis. Senden Sie das Signal 10 oder SIGUSR1 an das Programm, zeigt es die Belegung der Massenspeicher im Rechner an.

Listing 13

#!/bin/sh
# Signale ignorieren
trap '' 2 20
# Auf Signal 10 reagieren
trap 'df -h' 10
while true; do
  echo "$$: $a"
  sleep 3
done

Verzweigungen und Schleifen

Das Skript aus Listing 14 bildet ein Auswahlmenü mitsamt eines Untermenüs ab. Es setzt die folgenden Anforderungen um:

  • Komfortable Eingabe über die Tastatur mit je einem Tastendruck.
  • Auswerten der Eingabe, sodass fehlerhafte Eingaben nicht auf dem Bildschirm erscheinen.
  • Für jede Auswahl gibt es genau eine Eingabemöglichkeit.

Listing 14

#!/bin/bash
while true; do
  # Bildschirm löschen
  clear
  # Hauptmenü
  echo "(1) Punkt 1"
  echo "(2) zum Untermenü"
  echo "(9) Beenden"
  # Auswahl einlesen, einstellig
  read -n1 -p "Aufgabe: " auf
  # Auswertung. Zahlenwerte als String
  # verhindern Fehleranzeigen, falls
  # Buchstaben gedrückt werden
  if [ "$auf" = "1" ]; then
    echo "Punkt 1"
    sleep 5
  elif [ "$auf" = "2" ]; then
    # neue Schleife
    while true; do
      # Bildschirm löschen
      clear
      # Untermenü
      echo "(a) Punkt a "
      echo "(b) Punkt b "
      echo "(z) zurück"
      read -n1 -p "Aufgabe: " umenu
      if [ "$umenu" = "a" ]; then
        echo "Punkt a"
        sleep 5
      elif [ "$umenu" = "b" ]; then
        echo "Punkt b"
        sleep 5
      elif [ "$umenu" = "z" ]; then
        # Zurück zum Hauptmenü
        break
      fi
    done
  elif [ "$auf" = "9" ]; then
    # Skriptende
    break
  fi
done
clear
echo "Skriptende"

Um das Beispiel einfach zu gestalten, besteht jede Funktion nur aus der Zeile echo "Punkt N", deren Ausgabe für jeweils fünf Sekunden auf dem Bildschirm verbleibt. Für das Hauptmenü stehen drei Möglichkeiten bereit, von denen eine das Skript beendet und eine weitere das Untermenü startet. Beim Beenden zeigt das Skript eine abschließende Nachricht an. Das Untermenü enthält wieder drei Punkte, wobei einer zum Rücksprung ins Hauptmenü dient. Zum Löschen des Bildschirms kommt clear zum Einsatz.

Einige der bereits im vorigen Teil des Workshops angesprochenen Techniken finden sich in diesem Programm wieder. Zum Einlesen der Eingabe steht bei read eine Länge der Zeichenkette von 1; der Anwender braucht also nicht mehr [Eingabe] zu drücken.

Um Fehlermeldungen aufgrund nicht numerischer Zeichen bei test zu vermeiden, vergleichen Sie einfach Zeichenketten anstelle numerischer Werte. Drückt der Benutzer eine Taste, die einem Buchstaben zugeordnet ist, erzeugt das keine Fehlermeldung.

Für das Untermenü starten Sie eine zweite While-Schleife, die das Programm ausschließlich über break wieder verlassen kann. Die Nachricht beim Beenden des Skripts schreiben Sie ans Dateiende, wo das Skript bei einem Abbruch der Schleife via break landet. Alternativ wäre es möglich, die Nachricht vor ein exit innerhalb des If-Konstrukts zu setzen.

Ein weiteres Skript (Listing 15) demonstriert, wie Sie einen Wert, der in einer Datei (im Beispiel lang.conf) hinterlegt ist, wieder auslesen und nutzen. Die möglichen Werte lauten de für die deutsche und en für die englische Sprache. Das Skript liest diese Datei beim Programmstart aus, erstellt das Menü mit drei Punkten: je einem zum Einstellen der Sprache und einem zum Beenden. Die Texte für das Menü liegen in deutscher und in englischer Sprache vor, die Auswahl innerhalb der Menüpunkte gelingt mit Klein- wie Großbuchstaben.

Listing 15

#!/bin/bash
# Auslesen Spracheinstellungen
sprache=$(cat lang.conf)
# Menütext anzeigen, Aufgabe abfragen
while true; do
  if [ "$sprache" = "de" ]; then
    clear
    echo -e "(a) Punkt 1\n(b) Punkt 2\n(c) Punkt 3\n(s) Spracheinstellungen\n(e) Ende"
    read -n1 -p "Aufgabe: " aufg
  elif [ "$sprache" = "en" ]; then
    clear
    echo -e "(a) first\n(b) second\n(c) third\n(l) Language\n(q) Quit"
    read -n1 -p "Task: " aufg
  else
    echo "Fehler in der Spracheinstellung"
    exit
  fi
  case "$aufg" in
    [aA])
      echo "1"; sleep 5; ;;
    [bB])
      echo "2"; sleep 5; ;;
    [cC])
      echo "3"; sleep 5; ;;
    [sS]|[lL])
      while true; do
        clear
        echo "(1) Deutsch"
        echo "(2) English"
        read -n1 -p "Sprache/Language: " ehcarps
        if [ "$ehcarps" = "1" ]; then
          sprache="de"
          echo $sprache > lang.conf
          break
        elif [ "$ehcarps" = "2" ]; then
          sprache="en"
          echo $sprache > lang.conf
          break
        fi
      done ;;
    [eE]|[qQ])
      exit ;;
  esac
done

Das Einstellen der Sprache erfolgt durch das Auslesen der Konfiguration und das Belegen der Variable sprache. Deren Wert ändern Sie in den Zeilen 35 oder 39. Den neuen Inhalt von lang.conf schreiben Sie in den Zeilen 36 beziehungsweise 40. Das break danach beendet das Menü.

Mehrsprachige Eingaben erfordern den Einsatz eines Case-Konstrukts. Damit bleibt das Skript kompakt und gut zu verstehen. In der Praxis würden Sie die Texte für die Menüs nicht unbedingt in den Shell-Code aufnehmen, sondern aus einer Datei einlesen.

TIPP

Vermeiden Sie einen häufigen Fehler: Anweisungen im Case-Konstrukt erfordern ein doppeltes Semikolon (;;) am Ende.

Funktionen definieren

Funktionen nehmen generische Code-Teile auf, die Sie immer wieder verwenden. Sie bringen sie direkt am Anfang des Skripts unter. Bei der Wahl des Namens für die Funktion haben Sie weitgehend freie Hand. Vermeiden Sie jedoch Namen, die schon als Programm, Skript oder Variable im Einsatz sind – das führt in der Praxis nur zu Verwirrung.

Wählen Sie trotzdem einen bereits als Befehl verwendeten Namen für eine Funktion, gilt es, die Reihenfolge zu bedenken, in der die Shell diesen Namen aufruft: Zuerst sucht sie nach einer Funktion, dann nach einem eingebauten Befehl der Shell und zuletzt nach einem externen Tool.

Den Exit-Code einer Funktion dürfen Sie selbst definieren. Planen Sie, dass das Skript weiterarbeitet, erreichen Sie das mittels return Exit-Code. Soll das Skript hingegen komplett abbrechen, etwa weil die Quelle an Argumenten erschöpft ist, verwenden Sie exit Exit-Code.

Eine Funktion besteht aus dem einleitenden Namen, einer folgenden leeren Klammer () und dem Körper der Funktion, den Sie mit geschweiften Klammern umschließen (Listing 16, erste Definition). Die leere Klammer entfällt, wenn Sie die Funktion mit dem Schlüsselwort function einleiten – das gilt aber nur für die Bash (Listing 16, zweite Definition).

Listing 16

#!/bin/bash
# Funktionsdefinition
hallo () {
  echo "Hallo"
  return 0
}
# alternative Funktionsdefinition
function hallo2 {
  echo "Hallo Hallo"
  return 0
}
# Aufruf der Funktionen
hallo
hallo2

Benötigen Sie eine Funktion im laufenden Skript nicht mehr, etwa, weil es nicht mehr sinnvoll ist, sie aufzurufen, so verfahren Sie mit ihr wie mit einer zu überflüssigen Variablen und löschen sie mit unset Funktion.

Global vs. lokal

In der Bash definieren Sie Variablen global (für das gesamte Skript) oder lokal (nur für innerhalb der Funktion). Für Letzteres setzen Sie das Schlüsselwort local vor die Definition der Variablen (Listing 17).

Listing 17

#!/bin/bash
# Variablendefinition
a="Wert1"
b="Wert2"
echo "Ausgangsdefinition:"
echo "  " $a
echo "  " $b
echo " "
function hallo {
  a="Neuer Wert a"
  local b="Neuer Wert b"
  echo "In der Funktion:"
  echo "  " $a
  echo "  " $b
  echo " "
  return 0
}
hallo
echo "Nach Funktionsausführung:"
echo "  " $a
echo "  " $b
echo " "

Fazit

Mithilfe von Fallunterscheidungen programmieren Sie schnell und einfach Menüs für Ihre Software. Welches Konstrukt dabei das optimale ist, unterscheidet in der Regel den konkreten Anwendungsfall.

Funktionen helfen Ihnen, Teile des Codes in einfache Blöcke zu strukturieren, die Sie im gesamten Skript einsetzen dürfen. Das fördert die Übersichtlichkeit und Wartbarkeit, da es etwa das Austauschen eines Parameters drastisch vereinfacht. 

Der Autor

Harald Zisler beschäftigt sich seit den frühen 90er-Jahren mit FreeBSD und Linux. Zu Technik- und EDV-Themen verfasst er Bücher und Beiträge für Zeitschriften. Aktuell hat er die vierte Auflage von “Computer-Netzwerke” veröffentlicht, erschienen beim Rheinwerk Verlag.

Infos

  1. Shell-Workshop, Teil 1: Harald Zisler, “Solides Fundament”, LU 02/2017, S. 46, https://www.linux-community.de/38040

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDF
LinuxUser 03/2017 KAUFEN
EINZELNE AUSGABE
ABONNEMENTS
TABLET & SMARTPHONE APPS
E-Mail Benachrichtigung
Benachrichtige mich zu:

Hinweis: Dieser Artikel ist älter als ein Jahr, enthaltene Informationen sind möglicherweise veraltet.

0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben