关于eval:bash中的间接变量赋值

Indirect variable assignment in bash

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

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

问题是eval的常见问题:

1
2
3
var=x; val=1$'
'
pwd
eval $var=$val  # bad output here

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

在任何情况下,使用(转义)引号的明显解决方案并不真正起作用:

1
2
3
var=x; val=1"$'
'pwd"

eval $var="$val"  # fail with the above

问题是bash中有间接变量引用(用${!foo}),但我没有看到任何这种方式来进行间接赋值 - 是否有任何理智的方法来做到这一点?

为了记录,我找到了解决方案,但这不是我认为"理智"的东西......:

1
eval"$var='"${val//\'/\'"\'"\'}"'"

Bash有printf的扩展名,可将结果保存到变量中:

1
printf -v"${VARNAME}" '%s'"${VALUE}"

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

如果对$varname使用无效标识符,该命令将失败并返回状态代码2:

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


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

1
declare"$var=$val"

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

1
typeset"$var=$val"

bash的现代版本中,应该使用nameref。

1
2
declare -n var=x
x=$val

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


1
eval"$var=\$val"

eval的参数应始终是用单引号或双引号括起来的单个字符串。偏离此模式的所有代码在边缘情况下都有一些意外行为,例如具有特殊字符的文件名。

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

1
varname=$value

这正是你想要的。

通常,形式$varname的所有表达式都应该用双引号括起来。只有两个地方可以省略引号:变量赋值和case。由于这是一个变量赋值,因此这里不需要引号。但是,他们不会受到伤害,因此您也可以将原始代码编写为:

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


重点是建议的方法是:

1
eval"$var=\$val"

RHS间接完成了RHS。由于eval用于相同的
环境,它将$val绑定,因此推迟它的工作,从那以后
现在它只是一个变量。由于$val变量具有已知名称,
引用没有问题,它甚至可以写成:

1
eval $var=\$val

但是因为总是添加引号更好,前者更好,或者
即使这样:

1
eval"$var="\$val""

在整个事情中提到的bash中更好的替代方案
完全避免eval(并且不像declare那样微妙):

1
printf -v"$var""%s""$val"

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


较新版本的bash支持称为"参数转换"的东西,在bash(1)中的同名部分中有记录。

"${value@Q}"扩展为shell引用的"${value}"版本,您可以将其重新用作输入。

这意味着以下是一个安全的解决方案:

1
eval="${varname}=${value@Q}"

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

但是在使用read来确保输入被清理时需要非常小心(-d''读取直到null终止并且printf"... 0"以null结束该值),并且读取本身在需要变量的主shell而不是子shell(因此<<(...)语法)。

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

我用 n t r n空格和0等测试了它,它在我的bash版本上按预期工作。

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

此方法在美学上可能不像eval或printf解决方案那样令人满意,并且如果值来自文件或其他输入文件描述符会更有用

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

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

1
2
3
4
5
6
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")