Bash Namerefs: Passing Variables by Name Without eval
How declare -n and local -n create references, work with arrays, follow dynamic scope, interact with unset, and avoid unsafe variable-name injection.
Bash normally passes expanded values to a function. Sometimes a function needs to update a caller’s variable or fill an array chosen by the caller. A nameref created with declare -n or local -n makes one variable refer to another variable by name, avoiding the quoting and code-injection hazards of constructing an assignment for eval.
Namerefs are a Bash feature, not POSIX shell syntax. They should be used only in scripts that select Bash explicitly and test the Bash versions supported by the deployment environment.
Create a local reference inside the function
The most useful pattern receives a variable name as an argument and gives the reference its own distinctive local name:
set_status() {
local -n _output_ref=$1
local message=$2
_output_ref=$message
}
status='pending'
set_status status 'complete'
printf '%s\n' "$status"
Assignments to _output_ref update status. Expansions of _output_ref read status. Most attribute changes and operations act on the referenced variable as well. The function avoids printing a value for command substitution and preserves spaces and newlines without another serialization layer.
Use a reference name unlikely to collide with caller variables. Bash uses dynamic scoping for local variables: a called function can see locals in its caller. If the caller passes the same name the callee uses for its nameref, the reference may become self-referential or resolve differently than intended.
Validate names at the trust boundary
Nameref avoids executing a constructed command, but it still lets the caller select shell state. A function that accepts an arbitrary external string as a reference name might overwrite PATH, IFS, a control variable, or an internal array.
Validate syntax before creating the reference and, when appropriate, allowlist the actual destination:
is_identifier() {
[[ $1 =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]
}
read_setting() {
local target=$1 key=$2
is_identifier "$target" || {
printf 'invalid destination: %q\n' "$target" >&2
return 2
}
case $target in
theme|editor|pager) ;;
*) printf 'destination not allowed: %s\n' "$target" >&2; return 2 ;;
esac
local -n _destination_ref=$target
_destination_ref=${config[$key]-}
}
Identifier validation prevents array-subscript and expansion syntax from being interpreted as part of the reference. An allowlist enforces the stronger policy of which variables the function may change. Use declare -p -- "$target" when the function requires an existing variable of a particular kind, and handle its failure explicitly.
A nameref can point to an array or element
A nameref variable cannot itself carry the array attribute, but it can refer to an indexed array, an associative array, or an individual subscript. This makes reusable collection functions possible without copying data through standard output.
append_unique() {
local -n _array_ref=$1
local candidate=$2 item
for item in "${_array_ref[@]}"; do
[[ $item == "$candidate" ]] && return 0
done
_array_ref+=("$candidate")
}
declare -a hosts=(api-1 api-2)
append_unique hosts api-3
Quote array expansions normally. Nameref changes name resolution, not word splitting or globbing rules. "${_array_ref[@]}" preserves elements, while an unquoted expansion can still split and expand filenames.
Passing a literal subscript as the reference target can be useful inside trusted code, but it increases parsing and injection risk at an external boundary. Prefer passing the array name and a separately validated key, then using quoted associative-array subscripts in the function.
unset has two different meanings
By default, unset ref where ref has the nameref attribute unsets the variable it references. unset -n ref removes the nameref variable itself instead. The distinction is easy to miss in cleanup code.
value='important'
declare -n ref=value
unset -n ref # remove the reference; value remains
printf '%s\n' "$value"
If a function declares its nameref with local -n, the local reference disappears automatically when the function returns. It usually does not need an explicit unset. To clear the caller’s value intentionally, use an assignment or unset -- "$target" only after the destination has been validated and the destructive behavior is documented.
Attribute changes normally reach the target
An assignment through a nameref and many declare operations affect the referenced variable. The -n option itself is special because it changes the reference variable. Before adding integer, lowercase, readonly, or export attributes through a general helper, decide whether changing the caller’s variable metadata is really part of the function contract.
A function named fill_array should fill an array, not quietly make it readonly. Inspect with declare -p during development to see the attributes of both the nameref and target. Remember that a readonly destination will reject assignment even though creating the reference succeeded.
Bash also treats a nameref loop-control variable specially: successive words can name variables that the loop body then accesses through the reference. This is powerful metaprogramming, but ordinary scripts are usually clearer with an explicit destination list and one small helper.
Avoid reference chains and cycles
A nameref can name another nameref, causing Bash to follow a chain. Short internal chains may work, but they make ownership and unset behavior harder to reason about. A cycle or self-reference produces errors and can vary in how it surfaces across operations.
Keep references local and shallow. Name the final destination at the public function boundary, validate it once, and avoid returning the nameref name as data. If two nested helpers both need mutation, either pass the original destination name deliberately or let the outer helper perform the assignment.
Never use a nameref merely to save typing for a global variable. Direct access is clearer when the destination is fixed. The feature earns its complexity when a reusable function must operate on one of several caller-owned variables.
Test the mutation contract
Tests should cover scalar values containing spaces and newlines, empty values, indexed arrays with empty elements, associative keys, missing destinations, readonly targets, invalid names, and a caller variable whose name resembles the helper’s local reference. Run the script with set -u if production uses it because an unset referenced variable can expose assumptions hidden in normal mode.
Use ShellCheck for general shell mistakes, but preserve direct behavioral tests because static analysis cannot know every dynamic destination. Include a test that no output is emitted when the API promises mutation only; accidental diagnostic output often becomes data when callers later wrap a function in command substitution.
Namerefs provide a constrained form of indirection. They remove the need to turn data into shell code, but they do not make arbitrary destination names safe. A strong implementation uses Bash explicitly, validates or allowlists the target, keeps the reference local, quotes array operations, and documents whether it assigns, clears, or changes attributes on caller state.
Related:
- Command Substitution vs Process Substitution
- Fixing Scripts That Work Interactively but Fail Under cron
Sources: