Bash中的Indirection与Nameref
目录
在 Bash 中,有时候你需要通过一个变量来间接引用另一个变量。比如你知道变量名是一个字符串,你想根据这个字符串去访问对应的变量值。Bash 提供了两种机制来实现这个需求: Indirection (间接展开)和 Nameref (名称引用)。
Indirection:间接展开
基本用法
Indirection 的语法很简单:在参数展开中,在变量名前面加一个 ! 就行了。
fruit="apple" var="fruit" echo "${!var}"
apple
apple
这里 var 的值是 "fruit" ,而 ${!var} 会把 var 的值(也就是 "fruit" )当作变量名,再去取那个变量的值,最终得到 "apple" 。这就是"间接"的含义。
注意, ${!var} 只做 一层 间接,不会递归展开。即使 apple 也是一个变量名, ${!var} 也不会再去取 apple 的值。
一个实际例子:遍历配置变量
假设你有好几个配置项,想根据名字动态地读取它们的值:
db_host="192.168.1.100" db_port="3306" db_user="admin" for item in host port user; do varname="db_$item" echo "$item = ${!varname}" done
| host | = | 192.168.1.100 |
| port | = | 3306 |
| user | = | admin |
host = 192.168.1.100 port = 3306 user = admin
通过拼接变量名再使用 ${!varname} 间接展开,可以避免写一堆 if/elif 或者 case 分支。
注意 ! 的歧义
! 在参数展开中有两种不同的含义,容易混淆:
${!name}— 间接展开,取name的值所指向的变量${!name[@]}或${!name[*]}— 展开数组name的所有 索引 (下标)
fruit="apple" name="fruit" echo "${!name}" # 间接展开:取 fruit 的值
apple
arr=("a" "b" "c") name="arr" echo "${!name}" # 间接展开:取 arr 的值
a
注意这里的结果是 a 而不是 a b c 。因为对数组变量做间接展开时,等同于 ${arr} ,而 ${arr} 实际上就是 ${arr[0]} ,只展开数组的第一个元素。如果想通过间接方式获取数组的所有元素,Nameref 是更好的选择。
arr=("a" "b" "c") echo "${!arr[@]}" # 取数组 arr 的索引
0 1 2
0 1 2
简单记:有 [@] 或 [*] 时, ! 取的是索引;没有时, ! 是间接展开。
Indirection 的局限
Indirection 最大的局限是它 只能读,不能写 。你不能通过 ${!var} 来给目标变量赋值:
fruit="apple" var="fruit" ${!var}="banana" # 报错!这不是合法语法
bash: apple=banana: command not found...
如果你需要通过间接方式 修改 一个变量,就需要用到下面介绍的 Nameref 了。
Nameref:名称引用
基本用法
Nameref 从 Bash 4.3 开始支持,通过 declare -n 或 local -n 声明一个"引用变量":
fruit="apple" declare -n ref=fruit echo "$ref" # 读取:通过 ref 读 fruit 的值
apple
ref="banana" # 写入:通过 ref 修改 fruit 的值 echo "$fruit"
banana
ref 不是 fruit 的副本,而是指向 fruit 的一个"别名"。对 ref 的任何读写操作,实际上都作用于 fruit 。
一个有趣的细节: ${!nameref} 行为不同
当你对 Nameref 变量使用 ${!ref} 时,它 不是 间接展开,而是返回被引用变量的 名字 :
var="hello" declare -n ref=var echo "$ref" # 正常展开:通过 ref 得到 var 的值 echo "${!ref}" # 返回被引用变量的名字,而不是间接展开
hello var
这个特性在调试时很有用,可以让你知道一个 Nameref 到底指向了哪个变量。在函数中结合 ${FUNCNAME[0]} 可以实现很好的日志输出:
update_value() { local -n up_ref=$1 echo "函数 ${FUNCNAME[0]} 正在修改变量 '${!up_ref}'" up_ref="new_value" } my_var="old_value" update_value my_var echo "调用后 my_var = $my_var"
函数 update_value 正在修改变量 'my_var' 调用后 my_var = new_value
在函数中使用 Nameref
这是 Nameref 最常见的使用场景。当你需要让函数修改调用者的变量时,Nameref 比传统的 eval 或 Indirection 要安全得多。
swap() { local -n a=$1 local -n b=$2 local tmp="$a" a="$b" b="$tmp" } x=10 y=20 swap x y echo "x=$x, y=$y"
x=20, y=10
注意函数里用 local -n 而不是 declare -n ,这样引用变量的作用域就限制在函数内部,不会污染全局命名空间。
操作关联数组
Nameref 在操作关联数组(associative array)时尤其好用:
update_config() { local -n config=$1 config[host]="newhost.example.com" config[port]="8443" config[enabled]="yes" } declare -A server_config update_config server_config echo "${server_config[host]}" echo "${server_config[port]}" echo "${server_config[enabled]}"
newhost.example.com 8443 yes
如果用 Indirection,你没法直接操作关联数组的元素——因为 ${!var} 只能取出整个变量的值,不能写成 ${!var[key]} 这种形式。而 Nameref 完美解决了这个问题。
小心 Nameref 循环
两个 Nameref 变量互相引用会造成循环:
declare -n a=b declare -n b=a echo "$a" # bash: maximum nesting level exceeded
Bash 会检测到循环并报错,不会真的无限递归,但你仍然要避免写出这样的代码。
Indirection vs Nameref 对比
| 特性 | Indirection (${!var}) |
Nameref (declare -n) |
|---|---|---|
| 最低 Bash 版本 | 2.0+ | 4.3+ |
| 能否读取 | 能 | 能 |
| 能否写入 | 不能 | 能 |
| 生命周期 | 一次性展开 | 持久引用 |
| 操作数组元素 | 不方便 | 方便 |
| 在函数中使用 | 需配合 eval 才能写入 |
直接使用,更安全 |
什么时候用哪个?
- 只需要 读取 另一个变量的值,或者需要兼容老版本 Bash → 用 Indirection
- 需要 修改 另一个变量的值,或者在函数中传递复杂数据结构 → 用 Nameref
- 能用 Nameref 的场景,优先用 Nameref——它语义更清晰,也更安全
参考
- What's New in Bash Parameter Expansion - Linux Journal
man bash中 SEARCH 章节,查找Parameter Expansion和declare