Gewährleistung der Portabilität von Bash-Skripten auf verschiedenen Systemen

Schreiben Sie portable Bash-Skripte, die GNU-, BSD- und BusyBox-Unterschiede unter Linux, macOS und CI-Umgebungen bewältigen.

Sicherstellung der Bash-Skript-Portabilität über verschiedene Systeme hinweg

Bash-Skripte zu schreiben, die auf Ihrem Laptop, einem Linux-Server und einem CI-Runner funktionieren, ist schwieriger als es aussieht. Die Portabilität von Bash-Skripten scheitert meist an kleinen Unterschieden: Ein sed -i-Flag, das unter Linux funktioniert, aber unter macOS versagt, eine date-Option, die nur in GNU coreutils existiert, oder ein Skript, das annimmt, dass /bin/bash die von Ihnen getestete Version ist.

Die Kernschwierigkeit besteht darin, dass Bash nur ein Teil der Umgebung ist. Linux liefert normalerweise GNU-Dienstprogramme aus. macOS liefert BSD-artige Dienstprogramme. BusyBox-basierte Container können kleinere Implementierungen mit weniger Optionen bereitstellen. Ihr Skript muss klar angeben, was es benötigt.

Diese Anleitung konzentriert sich auf Bash-Skripte, nicht auf strikt POSIX-konforme sh-Skripte. Wenn Sie echte /bin/sh-Portabilität benötigen, vermeiden Sie ausschließlich Bash-Syntax und testen Sie mit Shells wie dash.

Beginnen Sie mit einem klaren Shell-Vertrag

Verwenden Sie einen Shebang, der Ihrer Absicht entspricht. Wenn das Skript Bash erfordert, sagen Sie es:

#!/usr/bin/env bash

/usr/bin/env findet Bash über $PATH, was nützlich ist, wenn Benutzer eine neuere Bash außerhalb von /bin installieren. Wenn Ihre Produktionshosts einen festen Interpreter-Pfad erfordern, dokumentieren und erzwingen Sie diesen Pfad stattdessen.

Der strikte Modus fängt viele Fehler frühzeitig ab, ist aber kein Wundermittel:

#!/usr/bin/env bash

set -euo pipefail
IFS=$'\n\t'

Diese Optionen helfen, mit Vorbehalten:

  • -e: Beendet, wenn viele einfache Befehle einen Nicht-Null-Status zurückgeben.
  • -u: Behandelt nicht gesetzte Variablen als Fehler.
  • pipefail: Lässt eine Pipeline fehlschlagen, wenn ein Befehl in der Pipeline fehlschlägt.

Behandeln Sie erwartete Fehler explizit:

if ! grep -q "ready" "$log_file"; then
    echo "Der Dienst ist noch nicht bereit"
fi

Kennen Sie Ihre Bash-Version

Verlassen Sie sich nicht versehentlich auf eine Bash-Funktion, die Ihre Zielsysteme nicht haben. macOS hat historisch gesehen eine ältere Bash in /bin/bash ausgeliefert, während viele Linux-Distributionen neuere Versionen ausliefern.

Zu den Funktionen, die Sie vorsichtig verwenden sollten, gehören:

  • Assoziative Arrays.
  • Erweiterte Globbing-Muster wie **.
  • Prozesssubstitution wie <(command).
  • Neuere Verhalten der Parametererweiterung.

Wenn Sie eine Mindestversion von Bash benötigen, überprüfen Sie diese nahe am Anfang:

if (( BASH_VERSINFO[0] < 4 )); then
    echo "Dieses Skript benötigt Bash 4 oder neuer." >&2
    exit 1
fi

Behandeln Sie GNU-, BSD- und BusyBox-Unterschiede

Die größten Portabilitätsprobleme treten oft bei externen Befehlen auf, nicht bei Bash selbst.

sed -i

GNU sed akzeptiert -i ohne eine Backup-Erweiterung. BSD sed unter macOS erfordert ein Erweiterungsargument nach -i, selbst wenn diese Erweiterung ein leerer String ist.

datei="data.txt"
muster="s/error/success/g"

case "$(uname -s)" in
    Darwin)
        sed -i '' "$muster" "$datei"
        ;;
    *)
        sed -i "$muster" "$datei"
        ;;
esac

Für kritische Skripte ist ein sichereres Muster, in eine temporäre Datei zu schreiben und sie dann an Ort und Stelle zu verschieben. Das vermeidet, sich auf das Verhalten der In-Place-Bearbeitung zu verlassen.

date

Datumsberechnungen unterscheiden sich zwischen Systemen:

Ziel GNU date BSD date unter macOS
Vor 30 Tagen date -d "30 days ago" +%Y%m%d date -v-30d +%Y%m%d

