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
- Leerzeichen sind entscheidend.
VAR = wertist ein Aufruf des ProgrammsVAR, keine Zuweisung. Richtig:VAR=wert. - Immer quoten.
$dateizerfällt bei Leerzeichen im Pfad in mehrere Wörter."$datei"ist die Regel, nicht die Ausnahme. - Nicht
lsparsen. Ausgaben vonlssind für Menschen gedacht. Für Dateilistenfindmit-print0oder Glob-Muster nutzen. - Exit-Codes prüfen. Nach jedem Befehl mit möglichem Fehler
|| { echo ...; exit 1; }ergänzen. - Kein
catin Pipelines.grep muster dateistattcat datei | grep muster– kürzer und ohne zusätzlichen Prozess. - Berechnungen mit
(( )).$((a + b))rechnet,$(( ))in Bedingungen prüft. Beides ist schnell, aber nicht für Fließkomma geeignet – dafürbcoderawk.
Verwandte Grundlagen: Shell, Cron und Crontab, Terminal-Werkzeuge, bash-Befehle.