0Pricing
Linux Command Line & Bash Scripting Mastery · 课时

在函数之间传递数组与关联映射

使用 nameref 和间接展开,安全地将复杂数据结构传入函数并从函数中传出。

在函数之间传递数组与关联映射 是 CoddyKit 上的免费 Linux Command Line & Bash Scripting Mastery 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Linux Command Line & Bash Scripting Mastery 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。

为什么将数组传递给函数很棘手

在 Bash 中,数组和关联数组不是一等值。尝试将数组传递给函数时,不能直接使用 $my_array——这样只会展开第一个元素。

请看下面这个有问题的尝试:

my_func "${my_array[@]}"

函数接收到的是由单词组成的扁平列表。它无法知道一个元素在哪里结束、下一个元素在哪里开始,并且数组结构也完全丢失了。

Bash 提供了两种可靠的机制来传递复杂数据结构:

  • 名称引用(declare -n)——通过名称引用另一个变量(Bash 4.3 及更高版本)
  • 间接展开(${!varname[@]})——访问名称存储在另一个变量中的变量(较旧版本的 Bash)

本课将介绍这两种技术,帮助您编写能够处理数组和映射的健壮、可复用函数。

使用名称引用按名称传递数组

现代 Bash 中最简洁的方法是使用名称引用:将数组的名称作为字符串传递,然后在函数内部声明一个引用该数组的局部变量。

关键语法:

  • declare -n ref="$1"——现在,ref 就是传入的第一个参数所指定变量名的别名
  • 您可以像操作原始数组一样读取、遍历和修改 ref

这样可以避免复制整个数组,并且函数可以处理任意大小的数组。

#!/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

在函数内部修改调用者的数组

由于名称引用是真正的别名,通过名称引用进行写入会修改调用者的原始变量。您无需回显值,也无需使用全局变量。

因此,名称引用非常适合在原地转换数组的函数,例如规范化字符串、筛选元素或排序。

#!/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[@]}"

通过名称引用返回新数组

函数还可以通过将数组名称作为输出参数接收,来写入调用者提供的数组。调用者创建一个空数组,传入它的名称,然后由函数填充该数组。

这种模式类似于 C 语言中传递指向结果缓冲区的指针这一经典惯用法,只不过这里完全通过 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[@]}"

名称引用冲突陷阱

名称引用有一个关键陷阱:如果局部名称引用变量与调用者的变量同名,Bash 就会进入无限自引用循环,导致脚本失败。

例如,如果调用者将数组命名为 arr,而函数执行 declare -n arr="$1",名称引用 arr 就会指向自身。

避免这种情况的约定:为内部名称引用变量添加下划线前缀(例如 _arr、_ref、_out)。调用者几乎不会使用这类名称,因此发生冲突的可能性极低。

#!/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

按名称传递关联数组(映射)

关联数组(declare -A)与名称引用配合使用时,工作方式完全相同。将映射的名称作为字符串传递,并在函数内部声明名称引用,即可完整读写所有键值对。

对于必须在多个库函数之间共享的配置映射、选项解析器和查找表,这种方式非常有用。

#!/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

使用名称引用合并两个映射

由于名称引用为函数提供了实时别名,因此可以简洁地实现映射合并等实用工具。按名称传入目标映射和一个或多个源映射;函数遍历源映射的键,并将它们写入目标映射。

名称引用使您能够编写一个通用的 merge_map 函数,用于处理脚本中的任意关联数组。

#!/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

间接展开:兼容较旧版本的 Bash

如果必须支持Bash 4.0–4.2(或较旧版本 macOS 的默认 shell),则无法使用 declare -n。替代方案是使用 ${!varname} 进行间接展开。

间接展开会将 varname 的值当作变量名进行求值,并返回该变量的值。对于数组,还需要结合特殊语法才能获取所有元素。

与名称引用相比,它有以下限制:

  • 实际上只能读取(写回需要使用 eval,这会带来注入风险)
  • 如果不使用 eval,就无法安全地遍历关联数组的键
  • 只要可能,就应使用名称引用;仅在必要时才退回使用间接展开
#!/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"

构建模块化脚本库

在专业的 Bash 项目中,实用函数会存储在库文件中(例如 lib/array_utils.sh),由需要它们的脚本加载。

具体模式如下:

  • 每个库文件只定义函数,不产生顶层副作用
  • 使用哨兵变量防止重复加载:[[ -n $__LIB_LOADED ]] && return; __LIB_LOADED=1
  • 使用者脚本通过 source 或 . 加载库
  • 函数通过名称引用交换复杂数据,使所有逻辑保持自包含

这样得到的脚本易于测试、复用和维护,而不是一次性的 shell 小技巧。

实际示例:返回映射的选项解析器

一个实际应用场景是:选项解析器将结果填充到调用者提供的关联数组中。调用者将数组名称作为第一个参数传入,后面接实际的 "$@" 参数。

这种模式使任何脚本都能复用同一个解析器,无需复制代码,也不依赖全局变量。

#!/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]}"

在一个函数中组合输入和输出名称引用

函数可以同时持有多个名称引用,一个用于输入,一个用于输出。这样就能实现纯转换函数:从源结构读取数据,计算结果,再将结果写入目标结构,全程不接触全局状态。

为每个名称引用使用不同的下划线前缀名称,即使调用者的变量名称很简单,也能避免冲突。

#!/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

知识检查:名称引用与数组传递

请检验您对在 Bash 函数之间传递数组和映射的理解。

回顾:在 Bash 中传递复杂数据结构

本课介绍了以下内容:

  • 数组无法按值传递——应改为将变量的名称作为字符串传递。
  • declare -n ref="$varname" 会创建名称引用:它是调用者变量的实时别名。读取和写入都会作用于原始变量。需要 Bash 4.3 及更高版本。
  • 当内部变量与外部变量同名时,就会发生名称引用冲突。为内部名称引用添加前缀即可避免冲突(例如 _arr、_map)。
  • 输出参数通过传入空结果数组或映射的名称来工作;函数通过名称引用填充它。
  • 在一个函数中使用多个名称引用,可以实现没有全局副作用的纯输入到输出转换。
  • 间接展开(${!varname[@]} + eval)是 Bash < 4.3 中较旧的备用方案,但存在注入风险,并且实际上只能读取。
  • 将这些技术与加载的库文件结合起来,即可构建模块化、可复用、易于测试的 Bash 代码库。

常见问题解答

「在函数之间传递数组与关联映射」课时是免费的吗?

是的 — 「在函数之间传递数组与关联映射」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Linux Command Line & Bash Scripting Mastery 课程的其余内容,请升级到 CoddyKit PRO。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。

「在函数之间传递数组与关联映射」这节课中我会学到什么?

使用 nameref 和间接展开,安全地将复杂数据结构传入函数并从函数中传出。 你通过在浏览器中直接运行的动手代码来练习 Linux Command Line & Bash Scripting Mastery,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Linux Command Line & Bash Scripting Mastery 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Linux Command Line & Bash Scripting Mastery 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「在函数之间传递数组与关联映射」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Linux Command Line & Bash Scripting Mastery 课中编写并运行代码吗?

能。每节 Linux Command Line & Bash Scripting Mastery 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 使用局部作用域与返回码设计函数
  2. 构建并引入可复用的 Bash 库
  3. 使用 getopts 解析标志与参数
  4. 在函数之间传递数组与关联映射
← 返回 Linux Command Line & Bash Scripting Mastery