Shell-Scripting: Bash-Automatisierung für Server

Learn Bash shell scripting from fundamentals to automated server monitoring with practical, immediately usable scripts.

Shell-Scripting: Bash-Automatisierung für Server

Bash ist keine schöne Sprache. Die Quoting-Regeln sind grausam, [[ ]] und [ ] und (( )) sind drei Welten mit ähnlicher Syntax, und Variablen-Scoping ist ein laufender Witz. Trotzdem schreibe ich quasi täglich Bash, weil es die einzige Sprache ist, die garantiert auf jedem Linux-System läuft, ohne dass ich Dependencies installiere.

Die Frage ist nicht "Bash oder Python", sondern "wann ist das Script lang genug, dass es sich lohnt, in eine richtige Sprache zu wechseln". Meine Heuristik: ab ca. 100 Zeilen oder sobald ich Dictionaries brauche, wird's Python.

Die drei Optionen, die jedes Script haben sollte

#!/usr/bin/env bash
set -euo pipefail

-e bricht bei Fehler ab. Klingt offensichtlich, ist es nicht — Bash ignoriert Fehler standardmäßig. Wer das nicht setzt, schreibt Scripts, die fröhlich weiterlaufen, nachdem cd fehlgeschlagen ist.

-u bricht bei undefinierten Variablen ab. Findet Tippfehler in Variablennamen — ohne diese Option produziert echo $usr einfach einen leeren String.

-o pipefail macht Pipes konsistent. Ohne die Option zeigt cmd | grep foo den Exit-Code von grep, nicht von cmd. Wenn cmd fehlschlägt, geht das unter.

Das ist das absolute Minimum. Jedes Script ohne diese drei Optionen ist tickende Zeit.

Argumente sauber parsen

Inline-Argumente sind okay für kleine Scripts. Sobald du mehr als zwei hast, lohnt sich getopts:

while getopts "f:n:v" opt; do
  case "$opt" in
    f) FILE=$OPTARG ;;
    n) NAME=$OPTARG ;;
    v) VERBOSE=1 ;;
    *) echo "Unbekannte Option" >&2; exit 1 ;;
  esac
done

: "${FILE:?Option -f ist erforderlich}"

Das ${VAR:?msg} ist ein nettes Konstrukt: bricht ab, wenn die Variable nicht gesetzt ist, mit Custom-Fehlermeldung.

Cron-Jobs, die nicht still sterben

Klassisches Anti-Pattern:

0 3 * * * /opt/scripts/backup.sh

Wenn das Script crasht, schickt Cron eine Mail an root, die niemand liest. In der Praxis: niemand merkt, dass das Backup seit drei Wochen nicht läuft.

Mein Pattern:

0 3 * * * /opt/scripts/backup.sh 2>&1 | logger -t backup

logger -t backup schreibt in syslog mit Tag. Dann ein zweites Script, das einmal pro Woche prüft, ob das Tag im Log auftaucht:

if ! journalctl --since "1 day ago" | grep -q "backup\["; then
  echo "WARNUNG: Backup-Job lief nicht" | mail -s "Backup fehlt" [email protected]
fi

Das ist die "habe ich nichts vergessen"-Schleife, die mir mehrfach den Hintern gerettet hat.

Loops korrekt schreiben

for f in $(ls *.txt) ist falsch. Bricht bei Leerzeichen, Sonderzeichen oder leeren Verzeichnissen.

Richtig:

for f in *.txt; do
  [[ -e $f ]] || continue
  process "$f"
done

-e checkt, ob die Datei existiert (nötig, weil *.txt als Literal stehen bleibt, wenn nichts matcht).

Für Dateien aus find mit Sonderzeichen:

find . -name "*.log" -print0 | while IFS= read -r -d '' f; do
  process "$f"
done

-print0 und -d '' nutzen Null-Bytes als Trenner — die einzigen Zeichen, die in Filenamen nicht vorkommen können.

Trap für Cleanup

Wenn ein Script temporäre Dateien anlegt, sollte es sie auch wieder aufräumen — auch im Fehlerfall:

TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT

# arbeite mit $TMP

trap ... EXIT läuft beim Verlassen, egal ob normal oder durch Fehler. Ohne das bleiben tmp-Dateien zurück und sammeln sich an.

Logging mit Pegelangabe

Statt überall echo "blah" zu schreiben:

log() { echo "[$(date '+%H:%M:%S')] $*"; }
err() { echo "[$(date '+%H:%M:%S')] ERROR: $*" >&2; }

log "Starte Backup"
err "Datei nicht gefunden: $FILE"

Errors auf stderr (>&2), normaler Output auf stdout. Damit kann der Aufrufer trennen.

ShellCheck nutzen, immer

shellcheck ist ein statischer Analyzer für Bash. Findet Quoting-Probleme, ungenutzte Variablen, falsche Loop-Patterns. Bei mir hängt es im Editor und in CI.

apt install shellcheck
shellcheck mein-script.sh

Die meisten Bash-Bugs, die ich in den letzten Jahren geschrieben habe, hätte ShellCheck sofort gefunden.

Wann doch Python

Wenn ich Strukturen brauche (Dicts, verschachtelte Listen). Wenn ich JSON oder YAML lese. Wenn das Script über 100 Zeilen geht und Tests sinnvoll wären. Bash ist gut für "starte ein Programm, dann das andere, kopiere ein paar Dateien". Sobald Logik dazukommt, leidet die Lesbarkeit schnell.