Lokale vs. globale Variablen in verschachtelten Bash-Funktionen
AI generated
$_
#!/
Bash · Funktionen · Scoping · Linux
Lokale vs. globale Variablen
in verschachtelten Bash-Funktionen richtig verstehen

Bash-Funktionen teilen sich standardmaessig einen einzigen globalen Namensraum, jede Variable ist global, solange sie nicht explizit mit local deklariert wird. Wer das nicht weiss, ueberschreibt in einer Hilfsfunktion versehentlich die Variable einer ganz anderen Funktion, oft erst sichtbar, wenn zwei Funktionen zufaellig denselben Variablennamen wie i oder result verwenden.

16 Min. Lesezeit local · declare -g · dynamic scoping Bash 4.x · 5.x

1. Warum Bash-Funktionen standardmaessig einen globalen Namensraum teilen

Anders als in den meisten modernen Programmiersprachen ist in Bash jede Variable, die innerhalb einer Funktion ohne weiteres Schluesselwort zugewiesen wird, automatisch global sichtbar, also fuer jede andere Funktion und den restlichen Skriptkoerper lesbar und ueberschreibbar. Eine Funktion foo, die result=5 setzt, veraendert damit dieselbe Variable result, die auch eine ganz andere, spaeter aufgerufene Funktion bar sieht und beliebig weiterverwendet.

Dieses Verhalten ist eine bewusste Design-Entscheidung aus der Bourne-Shell-Tradition, in der Funktionen urspruenglich eher als benannte Befehlssequenzen denn als isolierte Programmeinheiten mit eigenem Speicherbereich gedacht waren. Fuer kurze, einmalig genutzte Skripte faellt das selten auf, in laenger lebenden Bibliotheksfunktionen mit mehreren Aufrufern wird es aber schnell zur Quelle schwer nachvollziehbarer Bugs.

2. Das local-Schluesselwort: Variablen auf eine Funktion begrenzen

Mit local name=wert am Anfang einer Funktion wird eine Variable erzeugt, die ausschliesslich innerhalb dieser Funktion und, wie im naechsten Abschnitt gezeigt, innerhalb der von ihr aufgerufenen Funktionen sichtbar ist. Verlaesst die Funktion ihren Geltungsbereich durch return oder das natuerliche Ende, wird die lokale Variable automatisch entfernt, und eine gleichnamige globale Variable, falls vorhanden, ist danach wieder unveraendert sichtbar.

Wichtig ist, local als eigenes Kommando und nicht als reine Deklaration zu verstehen: local x=$(befehl) in einer Zeile maskiert den Exit-Code von befehl, weil der Exit-Code von local selbst zurueckgegeben wird, nicht der der Substitution. ShellCheck markiert das als Regel SC2155 und empfiehlt, Deklaration und Zuweisung bei Bedarf auf zwei Zeilen zu trennen, wenn der Exit-Code der Zuweisung geprueft werden soll.


process_file() {
  local filename="$1"
  local line_count
  line_count=$(wc -l < "$filename") || return 1
  echo "Zeilen in $filename: $line_count"
}

3. Dynamic Scoping: warum verschachtelte Funktionen lokale Variablen des Aufrufers sehen

Bash verwendet fuer local-Variablen sogenanntes dynamic scoping, kein lexikalisches Scoping wie JavaScript oder Python. Der Unterschied ist fundamental: Sichtbarkeit haengt nicht davon ab, wo eine Funktion im Quelltext steht, sondern ausschliesslich davon, ueber welchen Aufrufpfad sie zur Laufzeit erreicht wurde. Ruft Funktion A Funktion B auf, sieht B alle lokalen Variablen von A, obwohl B moeglicherweise an einer ganz anderen Stelle im Skript definiert ist.

Das unterscheidet sich fundamental von einer Closure in JavaScript, wo eine verschachtelte Funktion nur die Variablen des Kontexts sieht, in dem sie definiert wurde, unabhaengig davon, von wo sie spaeter aufgerufen wird. In Bash gilt das genaue Gegenteil: massgeblich ist der Call-Stack zur Laufzeit, nicht die textuelle Position der Funktionsdefinition im Skript.


outer() {
  local wert="aus outer"
  inner
}
inner() {
  echo "inner sieht: $wert"
}
outer
# inner sieht: aus outer   <- inner sieht die local-Variable von outer,
#                              obwohl inner ganz woanders definiert ist

4. Sichtbarkeit ueber mehrere Funktionsebenen: der Call-Stack zaehlt, nicht die Textposition

Diese Sichtbarkeit setzt sich ueber beliebig viele Aufrufebenen fort: ruft outer die Funktion middle auf, und middle wiederum inner, dann sieht inner sowohl die lokalen Variablen von middle als auch die von outer, sofern middle selbst keine gleichnamige eigene local-Variable deklariert hat, die die aeussere verdeckt.

Deklariert middle dagegen eine eigene lokale Variable mit demselben Namen wie outer, greift innerhalb von middle und allem, was middle aufruft, ab diesem Punkt die naeher liegende Deklaration von middle, waehrend die urspruengliche Variable von outer voruebergehend verdeckt, aber nicht geloescht wird und nach Rueckkehr aus middle wieder sichtbar ist. Dieses Verhalten entspricht einem klassischen Namens-Shadowing, nur eben zur Laufzeit ueber den Aufrufstapel statt ueber lexikalische Bloecke.

5. Der typische Bug: fehlendes local ueberschreibt still eine fremde Variable

Der haeufigste praktische Bug entsteht, wenn eine Hilfsfunktion in einer Bibliothek eine Variable ohne local setzt und dieser Name zufaellig mit dem einer Variable in der aufrufenden Funktion kollidiert, besonders bei kurzen, generischen Namen wie i, tmp, result oder line. Die aufrufende Funktion sieht danach einen unerwarteten Wert in ihrer eigenen Variable, obwohl sie diese nie selbst veraendert hat, was die Fehlersuche erschwert, weil die Ursache in einer voellig anderen Funktion liegt.

Besonders tueckisch ist dieser Bug in Schleifen: ruft eine for i in ...-Schleife eine Funktion auf, die intern selbst ohne local eine Variable i als Zaehler verwendet, ueberschreibt die Funktion die Schleifenvariable der aufrufenden Ebene, was zu einer Endlosschleife oder zu uebersprungenen Iterationen fuehren kann, je nachdem, welchen Wert die Funktion i beim Verlassen hinterlaesst.


# Fehlendes local: reset_counter ueberschreibt das i der aufrufenden Schleife
reset_counter() {
  i=0   # kein local -- ueberschreibt jedes gleichnamige i im Aufrufstapel
}

for i in 1 2 3 4 5; do
  echo "Durchlauf: $i"
  reset_counter
done
# Durchlauf: 1
# Durchlauf: 1   <- i wurde durch reset_counter auf 0 gesetzt, +1 durch for ergibt wieder Wiederholungen

6. Das Gegenmittel: konsequent jede funktionsinterne Variable mit local deklarieren

Die zuverlaessige Absicherung ist, in jeder Funktion ausnahmslos jede intern benoetigte Variable beim ersten Gebrauch mit local zu deklarieren, auch wenn sie nur ein kurzlebiger Zwischenwert ist. Zusaetzliche Modifikatoren wie local -r fuer schreibgeschuetzte Werte oder local -i fuer garantiert numerische Variablen machen die Absicht noch expliziter und lassen versehentliche Zuweisungen frueher auffallen.

Statisches Pruefen mit ShellCheck deckt fehlende local-Deklarationen nicht in jedem Fall zuverlaessig auf, weil eine ohne local gesetzte Variable syntaktisch vollkommen gueltig ist. Ein bewaehrter Ersatz ist eine feste Team-Konvention, in Bibliotheksfunktionen alle internen Namen mit einem funktionsspezifischen Praefix zu versehen, etwa _pf_tmp statt tmp in einer Funktion process_file, um Kollisionen unwahrscheinlicher zu machen.


reset_counter() {
  local i=0   # jetzt lokal -- beeinflusst kein i im Aufrufstapel
  echo "interner Zaehler zurueckgesetzt: $i"
}

for i in 1 2 3; do
  echo "Durchlauf: $i"
  reset_counter
done
# Durchlauf: 1
# interner Zaehler zurueckgesetzt: 0
# Durchlauf: 2
# interner Zaehler zurueckgesetzt: 0
# Durchlauf: 3

7. Rueckgabewerte aus Funktionen: Globals, Nameref und Command Substitution

Weil Bash-Funktionen nur einen numerischen Exit-Code, keinen beliebigen Rueckgabewert liefern koennen, greifen Skripte oft zu einer von zwei Techniken: entweder das Ergebnis ueber echo ausgeben und beim Aufruf per $(funktion) als Command Substitution einfangen, oder das Ergebnis gezielt in eine vom Aufrufer uebergebene Variable schreiben. Fuer Letzteres bietet local -n einen Nameref, eine lokale Variable, die als Referenz auf eine vom Aufrufer benannte Variable fungiert.

Ein Nameref erlaubt es einer Funktion, gezielt in die Variable eines Aufrufers zu schreiben, ohne dafuer eine ungeschuetzte globale Variable zu benoetigen, und macht die Absicht im Funktionskopf explizit sichtbar, welche Variable veraendert werden soll. Command Substitution mit $(...) ist dagegen einfacher zu lesen, aber wie im Artikel zu Command Substitution beschrieben mit Prozess-Overhead und einer eigenen Subshell verbunden, was bei sehr grossen Rueckgabewerten oder in engen Schleifen ins Gewicht fallen kann.


get_config_value() {
  local -n out_ref="$1"   # Nameref auf die vom Aufrufer uebergebene Variable
  out_ref="produktion"
}

get_config_value zielwert
echo "Konfiguration: $zielwert"
# Konfiguration: produktion

8. declare -g: eine Variable bewusst global machen

Manchmal ist eine tatsaechlich globale Variable gewuenscht, etwa ein Cache oder ein Aufrufzaehler, der ueber mehrere Funktionsaufrufe hinweg bestehen bleiben soll. Wird eine solche Variable innerhalb einer Funktion mit declare -g name=wert statt mit local gesetzt, macht das die Absicht im Code explizit sichtbar, statt sich auf das ohnehin globale Standardverhalten von Bash zu verlassen, das im Nachhinein nicht mehr von einem versehentlich fehlenden local zu unterscheiden ist.

Der Unterschied zwischen einem bewussten declare -g und einer versehentlich globalen Variable liegt also nicht im Laufzeitverhalten, beide erzeugen technisch dieselbe globale Sichtbarkeit, sondern ausschliesslich in der Lesbarkeit fuer Menschen: declare -g markiert im Quelltext eindeutig eine bewusste Design-Entscheidung, waehrend ein blankes name=wert ohne jedes Schluesselwort fuer den naechsten Leser nicht erkennen laesst, ob Globalitaet Absicht oder Versehen war.

9. local, global, declare -g und local -n im Vergleich

Welches Werkzeug in einer verschachtelten Funktion die richtige Wahl ist, haengt davon ab, ob ein Wert nur innerhalb eines Aufrufs gebraucht wird, an Unterfunktionen weitergereicht werden soll oder tatsaechlich ueber mehrere Aufrufe hinweg bestehen bleiben muss. Die folgende Tabelle fasst die vier Muster zusammen.

Muster Sichtbarkeit Ueberlebt Funktionsende Typischer Einsatz
local name=wert Aufrufende Funktion und deren Unterfunktionen Nein Zwischenwerte, Standardfall in jeder Funktion
Blankes name=wert Global, ganzes Skript Ja Meist ein Bug, wenn unbeabsichtigt
declare -g name=wert Global, ganzes Skript Ja Bewusster Cache oder Zaehler ueber Aufrufe hinweg
local -n ref="$1" Referenz auf Variable des Aufrufers Nein, nur die Referenz selbst Rueckgabewerte ohne globale Variable

Mironsoft

Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur

Shell-Skripte, die in der Produktion zuverlässig laufen?

