0Pricing
DevOps Bootcamp · Lesson

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"ref is now an alias for whatever variable name was passed as the first argument
  • You read, iterate, and modify ref exactly 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 fruits

Modifying 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 arr

Passing 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 config

Merging 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]}"
done

Indirect 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 source or . 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]}"
done

Knowledge 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

  1. Designing Functions with Local Scope and Return Codes
  2. Building and Sourcing Reusable Bash Libraries
  3. Parsing Flags and Arguments with getopts
  4. Passing Arrays and Associative Maps Between Functions
← Back to DevOps Bootcamp