skip to content

In bash, a variable assigned inside a function is still set after the function returns and can overwrite a caller's variable of the same name. Why does bash behave this way, and what makes a variable exist only for the duration of the function?

level: juniorimportance: must knowfreq 78%

answer

  1. no implicit function scope
  2. one shell-wide variable table
  3. the keyword that saves and restores
  4. declare inside a function behaves the same
  5. global still is not exported

basics

~20 s

Bash variables are global by default, so an assignment inside a function writes into the one shell-wide namespace and survives the return. Declaring the name with local (or declare) inside the function creates a temporary copy that disappears when the function returns.

solid answer

~50 s

Bash has a single shell-wide variable namespace, and a function body is just code running in that same shell — there is no implicit function scope. So `count=5` inside a function is the same assignment it would be at the top of the script: it creates or overwrites the shell's `count` and leaves it set after the function returns. To get a variable that only lives for the call, declare it with `local` (or, equivalently inside a function, `declare`/`typeset`): bash saves any previous value, gives the function its own, and restores the old one on return. Because of that, the usual discipline is that every variable a function uses for its own bookkeeping — including loop counters like `i` — gets declared `local` on the line where the function first needs it. Note that global here means shell-wide, not exported: without `export`, external commands still do not see the variable in their environment.

code

bash · 11 lines
bash
#!/usr/bin/env bash
count=1

bump_global() { count=99; }
bump_local()  { local count=42; echo "inside bump_local: $count"; }

bump_global
echo "after bump_global: $count"   # 99 - the function wrote the caller's variable

bump_local
echo "after bump_local:  $count"   # 99 - unchanged; the local vanished on return

go deeper

for a junior

Be ready to say plainly that bash variables are global unless declared with local inside a function, and to show the one-line fix for a helper that clobbers a caller's variable.

for a middle

Explain the save-and-restore mechanics local performs per invocation, that declare inside a function does the same thing, and that declare -g is the deliberate escape hatch in bash 4.2 and later.

for a senior

Show the discipline you enforce in shared shell libraries: locals for all function bookkeeping including loop counters, a small readonly set of deliberate globals, and how you spot the clobber during review rather than in production.

for a principal

Own the tradeoff of a language with one namespace and no compiler to catch collisions: what conventions and review gates you mandate for shell in production, and at what size or complexity the risk justifies moving the logic out of bash.

## One namespace by default Most languages give a function its own scope: names bound inside it are invisible outside. Bash does not. A shell function is not a separate environment — it is a named block of commands executed by the same shell process, against the same variable table the rest of the script uses. An assignment is an assignment wherever it appears. ```bash count=1 bump() { count=99; } bump echo "$count" # 99 — the function wrote the script's variable ``` That is the whole rule, and it explains both directions of surprise: a function can silently clobber a caller's variable, and a value a function set is still readable afterwards (which is one way scripts return data, though a dedicated mechanism is usually better). ## What local actually does `local NAME` may only be used inside a function; at the top level bash reports `local: can only be used in a function` and returns non-zero (ShellCheck flags the same mistake as SC2168). Inside a function it does three things: 1. saves the current value and attributes of `NAME`, if any; 2. gives this invocation of the function a fresh instance of `NAME` (unset until assigned, unless you assign on the same line); 3. restores the saved state when the function returns. ```bash count=1 bump() { local count=42; echo "inside: $count"; } bump # inside: 42 echo "$count" # 1 — the caller's value came back ``` Because the save/restore is per invocation, each recursive call of a function also gets its own copy of its locals. `local` accepts several attribute options, the common ones being `-r` for a read-only local, `-a` for an indexed array and `-i` for an integer. You can declare several names in one statement: `local a b c=3`. ## declare and typeset inside a function `declare` (and its older synonym `typeset`) behaves like `local` when it is used inside a function: `declare tmp=1` in a function body creates a function-local `tmp`. That equivalence trips people who use `declare -a items` inside a function expecting a global array. If you genuinely want a function to create or modify a global, bash 4.2 and later provide `declare -g NAME=value`, which forces the assignment into the global scope even from inside a function. Doing that deliberately and with an obvious naming convention is fine; doing it by accident is the bug this question is about. ## Global is not the same as exported "Global" in bash means visible to everything running in this shell. It says nothing about child processes. Environment variables are a separate, narrower thing: only names marked with `export` (or assigned as a command prefix, `FOO=bar cmd`) are copied into the environment of an external command the script runs. ```bash MSG=hello # shell variable, global to this shell export LOG_LEVEL=debug # also placed in the environment of children printenv MSG # prints nothing — printenv is a separate process ``` So a function assigning a global has not "leaked it to the system"; it has leaked it to the rest of the script, which is bad enough. ## Why it bites in real scripts The classic failure is a shared library of helper functions where two authors both used `i`, `tmp`, `file` or `result`. A helper called from inside a caller's loop resets `i`, and the loop either runs forever or exits early — with no error message, because nothing illegal happened. The second classic is a helper that sets `result` and a caller that forgot to reset it, so a call that failed early leaves the previous call's value in place and the script proceeds on stale data. The habit that prevents both: **declare every variable a function uses for itself as `local`, on the first line that needs it, loop counters included.** Reserve unqualified globals for a small, deliberately named set of script configuration values, ideally marked `readonly` so a stray assignment fails loudly instead of quietly. ```bash process_files() { local dir=$1 count=0 f for f in "$dir"/*; do count=$((count + 1)) done printf '%d\n' "$count" } ``` One caveat worth knowing: `local` is not in POSIX. Every shell you are likely to meet — bash, dash, ksh, BusyBox ash — supports it, but a strictly POSIX script cannot rely on it being specified, which is one more reason a script that needs real function scope is more honestly written with a `#!/usr/bin/env bash` shebang.

  • What does `declare tmp=1` do inside a function compared with the same line at the top level of a script?
    Inside a function, `declare` behaves like `local`: `tmp` is function-scoped and is restored on return. At the top level it just creates a global with whatever attributes you asked for. If you need a function to write a global explicitly, bash 4.2 and later offer `declare -g tmp=1`, which bypasses the function scope.
  • What happens if you use `local` outside a function?
    Bash refuses it: the builtin prints `local: can only be used in a function` and returns a non-zero status. Under `set -e` that aborts the script. ShellCheck reports the same mistake statically as SC2168, which is why it shows up in linting long before it shows up at runtime.
  • A function sets a variable without exporting it. Which things can see it?
    Everything running in the same shell, including functions it calls and the rest of the script after it returns. Subshells forked from the shell inherit a copy. External commands do not — they are separate processes and only receive names marked with `export`, or an assignment used as a command prefix.

saying these in an interview costs you the question

  • Says bash functions scope their variables automatically, like Python
  • Thinks local is only a style or readability convention
  • Claims local can be used at the top level of a script
  • Believes a function's assignments are discarded when it returns
  • Confuses global with exported and expects child commands to see it

context