Ein Bash-Skript ist erstmal nur eine Textdatei mit Befehlen — aber zwischen „läuft auf meinem Rechner” und „läuft zuverlässig auf jedem System” liegt eine Reihe von Konventionen: der richtige Shebang, das passende Permissions-Bit, ein robuster Skript-Header und ein paar Spielregeln rund um Encoding und Pfadauflösung. Wer diese Grundlagen sauber setzt, spart sich später viele Stunden Fehlersuche.
Was ein Skript ist
Ein Skript ist eine Textdatei, deren Zeilen ein Interpreter — bei uns die Bash — der Reihe nach abarbeitet. Es gibt keinen Compiler, keine Binärdatei, kein Linker. Du speicherst Befehle, die du auch interaktiv eintippen könntest, und die Shell liest sie wie ein Mensch: Zeile für Zeile.
Der Unterschied zu einem kompilierten Programm (etwa C oder Go) liegt im Ablauf:
| Aspekt | Skript | Kompiliertes Programm |
|---|---|---|
| Ausführung | Interpreter liest und führt Zeile für Zeile aus | CPU führt fertige Maschinensprache aus |
| Build-Schritt | keiner — Datei ist sofort lauffähig | Compiler erzeugt Binary |
| Performance | langsamer, da Parsing zur Laufzeit | schneller, da vorab übersetzt |
| Portabilität | abhängig vom Interpreter (Bash-Version, Tools) | je nach Plattform neu kompilieren |
| Fehler | erst bei der Ausführung der jeweiligen Zeile | viele bereits beim Compilieren |
Für Automatisierung, System-Administration und Glue-Code zwischen Werkzeugen sind Skripte unschlagbar — du editierst die Datei und das war’s, kein Build-Schritt, keine Toolchain.
Shebang #!
Die erste Zeile eines Skripts ist die Shebang-Zeile (auch „Hashbang”). Sie beginnt mit #! und gibt dem Kernel an, welcher Interpreter die Datei ausführen soll.
#!/bin/bash
echo "Hallo Welt"Wenn du das Skript per ./hello.sh startest, liest der Kernel die ersten beiden Bytes (#!), findet den Pfad zum Interpreter und ruft ihn mit der Skriptdatei als Argument auf. Aus deiner Sicht passiert das transparent — der Kernel führt also nicht die Datei direkt aus, sondern startet bash hello.sh.
Was passiert, wenn der Shebang fehlt? Dann übergibt die aktuelle Shell die Datei an einen Interpreter ihrer Wahl — oft /bin/sh, manchmal die aktuelle Login-Shell. Das ist nicht nur unportabel, es bricht auch alle Bash-spezifischen Features (Arrays, [[ ... ]], $(...) ist OK, aber <() nicht überall). Ein Skript ohne Shebang ist eine tickende Zeitbombe.
Es gibt zwei gängige Varianten:
| Shebang | Bedeutung | Wann verwenden |
|---|---|---|
#!/bin/bash | Direkter Pfad zur Bash | Wenn du sicher bist, dass Bash dort liegt — auf Linux fast immer |
#!/usr/bin/env bash | Sucht bash im PATH über env | Maximale Portabilität, vor allem für macOS und BSD |
#!/bin/sh | POSIX-Shell (oft dash auf Debian/Ubuntu) | Nur wenn du wirklich POSIX-konform bleiben willst, keine Bash-Features |
#!/usr/bin/env python3 | Gleiches Prinzip für andere Interpreter | Python, Node, Ruby etc. |
Auf macOS liegt die System-Bash in Version 3.2 unter /bin/bash — moderne Bash 5.x installiert Homebrew unter /opt/homebrew/bin/bash oder /usr/local/bin/bash. Für Skripte, die Features ab Bash 4 nutzen (assoziative Arrays, mapfile, ${var,,}), ist #!/usr/bin/env bash deshalb der robustere Default — es greift die Bash, die im PATH zuerst gefunden wird.
Ausführbar machen
Eine frisch erstellte Skriptdatei ist standardmäßig nicht ausführbar. Das siehst du in den Permissions:
ls -l hello.sh-rw-r--r-- 1 michael michael 38 May 5 10:00 hello.shDie rw--Bits zeigen: lesen und schreiben ja, ausführen nein. Mit chmod +x setzt du das Execute-Bit für alle Klassen (Owner, Group, Others):
chmod +x hello.sh
ls -l hello.sh-rwxr-xr-x 1 michael michael 38 May 5 10:00 hello.shDetails zu den Bits stehen im Artikel Berechtigungen. Restriktiver wäre chmod 750 hello.sh (nur Owner und Group dürfen ausführen). Jetzt kannst du das Skript direkt aufrufen:
./hello.shDas ./ ist nötig, weil das aktuelle Verzeichnis aus Sicherheitsgründen nicht im PATH steht — sonst könnte ein präpariertes ls im aktuellen Ordner das echte ls überschreiben. Mehr dazu im Artikel zum PATH.
Alternativ kannst du den Interpreter explizit aufrufen:
bash hello.shDiese Variante umgeht den Shebang und braucht kein Execute-Bit — die Datei muss nur lesbar sein. Praktisch zum Testen oder wenn du bewusst eine andere Bash-Version verwenden willst (/opt/homebrew/bin/bash hello.sh).
Ausführungs-Modi
Ein Skript kann auf sehr unterschiedliche Weise laufen — und das hat reale Folgen für Variablen, Working Directory und Fehlerbehandlung. Die wichtigsten Modi:
| Aufruf | Modus | Was passiert |
|---|---|---|
./script.sh | Subshell (non-interaktiv) | Neuer Bash-Prozess, eigene Variablen, Shebang entscheidet Interpreter |
bash script.sh | Subshell (non-interaktiv) | Wie oben, aber Shebang ignoriert — bash ist der Interpreter |
source script.sh | Source (im aktuellen Prozess) | Befehle laufen in der aktuellen Shell, Variablen bleiben erhalten |
. script.sh | Source — Kurzform | Identisch zu source |
(cd /tmp; ./script.sh) | Subshell mit Klammern | Side-Effects (cd, Variablen) bleiben in der Klammer |
bash -n script.sh | Syntax-Check | Liest und parst, führt aber nicht aus |
bash -x script.sh | Debug-Trace | Gibt jede Zeile vor der Ausführung aus (+ als Prefix) |
bash -v script.sh | Verbose | Gibt jede Zeile so aus, wie sie eingelesen wird |
Der Unterschied zwischen Subshell und Source ist der Klassiker:
#!/usr/bin/env bash
export PROJECT_DIR="/srv/app"
cd "$PROJECT_DIR"./setvars.sh
echo "$PROJECT_DIR"
pwd
source ./setvars.sh
echo "$PROJECT_DIR"
pwd
/home/michael
/srv/app
/srv/appNach ./setvars.sh ist PROJECT_DIR leer und pwd unverändert — der Subshell-Prozess war beendet, bevor seine Variablen die Eltern-Shell erreichen konnten. Nach source bleibt beides bestehen. Genau deshalb wird ~/.bashrc immer gesourced, nie ausgeführt: Die Aliase und Variablen sollen ja in deiner aktuellen Session leben.
Der Debug-Modus bash -x ist dein bester Freund bei der Fehlersuche:
bash -x hello.sh+ echo 'Hallo Welt'
Hallo WeltAlternativ aktivierst du den Trace nur für einen Block: set -x an, set +x aus.
Best-Practice-Skript-Header
Jedes ernsthafte Bash-Skript sollte mit einem robusten Header starten. Die Default-Einstellungen der Bash sind aus historischen Gründen sehr nachgiebig — Fehler werden geschluckt, undefinierte Variablen werden zu leeren Strings, Pipes maskieren Probleme. Mit drei Zeilen drehst du das um:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'Was die einzelnen Optionen bewirken:
| Option | Wirkung |
|---|---|
set -e | Skript bricht sofort ab, wenn ein Befehl mit Exit-Code ungleich 0 endet |
set -u | Verwendung undefinierter Variablen wirft einen Fehler statt leerer Strings |
set -o pipefail | Pipes melden den Exit-Code des ersten fehlgeschlagenen Befehls, nicht nur des letzten |
IFS=$'\n\t' | Wort-Trennung nur an Newline und Tab, nicht mehr an Leerzeichen |
set -e allein reicht nicht: Ohne pipefail würde cmd_das_failt | tee log.txt als erfolgreich gelten, weil tee zurückgibt. Und ohne set -u rutscht ein Tippfehler wie rm -rf "$PORJECT_DIR/" durch zu rm -rf / — weil $PORJECT_DIR einfach leer ist. Das hat 2015 ein bekanntes Hosting-Skript zerlegt.
Die IFS-Zeile ist das kleinere Mosaiksteinchen: Standardmäßig trennt Bash Wörter an Space, Tab und Newline. Das frisst dir Dateinamen mit Leerzeichen. Mit dem engeren IFS wird das Verhalten vorhersehbarer — und du gewöhnst dir an, Variablen konsequent in Anführungszeichen zu setzen ("$datei" statt $datei).
Tiefer in das Thema geht der Artikel zur Fehlerbehandlung — inklusive trap, Cleanup-Funktionen und gezieltem Override mit || true.
Encoding und Line-Endings
Bash erwartet UTF-8 ohne BOM und LF (Line Feed, \n) als Zeilenende. Klingt selbstverständlich, ist aber die Quelle eines der häufigsten Anfänger-Probleme: Du editierst ein Skript auf Windows und plötzlich tut es nichts mehr.
Der Grund: Windows-Editoren speichern standardmäßig mit CRLF (\r\n). Bash liest dann den Shebang als #!/usr/bin/env bash\r — und sucht nach einem Interpreter namens bash\r, den es natürlich nicht gibt. Die Fehlermeldung ist legendär kryptisch:
./script.shTypische Fehlermeldungen sind bad interpreter: No such file or directory oder '\r': command not found mitten im Skript. Beides bedeutet: Line-Endings prüfen.
So räumst du das auf:
dos2unix script.shFalls dos2unix nicht installiert ist, geht es auch mit sed:
sed -i 's/\r$//' script.shPrüfen lässt sich das mit file:
file script.shscript.sh: Bourne-Again shell script, ASCII text executableBei CRLF-Endings stünde dort with CRLF line terminators. Eine BOM (Byte Order Mark, EF BB BF am Dateianfang) ist genauso tödlich wie CRLF: Sie sitzt vor dem #! und macht den Shebang unsichtbar. UTF-8 in Linux-Tooling ist immer ohne BOM.
Editoren wie VS Code, Sublime und JetBrains erkennen .sh-Dateien meist und schalten automatisch auf LF — sicherheitshalber lohnt sich aber ein Blick in die Statusleiste oder eine .editorconfig mit end_of_line = lf im Repo.
Skript-Pfad und Working Directory
Eine häufige Aufgabe: Das Skript soll Dateien neben sich selbst lesen — etwa eine Config oder ein Helper-Skript. Das aktuelle Working Directory ist dabei nicht zwingend der Skript-Ordner, sondern der Ordner, aus dem das Skript aufgerufen wurde.
cd /tmp
/home/michael/scripts/info.shWorking Directory: /tmp
Skript-Pfad: /home/michael/scripts/info.shIn der Bash gibt es mehrere Variablen, die mit dem Skript-Pfad zu tun haben:
| Variable | Inhalt | Hinweis |
|---|---|---|
$0 | Aufruf-Name des Skripts | Nicht zuverlässig: bei source zeigt es auf die Eltern-Shell |
${BASH_SOURCE[0]} | Pfad der aktuell ausgeführten Datei | Funktioniert auch in gesourced-Dateien — robust |
$PWD | aktuelles Working Directory | Ändert sich mit cd |
$OLDPWD | vorheriges Working Directory | Wird von cd - genutzt |
Das klassische Idiom, um den Verzeichnispfad des Skripts selbst robust zu bestimmen:
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "Skript liegt in: $SCRIPT_DIR"Das geht so vor:
${BASH_SOURCE[0]}liefert den Pfad zur Skript-Datei — auch wenn das Skript gesourced wurde.dirname "..."extrahiert den Verzeichnis-Anteil (/home/michael/scripts/info.shzu/home/michael/scripts).cd "..."wechselt dort hinein — innerhalb einer Command-Substitution, also ohne dauerhaftes Wechseln des Working Directory.pwdgibt den absoluten, aufgelösten Pfad zurück (auch bei relativen Aufrufen wie./info.sh).
Mit SCRIPT_DIR kannst du dann verlässlich auf Geschwister-Dateien zugreifen:
source "$SCRIPT_DIR/lib/helpers.sh"
config_file="$SCRIPT_DIR/config.env"Warum BASH_SOURCE statt $0? Wenn ein Skript via source other.sh von einem anderen geladen wird, ist $0 der Name des äußeren Skripts — nicht der gerade laufenden Datei. ${BASH_SOURCE[0]} hingegen zeigt immer auf die aktuelle Datei. Für Bibliotheks-Skripte, die per source eingebunden werden, ist das der Unterschied zwischen „funktioniert immer” und „funktioniert nur, wenn niemand es sourced”.
Häufige Stolperfallen
chmod +x vergessen
Du erstellst das Skript, willst es per ./script.sh starten, und bekommst nur „Permission denied”. Klassiker. Ein neu angelegtes File hat kein Execute-Bit. Entweder du setzt es einmalig mit chmod +x script.sh, oder du rufst das Skript über den Interpreter direkt auf: bash script.sh. Bei git-versionierten Skripten lohnt sich git update-index --chmod=+x script.sh, damit die Berechtigung auch beim Klonen erhalten bleibt.
CRLF von Windows-Editoren
Skripte, die in Notepad, einem alten Editor oder über einen FTP-Transfer im ASCII-Modus auf Windows landen, bekommen CRLF-Zeilenenden. Bash bricht dann mit kryptischen Meldungen ab — typisch ist „bad interpreter” beim Shebang oder „command not found” mitten im Code. Lösung: dos2unix script.sh oder sed -i 's/\r$//' script.sh. Vorbeugend hilft eine .editorconfig mit end_of_line = lf für *.sh-Dateien.
cd im Skript ohne Rückkehr
Ein cd /pfad in einem gesourceten Skript ändert dein aktuelles Working Directory dauerhaft — was du beim normalen Ausführen (./script.sh) nicht hast, weil es in einer Subshell läuft. Wer Funktionen schreibt, die in eine Library gesourced werden, sollte cd deshalb in eine Subshell-Klammer packen: (cd /pfad && mache_etwas). Damit ist der Working-Directory-Wechsel garantiert nur lokal.
BASH_SOURCE statt 0 — vor allem in Bibliotheken
$0 zeigt auf das Programm, das die Bash gestartet hat — nicht zwingend auf die laufende Datei. Wenn lib.sh per source lib.sh aus main.sh heraus geladen wird, ist $0 weiterhin main.sh. Pfad-Auflösungen über $0 sind in solchen Library-Skripten falsch. ${BASH_SOURCE[0]} zeigt dagegen immer auf die aktuelle Datei und ist deshalb der robuste Default für SCRIPT_DIR-Idiome.
set -e in Pipes greift nicht ohne pipefail
Mit set -e glaubst du, jeder Fehler bricht ab — aber cmd_a | cmd_b liefert standardmäßig nur den Exit-Code von cmd_b. Schlägt cmd_a fehl und cmd_b schluckt das (z. B. tee, cat, sort), gilt die Pipe als erfolgreich und set -e zieht nicht. Erst set -o pipefail macht die Pipe ehrlich. Deshalb gehört das Trio set -euo pipefail immer zusammen — nicht nur set -e allein.
Spaces im Skript-Pfad
Wenn dein Skript unter /home/michael/My Scripts/script.sh liegt, brechen viele Pfad-Idiome ohne Anführungszeichen. cd $(dirname $0) schlägt fehl, weil Bash das an Spaces splittet. Konsequent doppelte Anführungszeichen verwenden: cd "$(dirname "${BASH_SOURCE[0]}")". Die verschachtelten Quotes sind kein Tippfehler, sondern Pflicht — jede Variable und jede Substitution gehört in eigene Quotes.
Skript läuft als root, aber Variablen kommen vom User-Aufruf
Wenn ein Nutzer sudo ./mein-skript.sh aufruft, läuft das Skript als root — aber Variablen wie $HOME oder $USER können je nach sudo-Konfiguration auf den ursprünglichen Nutzer zeigen. Auf vielen Distributionen behält sudo HOME standardmäßig nicht. Wer wirklich den Original-User braucht, nutzt $SUDO_USER. Wer das Home des Original-Users will: getent passwd "$SUDO_USER" | cut -d: -f6. Pauschal $HOME zu nutzen, schreibt Config-Dateien gerne nach /root statt ins User-Home.
Weiterführende Ressourcen
Externe Quellen
- Bash-Manpage (man7.org) — Vollständige Referenz zu
set,BASH_SOURCEund allen Built-ins - GNU Bash-Handbuch — Offizielle Dokumentation mit Beispielen zu Shell-Modi und Optionen
- ShellCheck — Statischer Linter für Bash-Skripte, findet Quoting-Fehler und unsichere Patterns
- Google Shell Style Guide — Bewährte Konventionen für robuste, lesbare Skripte
- Bash Pitfalls (mywiki.wooledge.org) — Sammlung typischer Fehler mit ausführlicher Erklärung
Verwandte Artikel
- Linux Shell — Bash, Zsh und die Grundlagen der Kommandozeile
- Streams und Pipes — stdin, stdout, stderr und ihre Umleitung
- Berechtigungen — Permissions-Bits und das Execute-Bit im Detail
- PATH — Wie die Shell Programme findet und warum
./nötig ist - Variablen — Shell- und Umgebungsvariablen,
exportund Vererbung