Bash ist die Standard-Shell auf den meisten Linux-Systemen und die Sprache, in der Systemadministratoren den größten Teil ihrer Arbeit erledigen. Wer Bash beherrscht, muss für Automatisierung selten ein Skript in einer anderen Sprache schreiben. Dieser Artikel führt durch die Grundlagen mit praxistauglichen Beispielen.

Grundgerüst eines Skripts

#!/usr/bin/env bash
set -euo pipefail          # Wichtigste Zeile! Siehe unten.

# Variablen (kein Leerzeichen um das Gleichheitszeichen!)
NAME="Server"
anzahl=3

# Verwendung mit ${...} — sicherer als $NAME
echo "Hallo ${NAME}, ${anzahl} Instanzen"

# Schreibgeschützt
readonly PFAD="/var/log"

# Umgebung mit Standardwert
PORT="${PORT:-8080}"
echo "Port: ${PORT}"

Die Zeile set -euo pipefail ist in Skripten praktisch Pflicht:

  • -e – bricht bei einem fehlgeschlagenen Befehl ab (statt weiterzulaufen).
  • -u – Fehler bei der Verwendung nicht gesetzter Variablen (statt leer zu expandieren).
  • -o pipefail – ein Fehler in einer Pipe führt zum Abbruch, nicht nur der letzte Befehl zählt.

Ohne diese Optionen läuft ein Skript nach einem Fehler fröhlich weiter und richtet womöglich Schaden an.

Bedingungen

# Dateien und Verzeichnisse prüfen
if [[ -f "$DATEI" ]]; then        echo "Datei existiert"; fi
if [[ -d "$ORDNER" ]]; then       echo "Verzeichnis existiert"; fi
if [[ -r "$DATEI" && -w "$DATEI" ]]; then echo "lesbar und schreibbar"; fi

# Zeichenketten
if [[ -z "$VAR" ]]; then   echo "leer"; fi
if [[ -n "$VAR" ]]; then   echo "nicht leer"; fi
if [[ "$A" == "$B" ]]; then echo "gleich"; fi
if [[ "$A" =~ ^[0-9]+$ ]]; then echo "nur Ziffern"; fi

# Zahlen (bei [[ ]] mit -lt etc., oder (( )) für Arithmetik)
if (( anzahl > 5 )); then echo "mehr als fuenf"; fi
if (( anzahl % 2 == 0 )); then echo "gerade"; fi

# case für mehrere Faelle
case "$1" in
  start)   echo "starten"   ;;
  stop)    echo "stoppen"   ;;
  restart) echo "neu starten" ;;
  *)       echo "unbekannt: $1" >&2; exit 1 ;;
esac

[[ ]] verwenden, nicht [ ]: Die doppelten Klammern sind ein Bash-Schlüsselwort mit sichererem Verhalten – ungesetzte Variablen und Wortaufteilung führen nicht zu überraschenden Ergebnissen.

Schleifen

# Über eine Liste
for dienst in nginx postgresql redis; do
  systemctl is-active --quiet "$dienst" || echo "$dienst laeuft nicht"
done

# Über Dateien (immer mit null-Trennung bei Leerzeichen im Namen!)
while IFS= read -r -d '' datei; do
  echo "gefunden: $datei"
done < <(find /var/log -name '*.log' -print0)

# Zählen
for ((i = 1; i <= 5; i++)); do echo "Durchlauf $i"; done

# Solange bis Bedingung erfuellt
versuche=0
until curl -sf http://localhost:8080/health > /dev/null; do
  versuche=$((versuche + 1))
  if (( versuche > 30 )); then echo "aufgeben"; exit 1; fi
  sleep 2
done

Funktionen und Argumente

log() {
  local stufe="$1"; shift          # shift entfernt das erste Argument
  printf '%s [%s] %s\n' "$(date -Is)" "$stufe" "$*" >&2
}

log INFO "Sicherung startet"
log WARN "Speicher zu ${PROZENT}% voll"

# Argumente des Skripts selbst
#   $0 = Skriptname  $1..$n = Argumente  $# = Anzahl  $@ = alle
if (( $# < 1 )); then
  echo "Aufruf: $0 <verzeichnis>" >&2
  exit 2
fi

local in Funktionen ist wichtig, damit Variablen nicht versehentlich global wirken. Und "$@" immer in Anführungszeichen verwenden, sonst zerfallen Argumente mit Leerzeichen.

Praxismuster: mit Fehlerbehandlung und Aufräumen

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

QUELLE="${1:?Pflichtargument: Quellverzeichnis}"
ZIEL="/srv/backup/$(date +%Y-%m-%d)"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT          # laeuft in JEDEM Fall

[[ -d "$QUELLE" ]] || { echo "Kein Verzeichnis: $QUELLE" >&2; exit 1; }
mkdir -p "$ZIEL"

tar -czf "${TMP}/archiv.tar.gz" -C "$QUELLE" .
mv "${TMP}/archiv.tar.gz" "$ZIEL/"

# Pruefsumme zur Kontrolle
sha256sum "$ZIEL/archiv.tar.gz" > "$ZIEL/archiv.sha256"
echo "Fertig: $ZIEL"

trap ... EXIT ist das Gegenstück zum finally-Block anderer Sprachen: Das temporäre Verzeichnis wird entfernt, egal ob das Skript erfolgreich endet oder abbricht.

Häufige Stolpersteine

  1. Leerzeichen sind entscheidend. VAR = wert ist ein Aufruf des Programms VAR, keine Zuweisung. Richtig: VAR=wert.
  2. Immer quoten. $datei zerfällt bei Leerzeichen im Pfad in mehrere Wörter. "$datei" ist die Regel, nicht die Ausnahme.
  3. Nicht ls parsen. Ausgaben von ls sind für Menschen gedacht. Für Dateilisten find mit -print0 oder Glob-Muster nutzen.
  4. Exit-Codes prüfen. Nach jedem Befehl mit möglichem Fehler || { echo ...; exit 1; } ergänzen.
  5. Kein cat in Pipelines. grep muster datei statt cat datei | grep muster – kürzer und ohne zusätzlichen Prozess.
  6. Berechnungen mit (( )). $((a + b)) rechnet, $(( )) in Bedingungen prüft. Beides ist schnell, aber nicht für Fließkomma geeignet – dafür bc oder awk.

Verwandte Grundlagen: Shell, Cron und Crontab, Terminal-Werkzeuge, bash-Befehle.