100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Bash

Local vs Global Variables

Understand how Bash variable scope actually works, why globals are the default, and how to use `local` deliberately to write safer scripts.

Functions & ScopeIntermediate8 min readJul 10, 2026
Analogies

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.

bash
#!/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 local keyword.
  • A local variable's lifetime ends when its function returns.
  • local var=$(cmd) masks cmd's exit status — declare and assign on separate lines to check it.
  • Use UPPER_CASE by convention for intentional script-wide/global variables.
  • Use readonly for global values that must not change after initialization.
  • Loop variables (for i in ...) are not automatically local — declare them local inside functions.
  • Treat unscoped variables inside functions as a code smell during review.

Practice what you learned

Was this page helpful?

Topics covered

#Bash#AdvancedBashScriptingStudyNotes#DevOps#LocalVsGlobalVariables#Local#Global#Variables#Default#StudyNotes#SkillVeris