Bash's Default Scope Is Global
Unlike most modern languages, Bash does not scope variables to the block or function they're declared in unless you explicitly say so with local. A variable assigned anywhere — inside an if, a for loop, or a function — is visible to the entire script and every function called afterward, unless that specific declaration used local. This 'global by default' behavior is a frequent source of bugs when scripts grow beyond a few dozen lines and multiple functions start reusing common variable names like i, tmp, or result.
Cricket analogy: A stadium PA announcement is heard by everyone in the ground, not just one stand — that's how an unscoped Bash variable behaves: any function 'in the ground' can hear and overwrite it.
Declaring Local Variables Correctly
local varname=value inside a function creates a variable that exists only for the duration of that function call and any nested calls made from it, and it is automatically removed when the function returns. A subtle pitfall: local varname=$(some_command) discards the exit status of some_command, because the exit status of the whole line becomes that of the local builtin itself. Under set -e, this means a failing command substitution inside a local declaration will not stop the script — split the declaration and assignment onto two lines to avoid this.
Cricket analogy: A player's individual net run rate in a bilateral series only matters for that series and resets afterward — like a local variable that disappears once the function (series) ends.
#!/usr/bin/env bash
set -euo pipefail
counter=0 # global
increment_wrong() {
counter=$((counter + 1)) # mutates the GLOBAL counter
}
increment_safe() {
local counter # shadow: local now, separate from global
counter=$((counter + 1))
echo "inside function, local counter=$counter"
}
increment_wrong
echo "after increment_wrong: counter=$counter" # 1
increment_safe
echo "after increment_safe: counter=$counter" # still 1, global untouched
# Pitfall: splitting declaration and assignment when checking exit status
check_disk() {
local usage
usage=$(df --output=pcent / | tail -n1 | tr -dc '0-9') || return 1
echo "$usage"
}
local result=$(risky_command) always reports success (exit status 0) from the local builtin itself, even if risky_command fails, because the exit status of a local declaration is that of the local command, not the assignment. Always declare (local result) and assign (result=$(risky_command)) on separate lines when you need to check the command's real exit status, especially under set -e.
Deliberately Sharing State Between Functions
Sometimes global state is the right tool — for example, a script-wide VERBOSE flag or a LOG_FILE path that many functions need to read. The key is making that choice deliberate and visible: declare such variables at the top of the script in UPPER_CASE, treat lowercase names as function-local by convention, and use readonly for values that should never change after initialization. This convention lets a reader scan a function body and immediately know, from casing alone, whether a variable is local or intentionally shared.
Cricket analogy: The toss result in a cricket match is genuinely shared state — both captains and every player need to know it, so it's announced to everyone rather than kept private, like a deliberate global.
A common convention: UPPER_CASE names for intentional script-wide configuration (LOG_FILE, VERBOSE, MAX_RETRIES), and lower_case names declared with local for everything else. Combined with readonly LOG_FILE=... for values that must never change, this makes scope intent visible without needing to trace every assignment.
- Bash variables are global by default; scoping to a function requires the explicit
localkeyword. - A
localvariable's lifetime ends when its function returns. local var=$(cmd)maskscmd's exit status — declare and assign on separate lines to check it.- Use
UPPER_CASEby convention for intentional script-wide/global variables. - Use
readonlyfor global values that must not change after initialization. - Loop variables (
for i in ...) are not automatically local — declare themlocalinside functions. - Treat unscoped variables inside functions as a code smell during review.
Practice what you learned
1. By default, how is a variable's scope determined in Bash?
2. What is the problem with `local result=$(risky_cmd)` under `set -e`?
3. What naming convention is commonly used to signal an intentional, script-wide global variable?
4. When does a `local` variable's value disappear?
5. Which keyword prevents a global configuration variable from being accidentally reassigned later in the script?
Was this page helpful?
You May Also Like
Advanced Bash Functions
Go beyond basic function syntax to master argument handling, scoping, namerefs, and composition patterns used in production Bash scripts.
Function Return Values and Exit Codes
Learn how Bash functions actually communicate results — via numeric exit codes, stdout capture, and namerefs — and how to check them reliably.
Sourcing Scripts and Libraries
Learn how `source`/`.` works, how it differs from executing a script, and how to structure reusable Bash libraries that are safe to source.
Related Reading
Related Study Notes in DevOps
Browse all study notesNginx Study Notes
DevOps · 30 topics
DevOpsAnsible Study Notes
DevOps · 30 topics
DevOpsAdvanced Kubernetes Study Notes
Kubernetes · 30 topics
DevOpsApache Kafka Study Notes
Kafka · 30 topics
DevOpsDocker & Kubernetes Study Notes
YAML · 40 topics
DevOpsCI/CD Tools & Pipelines Study Notes
YAML · 37 topics