Erste Schritte mit Bash-Skripten

Aus LinuxUser 10/2011

Erste Schritte mit Bash-Skripten

© Svilen001, sxc.hu

Kleine Helfer

Das Programmieren von Shell-Skripten ist keine Hexerei. Schon mit wenigen Grundkenntnissen sparen Sie durch das Automatisieren alltäglicher Aufgaben viel Zeit.

Shell-Skripte sind der beste Freund bequemer Menschen. Das mag seltsam klingen, denn das Schreiben eines Shell-Skripts setzt Können und Arbeit voraus, trotzdem stimmt es: Schreiben Sie ein Shell-Skript zum Erledigen von sich wiederholenden Aufgaben, rentiert sich die investierte Zeit in der Zukunft mehrfach. Außerdem ist das Schreiben eines Shell-Skripts eine Herausforderung, die viel Freude macht. Die nicht nur Bequemen, sondern auch Cleveren nehmen sich deshalb Zeit fürs Erlernen der Shell-Befehle und das Schreiben von Skripten.

Dieser Artikel fasst die Grundlagen zum Schreiben von Shell-Skripten mit Bash im Kontext einiger allgemeinen Aufgaben im Zusammenhang mit dem PC zusammen. Dabei erhalten Sie die wichtigsten Informationen, um gleich mit dem Schreiben eigener Skripte zu beginnen. Eine kurze Zusammenfassung der im Artikel behandelten Kommandos und Optionen finden Sie in der Tabelle “Schnellübersicht” am Ende des Artikels.

Hallo Bash!

In der einfachsten Form besteht ein Shell-Skript nur aus einer Datei mit einer Liste von auszuführenden Befehlen. Das folgende Skript führt beispielsweise einen langen Tar-Befehl aus, um ein Backup von Bilddateien zu erstellen:

#!/bin/bash
tar cvzf /save/pix.tgz /home/chavez/pix  /graphics/rdc /new/pix/rachel

Das Skript beginnt mit einer speziellen Zeile, die die Datei selbst als Skript identifiziert. Die Kombination #! heißt “Shebang”, darauf folgt der vollständige Dateipfad zur entsprechenden Shell. Der Shebang #!/bin/bash ruft ausdrücklich die Bourne-Again-Shell zum Ausführen des Skripts auf. Verwenden Sie das allgemeinere #!/bin/sh, kommt die Standard-Shell des Systems zum Zug, auf die der Symlink /bin/sh verweist – unter Ubuntu ist das beispielsweise die schlanke, weitgehend zur Bash kompatible Dash. Sie können die Ausführung aber auch einem anderen Programm übertragen: Der Shebang #!/bin/cat etwa führt dazu, dass das entsprechende Programm den Inhalt des “Skripts” auf der Konsole ausgibt.

Damit die Shell das Skript als ausführbare Datei erkennt, setzen Sie zunächst die Ausführungsrechte entsprechend. Heißt die Datei zum Beispiel mytar, erledigen Sie das mit dem Befehl chmod u+x mytar – vorausgesetzt, die Datei liegt im aktuellen Verzeichnis.

Der Rest des Skripts nach dem Shebang besteht aus dem Tar-Befehl mit den zu verarbeitenden Pfaden und Dateinamen. Der folgende Befehl führt das Skript aus, worauf einige Meldungen von Tar folgen:

$ ./mytar

Damit haben Sie die Anzahl der Anschläge, die zum Erzeugen eines Archivs nötig sind, von etwa 75 Zeichen auf 8 Zeichen reduziert. Allerdings ließe sich das Skript noch etwas allgemeiner – und damit nützlicher – gestalten, indem Sie die zu archivierende Dateien in der Kommandozeile angeben:

$ ./mytar /home/chavez /new/pix/rachel /jobs/proj5

Mit diesem Befehl archivieren Sie eine andere Gruppe von Dateien. Das modifizierte Skript sehen Sie in Listing 1.

Listing 1

