Thuta Learning
BasicDevOps & Toolsbeginner

Functions and Variable Scope

What you'll walk away with

  • Explain the core ideas behind Functions and Variable Scope
  • Run the sample code and verify its output
  • Apply the technique correctly to the Tutorial Platform and production scenarios

Build the mental model

You define a bash function with `name() { ... }`, and once defined, you call it just like any other command — by name, with no special syntax for 'calling' it. Arguments passed to a function are available inside it through the exact same mechanism as script arguments: `$1`, `$2`, `$@`, `$#`, all scoped to that particular function call, which means everything already learned about positional parameters and quoting `"$@"` applies directly inside functions too, without any new syntax to learn. By default, every variable in bash is global regardless of where it's assigned, even deep inside a function — so a variable assigned inside a function silently overwrites any variable with the same name that exists outside it, unless you explicitly declare it with `local`, which scopes that variable to the current function call and restores whatever value (or absence of a value) existed before the function ran. Bash functions don't 'return' a computed value the way functions do in most other languages — `return` only sets a numeric exit status between 0 and 255, meant purely for signaling success or failure to the caller (typically checked with `if function_name; then`), not for handing back data; any attempt to `return` a string, or a number outside that range, either fails or gets silently truncated. To actually get a computed value out of a function, the idiomatic pattern is to `echo` the result and capture it with command substitution, `result=$(my_function)`, or, when that's awkward, to deliberately assign to a global variable instead.

Connect it to a real scenario

A script with several repeated steps — validating input, formatting a message, logging an event — benefits from wrapping each into a small function instead of copy-pasting the same lines everywhere, the way the lesson's example wraps greeting logic into `greet()` and summing logic into `sum_args()`. Look closely at `sum_args`: it declares `local total=0` before accumulating into it, so calling it repeatedly never leaks a stale total from a previous call, and it returns its actual answer by `echo`-ing `$total` and letting the caller capture it with `result=$(sum_args 2 4 6)` — not through `return`, which is reserved for the 0/1 exit status `is_even` uses instead. The example's final section shows exactly what goes wrong without `local`: `no_local_demo` assigns `counter=1` without declaring it local, silently overwriting the outer `counter=100`, so it stays overwritten at `1` even after the function returns; `with_local_demo`, by contrast, declares `local counter=999`, and once it returns, the outer `counter` is completely untouched by whatever happened inside.

Try the working example

bash
#!/usr/bin/env bash
greet() {
  local name="$1"
  echo "Hello, $name!"
}

greet "Maung Maung"

sum_args() {
  local total=0
  for n in "$@"; do
    total=$((total + n))
  done
  echo "$total"
}

result=$(sum_args 2 4 6)
echo "Sum: $result"

is_even() {
  local n="$1"
  if [ $((n % 2)) -eq 0 ]; then
    return 0
  else
    return 1
  fi
}

if is_even 8; then
  echo "8 is even (exit status was 0)"
fi

counter=100

no_local_demo() {
  counter=1   # missing "local" - overwrites the outer/global counter!
  echo "inside no_local_demo, counter = $counter"
}

with_local_demo() {
  local counter=999   # local - only affects this function
  echo "inside with_local_demo, counter = $counter"
}

no_local_demo
echo "after no_local_demo, global counter = $counter"

with_local_demo
echo "after with_local_demo, global counter = $counter"
You should see
Running this script prints:

Hello, Maung Maung!
Sum: 12
8 is even (exit status was 0)
inside no_local_demo, counter = 1
after no_local_demo, global counter = 1
inside with_local_demo, counter = 999
after with_local_demo, global counter = 1

5-minute try-it

Write a function called `to_upper` that takes one string argument and echoes its uppercase version (you can pipe the argument through `tr 'a-z' 'A-Z'` inside the function), capture its result into a variable with command substitution, and print the result alongside a global variable to confirm the function didn't accidentally change it.

One important caution

Omitting local inside a function, so a variable name that coincidentally matches one used elsewhere in the script gets silently overwritten (accidental global leakage)

Trying to 'return' a computed string or number with return, not realizing return only accepts exit-status integers from 0-255 and truncates or errors on anything else

GNU Bash Reference Manual — Shell FunctionsBash / Shell Scripting

Easy traps

  • Omitting local inside a function, so a variable name that coincidentally matches one used elsewhere in the script gets silently overwritten (accidental global leakage)
  • Trying to 'return' a computed string or number with return, not realizing return only accepts exit-status integers from 0-255 and truncates or errors on anything else
  • Validate sample code in a local or test environment before applying it to a production system.

Exercise

Write a function called `to_upper` that takes one string argument and echoes its uppercase version (you can pipe the argument through `tr 'a-z' 'A-Z'` inside the function), capture its result into a variable with command substitution, and print the result alongside a global variable to confirm the function didn't accidentally change it.

You'll know it worked when: Running this script prints: Hello, Maung Maung! Sum: 12 8 is even (exit status was 0) inside no_local_demo, counter = 1 after no_local_demo, global counter = 1 inside with_local_demo, counter = 999 after with_local_demo, global counter = 1

Functions and Variable Scope | Thuta Learning