Wir analysieren bestehende Bash-Skripte, erkennen fragile Muster und ersetzen sie durch robuste Bash-Patterns: mit vollständiger Fehlerbehandlung, Logging und sicherer Parallelisierung für euren Deployment-Stack.

Code-Review

ShellCheck-Analyse und manuelle Prüfung auf kritische Bash-Pattern-Verstöße.

Refactoring

Fehlerbehandlung, Logging und sichere Dateioperationen nachrüsten.

CI-Integration

ShellCheck und BATS in Pipelines integrieren und Regressionstests aufbauen.

10. Zusammenfassung

Lokale und globale Variablen: Das Wichtigste auf einen Blick

Grundregel

Ohne local ist jede Bash-Variable global. local begrenzt Sichtbarkeit auf die Funktion und deren Aufrufkette.

Dynamic Scoping

local-Variablen sind ueber den Call-Stack sichtbar, nicht ueber die Textposition im Skript, anders als bei Closures in JavaScript oder Python.

Typischer Bug

Fehlendes local in einer Hilfsfunktion ueberschreibt still eine gleichnamige Variable der aufrufenden Funktion, oft sichtbar erst bei generischen Namen wie i oder tmp.

Bewusste Globals

declare -g macht eine gewollt globale Variable im Code explizit sichtbar und unterscheidet sie von einem versehentlich fehlenden local.

11. FAQ: Lokale und globale Variablen: Das Wichtigste auf einen Blick

1Sind Bash-Variablen standardmaessig lokal oder global?
Global. Jede Variable, die ohne local, declare oder typeset gesetzt wird, ist fuer das gesamte Skript und alle Funktionen sichtbar und ueberschreibbar.
2Was macht local genau?
local begrenzt die Sichtbarkeit einer Variable auf die aktuelle Funktion und alle von ihr aufgerufenen Funktionen. Nach Verlassen der Funktion wird die lokale Variable automatisch entfernt.
3Kann eine verschachtelte Funktion lokale Variablen der aufrufenden Funktion sehen?
Ja, Bash verwendet dynamic scoping. Eine aufgerufene Funktion sieht alle local-Variablen aller Funktionen im aktuellen Aufrufstapel, unabhaengig von der Textposition im Skript.
4Was ist der Unterschied zu Closures in JavaScript?
Eine JavaScript-Closure sieht die Variablen des Kontexts, in dem sie definiert wurde. Eine Bash-Funktion sieht die local-Variablen des Kontexts, aus dem sie zur Laufzeit aufgerufen wurde, das genaue Gegenteil.
5Warum ist fehlendes local in Bibliotheksfunktionen gefaehrlich?
Weil eine ohne local gesetzte Variable jede gleichnamige Variable im gesamten Aufrufstapel ueberschreibt, was besonders bei kurzen, generischen Namen wie i oder result zu schwer auffindbaren Bugs fuehrt.
6Was bedeutet SC2155 bei ShellCheck?
Die Regel warnt davor, local und eine Zuweisung mit Command Substitution in einer Zeile zu kombinieren, weil dabei der Exit-Code der Substitution durch den Exit-Code von local maskiert wird.
7Wie kann eine Funktion einen Wert an den Aufrufer zurueckgeben?
Entweder per echo und Einfangen mit $(funktion) im Aufrufer, oder mit local -n als Nameref, der direkt in eine vom Aufrufer benannte Variable schreibt.
8Was ist ein Nameref mit local -n?
Eine lokale Variable, die als Referenz auf eine vom Aufrufer per Namen uebergebene Variable fungiert. Schreibzugriffe auf den Nameref landen direkt in der Variable des Aufrufers.
9Wann sollte ich declare -g statt local verwenden?
Wenn eine Variable tatsaechlich ueber mehrere Funktionsaufrufe hinweg bestehen bleiben soll, etwa ein Cache oder Zaehler. declare -g macht diese Absicht im Code explizit sichtbar.
10Erkennt ShellCheck fehlendes local zuverlaessig?
Nicht in jedem Fall, weil eine ohne local gesetzte Variable syntaktisch vollkommen gueltig ist. Eine Namenskonvention mit funktionsspezifischen Praefixen ist eine zusaetzliche, verlaessliche Absicherung.