bash中的间接变量赋值

Posted

技术标签:

【中文标题】bash中的间接变量赋值【英文标题】:Indirect variable assignment in bash 【发布时间】:2012-04-13 21:11:58 【问题描述】:

似乎在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//\'/\'\"\'\"\'"'"

【问题讨论】:

【参考方案1】:

不使用 eval 的另一种方法是使用“read”:

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

【讨论】:

这似乎是 Michael C. Chen 回答的一个子集,不是吗?【参考方案2】:
eval "$var=\$val"

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

eval的参数被shell扩展时,$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

【讨论】:

RHS 上的间接性不是我想要的。 (forehead-slap) 呸,我完全错过了为什么我确实想要在 RHS 上间接。由于您的回复根本没有谈论它,我现在将对其进行编辑,而不是回答-myself-and-pat-my-own-back... 太棒了!过去我做过一些复杂的eval eval export 废话。真的非常感谢你。对于谷歌员工,请使用上面的答案,而不是 eval eval 导出格式。 如果有人对上述措辞感到困惑,也许eval "$var"='$val'会更清楚。或者可能不太清楚,但是现在您需要考虑和比较两个措辞以确保您理解。 :-) @Eli 回答你自己的问题没有错。我已恢复您的编辑,因为它与我的回答风格不符。【参考方案3】:

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 之前只能将变量声明为本地),在此解决方案中,变量不声明为本地。解决了我的问题! 我对中间的论点感到困惑,直到我发现'%s' 是字面意思——我觉得这很令人困惑,因为'%s' 在给出的示例中没有任何地方......我想示例之所以有效,是因为除了它是文字之外,它似乎也是可选的,用于间接分配变量的用例。 @Cognitiaclaeves:感谢您的通知。我修复了示例以使其更清晰。在实践中,它不应该有任何区别,因为如果目标变量名无效,格式化字符串并不重要。【参考方案4】:

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

但是在使用 read 时需要非常小心,以确保输入被清理(-d'' 读取直到 null 终止并且 printf "...\0" 以 null 终止值),并且 read 本身是在需要变量的主 shell 中执行,而不是在子 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")

【讨论】:

【参考方案5】:

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

declare "$var=$val"

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

typeset "$var=$val"

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

declare -n var=x
x=$val

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

【讨论】:

这看起来不能移植到小于 bash 的 shell 确实;虽然declare 是对POSIX 标准的扩展,但它也只是typeset 的同义词,它 被其他主要shell 支持(即kshzsh)。不支持类似功能的 Shell 必须小心使用 eval eval "$var='$val'" 不够小心:如果内容包含文字单引号,它们很容易转义。 请注意,typesetdeclare 在 bash 函数中执行时,将变量定义为本地变量。这对我来说毫无用处,因为我在 bash 函数内部操作并希望从函数外部访问结果。下面大卫的“printf”解决方案对我有用。 bash 4.2 开始,declare 采用-g 选项强制全局变量。【参考方案6】:

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

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

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

eval="$varname=$value@Q"

【讨论】:

【参考方案7】:

重点是推荐的方法是:

eval "$var=\$val"

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

eval $var=\$val

但由于总是加上引号更好,所以前者更好,或者 即使这样:

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

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

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

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

【讨论】:

确实强调要获得的要点的答案:通过转义$ 不重新评估右侧变量。并且总是添加引号(让你很年轻!)。我认为eval 版本更好。

以上是关于bash中的间接变量赋值的主要内容,如果未能解决你的问题,请参考以下文章

C 语言指针间接赋值 ( 直接赋值 和 间接赋值 | 在子函数中间接赋值 )

变量的直接赋值和间接赋值

C 语言指针间接赋值 ( 直接修改 和 间接修改 指针变量 的值 | 在函数中 间接修改 指针变量 的值 | 在函数中 间接修改 外部变量 的原理 )

C 语言指针间接赋值 ( 间接赋值三要素 | 间接赋值 使用的三种场景 )

case语句中的变量赋值(bash)[重复]

为啥变量赋值中的空格会在 Bash 中出错? [复制]