【问题标题】:Indirect variable assignment in bashbash中的间接变量赋值
【发布时间】: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//\'/\'\"\'\"\'}"'"

【问题讨论】:

    标签: bash eval variable-assignment quotes


    【解决方案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

    只有两个地方可以省略引号,因为它们被定义为不扩展路径名和拆分字段:变量赋值和casePOSIX 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 版本更好。
        猜你喜欢
        • 2013-04-14
        • 2022-01-10
        • 2015-12-07
        • 2020-12-29
        • 2010-12-29
        • 1970-01-01
        相关资源
        最近更新 更多