我正在编写一组 shell 函数,希望它们能够在 Bash 和 KornShell93 中工作,但是在使用 Bash 时,我遇到了“循环名称引用”警告。
这就是问题的本质:
function set_it {
typeset -n var="$1"
var="hello:$var"
}
function call_it {
typeset -n var="$1"
set_it var
}
something="boff"
call_it something
echo "$something"
运行它:
$ ksh script.sh
hello:boff
$ bash script.sh
script.sh: line 4: warning: var: circular name reference
hello:
something
KornShell93 完全符合我的要求,但 Bash 失败,并且如果脚本中的变量被命名,也会在第 2 行发出同样的警告var
。
我希望该var
变量对于每个函数都是本地的,这就是我使用 的原因typeset
,但 Bash 似乎不喜欢将 nameref“取消引用”到与 nameref 本身同名的变量。我不能使用local -n
ordeclare -n
因为它会在缺少这些的情况下崩溃ksh
,即使我使用了,它也不能解决问题。
我找到的唯一解决方案是在每个函数中使用唯一的变量名称,这看起来相当愚蠢,因为它们是本地的。
Bash 手册中有以下内容typeset
:
typeset
[...]
-n
为每个名称赋予nameref
属性,使其成为对另一个变量的名称引用。该另一个变量由 的值定义name
。name
除了更改 属性本身之外,对 的所有引用和赋值-n
都是在名称值引用的变量上执行的。[...]
当在函数中使用时,
declare
使typeset
每个名称成为本地名称,就像使用local
命令一样,除非-g
提供了该选项。如果变量名后跟=value
,则该变量的值设置为value
。
显然,我对 Bash 的名称引用和函数局部变量有些不理解。
所以,问题是:在这种情况下,我是否遗漏了 Bash 对名称引用变量的处理,或者这是 Bash 中的错误/错误功能?
更新:我目前正在与GNU bash, version 4.3.39(1)-release (x86_64-apple-darwin15)
以及 一起工作GNU bash, version 4.3.46(1)-release (x86_64-unknown-openbsd6.0)
。 macOS 附带的 Bash 太旧了,根本不知道名称引用。
更新: 更短:
function bug {
typeset -n var="$1"
printf "%s\n" "$var"
}
var="hello"
bug var
结果是bash: warning: var: circular name reference
。var
函数中的应该有不同的范围从var
全球范围来看。这对调用者施加了不必要的限制。限制是“不允许您随意命名变量,因为此函数中可能会与(本地)nameref 发生名称冲突”。
答案1
Chet Ramey(Bash 维护者)说
今年早些时候,bug-bash 上对 namerefs 进行了广泛的讨论。我对如何改变这种行为有一个合理的建议,我将在 bash-4.4 发布后查看它。
与此同时,我稍微混淆了本地 nameref 变量的名称,这样它们就不会在库内发生冲突,也不会(希望)与全局 shell 变量名称发生冲突。
在bash
5.0 中,这个问题得到了轻微的补救(但没有真正修复)。以下是观察到的行为:
$ foo () { typeset -n var="$1"; echo "$var"; }
$ var=hello
$ foo var
bash: typeset: warning: var: circular name reference
bash: warning: var: circular name reference
bash: warning: var: circular name reference
hello
这表明它有效,但也有一些警告。
相关的消息条目说
i. A nameref name resolution loop in a function now resolves to a variable by that name in the global scope.
答案2
Bash 的 namerefs 有点愚蠢,而且它们几乎按照上面所说的那样进行:它们引用了姓名。它们不会像大多数通用编程语言中的指针和引用那样捕获引用变量的“身份”。
因此,如果您运行declare -n ref=var
,则 usingref
会查找变量的名称var
,并使用其值。
如果你运行declare -n foo=foo
,那么 usingfoo
会对变量进行名称查找foo
,并且......好吧,意识到这是一个循环,并发出警告。
此外,引用找到的变量可能与设置 nameref 时与名称一起存在的变量不同。例如考虑下面的脚本:
$ cat nameref.sh
#!/bin/bash
var="from main"
declare -n ref=var # a
echo "main: \$ref='$ref'"
foo() {
local var="from foo"
bar
}
bar() {
echo "bar(): \$ref='$ref'" # b
}
foo
在“a”行中,顶层var
是可见的(您可能认为您正在保存对它的引用),但是当$ref
在“b”行上使用时,可见的是var
from foo()
,这就是引用找到的内容。
$ bash nameref3.sh
main: $ref='from main'
bar(): $ref='from foo'
您也可以在一个函数中得到相同的结果:
$ cat nameref4.sh
var=outer
foo() {
declare -n ref=var
echo "ref=$ref"
local var=inner
echo "ref=$ref"
}
foo
第一个echo
从外部上下文获取值,而第二个获取刚刚在 中分配的值foo()
。
正如 Kusalananda 的回答中提到的,Bash 5.0 在遇到循环时会获取指定变量的顶级实例。但这并没有多大帮助,因为它仍然会发出警告,并且如果您有多个嵌套函数,它不会让您从调用堆栈中间引用变量。
Ksh 在这方面有所不同。我不完全是它的作用,但似乎在那里,typeset -n ref=var
从直接外部上下文解析变量,并存储其实际身份。再说一遍,局部变量一般来说在 Ksh 中与在 Bash 中不同,这可能是使用它们的复杂程序的问题,即使混合中没有名称引用。