Häufige Bash-Skripting-Fallstricke und wie man sie vermeidet

Vermeiden Sie häufige Bash-Scripting-Fehler mit sicherer Fehlerbehandlung, korrektem Zitieren, Arrays, Traps und Argument-Parsing.

Häufige Bash-Scripting-Fallstricke und wie man sie vermeidet

Bash-Scripting-Fallstricke treten normalerweise auf, wenn Ihr Skript auf echte Dateinamen, fehlende Variablen, fehlgeschlagene Befehle oder unerwartete Eingaben trifft. Ein Skript, das auf Ihrem Laptop funktioniert, kann in CI oder Produktion brechen, wenn es auf lockere Standardeinstellungen angewiesen ist.

Sie müssen nicht jedes Shell-Skript kompliziert machen. Sie müssen Expansionen zitieren, Fehler absichtlich überprüfen und mit Namen testen, die Leerzeichen enthalten.

Setzen Sie sicherere Standardeinstellungen sorgfältig

Viele Skripte beginnen mit:

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

Das ist eine gute Grundlage für viele Automatisierungsskripte, aber jede Option hat scharfe Kanten:

  • set -e beendet das Skript, wenn ein einfacher Befehl fehlschlägt, außer in Situationen wie if-Tests, Teilen von &&- und ||-Listen und einigen Befehlssubstitutionen.
  • set -u beendet das Skript, wenn Sie eine nicht gesetzte Variable expandieren.
  • set -o pipefail lässt eine Pipeline fehlschlagen, wenn ein Befehl in der Pipeline fehlschlägt, nicht nur der letzte Befehl.

Verwenden Sie diese Optionen, wenn ein früher Fehler sicherer ist als das Fortsetzen. Für Befehle, bei denen ein Fehler erwartet wird, behandeln Sie den Status explizit.

if ! grep -q "ready" status.txt; then
  echo "Der Dienst ist noch nicht bereit"
  exit 1
fi

Zitieren Sie Variable-Expansionen

