Passing Arrays and Associative Maps Between Functions
Use namerefs and indirect expansion to pass complex data structures into and out of functions safely.
Passing Arrays and Associative Maps Between Functions is a free DevOps Bootcamp lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the DevOps Bootcamp learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why Passing Arrays to Functions Is Tricky
In Bash, arrays and associative arrays are not first-class values. When you try to pass an array to a function, you cannot simply use $my_array — that only expands the first element.
Consider this broken attempt:
my_func "${my_array[@]}"The function receives a flat list of words. It has no way to know where one element ends and the next begins, and it completely loses the array structure.
Bash provides two reliable mechanisms to pass complex data structures:
- Namerefs (
declare -n) — a reference to another variable by name (Bash 4.3+) - Indirect expansion (
${!varname[@]}) — access a variable whose name is stored in another variable (older Bash)
This lesson covers both techniques so you can write robust, reusable functions that work with arrays and maps.
Passing an Array by Name Using a Nameref
The cleanest modern approach is the nameref: you pass the name of the array as a string, and inside the function you declare a local variable that is a reference to it.
Key syntax:
declare -n ref="$1"—refis now an alias for whatever variable name was passed as the first argument- You read, iterate, and modify
refexactly as if it were the original array
This avoids copying the entire array and lets the function work with arrays of any size.
#!/usr/bin/env bash
print_array() {
declare -n arr="$1" # nameref: arr -> caller's variable
echo "Array has ${#arr[@]} elements:"
for elem in "${arr[@]}"; do
echo " - $elem"
done
}
fruits=(apple banana cherry)
print_array fruitsModifying a Caller's Array Inside a Function
Because a nameref is a true alias, writes through the nameref change the caller's original variable. There is no need to echo values back or use global variables.
This makes namerefs ideal for functions that transform arrays in place — for example, normalising strings, filtering elements, or sorting.
#!/usr/bin/env bash
to_uppercase() {
declare -n _arr="$1"
for i in "${!_arr[@]}"; do
_arr[$i]="${_arr[$i]^^}" # ^^ = uppercase in Bash 4+
done
}
words=(hello world bash)
echo "Before: ${words[@]}"
to_uppercase words
echo "After: ${words[@]}"Returning a New Array Through a Nameref
A function can also write into a caller-supplied array by accepting its name as an output parameter. The caller creates an empty array, passes its name, and the function populates it.
This pattern mirrors the classic C idiom of passing a pointer to a result buffer — except here it is done entirely in shell.
#!/usr/bin/env bash
get_even_numbers() {
local -a source=("$@")
# Last argument is the output array name — pop it off
local out_name="${source[-1]}"
unset 'source[-1]'
declare -n _out="$out_name"
_out=() # clear
for n in "${source[@]}"; do
(( n % 2 == 0 )) && _out+=("$n")
done
}
declare -a evens
get_even_numbers 1 2 3 4 5 6 7 8 evens
echo "Evens: ${evens[@]}"The Nameref Collision Trap
There is one critical pitfall with namerefs: if the local nameref variable shares the same name as the caller's variable, Bash enters an infinite self-reference loop and the script fails.
For example, if the caller names their array arr and the function does declare -n arr="$1", the nameref arr points to itself.
Convention to avoid this: prefix internal nameref variables with an underscore (e.g., _arr, _ref, _out). Callers almost never use such names, making collisions extremely rare.
#!/usr/bin/env bash
# DANGER: caller uses variable named 'arr'
arr=(one two three)
bad_func() {
declare -n arr="$1" # collision! arr refers to itself
echo "${arr[@]}"
}
good_func() {
declare -n _arr="$1" # underscore prefix avoids collision
echo "${_arr[@]}"
}
# bad_func arr # would hang / error
good_func arrPassing Associative Arrays (Maps) by Name
Associative arrays (declare -A) work identically with namerefs. Pass the map's name as a string and declare a nameref inside the function — you get full read/write access to all key-value pairs.
This is invaluable for configuration maps, option parsers, and lookup tables that must be shared across library functions.
#!/usr/bin/env bash
print_map() {
declare -n _map="$1"
echo "Map contents:"
for key in "${!_map[@]}"; do
printf ' %-15s => %s\n' "$key" "${_map[$key]}"
done
}
declare -A config
config[host]="localhost"
config[port]="5432"
config[db]="myapp"
print_map configMerging Two Maps with a Nameref
Because namerefs give the function a live alias, you can implement utilities like map merging cleanly. Pass the destination map and one or more source maps by name; the function iterates source keys and writes them into the destination.
Namerefs make it possible to write a single generic merge_map function that works on any associative arrays in your script.
#!/usr/bin/env bash
merge_map() {
# merge_map dest src
declare -n _dest="$1"
declare -n _src="$2"
for key in "${!_src[@]}"; do
_dest["$key"]="${_src[$key]}"
done
}
declare -A defaults=([timeout]=30 [retries]=3 [verbose]=false)
declare -A overrides=([retries]=5 [log_level]=debug)
merge_map defaults overrides
for k in "${!defaults[@]}"; do
echo "$k = ${defaults[$k]}"
doneIndirect Expansion: Older Bash Compatibility
If you must support Bash 4.0–4.2 (or older macOS default shells), declare -n is unavailable. The alternative is indirect expansion with ${!varname}.
Indirect expansion evaluates the value of varname as a variable name and returns that variable's value. For arrays you combine it with a special syntax to get all elements.
Limitations compared to namerefs:
- Read-only in practice (writing back requires
eval, which carries injection risks) - Associative array keys cannot be iterated safely without
eval - Use namerefs wherever possible; fall back to indirect only when necessary
#!/usr/bin/env bash
sum_array() {
local arr_name="$1"
# Indirect: build a temporary copy by expanding arr_name[@]
local -a items
eval "items=( \"\${${arr_name}[@]}\" )"
local total=0
for n in "${items[@]}"; do
(( total += n ))
done
echo "$total"
}
numbers=(10 20 30 40)
result=$(sum_array numbers)
echo "Sum: $result"Building a Modular Script Library
In professional Bash projects, utility functions are stored in library files (e.g., lib/array_utils.sh) and sourced by scripts that need them.
The pattern is:
- Each library file defines functions only — no top-level side effects
- Guard against double-sourcing with a sentinel variable:
[[ -n $__LIB_LOADED ]] && return; __LIB_LOADED=1 - Consumer scripts use
sourceor.to load the library - Functions exchange complex data via namerefs, keeping all logic self-contained
This produces scripts that are testable, reusable, and maintainable — not one-off shell hacks.
Practical Example: Option Parser Returning a Map
A real-world use case: a command-line option parser that populates a caller-supplied associative array. The caller passes the array name as the first argument followed by the actual "$@" arguments.
This pattern lets any script reuse the same parser without copying code or relying on global variables.
#!/usr/bin/env bash
parse_opts() {
declare -n _opts="$1"
shift
while [[ $# -gt 0 ]]; do
case "$1" in
--host) _opts[host]="$2"; shift 2 ;;
--port) _opts[port]="$2"; shift 2 ;;
--user) _opts[user]="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; shift ;;
esac
done
}
declare -A opts=([host]=localhost [port]=5432 [user]=admin)
parse_opts opts --host db.example.com --port 3306
echo "Connecting to ${opts[host]}:${opts[port]} as ${opts[user]}"Combining Input and Output Namerefs in One Function
Functions can hold multiple namerefs simultaneously — one for input, one for output. This enables pure transformation functions: they read from a source structure, compute a result, and write it into a destination structure, all without touching global state.
Using distinct underscore-prefixed names for each nameref keeps collisions impossible even when caller variables have simple names.
#!/usr/bin/env bash
filter_map_by_key_prefix() {
# Usage: filter_map_by_key_prefix src_map dest_map prefix
declare -n _src="$1"
declare -n _dst="$2"
local prefix="$3"
_dst=()
for key in "${!_src[@]}"; do
if [[ $key == ${prefix}* ]]; then
_dst["$key"]="${_src[$key]}"
fi
done
}
declare -A settings=(
[db_host]=localhost [db_port]=5432
[cache_ttl]=300 [cache_driver]=redis
[app_debug]=true
)
declare -A db_settings
filter_map_by_key_prefix settings db_settings db_
for k in "${!db_settings[@]}"; do
echo "$k = ${db_settings[$k]}"
doneKnowledge Check: Namerefs and Array Passing
Test your understanding of passing arrays and maps between functions in Bash.
Recap: Passing Complex Data Structures in Bash
Here is what you learned in this lesson:
- Arrays cannot be passed by value — pass the variable name as a string instead.
declare -n ref="$varname"creates a nameref: a live alias to the caller's variable. Both reads and writes go through to the original. Requires Bash 4.3+.- Nameref collision occurs when the internal and external variable share the same name. Prevent it by prefixing internal namerefs (e.g.,
_arr,_map). - Output parameters work by passing the name of an empty result array/map; the function populates it through a nameref.
- Multiple namerefs in one function enable pure input-to-output transformations with no global side effects.
- Indirect expansion (
${!varname[@]}+eval) is an older fallback for Bash < 4.3, but carries injection risks and is read-only in practice. - Combine these techniques with sourced library files to build modular, reusable, testable Bash codebases.
Frequently asked questions
Is the “Passing Arrays and Associative Maps Between Functions” lesson free?
Yes — the full text of “Passing Arrays and Associative Maps Between Functions” is free to read here on the web, and the DevOps Bootcamp course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the DevOps Bootcamp course, upgrade to CoddyKit PRO.
What will I learn in “Passing Arrays and Associative Maps Between Functions”?
Use namerefs and indirect expansion to pass complex data structures into and out of functions safely. You practise DevOps Bootcamp with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start DevOps Bootcamp?
No prior experience is required. DevOps Bootcamp on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Passing Arrays and Associative Maps Between Functions” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this DevOps Bootcamp lesson?
Yes. Every DevOps Bootcamp lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Designing Functions with Local Scope and Return Codes
- Building and Sourcing Reusable Bash Libraries
- Parsing Flags and Arguments with getopts
- Passing Arrays and Associative Maps Between Functions