Variable Scope in Functions
Understanding variable scope prevents bugs and makes functions truly reusable. Learn the difference between local and global variables.
Global Variables (Default)
By default, all variables in bash are global:
#!/bin/bash
MY_VAR="initial"
change_var() {
MY_VAR="changed inside function"
}
echo "Before: $MY_VAR" # initial
change_var
echo "After: $MY_VAR" # changed inside function
This can cause unexpected side effects!
The local Keyword
Use local to create function-private variables:
#!/bin/bash
MY_VAR="global value"
my_function() {
local MY_VAR="local value"
echo "Inside: $MY_VAR" # local value
}
echo "Before: $MY_VAR" # global value
my_function
echo "After: $MY_VAR" # global value (unchanged!)
Local variables:
- Only exist inside the function
- Don't affect global variables with the same name
- Are destroyed when the function returns
Exercise: Use Local Variables
Prevent global variable modification:
Local Variables with Types
Combine local with other declarations:
#!/bin/bash
calculate() {
local -i sum=0 # Integer
local -r PI=3.14159 # Read-only
local -a items=() # Array
for num in "$@"; do
sum+=num
done
echo $sum
}
calculate 1 2 3 4 5
Scope Inheritance
Local variables are visible in called functions:
#!/bin/bash
outer() {
local VAR="from outer"
inner
}
inner() {
echo "Inner sees: $VAR" # from outer
}
outer
This is called "dynamic scoping" - variables are looked up at runtime.
Best Practice: Declare Everything Local
#!/bin/bash
# BAD: Pollutes global namespace
process_data() {
result=0
count=0
for item in "$@"; do
result=$((result + item))
count=$((count + 1))
done
echo $((result / count))
}
# GOOD: All variables are local
process_data() {
local result=0
local count=0
local item
for item in "$@"; do
result=$((result + item))
count=$((count + 1))
done
echo $((result / count))
}
Loop Variables
Even loop variables should be local:
#!/bin/bash
# BAD: i leaks out
count_files() {
for i in *; do
echo "$i"
done
}
count_files
echo "Loop var leaked: $i"
# GOOD: i is local
count_files() {
local i
for i in *; do
echo "$i"
done
}
When to Use Global Variables
Globals are appropriate for:
- Configuration: Set once, read everywhere
- Constants:
readonlyvalues - Intentional sharing: When you want functions to modify state
#!/bin/bash
# Configuration (set once)
readonly CONFIG_FILE="/etc/myapp.conf"
readonly LOG_LEVEL="INFO"
# Shared state (intentional)
TOTAL_PROCESSED=0
process_item() {
local item="$1"
echo "Processing: $item"
TOTAL_PROCESSED=$((TOTAL_PROCESSED + 1))
}
# Usage
process_item "file1"
process_item "file2"
echo "Total: $TOTAL_PROCESSED"
Exercise: Mixed Scope
Understand scope interaction:
Subshell Isolation
Subshells create complete isolation:
#!/bin/bash
VAR="parent"
# Subshell (in parentheses)
(
VAR="child"
echo "In subshell: $VAR" # child
)
echo "After subshell: $VAR" # parent (unchanged)
Pipeline commands run in subshells:
#!/bin/bash
COUNT=0
echo -e "a\nb\nc" | while read -r line; do
COUNT=$((COUNT + 1))
done
echo "Count: $COUNT" # 0! Loop ran in subshell
Avoiding Subshell Issues
Use process substitution instead of pipes:
#!/bin/bash
COUNT=0
while read -r line; do
COUNT=$((COUNT + 1))
done < <(echo -e "a\nb\nc")
echo "Count: $COUNT" # 3 (correct!)
Passing By Reference (Bash 4.3+)
Use local -n for nameref:
#!/bin/bash
double_value() {
local -n ref=$1 # Reference to passed variable
ref=$((ref * 2))
}
MY_NUM=5
double_value MY_NUM # Pass variable NAME, not value
echo "$MY_NUM" # 10
Common Scope Bugs
Bug 1: Forgotten local
# BUG: temp is global
process() {
temp=$(some_command) # Leaks!
echo "$temp"
}
# FIX: Make it local
process() {
local temp
temp=$(some_command)
echo "$temp"
}
Bug 2: Overwriting global
# BUG: Destroys caller's i
iterate() {
for i in 1 2 3; do echo $i; done
}
i=100
iterate
echo $i # 3 instead of 100!
# FIX: Local loop variable
iterate() {
local i
for i in 1 2 3; do echo $i; done
}
Bug 3: Pipeline subshell
# BUG: total stays 0
total=0
cat file.txt | while read line; do
total=$((total + 1))
done
echo $total # 0!
# FIX: Redirect instead of pipe
total=0
while read line; do
total=$((total + 1))
done < file.txt
echo $total # Correct count
Scope Summary Table
| Declaration | Visible In | Lifetime |
|---|---|---|
VAR=value | Everywhere | Script duration |
local VAR=value | Function + children | Until function returns |
export VAR=value | Child processes | Script duration |
readonly VAR=value | Everywhere (read-only) | Script duration |
(VAR=value) | Subshell only | Subshell duration |
Key Takeaways
- Variables are global by default in bash
- Use
localto create function-private variables - Always declare loop variables as local
- Local variables are visible in called functions (dynamic scope)
- Subshells (parentheses, pipes) create isolated scope
- Use process substitution
< <()to avoid pipe subshell issues - Globals are OK for configuration and intentional shared state
- Make
localyour default; use global intentionally

