Bash-Skripte leserlich programmieren

Aus LinuxUser 03/2014

Bash-Skripte leserlich programmieren

© Natalia Klenova, 123RF

Die richtigen Zutaten

Das geniale Skript aus dem letzten Monat enthält nur konfusen Spaghetti-Code, der nicht mehr verrät, was er tut? Mit unseren Tipps aus erprobten Styleguides sorgen Sie für Ordnung und Lesbarkeit.

Bash-Skripte helfen dabei, Dateien umzubenennen, eine große Adressliste zu sortieren oder ähnliche lästige Aufgaben zu erledigen. Die dabei meist unter hohem Zeitdruck geschriebenen Skripte sehen häufig abenteuerlich aus (Listing 1). Schnelle Hacks erfüllen zwar ihren Zweck, sind aber oft unübersichtlich, und die Funktionsweise ist meist eine Woche später komplett unklar. Aus diesem Grund geben viele Anwender die berühmten Einzeiler nicht weiter: Die Empfänger müssten sich erst langwierig einlesen. Schlussendlich ist ein solches kryptisches Skript nur schwer zu erweitern oder zu ändern.

Listing 1

#!/bin/sh
a="100"; b="3"; c="."; d="shot"; for i in `seq 1 $a`; do import -window root $c/$d$i.png; sleep $b; done; exit

Regulator

Aus diesen Gründen sollten Sie Bash-Skripte von Anfang an strukturieren, formatieren und vor allem kommentieren. Dabei helfen Richtlinien, wie sie erfahrene Programmierer in sogenannten Styleguides festgehalten haben. Zwei bekannte Leitfäden stammen von Google [1] und der Fachhochschule Südwestfalen [2]. Ob Sie lieber der privatwirtschaftlichen Anleitung (Abbildung 1) folgen oder der akademischen (Abbildung 2), das bleibt ganz Ihrem Geschmack überlassen; die grundlegenden Richtlinien sind überall gleich.

Abbildung 1: Detaillierte Informationen zu den einzelnen Richtlinien erhalten Sie bei Google, wenn Sie das Dreieck vor einer der Regeln anklicken.

Abbildung 1: Detaillierte Informationen zu den einzelnen Richtlinien erhalten Sie bei Google, wenn Sie das Dreieck vor einer der Regeln anklicken.

Abbildung 2: Der Styleguide der FH Südwestfalen liegt als PDF vor – und geht nicht nur auf die Formatierung ein.

Abbildung 2: Der Styleguide der FH Südwestfalen liegt als PDF vor – und geht nicht nur auf die Formatierung ein.

So sollte Ihr Skript zu Beginn immer die passende Shell nennen. Nutzt es etwa die Funktionen der Bash, schreiben Sie #!/bin/bash in die erste Zeile. Damit funktioniert das Skript auch auf Systemen, die die Bash nicht als Standard-Shell einsetzen, diese aber trotzdem installiert haben.

Frischluftzufuhr

Um das Skript übersichtlicher und leserlicher zu gestalten, sollten Sie den Code immer mit Einrückungen und Leerzeilen versehen. Das schlecht lesbare Listing 1 verwandelt sich auf diese Weise in das schon nicht mehr ganz so kryptische Listing 2.

Listing 2

#!/bin/bash
a="100"
b="3"
c="."
d="shot"
for i in `seq 1 $a`
do
  import -window root $c/$d$i.png;
  sleep $b
done;
exit

Jede Zeile nimmt dabei eine Anweisung auf, eine Leerzeile trennt zusammengehörende Teile des Codes. Letzteres trifft in Listing 2 etwa auf die For-Schleife zu. Die Anweisungen innerhalb eines solchen Blocks sollten Sie jeweils um eine Stufe nach rechts einrücken. Nutzen Sie dazu immer Leerzeichen und keine Tabulatoren: Letztere interpretieren Editoren unterschiedlich. Beinahe unisono empfehlen die Styleguides, jeden Tabulator durch zwei Leerzeichen zu ersetzen.

In Listing 2 steht das do in einer eigenen Zeile. Google empfiehlt, es noch in die Zeile mit dem for zu ziehen. Analoges gilt für eine While-Schleife sowie das then bei einem If-Block. Das else steht jedoch vorzugsweise wieder in einer eigenen Zeile, genauso wie das abschließende done. Apropos If-Anweisung: Google rät, anstelle von test und für dessen Abkürzung mit eckigen Klammern die doppelten Klammern zu verwenden:

if [[ ${dateiname} == "bild*" ]]; then ...

