Skip to content

Bessere Shell-Skripte

Shell-Skripte sind gegenüber anderen Programmiersprachen natürlich nicht das "Non-plus-ultra", aber sie sind für Ablaufsteuerungen - dafür sind sie gemacht - eine gute Wahl. Für alles, was grösser ist, empfehle ich eine "richtige Programmiersprache". Ich bin als Systemadministrator ein grosser Fan von Python und - schon länger nicht mehr genutzt - Perl, aber auch Sprachen wie Raku, Ruby oder irgendetwas mit Compiler sind natürlich gute Alternativen.

Ich beziehe mich im Folgenden auf die Bash, weil das die Shell ist, die ich täglich auf verschiedenen Systemen und Architekturen benutze.

Tipp 1:

Schreibt in den Shebang #! zu Beginn des Skriptes genau die Skriptsprache mit der Ihr auch getestet habt und nicht - weil alle das machen - /bin/sh. Auf Debian basierten Systemen ist /bin/sh ein Link auf dash, bei Alpine ist es ein Link auf Busybox, auf Red Hat basierten Systemen ein Link auf bash.

An dieser Stelle möchte ich gerne noch auf diesen alten Artikel hinweisen.

Tipp 2:

Skripte laufen auch im Falle eines Fehlers weiter. Ich halte das für ein blödes Verhalten, was sehr häufig zu Fehlern führt. Glücklicherweise kann man das Verhalten abstellen.

Entweder man ruft die Shell mit -e auf, setzt den Shebang entsprechend oder schreibt set -e an den Anfang des Skriptes oder vor die Zeilen für die das Setting gelten soll. Mit set +e kann man wieder das alte Verhalten herstellen.

Meine Empfehlung ist, die Langform set -o errexit zu verwenden, das ist deutlich lesbarer. (Altes Verhalten kann man mit set +o errexit wieder herstellen).

Shellzeilen gelten als fehlerhaft, wenn der exit-Code des letzten Kommandos der Zeile ungleich 0 (null) ist. Das bedeutet unter anderem, dass man den Exitcode einer einzelnen Zeile durch Hinzufügen von || true auf "nicht fehlerhaft" ändern kann.

Tipp 3:

Wie im letzten Tipp beschrieben, ist das letzte ausgeführte Kommando einer Zeile ausschlaggebend dafür, ob eine Zeile mit oder ohne Fehlercode beendet wird.

Der Eintrag set -o pipefail sorgt dafür, dass eine Zeile als fehlerhaft "gesehen" wird, wenn auch nur ein Kommando der über Pipes vernetzten Kommandos einer Zeile fehlschlägt.

Tipp 4:

Nicht gesetzte oder "leere" Variablen sind häufig ein Problem.

Um eine nicht gesetzte Variable mit einem Fehler zu quittieren, kann man das Kommando set -o nounset oder set -u verwenden.

Tipp 5:

Es ist generell eine gute Idee, alle Variablen mit doppelten Anführungszeichen zu umgeben, ganz besonders dann, wenn es um Dateien geht. Auch, wenn man selber keine Dateien mit Leerzeichen (oder anderen "Internal Field Separators" (IFS)) erstellt, heisst es nicht, dass man nicht auf solche treffen kann.

Nachtrag:

Christoph hat in diesem Kommentar zur recht darauf hingewiesen, dass es besser ist /usr/bin/env bash zu verwenden, das benutzt die erste Bash, die der Benutzer im Pfad hat und funktioniert auch auf Systemen, auf denen die Bash in einem anderem Pfad liegt als /bin/bash.

Zusammenfassung:

Meine Erfahrung ist, dass man mit den fünf Tipps rund 80% aller Probleme mit Shellskripten umschifft bzw. Skripte mit Fehlern rechtzeitig abbricht.

#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail

# Und Variablen immer mit doppelten Anführungszeichen verwenden.

echo "${Variable}"

Es gibt noch viele weitere Tipps, aber das sind meiner Ansicht nach die wichtigsten.

Hashes in der Bash ...

Weil ich es gerade für einen Kollegen gebraucht habe.

