在Bash中的间接变量赋值

47

似乎在bash中进行间接变量设置的推荐方法是使用eval

var=x; val=foo
eval $var=$val
echo $x  # --> foo

问题是使用eval时通常会遇到的问题:

var=x; val=1$'\n'pwd
eval $var=$val  # bad output here

(因为它在许多地方被推荐使用,我想知道有多少脚本因此而容易受到攻击...)

无论如何,显而易见的解决方案——使用(转义后的)引号并不真正起作用:

var=x; val=1\"$'\n'pwd\"
eval $var=\"$val\"  # fail with the above

问题是bash内置了间接变量引用(使用${!foo}),但我没有看到任何这样的方式来进行间接赋值 - 有没有什么明智的方法来做到这一点?

记录一下,我确实找到了一个解决方案,但这不是我认为“明智”的东西...

eval "$var='"${val//\'/\'\"\'\"\'}"'"
7个回答

42
Bash有一个扩展功能,可以将printf的结果保存到变量中:
printf -v "${VARNAME}" '%s' "${VALUE}"

这可以防止所有可能的转义问题。

如果您使用无效的标识符$VARNAME,则命令将失败并返回状态代码2:

$ printf -v ';;;' '%s' foobar; echo $?
bash: printf: `;;;': not a valid identifier
2

declaretypeset不同(在bash 4.2之前只能将变量声明为本地变量),使用此解决方案时,变量不会被声明为本地变量。问题已解决! - Law29
我对中间的参数感到困惑,直到我意识到'%s'是字面值。这让我感到困惑,因为在给出的例子中没有看到'%s'...我猜测这个例子之所以能够运行,是因为它不仅是字面值,而且还似乎是可选的,适用于间接赋值变量的用例。 - Cognitiaclaeves
@Cognitiaclaeves:感谢您的通知。我已修复示例以使其更清晰。实际上,如果目标变量名无效,则格式化字符串无关紧要,这在实践中不会产生影响。 - David Foerster

41

一种稍微更好的方式,避免使用 eval 可能带来的安全隐患,是

declare "$var=$val"

请注意,在bash中,declaretypeset的同义词。typeset命令得到更广泛的支持(kshzsh也使用它):

typeset "$var=$val"
在现代版本的bash中,应该使用nameref。
declare -n var=x
x=$val

它比eval更安全,但仍然不完美。


1
这似乎不适用于比bash更低版本的shell。 - MarcH
确实,虽然declare是POSIX标准的扩展,但它只是typeset的同义词,而typeset又被其他主要的shell(即kshzsh)所支持。不支持类似功能的shell必须小心使用eval - chepner
5
“eval "$var='$val'"” 的谨慎程度远远不够:如果内容包含单引号,它们很容易逃脱。 - Charles Duffy
2
请注意,当在bash函数内执行typesetdeclare时,它们会将变量定义为局部变量。对我来说这是无用的,因为我正在函数内操作,并希望从函数外访问结果。David下面的“printf”解决方案对我有用。 - Law29
3
bash 4.2 开始,declare 命令有一个 -g 选项,可以强制声明为全局变量。 - chepner
显示剩余7条评论

19
eval "$var=\$val"

eval的参数应该始终是一个被单引号或双引号包含的单个字符串。任何不遵循这种模式的代码都会在边缘情况下产生意外行为,例如带有特殊字符的文件名。

当shell扩展eval的参数时,$var将被替换为变量名,\$将被替换为简单的美元符号。因此,将要评估的字符串变为:

varname=$value

这正是你想要的。

通常,所有形如$varname的表达式都应该用双引号括起来,以防止意外扩展文件名模式,如*.c

只有两个地方可以省略引号,因为它们被定义为不扩展路径名和分割字段:变量赋值和case。根据POSIX 2018的规定:

赋值时,每个变量赋值都应在分配值之前进行~扩展、参数扩展、命令替换、算术扩展和去掉引号操作。

此扩展列表缺少参数扩展字段分割。当然,仅从阅读这句话很难看出来,但这是官方定义。

由于这是一个变量赋值,所以这里不需要引号。不过,它们也没有影响,所以你也可以将原始代码写成:

eval "$var=\"the value is \$val\""

请注意第二个美元符号是用反斜杠进行转义的,以防止它在第一次运行时被展开。具体情况如下:

eval "$var=\"the value is \$val\""

使用 eval 命令时,参数通过参数展开和反转义进行传递,得到以下结果:

varname="the value is $val"

接下来,将此字符串作为变量赋值进行评估,将以下值分配给变量varname

the value is value

右手边的间接引用不是我要找的。 - Eli Barzilay
1
(拍头) 哎呀,我完全忘记了为什么我在右手边要使用间接引用。由于你的回复根本没有提到这一点,我现在会编辑它,而不是自己回答并表扬自己... - Eli Barzilay
太棒了!以前我做过一些复杂的 eval eval export 的鬼扯操作。非常感谢你。对于那些使用 Google 搜索的人,采用上面的答案,而不是 eval eval export 格式。 - bgStack15
如果有人对上面的措辞感到困惑,也许eval "$var"='$val'可以更清楚地表达。或者可能更不清楚,但现在您有两种措辞可以考虑和比较,以确保您理解了。 :-) - clacke
@Eli 回答自己的问题没有任何问题。但是我已经还原了你的编辑,因为它与我的回答风格不符。 - Roland Illig
@RolandIllig 叹气 你等了6年就为了这个?不管怎样,既然你坚持,我已经完成了。此外,“始终是单引号或双引号括起来的单个字符串”并不真正与eval或我的问题相关(由于两个变量名都知道,而且$val的内容并不重要,因此不需要引号)。 - Eli Barzilay

17

主要的观点是推荐的做法是:

eval "$var=\$val"

同时,由于 RHS 是间接完成的。 由于在同一环境中使用了 eval,因此它将绑定 $val,因此延迟它是可行的,而且现在它只是一个变量。 由于 $val 变量具有已知名称,因此没有引用方面的问题,甚至可以写成:

eval $var=\$val

但是既然总是最好加引号,前者更好,或者甚至是这样:

eval "$var=\"\$val\""
在bash中,有一种更好的选择可以避免完全使用eval(并且不像declare等那么微妙):
printf -v "$var" "%s" "$val"

虽然这不是我最初要求的直接答案...


强调主要观点的答案是:通过转义 $ 来避免重新评估右侧变量。并且始终添加引号(让你看起来更年轻!)。我认为 eval 版本更好。 - Small Boy

7
较新版本的bash支持所谓的“参数转换”,在bash(1)的同名部分有文档记录。 "${value@Q}"会扩展为一个被shell引用的版本,可以将其重新用作输入。
这意味着以下方法是一个安全的解决方案:
eval="${varname}=${value@Q}"

2

为了完整起见,我还想建议可能使用bash内置的read。根据socowi的评论,我也对-d''进行了更正。

但是,在使用read时需要非常小心,以确保输入已经过消毒(-d''读取到空终止符,printf“...\0”用空字符终止值),并且在需要变量的主shell中执行read本身而不是子shell(因此使用<<(...)语法)。

var=x; val=foo0shouldnotterminateearly
read -d'' -r "$var" < <(printf "$val\0")
echo $x  # --> foo0shouldnotterminateearly
echo ${!var} # -->  foo0shouldnotterminateearly

我使用 \n \t \r 空格和 0 进行测试,在我的bash版本中,它按预期工作。

-r选项将避免转义\,因此如果您的值中包含字符“\”和“n”,而不是实际的换行符,则x也将包含两个字符“\”和“n”。

与eval或printf解决方案相比,这种方法可能不太美观,并且在值来自文件或其他输入文件描述符时更有用。

read -d'' -r "$var" < <( cat $file )

以下是一些替代< <()语法的建议

read -d'' -r "$var" <<< "$val"$'\0'
read -d'' -r "$var" < <(printf "$val") #Apparently I didn't even need the \0, the printf process ending was enough to trigger the read to finish.

read -d'' -r "$var" <<< $(printf "$val") 
read -d'' -r "$var" <<< "$val"
read -d'' -r "$var" < <(printf "$val")

0

另一种不使用 eval 实现此目的的方法是使用 "read":

INDIRECT=foo
read -d '' -r "${INDIRECT}" <<<"$(( 2 * 2 ))"
echo "${foo}"  # outputs "4"

1
那似乎是陈宸的答案的一个子集,不是吗? - Benjamin W.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接