Diese Notation hat den Vorteil, dass sie reguläre Ausdrücke verarbeitet. Zudem reduziert sie Fehler, da die doppelten Klammern weder eigenmächtig Wörter abschneiden noch Dateinamen erweitern.

Im Umbruch

Bei einer Case-Abfrage rücken Sie die Alternativen um zwei Leerzeichen nach rechts ein. Die Muster gehören – genau wie die zugehörigen Anweisungen – in jeweils eine eigene Zeile. Ein Beispiel dafür zeigt Listing 3.

Listing 3

case "${auto}" in
  porsche)
    motor="brumm"
    echo "${motor}"
    ;;
  *)
    echo "Kein bekanntes Auto"
    ;;
esac

Mehrere Kommandos verknüpfen Sie bei Bedarf über Pipes, wie etwa ls -la | grep TODO. Links und rechts vom Pipe-Zeichen steht dabei jeweils ein Leerzeichen. Besteht die Kette aus mehr als zwei Elementen, sollten Sie die einzelnen Befehle in jeweils eine eigene Zeile schreiben. Die nachfolgenden Zeilen rücken Sie dabei mit zwei Leerzeichen eine Stufe nach rechts ein, wie es Listing 4 zeigt. Ganz nebenbei fallen auch lange Parameterlisten so übersichtlicher aus (Listing 5).

Listing 4

Befehl1 \
  | Befehl2 \
  | Befehl3

Listing 5

./configure \
  --prefix=$HOME \
  --enable-audio \
  --enable-video

Des Weiteren rät Google, sämtliche Zeilen nach 80 Zeichen zu umbrechen; der Styleguide der FH Südwestfalen gestattet dagegen 88 Zeichen. Beachten Sie diese Grenzen, fällt es leicht, das Skript auszudrucken oder in kleinen Terminal-Fenstern ohne lästiges vertikales Scrollen zu begutachten und zu bearbeiten (Abbildung 3).

Abbildung 3: Zeilen mit mehr als 80 Zeichen bricht das Terminal um. Manche Editoren (wie hier Nano im Hintergrund) schneiden die Zeile einfach am rechten Rand ab.

Abbildung 3: Zeilen mit mehr als 80 Zeichen bricht das Terminal um. Manche Editoren (wie hier Nano im Hintergrund) schneiden die Zeile einfach am rechten Rand ab.

Lagerstätten

Welchen Wert die Variable a in einem beliebigen Skript enthält, das erahnt der geneigte Leser im besten Fall durch den Kontext. Benennen Sie Variablen deshalb immer möglichst aussagekräftig. Wählen Sie sprechende Namen, die den Inhalt der Variablen beschreiben: Was etwa die Variable dateiname speichert, erkennen Sie noch in einem Jahr ohne weitere Information.

Die Namen sollten zudem kurz und prägnant ausfallen, verwenden Sie nicht mehr als 31 Zeichen. Nehmen Sie sich auch unter Druck etwas Zeit, um passende Variablennamen zu finden – es zahlt sich später aus.

Die meisten Styleguides schlagen vor, Variablen durchgehend klein (zaehler), Konstanten hingegen durchgehend groß (ANZAHL) zu schreiben. Die Konstanten sollten Sie zudem gesammelt am Anfang des Skripts definieren. Achten Sie bei der Wahl des Namens darauf, dass Sie nicht versehentlich reservierte Namen der Umgebungsvariablen verwenden. Eine Liste dieser Namen finden Sie im Web [3].

Wollen Sie auf Nummer sicher gehen, stellen Sie den Namen der Konstanten ein Präfix voran (ABC_ANZAHL). Setzt sich der Name einer Variablen aus mehreren Wörtern zusammen, trennen Sie die Bestandteile durch einen Unterstrich (brief_anrede). Es empfiehlt sich zudem, mit Plural und Singular auf den Inhalt zu verweisen. Das hilft vor allem in Schleifen:

for zahl in ${zahlen}; do
  echo ${zahl}
done

Fassen Sie Variablen immer in geschweifte Klammern ein, wie etwa ${nachname}. Setzen Sie Strings, die Variablen enthalten, in Anführungszeichen:

name="Herr ${nachname}"

Damit stellen Sie sicher, dass die Bash die Variable korrekt ersetzt. Mit den neuen Namen für die Variablen sieht das Skript aus Listing 1 schon etwas verständlicher aus: Wie Listing 6 zeigt, geht es um eine Anzahl von Bildern, die das Skript in einer bestimmten Form verarbeitet.

Listing 6