Nicht zitierte Variablen sind der häufigste Bash-Fehler. Bash führt Wortteilung und Glob-Expansion bei nicht zitierten Expansionen durch, sodass ein Pfad wie release notes/*.txt zu mehreren Argumenten werden oder Dateien treffen kann, die Sie nicht beabsichtigt haben.

datei="release notes.txt"

# Schlecht: bricht, weil der Wert in zwei Wörter aufgeteilt wird.
rm $datei

# Gut: übergibt ein genaues Argument.
rm -- "$datei"

Verwenden Sie -- vor benutzergesteuerten Dateinamen, wenn ein Befehl dies unterstützt. Das verhindert, dass ein Dateiname wie -rf als Option interpretiert wird.

Verwenden Sie Arrays für Argumentlisten

Speichern Sie keinen Befehl mit Argumenten in einem String und führen Sie ihn dann aus. Das Zitieren wird schnell fragil.

# Schlecht
flags="-a --exclude node_modules"
rsync $flags "$src" "$dest"

# Gut
flags=(-a --exclude "node_modules")
rsync "${flags[@]}" "$src" "$dest"

Arrays bewahren Argumentgrenzen. Das ist wichtig, wenn ein Argument Leerzeichen, Wildcard-Zeichen oder Werte enthält, die mit einem Bindestrich beginnen.

Bevorzugen Sie $(...) gegenüber Backticks

Backticks sind schwer zu verschachteln und leicht falsch zu lesen. Verwenden Sie $(...) für Befehlssubstitution.

aktueller_branch="$(git rev-parse --abbrev-ref HEAD)"
echo "Baue Branch: $aktueller_branch"

Halten Sie Befehlssubstitutionen zitiert, es sei denn, Sie möchten absichtlich Wortteilung.

Lesen Sie Dateien ohne Datenverlust

Dieses Muster sieht harmlos aus, bricht aber bei Leerzeichen und kann Backslashes verstümmeln:

for zeile in $(cat hosts.txt); do
  echo "$zeile"
done

Lesen Sie Dateien stattdessen mit while IFS= read -r.

while IFS= read -r host; do
  echo "Überprüfe $host"
done < hosts.txt

IFS= bewahrt führende und nachfolgende Leerzeichen. -r verhindert, dass Backslash-Escapes interpretiert werden.

Behandeln Sie temporäre Dateien mit mktemp und trap

Hartcodierte temporäre Pfade können mit einem anderen Prozess kollidieren oder veraltete Dateien hinterlassen. Erstellen Sie einen eindeutigen Pfad und räumen Sie ihn beim Beenden auf.

tmp_datei="$(mktemp)"
cleanup() {
  rm -f "$tmp_datei"
}
trap cleanup EXIT

printf '%s\n' "Arbeitsdaten" > "$tmp_datei"

Für Verzeichnisse verwenden Sie mktemp -d und entfernen Sie das Verzeichnis in Ihrer Cleanup-Funktion.

Parsen Sie Optionen mit getopts

Manuelles Argument-Parsing übersieht oft Randfälle. Für kurze Optionen reicht Bashs eingebautes getopts normalerweise aus.

verbose=false
ausgabe=""

while getopts ":vo:" opt; do
  case "$opt" in
    v) verbose=true ;;
    o) ausgabe="$OPTARG" ;;
    :)
      echo "Option -$OPTARG erfordert ein Argument" >&2
      exit 2
      ;;
    \?)
      echo "Unbekannte Option: -$OPTARG" >&2
      exit 2
      ;;
  esac
done
shift "$((OPTIND - 1))"

getopts behandelt kurze Flags wie -v und -o datei. Wenn Ihr Skript lange Optionen wie --output benötigt, schreiben Sie einen sorgfältigen Parser oder verwenden Sie eine Sprache mit einer stärkeren Argument-Parsing-Bibliothek.

Überprüfen Sie Befehle, die fehlschlagen können

Gehen Sie nicht davon aus, dass ein Befehl funktioniert hat, nur weil er etwas ausgegeben hat. Überprüfen Sie wichtige Operationen, bevor Sie deren Ausgabe verwenden.

if ! archiv="$(tar -czf app.tar.gz app 2>&1)"; then
  echo "Archivierung fehlgeschlagen: $archiv" >&2
  exit 1
fi

Für Pipelines aktivieren Sie pipefail, wenn ein Fehler in der Mitte die gesamte Pipeline fehlschlagen lassen soll.

set -o pipefail
journalctl -u api.service | grep -i "error"

Ohne pipefail stammt der Pipeline-Status normalerweise vom letzten Befehl.

Vermeiden Sie Bash, wenn Portabilität wichtig ist

Wenn Ihr Skript Arrays, [[ ... ]], mapfile oder pipefail verwendet, ist es ein Bash-Skript. Beginnen Sie es mit:

#!/usr/bin/env bash

Wenn Sie POSIX sh-Portabilität benötigen, vermeiden Sie Bash-spezifische Funktionen und testen Sie mit der Shell, die Ihr Zielsystem verwendet. Schreiben Sie kein Bash-Skript mit #!/bin/sh und hoffen Sie, dass es sich überall gleich verhält.

Fazit

Der schnellste Weg, Ihre Bash-Skripte zu verbessern, ist, sie mit unordentlichen Eingaben zu testen: Leerzeichen in Dateinamen, fehlende Variablen, leere Dateien und fehlschlagende Befehle. Zitieren Sie Expansionen, verwenden Sie Arrays für Argumentlisten, räumen Sie temporäre Dateien mit trap auf und machen Sie Fehlerpfade explizit. Ihr zukünftiges Ich wird weniger Zeit damit verbringen, Skripte zu debuggen, die nur mit perfekten Eingaben funktioniert haben.