#!/bin/bash
if [ $# -gt 0 ]; then  # Mindestens ein Argument sollte hier stehen
  tar czf /save/mystuff.tgz $@ >/dev/null
fi

Es gibt darin einige neue Features: Der Tar-Befehl verwendet jetzt Ein- und Ausgabeumleitung, um alle Meldungen zu unterdrücken, die sich nicht auf Fehler beziehen. Der Befehl befindet sich innerhalb einer If-Anweisung. Erweist sich die in eckigen Klammern angegebene Bedingung als wahr, arbeitet das Skript die darauf folgenden Befehle ab.

Das Skript prüft, ob die Anzahl der Argumente zum Skript, die Sie in der Variable $# finden, höher als 0 liegt. Trifft das zu, hat der Benutzer einen oder mehrere Pfade fürs Archivieren angegeben. Fehlen die Parameter, gibt es gibt nichts zu tun, der Befehl kommt also nicht zum Einsatz.

Die neue Variante bietet außerdem einen weiteren Vorteil: Das Skript übergibt die Argumente, die es auf der Kommandozeile erhalten hat, mittels der speziellen Variable $@ an den Tar-Befehl. Diese Variable enthält die Liste der Argumente. Der Befehl von unserem Beispiel sieht dann wie folgt aus:

tar czf /save/mystuff.tgz /home/chavez /new/pix/rachel /jobs/proj5  >/dev/null

Auch hier haben Sie wieder jede Menge Tipparbeit gespart – zwar nicht ganz so viel, wie in der ersten Version des Skripts, dafür arbeitet diese Variante aber wesentlich flexibler.

Eingabedatei

Mit der Methode in Listing 2 ändern Sie die Arbeitsweise des Skriptes. Das erste Argument enthält nun eine Datei, die eine Liste der zu archivierenden Verzeichnisse enthält. Die übrigen Argumente behandelt das Skript als einzelne Elemente, die es ebenfalls verwendet, wieder in der schon bekannten Variable $@.

Listing 2

#!/bin/bash
DIRS="`cat $1`"            # DIRS = Inhalt der Datei im ersten Argument
shift                      # entfernt das erste Argument aus der Liste
OUTFILE="$(date +%y%m%d)"  # erstellt einen Archivnamen mit Datum
tar czf /tmp/$OUTFILE.tgz $DIRS $@ >/dev/null

Das Skript verwendet die Variablen DIRS und OUTFILE. Gemäß unausgesprochener Konvention und für die bessere Übersicht verwenden Sie am besten Großbuchstaben für die Variablennamen – zwingend erforderlich ist das aber nicht. Mit der erste Anweisung speichert das Skript den Inhalt der im ersten Argument angegebenen Datei. Durch das Setzen der Backticks fügen Sie die Ausgabe des Cat-Befehls statt des Befehls selbst einzufügen.

Das klappt mit jedem Befehl, den Sie zwischen die Backticks setzen. Auf diese Weise führt die Shell diesen zuerst aus und setzt die Ausgabe an dieser Stelle ein. Steht dieses Konstrukt innerhalb einer Anweisung, übernimmt diese anschließend die Ausgabe.

Im Beispiel gibt der Befehl cat den Inhalt der Datei, angegeben als erstes Argument zum Skript – nämlich die Verzeichnisliste – aus und fügt diese innerhalb der doppelten Anführungszeichen in die Zuordnung ein und definiert dadurch die Variable DIRS. Zeilenumbrüche in der Datei der Verzeichnisliste spielen dabei keine Rolle: Diese wandelt die Shell in Leerzeichen um.

Nachdem die Datei eingelesen ist und das Skript das erste Argument abgearbeitet hat, nutzen Sie die Anweisung shift, um den Dateinamen aus der Liste zu entfernen. Die neue Liste der Argumente enthält zudem Verzeichnisse, sofern Sie diese zuvor in der Kommandozeile angegeben haben, und $@ expandiert wieder zu der gewünschten Liste: Das Skript ersetzt die Variable durch die Verzeichnisnamen. Der Mechanismus ermöglicht es also, zuerst eine feste Liste von Elementen zum Archivieren im Skript zu speichern und – falls nötig – weitere Elemente als Parameter mit einzubeziehen.

Die dritte Anweisung definiert die Variable OUTFILE, diesmal unter Zuhilfenahme des Kommandos date. Die Syntax $(...) entspricht dem Verwenden von Backticks (siehe auch Kasten “Backticks oder nicht?”). Die Art von Operation ist unter dem Namen Kommando-Substitution bekannt.

In der letzten Zeile steht dann der eigentliche Tar-Befehl, der nun die Liste aus der Datei sowie eventuelle zusätzliche Argumente berücksichtigt, die Sie archivieren wollen. Verwenden Sie eine Variable innerhalb einer anderen Anweisung, vergessen Sie nie das Dollar-Zeichen vor dem Variablennamen zu setzen, wie zum Beispiel $DIRS.

Backticks oder nicht?

Bei der Kommandosubstitution hat bereits vor geraumer Weile die Schreibweise $(...) die ältere Variante mit Backticks abgelöst. Dafür gibt es gute Gründe:

  • Bessere Lesbarkeit: In vielen Darstellungsformen verwechselt man den Backtick ` sehr leicht mit dem einfachen Anführungszeichen ‘.
  • Besser einzutippen: Auf vielen internationalen Keyboard-Layouts lässt sich der Backtick nur schwer erreichen, auf manchen fehlt er ganz (etwa bei italienischen Standard-Keyboards).
  • Eindeutigere Syntax: Insbesondere beim Verschachteln von Substitutionen sowie beim Quoting entsteht ein übersichtlicheres Konstrukt.

Daher sollten Sie in Ihren Skripten der Variante $(...) gegenüber der in der Bash zwar noch funktionierenden, aber als obsolet geltenden Backtick-Methode den Vorzug einräumen. Es gibt eigentlich nur zwei Gründe, den Backtick noch zu nutzen: Die Macht der Gewohnheit (viele routinierte Skript-Autoren sind mit dem “Fliegenschiss” aufgewachsen) und den Zwang zur Kompatibilität mit älteren Shells (Bourne- und Korn-Shell) insbesondere auf anderen Unix-Systemen.

Tests

Das Skript in Listing 2 prüft weniger gründlich als die vorherigen Beispiele die Exaktheit der Argumente. Etwas anspruchsvoller gestaltet sich Listing 3, das diese Prüfung wieder einführt und noch mehr Flexibilität gewährleistet. Für das schnelle Bearbeiten von Argumenten kommt hier die Getopts-Funktion der Bash zum Einsatz.

Listing 3

#!/bin/bash
DEST="/save"     # Ablageort und Namensanfang (Prefix) vorgeben
PREFIX="backup"
while getopts "f:bn:d:" OPT; do      # Überprüfen der Parametern
  case $OPT in                       # gültige passende Muster vorgeben
    f) DIRS=$OPTARG  ;;              # -f <Datei mit der Verzeichnisliste>
    b) ZIP="j"; EXT="tbz" ;;         # -b = bzip2 statt gzip verwenden
    n) PREFIX=$OPTARG ;;             # -n <Prefix>
    d) if [ "${OPTARG:0:1}" = "/" ]  # -d <Archivverzeichnis>
         then
           DEST=$OPTARG
         else
           echo "Zielverzeichnis muss mit / beginnen."
           exit 1                    # Skript mit Fehler-Status beenden
       fi
       ;;
    :) echo "Sie müssen ein Argument für die Option -$OPTARG angeben."
       exit 1
       ;;
    *) echo "Kein gültiges Argument: -$OPTARG."
       exit 1
       ;;
  esac
done

Die ersten beiden Befehle weisen den Variablen DEST und PREFIX die gewünschten Werte zu: Zum einen das Verzeichnis, in dem die Archivdatei landet, sowie den Anfang des Dateinamens für das Archiv. Dieses Prefix erhält anschließend als Zusatz die Zeichenkette des aktuellen Datums (wie in Listing 2 oder Listing 4 definiert). Der Rest dieses Abschnittes im Skript folgt der Struktur einer While-Schleife:

while Bedingung;Befehle
done

Das Skript durchläuft die Schleife, solange die Bedingung erfüllt ist, und endet, wenn der Test negativ ausfällt – in diesem Fall das Konstrukt getopts "f:bn:d: " OPT. Die Ausdrücke für die Bedingung stehen häufig in eckigen Klammern, wie bei der If-Anweisungen im vorangegangenen Beispiel zu sehen, aber die eigentlichen Befehle nicht. Technisch gesehen rufen eckige Klammern den Test-Befehl auf.

Der Befehl Getopts geht jede Option der Reihe nach durch, zusammen mit den eventuellen Argumenten dazu. Die Optionsbuchstaben landen nacheinander im zweiten Argument von Getopts (im vorliegenden Fall OPT), die Argumente in OPTARG. Das erste Argument von Getopts ist ein String der erlaubten Buchstaben (Groß- und Kleinschreibung unterscheidend).

Einige Optionen verlangen ein Argument. Das zeigt immer der danach stehende Doppelpunkt – im Beispiel sind es die Buchstaben f, n und d. Wenn in der Kommandozeile so angegeben, steht nach Optionsbuchstaben ein Bindestrich.

Innerhalb der While-Schleife sorgt eine Case-Anweisung für das fachgerechte Bearbeiten der Optionen. Die Anweisung vergleicht den Wert von OPT mit einer Liste von Mustern. Bei jedem Muster handelt es sich um eine Zeichenkette, die Platzhalter enthalten darf. Ein runde schließende Klammer beendet das Muster. Die Reihenfolge ist wichtig: Das erste passende Muster gewinnt.

Im Beispiel sind neben den Mustern für die erlaubten Optionsbuchstaben zusätzlich ein Doppelpunkt und ein Sternchen als Jokerzeichen für alles definiert, was die anderen Muster nicht abfangen. Die auszuführenden Befehle bei den einzelnen Optionen unterscheiden sich, jeder Abschnitt endet mit doppeltem Semikolon.

Über -n geben Sie ein alternatives Präfix für den Dateinamen des Archivs an. Dabei überschreiben Sie die Voreinstellung aus dem zweiten Befehl des Skripts. Über -b setzen Sie Bzip2 statt Gzip zum Komprimieren der Daten ein. Mittels -f übergeben Sie den Dateiname mit der Liste der zu archivierenden Elemente, mittels -d das Zielverzeichnis für die Archivdatei (in der Voreinstellung /save).

Dabei überprüft das Skript, ob das Zielverzeichnis mit dem absoluten Pfadnamen beginnt. Das Konstrukt ${OPTARG:0:1} verdient besondere Aufmerksamkeit. Sie haben die Möglichkeit, die Variablen in geschweifte Klammern einzufassen: $1 können Sie als ${1} schreiben und $CAT als ${CAT}.

Diese Syntax ist nützlich. Sie erlaubt es Ihnen, Parameter über die neunte Position hinaus anzugeben: ${11} steht zum Beispiel für den elften Parameter des Skripts, wohingegen $11 zum ersten Argument des Skripts gefolgt von einer 1 expandiert. Diese Syntax ermöglicht es ebenfalls, Variablen vom umgebenden Text zu isolieren: Wenn der Wert von HAUSTIER zum Beispiel katze ist, dann expandiert ${HAUSTIER}2 zu katze2, während $HAUSTIER2 sich auf den Wert der Variable HAUSTIER2 bezieht, der vielleicht nicht definiert ist.

Die Zeichenkette :0:1 nach dem Variablennamen extrahiert einen Substring aus OPTARG, angefangen an der ersten Position (die Position der Schriftzeichen beginnt mit 0) und fortgesetzt bis zum ersten Schriftzeichen, mit anderen Worten: Es bleibt das erste Schriftzeichen übrig.

Die If-Anweisung überprüft dann, ob es sich dabei um einen Schrägstrich handelt. Falls nicht, erscheint eine Fehlermeldung, und das Skript endet mit einem Status von 1, was auf einen Fehler hinweist. Ein Status von 0 steht für einen erfolgreichen Durchlauf.

Wenn eine Option ein Argument verlangt, dieses aber fehlt, ersetzt Getopts die Variable OPT mit einem Doppelpunkt und fügt den entsprechenden String in OPTARGS ein. Der vorletzte Abschnitt der Case-Anweisung arbeitet diese Fehler auf. Der letzte Abschnitt behandelt alle ungültigen Optionen. Getopts ersetzt in diesem Fall die Variable durch ein Fragezeichen und legt die unbekannte Option in OPTARGS ab. Das Joker-Muster entdeckt und behandelt diese Ereignisse.

Dieser Code zum Behandeln von Argumenten ist nicht hundertprozentig sicher. Es gibt Kombinationen von Optionen, die erst das Skript erst später bemerkt. Fehlt zum Beispiel in der Folge -f -n das Argument von -f, hält das Skript fälschlicherweise -n dafür. Den Rest des Skripts von Listing 3 sehen Sie in Listing 4.

Listing 4

if [ -z $DIRS ]; then  # Datei mit Verzeichnisliste vorhanden?
  echo "Die Option -f für die Listendatei fehlt."
  exit 1
elif [ ! -r $DIRS ]; then
  echo "Kann die Datei $DIRS nicht finden oder einlesen."
  exit 1
fi
DAT="$(/bin/date +%d%m%g)"
/bin/tar -${ZIP-z} -c -f /$DEST/${PREFIX}_$DAT.${EXT-tgz} `cat $DIRS` > /dev/null

Die If-Anweisung überprüft zwei mögliche Probleme mit der Datei, welche die Verzeichnisliste enthält. Der erste Test überprüft, ob die Variable DIRS fehlt, dass heißt, ob sie die Länge Null hat. In diesem Fall endet das Skript mit einer Fehlermeldung.

Der zweite Test nach elif überprüft, ob die angegebene Datei existiert und der Zugriff funktioniert. Wenn nicht (das Ausrufezeichen in dem Ausdruck dient als ein logisches NOT), gibt das Skript ebenfalls eine Fehlermeldung aus und beendet sich.

Die letzten beiden Anweisungen richten den Datumsteil für das Archiv ein und starten den Tar-Befehl. Letzterer verwendet eine Art bedingter Auflösung von Variablen, zum Beispiel ${EXT-tgz}. Der Bindestrich hinter dem Variablennamen zeigt an, dass der darauf folgende String zum Einsatz kommt, falls die Variable nicht definiert ist.

Die Variablen EXT und ZIP sind nur dann definiert, wenn Sie die Option -b verwenden, nämlich durch die Werte tbz und j. Haben Sie diese nicht früher im Skript definiert, verwendet es nun die Werte z und tgz.

Alles nur Nummern

Die bisher gezeigten Beispiele haben beide Art von Bedingungen demonstriert – den Vergleich von Zeichenfolgen und das Testen von Dateieigenschaften. Listing 5 zeigt nun ein Beispiel für numerische Bedingungen. Dieses Skript entstand, um der Sekretärin des Vorsitzenden eines Unternehmens zu helfen: Sie war damit sie jederzeit in der Lage, schnell nachzusehen, wer gerade im Intranet angemeldet ist.

Listing 5

#!/bin/bash
if [ $# -lt 1 ]; then   # kein Argument, Eingabeaufforderung einblenden
  read -p "Wen wollten Sie überprüfen? " WHO
  if [ -z $WHO ]; then  # kein Name wurde eingetragen
    exit 0
  fi
else
  WHO="$1"              # speichert das Kommandozeilen-Argument
fi
LOOK=$(w | grep "^$WHO")
if [ $? -eq 0 ]; then   # vorherigen Befehlsstatus überprüfen
  WHEN=$(echo $LOOK | awk '{print $4}')
  echo "$WHO ist angemeldet seit $WHEN."
else
  echo "$WHO ist zurzeit nicht angemeldet."
fi
exit 0

Dieses Skript überprüft zuerst, ob die Dame ein Argument in der Befehlszeile angegeben hatte. Wenn nicht, also wenn die Anzahl von Argumenten kleiner als 1 ist, erscheint eine Eingabezeile zur Angabe des gewünschten Benutzers. Dabei kommt der eingebaute Befehl read zum Einsatz. Die Eingabe landet in der Variable WHO.

Hat die Variable WHO nach der Abfrage immer noch die Länge Null, hat die Sekretärin keinen Benutzernamen eingetippt, sondern lediglich [Eingabe] gedrückt. In diesem Fall beendet sich das Skript. Im anderen Fall, wenn die Sekretätin ein Argument bereits in der Befehlszeile angegeben hat, erhält WHO dieses als Wert. So oder so: Der Benutzername der gesuchten Person landet letztendlich immer am gleichen Platz.

Der zweite Teil des Skripts verwendet zwei Konstrukte mit Befehlssubstitution. Das erste sucht in der Ausgabe des Befehls w nach dem gewünschten Benutzernamen und speichert die entsprechende Zeile in LOOK. Die zweite definiert die Variable WHEN als das vierte Feld dieser Ausgabe (Zeitpunkt der letzten Anmeldung).

Dieses Feld extrahieren Sie mittels Awk. Sie brauchen das Tool nicht in- und auswendig zu kennen, um diesen einfachen Trick anzuwenden. Dabei kommt der Befehl nur zum Einsatz, wenn der Wert der Variable $? gleich 0 ist. Sie enthält den Statuscode des letzten Befehls, in diesem Fall also jenen von Grep. Liegt ein Ergebnis vor, hat sie den Wert 0, sonst den Wert 1.

Zum Abschluss zeigt das Skript eine entsprechende Meldung mit dem Status des Benutzers:

kyrre ist angemeldet seit 08:47.

Fragt der Chef jetzt nach kyrre, kann die Sekretärin beruhigt die Auskunft geben, der sei derzeit an seinem Rechner beschäftigt.

Schleifen binden

Das Skript aus Listing 6 demonstriert eine weitere Verwendung von while und read: Das Verarbeiten aufeinander folgender Zeilen einer Ausgabe oder einer Datei. Der Zweck des Skripts liegt im Versenden von Mails an Benutzer in einer Liste als separate E-Mails.

Listing 6

#!/bin/bash
/bin/cat /usr/local/sbin/email_list |
while read WHO WHAT SUBJ; do
  /usr/bin/mail -s "$SUBJ" $WHO < $WHAT
  echo $WHO
done

Das Skript sendet über eine Pipe (|) den Inhalt der entsprechenden Datei an die While-Schleife; Diese liest mittels read die Zeilen und speichert deren Inhalt in drei Variablen. Das erste Wort einer Zeile landet in WHO, das zweite in WHAT und alle weiteren in SUBJ. Diese enthalten anschließend die Mailadresse, den Inhalt der Nachricht in einer Datei sowie den Betreff für jede Person. Die Variablen kommen beim Erstellen des nachfolgenden Mail-Befehls zum Einsatz.

Beachten Sie, dass dieses Skript den vollständigen Pfadnamen für externe Befehle verwendet. Am besten geben Sie immer den vollständigen Pfadnamen an oder fügen eine PATH-Definition am Anfang des Skripts hinzu, um eventuelle Probleme mit den Berechtigungen von ausführbaren Dateien bei der Substitution zu vermeiden.

Was die Sicherheit betrifft, geht dieses Skript viel zu lässig mit dem Inhalt der Datei email_list um und vertraut darauf, dass diese ordnungsgemäß formatierte Mail-Adressen enthält. Möchten Sie das Skript aber weitergeben, gilt es die Adressen sorgfältig zu überprüfen. So besteht zum Beispiel die Möglichkeit, dass ein Nutzer ein Benutzer eine Eingabe in der Form user@example.com; /irgendwo/run_me in der Adressenliste versteckt. Das führt dazu, dass das Programm run_me unerlaubt startet.

Schleifen

Die nächsten beiden Skripte veranschaulichen andere Arten von Schleifen, die Sie in Shell-Skripten über den for-Befehl anwenden können. Listing 7 erstellt einen Bericht über den belegten Speicherplatz samt einer Liste von Verzeichnissen für eine Reihe von Anwendern.

Die Dateien mit der Liste der Benutzer und der zu prüfenden Verzeichnisse finden sich hier explizit im Skript, aber Sie können auch Optionen dafür setzen. Das Skript beginnt mit der Angabe des Pfades und der Einbeziehung einer anderen Datei in das Skript mithilfe des Include-Datei-Mechanismus, dem sogenannten Punktbefehl (aufgerufen mit einem Punkt).

Listing 7

#!/bin/bash
PATH=/bin:/usr/bin                # setzt den Pfad
. /usr/local/sbin/functions.bash  # . f => Datei f hier einbinden
printf "USER\tGB USED\n"          # Kopfzeile für Bericht drucken
for WHO in $(</usr/local/sbin/ckusers); do
  HOMESUM=`eval du -s ~$WHO | awk '{print $1}'`
  TMPLIST=$( ls -lR --block-size 1024 $(</usr/local/bin/ckdirs) |\
             egrep "^.......... +[0-9]+ $WHO" | awk '{print $5}' )
  TSUM=0
  for N in $TMPLIST; do
    TSUM=$(( $TSUM+$N ))
  done
  TOT=$(( $HOMESUM+$TSUM ))
  to_gb $WHO $TOT
done

Eine For-Schleife bildet das zentrale Konstrukt dieses Skripts. Innerhalb der Schleife erhält eine Variable bei jedem Durchgang einen neuen Wert. Das Schlüsselwort in verweist auf die Liste von Werten und der separate Befehl do leitet die eigentlichen Operationen ein. Die Schleife endet mit done, sobald alle sie alle Elemente der Liste abgearbeitet hat.

Im Beispiel erhält die Variable WHO immer das nächste Element aus der Datei ckusers. Das Konstrukt $(<file) arbeitet als Abkürzung für $(cat file).

Die Definition von HOMESUM verwendet ein Backtick-Konstrukt, um die Gesamtgröße des Home-Verzeichnisses eines Benutzers aus der Ausgabe von du -s via Awk zu extrahieren. Der Befehl eval bewirkt, dass das Kommando du das Konstrukt ~$WHO als Tilde-Schreibweise für das Benutzerverzeichnis interpretiert.

Die Definition von TMPLIST verwendet Befehlssubstitution zum Herausfiltern und Einfügen des Größe-Feldes (via Awk) aller Zeilen in der Ausgabe von ls -lR, die den Elementen des aktuellen Benutzers entsprechen (festgestellt durch Egrep). Der Befehl ls prüft alle Verzeichnisse, welche die Datei ckdirs auflistet. Er verwendet die Option --block-size, um das Ergebnis in der gleichen Größenordnung anzuzeigen, wie es du verwendet (KByte).

Sobald alles abgearbeitet ist, enthält TMPLIST eine Liste von Zahlen – je eine pro Datei, die zum aktuellen Benutzer gehören. Die zweite For-Schleife addiert die Zahlen von TMPLIST in TSUM. Diese Schleife weist die gleiche Struktur auf wie die obige. Als Variable agiert in diesem Fall N.

Die Bash bietet über das Konstrukt $(( Mathematischer Ausdruck )) eingebaute Ganzzahl-Arithmetik. Das Skript verwendet dieses Konstrukt zweimal. Eine Funktion namens to_gb (Listing 8) erledigt das Ausdrucken jeder Berichtszeile.

Listing 8

to_gb()
{
# arguments: user usage-in-KB
  local MB D1 D2 USER       # lokale Variablen
  USER=$1
  MB=$(( $2/1024 ))         # konvertieren in MByte
  D1=$(( $MB/1000 ))        # Ausgabe ganzzahlig in GByte
  D2=$(( $MB-($D1*1000) ))  # Rest berechnen
# display abcd MB as: a.bcd GB
  printf "%s\t%s\n" $USER $D1.${D2:0:1}
  return
}

Die Bash setzt voraus, dass Sie Funktionen vor dem Einsatz definieren. Es bietet sich dazu an, Funktionen in einer externen Datei zu speichern und über den Punktbefehl in Skripte einzubinden. Im Beispiel aus Listing*7 liegt die Funktion in der Datei functions.bash.

Die Funktion to_gb() beginnt mit der Definition einiger lokalen Variablen. Damit ignoriert sie jegliche Bedeutung, welche diese Namen im aufrufenden Skript eventuell haben könnten, und die lokalen Werte gelangen nicht in den übergeordneten Kontext zurück.

Der größte Teil der Funktion besteht aus arithmetischen Operationen. Die Bash unterstützt ausschließlich Ganzzahl-Arithmetik. Möchten Sie eine einigermaßen genaue Gesamtgröße in GByte anzeigen, müssen Sie dazu einen gängigen Trick nutzen: Sie entnehmen zuerst die Ganzzahlen, dann den Rest des GByte-Wertes, und setzen die Ausgabe per Hand zusammen.

Haben Sie zum Beispiel 2987 MByte und dividieren dies durch 1024, wäre das gerundete Ergebnis 2 GByte. Um ein genaueres Ergebnis anzuzeigen, dividieren Sie stattdessen zuerst 2987 durch 1000 (D1=2), dann berechnen Sie 2987-(2*1000). Der Wert für D2 beträgt dann 987. Anschließend geben Sie die VariableD1, ein Komma und dann die erste Ziffer von D2: Das Ergebnis lautet dann 2.9. So sähe eine beispielhafte Ausgabe des Skripts aus:

BENUTZER        BELEGT in GB
andrea          80.5
karsten         14.3
monika          0.3

Der Befehl printf ermöglicht eine formatierte Ausgabe. Er erfordert einen Format-String, gefolgt von Variablen, um den Ausdruck zu befüllen. Kennbuchstaben hinter Prozentzeichen geben an, wo die Variablen-Inhalte landen. Im Beispiel bestimmt %s die Stellen, an denen die Funktion die Werte einfügt. Hier gibt der Buchstabe s an, dass es sich um eine Zeichenkette handelt.

Die Zeichen \t und \n im Format-String entsprechen den Steuerzeichen Tabulator und Zeilenumbruch. Wollen Sie die Zeile explizit beenden, müssen Sie Letzteres angeben.

Das nächste Skript (Listing 9) berechnet Fakultäten und veranschaulicht eine andere Art von For-Schleife, die dem ähnelt, was in vielen Programmiersprachen üblich ist, wie zum Beispiel in C.

Listing 9

#!/bin/bash
F=1
for (( I=$1 ; I>1 ; I-- )); do
  F=$(( $F*$I ))
done
echo $1'! = '$F
exit 0

Die For-Syntax nutzt eine Schleifenvariable (I) zusammen mit einem Startwert ($1, also der erste Parameter, den Sie an das Skript übergeben), eine Bedingung zum Fortsetzen der Schleife sowie einen Ausdruck, der angibt, wie sich die Variable nach jedem Schleifendurchlauf ändert.

Die Schleife verarbeitet die Variable I. Am Ende jeder Iteration verringert das Skript den Wert von I um 1 (I++ würde ähnlich I um den Wert 1 erhöhen). Die Schleife läuft durch, solange I größer als 1 ist. Der Rumpf der Schleife multipliziert F (am Anfang auf 1 gesetzt) mit jedem nachfolgenden I. Das Skript endet mit der Ausgabe des Ergebnisses:

$ ./fact 6
6! = 720

Wie man hier sieht, lässt sich auch in der Shell recht zügig Mathematik betreiben. Probieren Sie zum Beweis einmal ./fact 10000.

Menüs erzeugen

Das letzte Skript demonstriert die eingebaute Fähigkeit der Bash zum Generieren von Menüs über den Select-Befehl (Listing 10).

Listing 10

#!/bin/bash
PATH=/bin:/usr/bin
PFILE=/usr/local/sbin/userpkgs  # Format der Eingabe: pkgname menu_item
PKGS=( $(cat $PFILE | awk '{print $1}') )       # Array von Paketnamen
MENU="$(cat $PFILE | awk '{print $2}') Fertig"  # Liste der Menüpunkte
select WHAT in $MENU; do
  if [ $WHAT = "Fertig" ]; then exit; fi
  I=$(( $REPLY-1 ))
  PICKED=${PKGS[$I]}
  echo Installiere Paket $PICKED ... Bitte haben Sie Geduld!... Befehle, um das Paket zu installieren ...
done

Die Parameter für den Select-Befehl setzen Sie in den Variablen PKGS und MENU. Das Konstrukt benötigt eine Liste von Elementen als zweites Argument; dazu dient MENU. Über ein Konstrukt von Befehlssubstitutionen gelangen die Werte in den Platzhalter. Zusätzlich fügt das Skript am Ende der Liste die Zeichenkette Fertig hinzu.

Die Definition von PKGS führt ein neues Feature ein: Arrays. Bei einem Array handelt es sich um eine Datenstruktur mit mehreren Elementen, die Sie über einen Index referenzieren. Folgendes Beispiel definiert und verwendet ein einfaches Array:

$ a=(1 2 3 4 5)
$ echo ${a[2]}
3

Ein Array zu definieren ist sehr einfach: Sie schließen die Elemente in Klammern ein. Um auf ein Element zuzugreifen, verwenden Sie die Syntax aus der zweiten Zeile: Der Name des Elements steht in geschweiften Klammern, der Positionsparameter in eckigen Klammern. Das Nummerieren der Elemente beginnt bei 0. Die Anzahl der Elemente in einem Array erhalten Sie über den Ausdruck ${#a[@]}.

Im Skript kommt ein Array in Form der Variable PKGS zum Einsatz. Darin finden sich die Werte aus dem zweiten Feld jeder Zeile der Eingabedatei. Das Select-Konstrukt verwendet den Inhalt von MENU für die Liste. Select erstellt aus den Elementen ein durchnummeriertes Textmenü und fordert den Benutzer zur Eingabe einer Auswahl auf. Das ausgewählte Element landet in der vor dem Schlüsselwort in angegebenen Variable (hier WHAT), die Nummer des Elements in REPLY.

Das Skript reduziert den Wert von REPLY um 1 für das Abrufen des entsprechenden Paketnamens aus dem Array PKGS, der in der Variable PICKED landet. Die Differenz von 1 kommt zustande, weil die Nummerierung im Menü mit 1, bei den Elementen eines Arrays aber bei 0 anfängt. Wählt der Benutzer das Element Fertig, terminiert das Skript. Listing 11 zeigt ein Beispiel für einen Durchlauf.

Listing 11

1) CD/MP3_Player  3) Photo_Album
2) Spider_Solitaire  4) Fertig
#? 2
Installiere Paket spider ... Bitte haben Sie Geduld!
[...]
#? 4

Fazit

Eine kurze Zusammenfassung von Skript-Befehlen und Bash-Terminologie finden Sie in der Tabelle “Schnellübersicht”. Sie beendet diesen Ausflug in die Welt des Bash-Skripting und macht hoffentlich Lust auf weitere Entdeckungen zu diesem Thema. 

Schnellübersicht

Argumente und Variablen
$1$2?$9 Befehlsargumente
${nn} allgemeines Format für Argument nn
$@ alle Befehlsargumente: Liste von separaten Elementen
$* alle Befehlsargumente: ein einzelnes Element
$# Anzahl der Befehlsargumente
$0 Skript-Name
$var Wert der Variable var
${var} allgemeines Format
${var:p:n} Substring von n Zeichen von var, beginnend bei p
${var-val2} val2 zurückgeben, falls var nicht definiert ist
${var+val2} val2 zurückgeben, falls var definiert ist
${var=val2} val2 zurückgeben, falls var undefiniert ist, und bestimme var=val2
${var?errmsg} var: errmsg einblenden, falls var undefiniert ist
arr=( var1, var2 ) definiert arr als ein Array
${arr[n]} Element n von Array arr
${#arr[@]} Anzahl der definierten Elemente in arr
getopts opts var Prozessoptionen, Optionsbuchstabe in var zurückgeben (oder ?, wenn ungültig, oder :, wenn das erforderliche Argument fehlt). Der Parameter opts listet erlaubte Buchstaben auf, optional gefolgt von einem Doppelpunkt, der ein Argument verlangt (Doppelpunkt am Anfang: invalide Optionen ignorieren). Gibt das Argument der Option in fest definierten Variable OPTARG zurück.
Allgemeine Anweisungskonstrukte
`cmd` Ausgabe von cmd erneut auswerten (obsolete Schreibweise)
$(cmd) Ausgabe von cmd erneut auswerten (kanonische Schreibweise)
$? Exit-Status des letzten Kommandos
$! PID (Prozess-ID) des zuletzt ausgeführten Hintergrundbefehls
eval string Substitutionsoperation am String vornehmen und dann ausführen
. file Datei-Inhalt im Skript einfügen
exit n Skript mit dem Status n beenden (0 bedeutet Erfolg)
Testmethoden
-xDatei überprüft, ob die Datei die durch den Code -x angegebene Bedingung erfüllt. Einige nützliche Operationen sind: -s Datei größer als 0 Byte; -r lesbar; -w schreibbar; -e existiert; -d ist ein Verzeichnis; -f ist eine einfache Datei.
Datei1 -nt Datei2 Abfrage, ob Datei1 neuer ist als Datei2
-z Wort Länge von Wort gleich 0
-n Wort Länge von Wort größer als 0
Wort1 = Wort2 Test auf identische Zeichenfolgen. Andere Operationen: !=, >, <.
Zahl1 -eq Zahl2 Test, ob Ganzzahlen gleich sind. Andere Operatoren: -ne, -gt, -lt, -ge, -le.
! Logisches NICHT
-a Logisches UND
-o Logisches ODER
(...) Gruppieren von Bedingungen.
Eingabe und Ausgabe
read Variable Eingabezeile lesen und aufeinanderfolgende Wörter einzelnen Variablen zuweisen.
read -p Zeichenkette var Eingabeaufforderung für einen einzelnen Wert anzeigen und var weitergeben
printf Format-StringVariablen Inhalte der Variablen dem Format-String entsprechend anzeigen. Der Format-String besteht aus Zeichenketten, Escape-Zeichen (\t für Tabulator, \n für Zeilenumbruch) und Format-Codes: zum Beispiel %s für Zeichenketten, %d für positive oder negative Ganzzahlen, %f für Fließkommazahlen. Ein Bindestrich hinter dem Prozent-Zeichen bedeutet rechtsbündige Anordnung. Durch eine eine Ziffer vor dem Kennbuchstaben bestimmen Sie die Feldlänge. Beispiele: %-5d ist eine fünfstellige Ganzzahl, rechtsbündig angeordnet; %6.2f definiert eine Feldlänge von 6 mit zwei Dezimalstellen für eine Fließkommazahl.
Arithmetik
$(( Ausdruck )) Ausdruck als Ganzzahl-Operation auffassen.
+-*/ Addition, Subtraktion, Multiplikation, Division
++-- Wert um 1 erhöhen/verringern
% Modulo (Divisionsrest)
** Potenzieren
Funktionen
name () { Befehle } Funktion name definieren. Verwenden Sie innerhalb der geschweiften Klammern bei Variablendefinitionen das Schlüsselwort local, um den Geltungsbereich von Variablen auf die Funktion zu begrenzen.
LinuxUser 10/2011 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