Wie Sie Fehlerbehandlung in Shellskripten nutzen können
Im folgenden Text soll es um ein in der Praxis oft stiefmütterlich behandeltes Thema gehen: Fehlerbehandlung und -vermeidung in Shellskripten.
Shellskripte werden in der Linux-Administration gerne dazu verwendet, um kleinere und größere, wiederkehrende Aufgaben zu automatisieren. Passiert Administratoren ein Fehler in der Programmierung, hat Bash den Nachteil, dass ein Skript nach Fehlern gnadenlos weiterläuft.
Dies kann im schlimmsten Fall dazu führen, dass ein Skript großen Schaden anrichtet, wenn es beispielsweise Daten erfassen und neu schreiben soll, bei der Erfassung aber scheitert. Infolgedessen überschreibt es beispielsweise bestehende Dateien mit leeren Dateien.
Schritt 1: Zielverzeichnis fehlt
Als Beispiel sei folgende Aufgabe gegeben: Ein Skript soll den Inhalt von /etc
regelmäßig nach /virtual/etc-backup/
schreiben und dabei einen Zeitstempel verwenden. Dies wird häufig, wie folgt, realisiert:
#!/bin/bash BACKUP_DIR="/virtual/etc-backup" BACKUP_DATE=$(date +"%Y-%m-%d %H:%M:%S") cp -a /etc "$BACKUP_DIR/$BACKUP_DATE"
Damit erhalten wir fortlaufende Verzeichnisse nach Art 2025-01-08 15:37:30 im Verzeichnis /virtual/etc-backup
.
Folgende unberücksichtigte Fehler können Grund dafür sein:
- Das Backup-Zielverzeichnis existiert nicht oder ist kein Verzeichnis.
- Auf das Backup-Zielverzeichnis kann nicht geschrieben werden.
- Die Backup-Quelle existiert nicht oder darin vorhandene Dateien sind nicht lesbar
- Oder weitere Gründe.
Wir prüfen daher also zunächst auf das Vorhandensein des Backup-Verzeichnisses:
#!/bin/bash BACKUP_DIR="/virtual/etc-backup" if [ ! -d "$BACKUP_DIR" ]; then echo "Backup path $BACKUP_DIR does not exist or is not a directory." exit 1 fi BACKUP_DATE=$(date +"%Y-%m-%d %H:%M:%S") cp -a /etc "$BACKUP_DIR/$BACKUP_DATE"
Damit wird der mögliche Fehler eines nicht bestehenden Zielverzeichnisses abgefangen.
Dies ist umso wichtiger, wenn BACKUP_DIR
aus einer Benutzereingabe befüllt wird.
Daher wird dieser Fehler auch privilegiert behandelt. exit 1
beendet nicht nur das Skript, sondern gibt einen Rückgabewert ungleich 0
zurück. Dadurch kann nach Aufruf des Skripts überprüft werden, ob es fehlerfrei durchgelaufen ist.
Schritt 2: Server startet während des Kopiervorgangs neu oder stürzt ab
Wenden wir uns dem nächsten Fehler zu, den wir nicht immer über Fehlerbehandlung erfassen können: Während des Kopiervorgangs startet der Server neu oder stürzt ab. In dem Fall habe ich ein halbes Backup herumliegen.
Man kann argumentieren, dass ein halbes Backup besser ist als gar kein Backup, aber ich will zumindest darüber Bescheid wissen, dass es ein halbes Backup ist.
Daher pflege ich in diesen Fällen mit temporären Dateien zu arbeiten:
#!/bin/bash BACKUP_DIR="/virtual/etc-backup/" if [ ! -d "$BACKUP_DIR" ]; then echo "Backup path $BACKUP_DIR does not exist or is not a directory."; exit 1; fi BACKUP_DATE=$(date +"%Y-%m-%d %H:%M:%S") cp -a /etc "$BACKUP_DIR/$BACKUP_DATE.tmp" if [ $? -ne 0 ]; then echo "An error occurred while copying /etc to $BACKUP_DIR/$BACKUP_DATE.tmp."; exit 1; fi mv "$BACKUP_DIR/$BACKUP_DATE.tmp" "$BACKUP_DIR/$BACKUP_DATE"
Dadurch wird das Backup zunächst mit der Endung .tmp
angelegt. Scheitert der Kopiervorgang auf halbem Wege, beendet das Skript, behält aber das halbe Backup als solches ersichtlich bei.
Die Fehlerbehandlung können wir testen, indem wir z. B. /etc
durch ein nichtexistierendes Verzeichnis ersetzen.
Schritt 3: Verzeichnisse komprimieren
Im nächsten Schritt wollen wir Platz sparen und verwenden daher tar
und gzip
, um die Verzeichnisse zu komprimieren und überprüfen dabei jeden Einzelschritt:
#!/bin/bash BACKUP_DIR="/virtual/etc-backup/" if [ ! -d "$BACKUP_DIR" ]; then echo "Backup path $BACKUP_DIR does not exist or is not a directory."; exit 1; fi BACKUP_DATE=$(date +"%Y-%m-%d %H:%M:%S") cp -a /etc "$BACKUP_DIR/$BACKUP_DATE.tmp" if [ $? -ne 0 ]; then echo "An error occurred while copying /etc to $BACKUP_DIR/$BACKUP_DATE.tmp."; exit 1; fi tar -cf "$BACKUP_DIR/$BACKUP_DATE.tmp.tar" "$BACKUP_DIR/$BACKUP_DATE.tmp" if [ $? -ne 0 ]; then echo "An error occurred while creating the tar archive $BACKUP_DIR/$BACKUP_DATE.tmp.tar."; exit 1; fi gzip "$BACKUP_DIR/$BACKUP_DATE.tmp.tar" if [ $? -ne 0 ]; then echo "An error occurred while compressing the tar archive $BACKUP_DIR/$BACKUP_DATE.tmp.tar."; exit 1; fi mv "$BACKUP_DIR/$BACKUP_DATE.tmp.tar.gz" "$BACKUP_DIR/$BACKUP_DATE.tar.gz" if [ $? -ne 0 ]; then echo "An error occurred while renaming the tar archive $BACKUP_DIR/$BACKUP_DATE.tmp.tar.gz to $BACKUP_DIR/$BACKUP_DATE.tar.gz."; exit 1; fi # Cleanup of temporary files rm -rf "$BACKUP_DIR/$BACKUP_DATE.tmp" if [ $? -ne 0 ]; then echo "An error occurred while removing the temporary directory $BACKUP_DIR/$BACKUP_DATE.tmp."; exit 1; fi
Achtung: Hinweis zu rm
in Skripten
Man sollte sich immer die Frage stellen, wie das Skript reagiert, wenn alle Variablen eines rm
-Befehls, hier $BACKUP_DIR
und $BACKUP_DATE
leer wären - im vorliegenden Beispiel wird dann versucht, /.tmp
zu löschen.
Ohne den festen Anteil würde hier, abhängig von den Rechten des ausführenden Benutzers, das Rootverzeichnis gelöscht und das System zerstört werden. Daher empfiehlt es sich, Pfade nicht nur aus Variablen zusammenzusetzen.
Man mag an der Stelle einwenden, dass moderne Distributionen rm --preserve-root
als Defaultwert gesetzt haben und ein reines rm
es mithin nicht mehr erlaubt, das Rootverzeichnis zu löschen. Allerdings bleibt die Empfehlung weiterhin sinnvoll, da man ja auch an anderer Stelle mit zu weit gefassten Löschbefehlen viel Schaden anrichten kann.
Vorteile
Wir sehen den Vorteil der Fehlerbehandlung: falls nur ein Befehl in der Kette fehlschlägt, scheitert das Skript vorzeitig, belässt aber den bis dahin erreichten Stand im Verzeichnis.
Nun wird der Code aber durch die ganze Fehlerbehandlung recht unübersichtlich und die Frage stellt sich, ob eine derart detaillierte Behandlung nötig ist. Dies auch in Hinblick darauf, dass die Befehle selbst schon Fehlermeldungen absetzen.
Hier gibt es aber die Möglichkeit eines ‘catchall’
:
#!/bin/bash BACKUP_DIR="/virtual/etc-backup/" if [ ! -d "$BACKUP_DIR" ]; then echo "Backup path $BACKUP_DIR does not exist or is not a directory."; exit 1; fi trap 'echo "An error occurred, check output. Exiting..."; exit 1;' ERR BACKUP_DATE=$(date +"%Y-%m-%d %H:%M:%S") cp -a /etc "$BACKUP_DIR/$BACKUP_DATE.tmp" tar -cf "$BACKUP_DIR/$BACKUP_DATE.tmp.tar" "$BACKUP_DIR/$BACKUP_DATE.tmp" gzip "$BACKUP_DIR/$BACKUP_DATE.tmp.tar" mv "$BACKUP_DIR/$BACKUP_DATE.tmp.tar.gz" "$BACKUP_DIR/$BACKUP_DATE.tar.gz" rm -rf "$BACKUP_DIR/$BACKUP_DATE.tmp"
Der Befehl trap
fängt den ersten Fehler ( ERR
) auf und beendet das Skript.
Dies erspart einerseits eine unnötig aufwändige Behandlung einzelner Rückgabewerte und bricht das Skript andererseits vorzeitig ab, so dass beispielsweise das temporäre Verzeichnis nicht gelöscht wird, wenn tar
fehlschlägt.
Dies kann mit einer Funktion weiter verschönert werden, was es uns auch erleichtert, SIGINT ( STRG+C )
abzufangen.
In letzterem Fall räumen wir als Beispiel zusätzlich auf:
#!/bin/bash BACKUP_DIR="/virtual/etc-backup/" # Check if backup directory exists and is a directory. if [ ! -d "$BACKUP_DIR" ]; then echo "Backup path $BACKUP_DIR does not exist or is not a directory."; exit 1; fi # Function to handle errors handle_error() { echo "An error occurred on line $1, check output. Exiting..."; exit 1; } # Function to handle SIGINT handle_sigint() { echo "SIGINT received, cleaning up and exiting..."; if [ -d "$BACKUP_DIR/$BACKUP_DATE.tmp" ]; then rm -rf "$BACKUP_DIR/$BACKUP_DATE.tmp" fi if [ -f "$BACKUP_DIR/$BACKUP_DATE.tmp.tar" ]; then rm -rf "$BACKUP_DIR/$BACKUP_DATE.tmp.tar" fi if [ -f "$BACKUP_DIR/$BACKUP_DATE.tmp.tar.gz" ]; then rm -rf "$BACKUP_DIR/$BACKUP_DATE.tmp.tar.gz" fi exit 1; } # Trap errors and SIGINT trap 'handle_error $LINENO' ERR trap 'handle_sigint' SIGINT # Backup BACKUP_DATE=$(date +"%Y-%m-%d %H:%M:%S") cp -a /etc "$BACKUP_DIR/$BACKUP_DATE.tmp" tar -cf "$BACKUP_DIR/$BACKUP_DATE.tmp.tar" "$BACKUP_DIR/$BACKUP_DATE.tmp" # Simulate a long running process echo "Sleeping for 10 seconds, press CTRL+C if you are impatient..." sleep 10 echo "Waking up." gzip "$BACKUP_DIR/$BACKUP_DATE.tmp.tar" mv "$BACKUP_DIR/$BACKUP_DATE.tmp.tar.gz" "$BACKUP_DIR/$BACKUP_DATE.tar.gz" rm -rf "$BACKUP_DIR/$BACKUP_DATE.tmp"
Fazit
Am vorliegenden Beispiel haben wir gezeigt, wie ein Skript durch Fehlerbehandlung Stück für Stück robuster gestaltet werden kann und wie mit trap
sowohl Fehler als auch POSIX-Standardsignale (man signal
für mehr) abgefangen werden können, so dass die Fehlerbehandlung übersichtlich bleibt. Auch wurde ein Beispiel für ein Design vorgestellt, das mögliche externe Faktoren wie z. B. ein Neustart des Systems mit berücksichtigt.

Sie haben Fragen zu Shellskripten? Nehmen Sie gern Kontakt zu uns auf.
André Wild, Consultant
Telefon +49172 541 142 29