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.
Inhaltsverzeichnis
- 1. Warum Bash-Funktionen standardmaessig einen globalen Namensraum teilen
- 2. Das local-Schluesselwort: Variablen auf eine Funktion begrenzen
- 3. Dynamic Scoping: warum verschachtelte Funktionen lokale Variablen des Aufrufers sehen
- 4. Sichtbarkeit ueber mehrere Funktionsebenen: der Call-Stack zaehlt, nicht die Textposition
- 5. Der typische Bug: fehlendes local ueberschreibt still eine fremde Variable
- 6. Das Gegenmittel: konsequent jede funktionsinterne Variable mit local deklarieren
- 7. Rueckgabewerte aus Funktionen: Globals, Nameref und Command Substitution
- 8. declare -g: eine Variable bewusst global machen
- 9. local, global, declare -g und local -n im Vergleich
- 10. Zusammenfassung
- 11. FAQ
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.