Wenn Ihr Skript komplexe Datumsberechnungen benötigt, verwenden Sie eine konsistente Abhängigkeit wie Python oder fordern Sie GNU coreutils unter macOS an und rufen Sie gdate explizit auf. Gehen Sie nicht stillschweigend davon aus, dass date -d existiert.

grep, find und xargs

Halten Sie sich nach Möglichkeit an weitgehend unterstützte Optionen:

  • Verwenden Sie grep -E anstatt sich auf egrep zu verlassen.
  • Vermeiden Sie grep -P, es sei denn, Sie überprüfen auf GNU grep mit PCRE-Unterstützung.
  • Seien Sie vorsichtig mit find-Prädikaten, die sich zwischen GNU- und BSD-Implementierungen unterscheiden.
  • Bevorzugen Sie null-getrennte Pipelines für Dateinamen, wenn unterstützt:
find "$root" -type f -name '*.log' -print0 | xargs -0 rm -f

Verwalten Sie Abhängigkeiten und Pfade

Verwenden Sie $PATH für die normale Befehlssuche, aber überprüfen Sie erforderliche Werkzeuge, bevor Sie arbeiten:

check_dependency() {
    if ! command -v "$1" >/dev/null 2>&1; then
        echo "Fehler: Erforderlicher Befehl '$1' nicht gefunden." >&2
        exit 1
    fi
}

check_dependency jq
check_dependency curl

Bevorzugen Sie command -v gegenüber which, da es ein Shell-Builtin in Bash ist und sich in Skripten vorhersagbarer verhält.

Setzen Sie Variablen in Anführungszeichen, es sei denn, Sie möchten absichtlich Worttrennung:

cp "$source_file" "$target_dir/"

Dies ist wichtig für Pfade wie Project Files/report.txt und schützt Sie auch vor Wildcard-Erweiterung bei unerwarteten Eingaben.

Verwenden Sie temporäre Dateien sicher

Verwenden Sie mktemp für temporäre Arbeiten. Ein einfaches, portables Muster ist, ein temporäres Verzeichnis zu erstellen und Dateien darin abzulegen:

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

tmp_file="$tmp_dir/output.txt"
some_command > "$tmp_file"

Der in einfachen Anführungszeichen gesetzte Trap verhindert, dass $tmp_dir erweitert wird, bis der Trap ausgeführt wird. Da die Variable noch im Gültigkeitsbereich ist, entfernt die Bereinigung das richtige Verzeichnis.

Achten Sie auf Zeilenumbrüche und Dateisystem-Groß-/Kleinschreibung

Skripte, die unter Windows bearbeitet wurden, können CRLF-Zeilenumbrüche verwenden. Ein häufiges Symptom ist:

/usr/bin/env: bash\r: Datei oder Verzeichnis nicht gefunden

Konfigurieren Sie Ihren Editor, um Shell-Skripte mit LF-Zeilenumbrüchen zu speichern, oder führen Sie dos2unix in Ihrem Build-Prozess aus.

Denken Sie auch daran, dass die meisten Linux-Dateisysteme standardmäßig zwischen Groß- und Kleinschreibung unterscheiden, während standardmäßige macOS APFS-Setups oft nicht zwischen Groß- und Kleinschreibung unterscheiden. Wenn Ihr Skript Config.yml schreibt und später config.yml liest, kann es auf Ihrem Mac funktionieren und unter Linux fehlschlagen.

Testen Sie auf den Systemen, die Sie unterstützen

Die beste Portabilitätsprüfung ist eine kleine Testmatrix:

  • Linux mit GNU-Dienstprogrammen.
  • macOS mit BSD-Dienstprogrammen.
  • Minimale Container, wenn Ihr Skript in Alpine- oder BusyBox-Umgebungen läuft.

Führen Sie auch ShellCheck aus. Es wird nicht jedes Plattformproblem abfangen, aber es fängt viele Anführungszeichen-, undefinierte Variablen- und fragile Befehlsmuster ab, bevor Ihre Benutzer es tun.

Fazit

Die Portabilität von Bash-Skripten ergibt sich daraus, dass Sie Ihre Annahmen explizit machen. Wählen Sie die Shell, überprüfen Sie Abhängigkeiten, setzen Sie Variablen in Anführungszeichen, vermeiden Sie reine GNU-Flags, es sei denn, Sie benötigen sie, und testen Sie auf denselben Betriebssystemen, die Ihre Benutzer verwenden. Eine kleine CI-Matrix mit Linux und macOS fängt die meisten Portabilitätsfehler ab, bevor Ihre Automatisierung in die Produktion geht.