Ein eher selten genutztes Feature in der Bash sind assoziative Arrays (Hashes). Ab Bash Version 4 geht das ganz ohne Hilfskonstrukte.

#!/bin/bash

declare -A dns=(
    [mond]=192.168.0.1
    [erde]=192.168.2.3
    [saturn]=192.168.7.8
)

echo "Alle Keys:   ${!dns[@]}"
echo "Alle Values: ${dns[@]}"
echo

for host in ${!dns[@]}; do
    ip=${dns[${host}]}
    echo "${host} hat die IP-Adresse ${ip}"
done


Schlüssel und Werte dürfen "natürlich" auch Leerzeichen enthalten. Dann müssen diese allerdings - wie bekannt - in Anführungszeichen stehen.

Ergebnis des Skriptes:

Alle Keys:   erde saturn mond
Alle Values: 192.168.2.3 192.168.7.8 192.168.0.1

erde hat die IP-Adresse 192.168.2.3
saturn hat die IP-Adresse 192.168.7.8
mond hat die IP-Adresse 192.168.0.1

Shell-Magie ...

Je länger ich mich mit der Shell (BaSH in diesem Fall) beschäftige, desto mehr denke ich, dass ich schon alles gesehen habe.

Dieses Konstrukt aber ist mir neu, ja das mit < <(kommando) und vielleicht kann das auch jemand von Euch brauchen.

#!/bin/bash

while read line; do
        echo ${line}
        [[ /${line}/ =~ /.*Suchtext.*/ ]] && break
done < <(tail -f logfile)


Es macht einen tail -f logfile und durchläuft die Schleife für jede Zeile im logfile . Alle Zeilen werden ausgegeben und wenn ein Suchtext im logfile vorkommt, wird die Schleife abgebrochen. Für die, die es nicht kennen, mit =~ kann man ab BaSH Version 3 auch mit regulären Ausdrücken suchen. Die Schrägstriche / werden nur für den Fall benötigt, dass im logfile auch Leerzeilen vorkommen.

Das Konstrukt umschifft einige Schwächen, die ähnliche Beispiele haben, die mit anderen Arten von Ein- und Ausgabeumlenkung arbeiten.

Wer es nicht glaubt, kann mal das Folgende probieren:

#!/bin/bash

tail -f logfile | while read line; do
        echo ${line}
        [[ /${line}/ =~ /.*Suchtext*/ ]] && break
done

Shell Style Guide ...

Bei Google gibt es einen Shell Style Guide, den ich wirklich jedem nur ans Herz legen kann.

Die Regeln sind sinnvoll und helfen, dass Skripte auch eine Woche nachdem sie geschrieben wurden noch verstanden werden können. :-)

Einiges ist sicherlich diskussionswürdig und Geschmackssache, ich würde Perl statt Python für längere Skripte nehmen, aber in Summe ist das Dokument prima.

Brace Expansion ...

Viele kennen die Brace Expansion, einen Mechanismus von Shells, insbesondere der GNU Bash gar nicht.

Brace Expansion lässt sich nicht so leicht übersetzen, vielleicht am besten mit Klammererweiterung.

{a,b,d} erzeugt a b d, dieser Term wird dort ersetzt, wo er eingesetzt wird ab{a,b,d} ergibt aba abb abd. {1..10} macht 1 2 3 4 5 6 7 8 9 10, {01..04} entsprechend mit führenden Nullen 01 02 03 04. Das geht auch mit Inkrementen {1..9..2} entspricht den ungeraden Zahlen 1 3 5 7 9 oder bei Buchstaben {a..f} macht a b c d e f und {a..f..2} ergibt a c e. Das ganze ist verschachtelbar {a{1,2},c,e} erzeugt a1 a2 c e.

Manche nutzen es, um einfach eine Reihe an Verzeichnissen zu erstellen.

mkdir M{e,a}{i,y}er
mkdir cd{01..10}
mkdir /home/{dirk,jupp,hans}/{.ssh,vorlagen,arbeit}


Aber es gibt auch eine ganz einfache Anwendung, die jeden Tag hilft, Erstellung von Backup-Dateien:

cp datei{,.bak}
cp datei{,von-heute}.txt