skip to content

In bash, a function needs to fill an array that its caller declared, but arrays cannot be exported or passed by value. How does `declare -n` (or `local -n`) let the caller pass the array by name, and what naming pitfall comes with it?

level: middleimportance: should knowfreq 30%

answer

  1. arrays cannot travel as arguments
  2. pass the name, bind to it
  3. alias applies to the variable, not the value
  4. dynamic scope creates the collision
  5. underscore-prefix every nameref parameter

basics

~20 s

A nameref variable created with local -n out=$1 becomes an alias for whatever variable $1 names, so assignments to out write straight into the caller's array. It needs bash 4.3+, and the alias breaks if the caller's variable has the same name as the nameref.

solid answer

~50 s

Bash passes arguments as strings, and arrays cannot travel through the environment, so the caller passes the array's *name* and the function binds to it. Inside the function, `local -n out=$1` marks `out` as a nameref: every read and write of `out`, including `out+=(x)` and `out[k]=v`, is redirected to the variable whose name is in `$1`. That gives you output parameters without globals and without capturing stdout. Namerefs are bash 4.3 or newer. The pitfall is name collision: if the caller happens to use the same variable name as the nameref parameter, bash sees a circular name reference, warns, and the aliasing does not work as intended — bash's dynamic scoping means the local name shadows the very variable you are trying to reach. The convention is to give nameref parameters deliberately unlikely names such as `_out` or `__result`.

code

bash · 19 lines
bash
#!/usr/bin/env bash
add_all() {
  local -n _out=$1     # alias for the caller's array
  shift
  local item
  for item in "$@"; do _out+=("$item"); done
}

files=()
add_all files a.txt b.txt
declare -p files       # declare -a files=([0]="a.txt" [1]="b.txt")

put() {                # works for maps too
  local -n _m=$1
  _m[$2]=$3
}
declare -A cfg
put cfg region eu-west-1
declare -p cfg

go deeper

for a junior

Know that bash functions receive strings, so an array is handed over by name and bound inside the function with local -n alias=$1, which then behaves like the caller's array.

for a middle

Explain that the nameref attribute redirects every read, write, append and subscript to the target variable, that it needs bash 4.3+, and why the parameter is conventionally underscore-prefixed.

for a senior

Demonstrate the collision mechanism — dynamic scoping means a matching local shadows the target and bash reports a circular reference — and know that unset ref destroys the caller's variable while unset -n ref drops only the alias.

for a principal

Weigh the readability cost: namerefs make output parameters invisible at the call site and pin the script to bash 4.3+. Set a team rule for when a helper should take one instead of printing, and treat several output parameters as a sign the work belongs in another language.

## The problem namerefs solve Shell functions do not have parameters in the usual sense: they receive positional strings, and arrays are not strings. You cannot export an array, and `f "${arr[@]}"` flattens it into separate words that the function has to reassemble — and which loses the distinction between an empty array and an array holding one empty string. Returning an array is worse still, since `return` carries only an exit status. Before bash 4.3 the usual answers were a global variable by convention, printing the values and having the caller re-split them, or `eval` string-building. A nameref replaces all three with a language feature. ## How a nameref works ```bash add_all() { local -n _out=$1 # _out is now an alias for the caller's variable shift local item for item in "$@"; do _out+=("$item"); done } files=() add_all files a.txt b.txt declare -p files # declare -a files=([0]="a.txt" [1]="b.txt") ``` `declare -n name=target` (or `local -n` inside a function, which additionally scopes the nameref itself) gives `name` the nameref attribute. From then on, every expansion, assignment, subscripted assignment, `+=` append, and even `unset` performed through `name` is applied to `target` instead. The alias is on the *variable*, not on a value: it survives across statements for as long as the nameref is in scope. This works for scalars, indexed arrays and associative arrays alike, which is what makes it the standard way to hand a map into a helper: ```bash put() { local -n _m=$1 _m[$2]=$3 } declare -A cfg put cfg region eu-west-1 ``` ## The circular-reference pitfall Bash uses dynamic scoping: a `local` inside a function shadows the caller's variable of the same name for the duration of the call. So if the parameter name and the caller's variable name coincide, the nameref would have to point at itself. Bash detects that and reports a circular name reference warning rather than aliasing, and the function does not update what the caller expected. ```bash broken() { local -n arr=$1; arr+=(x); } arr=(); broken arr # circular name reference ``` The defence is purely conventional: name every nameref parameter something a caller would never choose — a leading underscore (`_out`, `_map`) or a function-specific prefix (`__addall_out`). It looks ugly, and the ugliness is the point: it is the visible marker of a bash limitation, and reviewers should treat a nameref parameter named `arr`, `out` or `result` as a latent bug. ## Unsetting through a nameref One more asymmetry worth knowing: `unset ref` follows the reference and unsets the *target* variable, while `unset -n ref` removes the nameref itself and leaves the target alone. Getting these backwards silently destroys the caller's data. ## When not to use one Namerefs make a function's contract implicit — you have to read the body to learn that argument one is an output parameter — and they are bash 4.3+, so they are unavailable on macOS's stock bash 3.2 and are not POSIX. For a small helper that produces a single string, printing to stdout and letting the caller capture it is simpler and portable. Namerefs earn their keep when the data is genuinely an array or a map, when the helper must both read and update it, or when copying a large structure would be wasteful. A reasonable house rule: name the array explicitly in the function's doc comment, use `_`-prefixed nameref parameters, and never take more than one or two output parameters. If a function needs three, that is usually the signal that the script has outgrown the shape you are forcing it into. ## What interviewers listen for Saying "pass the name and bind with `local -n`" answers the question; adding *why* the underscore prefix exists — dynamic scoping producing a circular reference — is what shows you have actually shipped a bash library rather than read about one.

  • What is the difference between `unset ref` and `unset -n ref` when `ref` is a nameref?
    `unset ref` follows the reference and unsets the variable it points at — the caller's data. `unset -n ref` removes the nameref itself and leaves the target intact. Mixing them up silently destroys a caller's array, so a helper that wants to drop only its own alias must use `-n`.
  • How did people pass arrays into functions before bash 4.3?
    By convention rather than by feature: agree on a global variable name, or have the function print its results and let the caller rebuild an array from the output. Some libraries built assignment strings and ran them through `eval`, which works but makes quoting the caller's problem and is easy to get wrong with values containing spaces.
  • When would you avoid a nameref even on a modern bash?
    When the helper produces one string, printing it is simpler and portable. Namerefs also hide the contract — nothing in the call site says argument one is an output parameter — and they rule out bash 3.2 hosts. Reserve them for real arrays and maps, or for helpers that must read and update the caller's structure.

saying these in an interview costs you the question

  • Thinks "${arr[@]}" passes the array itself into the function
  • Believes arrays can be exported to child processes
  • Names the nameref parameter the same as the caller's variable
  • Uses unset ref expecting to drop only the alias
  • Assumes namerefs work on macOS's stock bash

context