Bash und zsh kompatibel skripten ohne doppelten Code
AI generated
$_
#!/
Bash · zsh · macOS · Dotfiles
Bash und zsh kompatibel skripten
ohne Code doppelt zu pflegen

Seit macOS Catalina ist zsh das Standard-Login-Shell, waehrend Server und CI Umgebungen weiter auf Bash setzen. Wer ein Skript oder eine Dotfile-Funktion fuer beide Shells schreiben will, muss Array-Indizes, Globbing und Optionsnamen kennen, die sich fundamental unterscheiden.

17 Min. Lesezeit Array-Index · Globbing · emulate sh · Dotfiles macOS · zsh 5.x · Bash 5.x

1. Warum Bash und zsh Kompatibilitaet ueberhaupt zaehlt

Seit macOS Catalina im Jahr 2019 ist zsh die Standard Login Shell auf jedem neuen Mac, waehrend Linux Server, Docker Container und die meisten CI Umgebungen weiterhin auf Bash setzen. Entwickler, die taeglich zwischen macOS Terminal und Linux Server wechseln, stossen dadurch regelmaessig auf Skripte und Funktionen, die unter Bash und zsh unterschiedlich reagieren, obwohl beide Shells auf denselben Bourne Shell Wurzeln aufbauen und einen grossen gemeinsamen Funktionsumfang teilen.

Der praktische Anlass ist meist eine Dotfile Funktion oder ein kleines Utility Skript, das im .zshrc genauso funktionieren soll wie im .bashrc oder in einem eigenstaendigen Deployment Skript. Bash und zsh Kompatibilitaet zu erreichen bedeutet dabei nicht, auf den kleinsten gemeinsamen POSIX Nenner zu verzichten, sondern gezielt die Stellen zu kennen, an denen beide Shells unterschiedliche Defaults haben, und diese Unterschiede entweder zu vermeiden oder explizit zu behandeln, statt sie durch Zufall funktionieren zu lassen.

2. Array-Indizes: 0-basiert vs 1-basiert

Der bekannteste und am haeufigsten unterschaetzte Unterschied zwischen Bash und zsh betrifft die Array Indizierung. Bash Arrays sind wie in fast allen Programmiersprachen 0 basiert: ${array[0]} liefert das erste Element. zsh Arrays sind dagegen standardmaessig 1 basiert: ${array[1]} liefert das erste Element, waehrend ${array[0]} in der Standardkonfiguration ins Leere greift. Ein Skript, das unter Bash entwickelt wurde und Array Zugriffe mit Index 0 enthaelt, liefert unter zsh entweder leere Werte oder einen Off by One Fehler, der sich subtil durch die gesamte Logik zieht.

Fuer echte Kompatibilitaet zwischen Bash und zsh gibt es zwei Strategien: Entweder man vermeidet direkte numerische Array Indizes komplett und arbeitet ausschliesslich mit Iterationen ueber alle Elemente, was in beiden Shells identisch funktioniert, oder man setzt in zsh explizit die Option KSH_ARRAYS, die 0 basierte Indizierung fuer Kompatibilitaet mit Bash und ksh aktiviert. Letzteres aendert aber das Verhalten global fuer das gesamte Skript und kann andere zsh spezifische Konstrukte unerwartet beeinflussen, weshalb die iterationsbasierte Loesung meist die robustere Wahl ist.


#!/usr/bin/env bash
# This file is sourced by both .bashrc and .zshrc

hosts=(web1 web2 web3)

# WRONG: relies on 0-based indexing, breaks under default zsh
# echo "First host: ${hosts[0]}"

# RIGHT: iterate instead of indexing directly — identical in bash and zsh
for host in "${hosts[@]}"; do
  echo "Checking host: $host"
done

# If a single element is truly needed, detect the shell explicitly:
if [ -n "${ZSH_VERSION:-}" ]; then
  first_host="${hosts[1]}"   # zsh: 1-based by default
else
  first_host="${hosts[0]}"   # bash: 0-based
fi
echo "First host resolved as: $first_host"

3. Globbing-Unterschiede und nomatch

Ein zweiter zentraler Unterschied zwischen Bash und zsh betrifft das Verhalten bei Datei Mustern ohne Treffer. Wenn ein Glob Muster wie *.txt in einem Verzeichnis ohne passende Dateien verwendet wird, gibt Bash standardmaessig das Muster unveraendert als literalen String zurueck. zsh dagegen bricht mit einem Fehler ab: zsh: no matches found: *.txt, weil die Option NOMATCH standardmaessig aktiv ist. Ein Skript, das sich auf das Bash Verhalten verlaesst und den literalen String als Sonderfall abfaengt, funktioniert unter zsh gar nicht erst, weil es dort vorher mit einem Fehler abbricht.