#!/bin/bash
ANZAHL_BILDER=100
WARTEZEIT_SEKUNDEN=3
SPEICHERORT="."
PRAEFIX_DATEINAME="shot"
for i in `seq 1 ${ANZAHL_BILDER}`; do
  import -window root "${SPEICHERORT}/${PRAEFIX_DATEINAME}${i}.png";
  sleep ${WARTEZEIT_SEKUNDEN}
done;
exit

Zahlen stehen nur dann in Anführungszeichen, wenn sie Teil eines Strings sind. Damit keine falschen Werte in nachfolgende Berechnungen wandern, sollten Sie Variablen immer mit einem Wert initialisieren. Ändert sich der Inhalt einer Variablen nicht mehr, kennzeichnen Sie dies explizit mit dem Schlüsselwort readonly:

ergebnis = mach_was
readonly ergebnis

Gibt es im weiteren Verlauf dennoch eine Veränderung, meldet die Bash einen Fehler. Damit ersparen Sie sich eine langwierige Fehlersuche.

Hübsch eingekesselt

Funktionen benennen Sie nach dem gleichen Schema wie Variablen. Wählen Sie dabei keine Namen, die wie Systemfunktionen oder Befehle heißen – insbesondere ist test absolut tabu. Immer wieder streiten sich Entwickler leidenschaftlich darüber, wo die geschweiften Klammern stehen sollten. Google empfiehlt, die erste geschweifte Klammer wie im folgenden Beispiel direkt hinter den Namen der Funktion zu schreiben. Die schließende geschweifte Klammer steht hingegen in einer eigenen Zeile:

function meine_funktion() {... Kommandos ...
}

Im obigen Beispiel hilft das Schlüsselwort function, eine Funktion schnell als solche zu erkennen. Das erschwert jedoch das Portieren, weshalb die meisten Styleguides von function abraten.

Lokale Variablen kennzeichnen Sie innerhalb einer Funktion mit dem Schlüsselwort local. Damit überfluten Sie nicht den globalen Namensraum oder ändern gar versehentlich irgendwo die falsche Variable:

berechne() {
  local zahl=1
...

Des Weiteren sollten Sie sich angewöhnen, den Rückgabewert einer Funktion zu prüfen. Sofern eine Funktion nichts berechnet, sollte sie zumindest ihr erfolgreiches Ende oder einen Fehler verkünden. Prüfen sollten Sie unbedingt alle beim Aufruf an das Skript übergebenen Parameter: Von Benutzern falsch oder gar nicht übergebene Parameter könnten sonst zu unerfreulichen Ergebnissen führen.

Falls Sie Argumente mit $@ oder $* weiterreichen möchten, räumt dazu Googles Styleguide immer $@ den Vorzug ein: Während $* alle Argumente zu einem einzigen zusammenklebt und immer einen String ausliefert, belässt $@ die Argumente, wie sie sind [4].

Sammeln Sie alle Funktionsdefinitionen am Anfang des Skripts, idealerweise direkt hinter den Konstanten. Damit Sie später bei langen Skripten möglichst schnell den Startpunkt finden, verpacken Sie das Hauptprogramm in eine eigene Funktion namens main. Der Styleguide von Google schreibt dieses Vorgehen für alle Skripte vor, die mindestens eine Funktion definieren. Ganz am Ende des Skript steht dann der Aufruf von main "$@".

Außeneinsatz

Listing 6 nutzt sogenannte Backticks – also nach links geneigte, einfache Anführungszeichen – um ein Kommando einzubetten. Diese Syntax ist jedoch fehleranfällig und unübersichtlich, insbesondere bei verschachtelten Befehlen. Sie sollten die Backticks daher durch die modernere Notation $(Kommando) austauschen. In Listing 6 würde die fragliche Zeile damit so aussehen:

for i in $(seq 1 ${ANZAHL_BILDER}); do

Nutzen Sie zudem immer Pfadangaben, wenn Sie Dateien verarbeiten. Andernfalls landen Dateien schnell im falschen Verzeichnis. Beim Löschen mittels rm und ähnlichen Aktionen droht außerdem die Gefahr, dass Sie ungewollt wichtige Verzeichnisse ins Nirvana schicken. Google nennt hier als mahnendes Beispiel rm -v *, das neben den Dateien alle Unterverzeichnisse des aktuellen Ordners löscht, während rm -v ./* nur die Dateien von der Platte radiert.

Finger weg heißt es außerdem von ls und seinen Ausgaben: Zum einen besteht die Möglichkeit, dass die zurückgelieferten Dateinamen Zeilenumbrüche enthalten, zum anderen folgt die Ausgabe auf jedem System einer etwas anderen Konvention.

Meiden Sie auch eval. Mit dieser Funktion verlieren Sie die Kontrolle über den Code, den die Shell ausführt. Zudem erfahren Sie nicht, ob das Kommando erfolgreich war und was genau es wie verändert hat.

Geben Sie den in der Bash eingebauten Kommandos den Vorzug, und verketten Sie beispielsweise Strings nicht mit einem externen Programm. Zum einen wissen Sie nicht, ob dieses externe Programm auf anderen Computern vorhanden ist, zum anderen verhält es sich nach einem Update unter Umständen anders als erwartet. Da die Bash nicht extra einen neuen Prozess zu starten braucht, läuft Ihr Skript mit den internen Kommandos zudem wesentlich schneller ab – insbesondere, sobald Schleifen ins Spiel kommen.

Rufen Sie aus Ihrem Skript externe Programme auf, nutzen Sie die Parameter möglichst immer in der Langform, also grep --version anstelle von grep -V. In dieser Version erschließt sich die Bedeutung des Parameters schneller. Falls Ihr Skript Parameter anbietet, sollten diese ebenfalls in einer Lang- und Kurzfassung bereitstehen.

Anregungen für den Aufbau von Parametern liefert die Free Software Foundation in ihren GNU Coding Standards [5], häufig verwendete Parameter mit ähnlichen Bedeutungen listet das Linux Documentation Project [6] auf.

Lattenzäune

Auch, wenn Sie unter Zeitdruck stehen: Kommentieren Sie Ihre Skripts. Nur so verstehen Sie auch noch nach einer Woche, was das Skript an welchen Stellen macht und warum. Am Anfang Ihres Skripts beschreiben Sie zunächst kurz seine Aufgabe:

#!/bin/bash
#
# Schießt regelmäßig einen Screenshot

Der Styleguide der FH Südwestfalen empfiehlt zusätzlich noch weitere Angaben, wie den Autor und die Versionsnummer. Grundsätzlich sollte nach jedem Hash (#) ein Leerzeichen folgen. Damit lassen sich die Kommentare leichter erkennen und lesen.

Vor jede Funktion gehört ebenfalls ein Kommentar, der deren Zweck verrät, kurz die Funktionsweise erklärt, alle übergebenen Argumente vorstellt, den Rückgabewert nennt und schließlich noch beschreibt, welche globalen Variablen die Funktion verändert. Google empfiehlt den Aufbau aus Listing 7.

Geraten Sie dabei in Versuchung, einen kleinen Roman zu schreiben, handelt es sich wahrscheinlich um eine extrem komplexe Funktion. Sie sollten dann darüber nachdenken, sie in mehrere kleinere Funktionen aufzuspalten.

Listing 7

#######################################
# Schieße einen Screenshot
# Globals:
#   SPEICHERORT
#   DATEINAME
# Arguments:
#   None
# Returns:
#   None
#######################################
screenshot() {
  ...
}

Abschließend sollten Sie noch jedem wichtigen und sich nicht selbsterklärendem Block im Code einen Kommentar voranstellen. Heiße Kandidaten sind hier Schleifen und If-Abfragen. Kommentieren Sie jedoch nicht zu detailliert, also nicht jede Zeile. Einige Styleguides raten dazu, Kommentare nicht hinter eine Zeile zu stellen, sondern sie in eine eigene Zeile zu ziehen:

ANZAHL=100 # Falsch
# Richtig
ANZAHL=100

Grundsätzlich sollten die Kommentare nicht noch einmal den Code wiederholen, sondern seinen Zweck erklären. Anstelle von: für 1 bis 100 rufe image auf und warte dann 3 Sekunden verwenden Sie besser: schießt alle 3 Sekunden einen Screenshot. Einen guten Kommentar zu finden, ist häufig genauso schwierig, wie Variablen passende Namen zu geben.

Sofern Sie den entsprechenden Programmcode nur schnell hingeschrieben haben, Sie ihn aber später noch einmal überarbeiten möchten, er nur vorübergehend im Skript steht und Sie der Meinung sind, dass er ganz gut, aber nicht perfekt ist, stellen Sie ihm einen Kommentar der folgenden Form voran:

# TODO (tim@example.com)
# Pseudozufallszahl berechnen

In den Klammern notieren Sie die Kontaktperson, idealerweise in Form einer E-Mail-Adresse. Wer immer das Skript später sieht, ist auf diesem Weg in der Lage, Sie zu kontaktieren. Einige Editoren heben zudem alle mit # TODO beginnenden Zeilen optisch hervor. Neben TODO empfehlen einige Styleguides noch FIXME für fehlerhafte Stellen und XXX für extrem gruselige und schnellstmöglich zu löschende Code-Teile.

Startrampe

Vergessen Sie nicht, dem fertiggestellten Skript anschließend noch die zum Ausführen notwendigen Rechte zu erteilen:

$ chmod +x Skript.sh

Google rät dazu, die Endung .sh wegzulassen. Alle eingebundenen Skript-Dateien sollten dagegen nicht mit den Bits zum Ausführen versehen sein und die Endung .sh erhalten. Den Dateinamen schreiben Sie grundsätzlich klein. Setzt es sich aus mehreren Wörtern zusammen, trennen Sie diese Teile mit einem Unterstrich statt mit einem Bindestrich. Korrekt wäre demnach mache_screenshots. Gewähren Sie dem Skript keinesfalls per SUID oder SGID Sonderrechte: Die Gefahr von Sicherheitslücken beziehungsweise eines Missbrauchs ist hier zu hoch.

Das Skript sollte seinem Benutzer bei seiner Beendigung Auskunft darüber geben, ob es erfolgreich durchgelaufen ist oder die Arbeit abbrechen musste. In letzterem Fall muss es eine aussagekräftige Nachricht auf dem Fehlerkanal STDERR ausspucken. Diese Vorgehensweise erlaubt es, später alle Meldungen gezielt umzuleiten. Beenden Sie das Skript bei einem Fehler mit exit 1, andernfalls mit exit 0. Auf diese Weise integriert sich das Skript wesentlich einfacher in andere Projekte.

Listing 8 zeigt das kryptische Skript aus Listing 1 in der überarbeiteten Fassung. Es ist allerdings immer noch nicht perfekt: So sollten Sie keine deutschen Kommentare und Variablennamen verwenden, sondern möglichst englische. Das vereinfacht nicht nur die Weitergabe, sondern hilft auch, wenn Sie in englischen Foren Fragen stellen. Helfer haben dann die Möglichkeit, den Code wesentlich schneller zu analysieren.

Listing 8

#!/bin/bash
#
# Schießt regelmäßig einen Screenshot
ANZAHL_BILDER=100
WARTEZEIT_SEKUNDEN=3
SPEICHERORT="."
PRAEFIX_DATEINAME="shot"
# Schieße alle x Sekunden einen Screenshot,
# aber nicht mehr Screenshots als vorgegeben
for i in $(seq 1 ${ANZAHL_BILDER}); do
  import -window root "${SPEICHERORT}/${PRAEFIX_DATEINAME}${i}.png";
  sleep ${WARTEZEIT_SEKUNDEN}
done;
exit

Übrigens reduzieren Sie den Code aus Listing 8 mit einer aktuellen Version von ImageMagick auf eine Zeile:

$ import -window root -snaps 100 -pause 3 shot.png

Wie Sie daran sehen, erspart der Blick in eine Dokumentation beziehungsweise Manpage oft das Erstellen eines Skripts.

Fazit

Vielleicht erscheinen Ihnen einige der Richtlinien unnötig. Haben Sie sich jedoch einmal daran gewöhnt, schreiben Sie in Zukunft sehr viel übersichtlichere Bash-Skripte, deren Arbeitsweise Sie auch nach geraumer Zeit noch verstehen. Da viele andere Programmierer den beschriebenen Guides folgen, lesen Sie sich wesentlich schneller in deren Skripte ein. Bei Teamwork sollten Sie sich mit Ihren Mitstreitern unbedingt auf einen Standard einigen.

Ganz nebenbei reduziert das konsequente Befolgen der Regeln Programm- und Tippfehler. An welchem Standard Sie sich orientieren, bleibt ganz Ihnen überlassen. Wichtig ist nur, dass Sie bei den einmal gewählten Regeln bleiben.

Infos

[1] Google Shell Style Guide: http://google-styleguide.googlecode.com/svn/trunk/shell.xml

[2] Styleguide der FH Südwestfalen: http://lug.fh-swf.de/vim/vim-bash/StyleGuideShell.de.pdf

[3] The Open Group Base Specifications Issue 7, IEEE Std 1003.1: http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08

[4] Bash Reference Manual, Section 3.4.2 Special Parameters: http://www.gnu.org/software/bash/manual/bashref.html#Special-Parameters

[5] GNU Coding Standards: http://www.gnu.org/prep/standards/

[6] Standard Command-Line Options: http://tldp.org/LDP/abs/html/standard-options.html

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDF
LinuxUser 03/2014 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