Fuer Kompatibilitaet zwischen Bash und zsh sollte ein Skript entweder explizit pruefen, ob Dateien existieren, bevor es das Glob Ergebnis verwendet, oder in zsh gezielt setopt nullglob setzen, was leere Ergebnislisten statt eines Fehlers oder eines literalen Strings liefert und damit dem in Skripten ueblicherweise gewuenschten Bash Verhalten mit shopt -s nullglob entspricht. Diese explizite Angleichung ist deutlich robuster als sich auf implizites Shell Verhalten zu verlassen.


#!/usr/bin/env bash
# Portable globbing: works the same in bash and zsh

# Normalize glob behavior explicitly instead of relying on shell defaults
if [ -n "${ZSH_VERSION:-}" ]; then
  setopt null_glob   # zsh: empty match list instead of an error
else
  shopt -s nullglob   # bash: empty match list instead of literal string
fi

files=(*.txt)
if [ ${#files[@]} -eq 0 ]; then
  echo "No .txt files found"
else
  for f in "${files[@]}"; do
    echo "Found: $f"
  done
fi

4. Wortaufteilung: implizit vs explizit

Ein dritter Unterschied, der viele Bash Entwickler beim ersten zsh Kontakt ueberrascht, ist die Wortaufteilung unquotierter Variablen. Bash teilt eine unquotierte Variable mit mehreren durch Leerzeichen getrennten Woertern automatisch in mehrere Argumente auf, etwa bei for w in $words. zsh dagegen behandelt eine unquotierte Skalarvariable standardmaessig als ein einziges Argument, selbst wenn sie Leerzeichen enthaelt, und teilt sie nicht implizit auf. Ein Skript, das sich auf implizites Word Splitting verlaesst, verhaelt sich deshalb unter zsh fundamental anders als unter Bash.

Die zuverlaessige Loesung ist, sich in beiden Shells niemals auf implizites Splitting zu verlassen, sondern von Anfang an mit Arrays statt mit durch Leerzeichen getrennten Strings zu arbeiten. Wo das aus Kompatibilitaetsgruenden mit POSIX sh nicht moeglich ist, sollte in zsh explizit ${=variable} verwendet werden, was Word Splitting fuer diesen einen Ausdruck erzwingt und damit dem Bash Standardverhalten entspricht, ohne das globale Shell Verhalten zu aendern.

5. Optionsnamen und setopt vs shopt

Auch beim Setzen von Shell Optionen unterscheiden sich Bash und zsh fundamental in der Syntax, nicht nur im Namen einzelner Optionen. Bash nutzt shopt -s optionname fuer die meisten erweiterten Optionen und set -o optionname fuer POSIX definierte Optionen. zsh nutzt ausschliesslich setopt optionname und unsetopt optionname, unabhaengig davon, ob die Option POSIX definiert oder zsh spezifisch ist. Ein Skript, das shopt Aufrufe enthaelt und ohne Anpassung in zsh gesourced wird, bricht mit command not found ab, weil shopt in zsh schlicht nicht existiert.

Fuer eine Dotfile Funktion, die in beiden Shells funktionieren soll, empfiehlt sich deshalb eine kleine Wrapper Funktion, die abhaengig von der erkannten Shell die passende Syntax waehlt, oder die konsequente Vermeidung von Shell Optionen zugunsten von portablem POSIX Code, der ganz ohne shopt oder setopt auskommt. Fuer Optionen, die tatsaechlich beide Shells brauchen, etwa das oben genannte nullglob Verhalten, ist die explizite if Verzweigung mit ZSH_VERSION die robusteste Loesung.


#!/usr/bin/env bash
# Setting shell options portably across bash and zsh

set_shell_option() {
  local option="$1"
  if [ -n "${ZSH_VERSION:-}" ]; then
    setopt "$option" 2>/dev/null || true
  else
    shopt -s "$option" 2>/dev/null || set -o "$option" 2>/dev/null || true
  fi
}

set_shell_option nullglob
set_shell_option extended_glob

6. emulate sh und POSIX-Modus in zsh

zsh bringt einen eigenen, sehr weitreichenden Kompatibilitaetsmechanismus mit: den Befehl emulate. emulate sh am Anfang eines Skripts oder einer Funktion versetzt zsh in einen Modus, der viele zsh spezifische Verhaltensweisen deaktiviert und stattdessen POSIX Semantik erzwingt, inklusive 0 basierter Array Indizierung und Bash aehnlichem Word Splitting. emulate bash geht noch einen Schritt weiter und aktiviert speziell auf Bash Kompatibilitaet ausgelegte Optionen.

Der Vorteil von emulate ist, dass ein einzelner Befehl viele Einzeloptionen gleichzeitig setzt, statt jede Option manuell abzugleichen. Der Nachteil: emulate aendert das Verhalten global fuer die aktuelle Funktion oder das aktuelle Skript und kann andere zsh spezifische Features, die man eigentlich behalten moechte, unbeabsichtigt mit deaktivieren. Fuer klar abgegrenzte Kompatibilitaetsfunktionen in einem Dotfile ist emulate -L sh innerhalb einer einzelnen Funktion, mit dem Flag -L fuer lokale Geltung, oft die praktikabelste Loesung.


# zsh-only syntax, but the function still works when sourced from bash too
# because emulate is silently ignored / undefined there in a guarded call

portable_split() {
  # Local emulation: only affects this function, not the whole shell session
  if [ -n "${ZSH_VERSION:-}" ]; then
    emulate -L sh
  fi
  local input="$1"
  for word in $input; do
    echo "word: $word"
  done
}

portable_split "one two three"

7. Die aktive Shell zur Laufzeit erkennen

Die Grundlage jeder Bash und zsh Kompatibilitaetsloesung ist eine zuverlaessige Erkennung, welche Shell das Skript gerade ausfuehrt. Bash setzt die Variable BASH_VERSION automatisch, zsh setzt entsprechend ZSH_VERSION. Eine einfache Pruefung mit [ -n "${ZSH_VERSION:-}" ] oder [ -n "${BASH_VERSION:-}" ] funktioniert zuverlaessig in beiden Shells, ohne externe Abhaengigkeiten und ohne die im Vergleich fehleranfaelligere Pruefung des Programmnamens ueber $0, die je nach Aufrufkontext variieren kann.

Diese Erkennung sollte am Anfang jeder Dotfile Datei stehen, die in beiden Shells gesourced wird, und als Grundlage fuer alle folgenden Verzweigungen dienen, etwa bei der Wahl zwischen shopt und setopt oder bei Array Indizierung. Ein sauberer Aufbau kapselt diese Erkennung in einer einzigen Wrapper Variable oder Funktion am Dateianfang, statt die Pruefung an jeder einzelnen Stelle im Skript zu wiederholen.

8. Dotfiles fuer beide Shells strukturieren

Fuer Entwickler, die dieselben Aliase, Funktionen und Umgebungsvariablen in Bash und zsh nutzen wollen, ist eine gemeinsame Datei mit reinem POSIX kompatiblem Code die wartbarste Loesung. Diese gemeinsame Datei, etwa ~/.shell_common, wird sowohl vom .bashrc als auch vom .zshrc am Ende per source eingebunden und enthaelt ausschliesslich Konstrukte, die in beiden Shells identisch funktionieren: POSIX Funktionsdefinitionen, Iterationen statt Array Indizierung, printf statt echo.

Shell spezifische Anpassungen, etwa das Prompt Format oder zsh spezifische Completion Funktionen, bleiben dagegen in der jeweiligen .bashrc oder .zshrc und werden nicht in die gemeinsame Datei gemischt. Diese klare Trennung zwischen gemeinsamem, POSIX kompatiblem Kern und Shell spezifischer Konfiguration ist der Schluessel dazu, Bash und zsh Kompatibilitaet dauerhaft zu pflegen, ohne bei jeder Aenderung zwei fast identische Dateien synchron halten zu muessen.

9. Bash vs zsh im direkten Vergleich

Die folgende Tabelle fasst die wichtigsten Unterschiede zwischen Bash und zsh zusammen, die bei gemeinsamen Skripten am haeufigsten zu Fehlern fuehren.

Verhalten Bash zsh (Standard) Portable Loesung
Array-Index 0-basiert 1-basiert Iteration statt Index
Glob ohne Treffer literaler String Fehler (NOMATCH) nullglob explizit setzen
Word Splitting implizit bei $var nicht implizit Arrays statt Strings
Optionen setzen shopt / set -o setopt / unsetopt Wrapper-Funktion
Shell erkennen $BASH_VERSION $ZSH_VERSION Beide Variablen pruefen

Der Vergleich zeigt: Bash und zsh sind sich in der Grundsyntax sehr aehnlich, unterscheiden sich aber in genau den Details, die in Dotfiles und kurzen Utility Skripten am haeufigsten vorkommen. Wer diese fuenf Unterschiede kennt und konsequent auf portable Muster setzt, kann ein und dieselbe Codebasis in beiden Shells verwenden, ohne Funktionen doppelt zu pflegen.

Mironsoft

Shell-Automatisierung, Dotfile-Pflege und plattformuebergreifende Skripte

Dotfiles und Skripte, die in Bash und zsh gleich funktionieren?

Wir bauen gemeinsame Shell Bibliotheken, loesen Array-Index und Globbing Probleme sauber und trennen shellspezifische Konfiguration vom portablen Kern.

Dotfile-Audit

Bestehende .bashrc und .zshrc auf doppelten und inkompatiblen Code pruefen

Refactoring

Gemeinsame, POSIX kompatible Shell Bibliothek fuer Team-Dotfiles aufbauen

CI-Skripte

Utility Skripte fuer macOS Entwickler und Linux CI Runner gleichermassen absichern

10. Zusammenfassung

Bash und zsh Kompatibilitaet scheitert in der Praxis fast immer an denselben fuenf Stellen: Array Indizierung, Globbing ohne Treffer, implizites Word Splitting, die Syntax fuer Shell Optionen und das Fehlen einer klaren Erkennung, welche Shell gerade aktiv ist. Alle fuenf Unterschiede lassen sich mit einfachen, portablen Mustern loesen, ohne Code doppelt zu pflegen: Iteration statt Index, explizites nullglob, Arrays statt Strings, eine kleine Wrapper Funktion fuer Optionen und eine Pruefung von BASH_VERSION oder ZSH_VERSION am Dateianfang.

Der zsh eigene emulate Befehl bietet zusaetzlich einen schnellen Weg, POSIX oder Bash aehnliches Verhalten fuer eine einzelne Funktion zu erzwingen, ohne das globale Shell Verhalten zu veraendern. Wer eine gemeinsame Datei mit reinem, portablem Code fuer Bash und zsh pflegt und shellspezifische Konfiguration klar davon trennt, spart auf Dauer erheblichen Wartungsaufwand gegenueber zwei parallel gepflegten, fast identischen Dotfile Sammlungen.

Bash und zsh: das Wichtigste auf einen Blick

Array-Indizes

Bash ist 0-basiert, zsh standardmaessig 1-basiert. Iteration statt direktem Index vermeidet den Unterschied komplett.

Globbing

zsh bricht bei Mustern ohne Treffer standardmaessig mit einem Fehler ab. nullglob explizit in beiden Shells setzen.

Optionen

shopt und set -o in Bash, setopt und unsetopt in zsh. Eine Wrapper Funktion kapselt den Unterschied.

Shell erkennen

$BASH_VERSION und $ZSH_VERSION pruefen, als Grundlage fuer alle folgenden Verzweigungen im Skript.

11. FAQ: Bash und zsh kompatibel skripten

1Warum zaehlt Bash und zsh Kompatibilitaet heute mehr?
Weil macOS seit Catalina standardmaessig zsh nutzt, waehrend Server und CI weiter Bash verwenden.
2Ab welchem Index beginnen zsh Arrays?
Standardmaessig bei 1, nicht bei 0 wie in Bash. Iteration statt Index vermeidet den Unterschied.
3Was passiert bei Glob ohne Treffer in zsh?
zsh bricht standardmaessig mit Fehler ab. nullglob normalisiert auf leere Ergebnisliste in beiden Shells.
4Warum verhaelt sich for w in $words anders?
Bash splittet implizit an Leerzeichen, zsh nicht. Arrays statt Strings umgehen den Unterschied.
5Was macht emulate sh?
Versetzt zsh in einen POSIX naeheren Modus, mit -L Flag nur lokal innerhalb einer Funktion.
6Wie erkenne ich Bash vs zsh zur Laufzeit?
Mit einer Pruefung auf BASH_VERSION und ZSH_VERSION am Dateianfang.
7Warum funktioniert shopt nicht in zsh?
shopt ist Bash spezifisch, zsh nutzt stattdessen setopt und unsetopt.
8Wie strukturiere ich Dotfiles am besten?
Gemeinsame Datei mit portablem Code, shellspezifische Konfiguration getrennt davon.
9Sind Bash und zsh grundsaetzlich sehr unterschiedlich?
Nein, die Grundsyntax ist sich sehr aehnlich, die Unterschiede konzentrieren sich auf wenige Details.
10POSIX sh statt Bash und zsh gemeinsam?
Fuer eigenstaendige Skripte oft sinnvoll, fuer interaktive Dotfiles ist gezielte Kompatibilitaet meist praktischer.