Bash 中的 eval 命令及其典型用途

Posted

技术标签:

【中文标题】Bash 中的 eval 命令及其典型用途【英文标题】:eval command in Bash and its typical uses 【发布时间】:2012-06-19 08:58:21 【问题描述】:

在阅读了 bash 手册页和关于 post 之后。

我仍然无法理解 eval 命令的确切作用以及它的典型用途。例如,如果我们这样做:

bash$ set -- one two three  # sets $1 $2 $3
bash$ echo $1
one
bash$ n=1
bash$ echo $$n       ## First attempt to echo $1 using brackets fails
bash: $$n: bad substitution
bash$ echo $($n)       ## Second attempt to echo $1 using parentheses fails
bash: 1: command not found
bash$ eval echo \$$n ## Third attempt to echo $1 using 'eval' succeeds
one

这里到底发生了什么,美元符号和反斜杠如何与问题联系起来?

【问题讨论】:

作为记录,第二次尝试有效。 $($n) 在子 shell 中运行 $n。它尝试运行不存在的命令1 @MartinWickman 但要求是最终运行echo $1,而不是1。我不认为它可以使用 subshel​​ls 来完成。 你应该知道security implications of using eval @Raze2dust:我不相信他是在暗示它可以使用 subshel​​l 运行,而是在解释为什么 OP 列出的第 5 个命令不起作用。 【参考方案1】:

只需将 eval 视为“在执行前再评估一次表达式”

eval echo \$$n在第一轮评估后变为echo $1。需要注意的三个变化:

\$变成$(需要反斜杠,否则它会尝试计算$$n,这意味着一个名为$n的变量,这是不允许的) $n 被评估为 1 eval 消失了

第二轮基本是echo $1,可以直接执行。

所以eval <some command> 将首先评估<some command>(这里的评估是指替换变量,用正确的字符替换转义字符等),然后再次运行结果表达式。

eval 用于动态创建变量,或从专门设计为这样读取的程序中读取输出。有关示例,请参阅http://mywiki.wooledge.org/BashFAQ/048。该链接还包含一些使用eval 的典型方式以及与之相关的风险。

【讨论】:

作为第一个项目符号的注释,$VAR 语法 是允许的,并且在有任何歧义时首选($VAR == $V , 后跟 AR$VAR == $VA 后跟 R)。 $VAR 等价于 $VAR。其实它的变量名$n是不允许的。 eval eval echo \\\$\$$i 将执行三次取消引用。我不确定是否有更简单的方法可以做到这一点。此外,\$$n 在我的机器上工作正常(打印one).. @Konos5 echo \\\$\$$i 打印 \$$neval echo \\\$\$$i 等价于echo \$$n`` and prints $1. eval eval echo \\\$\$$i` 等价于eval echo $1 并打印one @Konos5 沿着相同的思路思考 - 第一个 ` escapes the second one, and the third ` 之后转义了 $。所以经过一轮评估后变成\$$n @Konos5 从左到右是考虑引号和反斜杠解析的正确方式。首先\\ 产生一个反斜杠。然后\$ 产生一美元。以此类推。【参考方案2】:

eval 将字符串作为其参数,并像在命令行上键入该字符串一样对其进行评估。 (如果你传递了几个参数,它们之间首先用空格连接。)

$$n 是 bash 中的语法错误。在大括号内,你只能有一个变量名,带有一些可能的前缀和后缀,但你不能有任意的 bash 语法,特别是你不能使用变量扩展。不过,有一种说法是“名称在此变量中的变量的值”:

echo $!n
one

$(…) 在子 shell 中运行括号内指定的命令(即在从当前 shell 继承所有设置(例如变量值)的单独进程中),并收集其输出。所以echo $($n)$n 作为shell 命令运行,并显示其输出。由于$n 的计算结果为1$($n) 尝试运行不存在的命令1

eval echo \$$n 运行传递给eval 的参数。展开后参数为echo$1。所以eval echo \$$n 运行命令echo $1

请注意,大多数情况下,您必须在变量替换和命令替换周围使用双引号(即,只要有 $):"$foo", "$(foo)"始终在变量和命令替换周围加上双引号,除非您知道需要将它们去掉。如果没有双引号,shell 会执行字段拆分(即,它将变量的值或命令的输出拆分为单独的单词),然后将每个单词视为通配符模式。例如:

$ ls
file1 file2 otherfile
$ set -- 'f* *'
$ echo "$1"
f* *
$ echo $1
file1 file2 file1 file2 otherfile
$ n=1
$ eval echo \$$n
file1 file2 file1 file2 otherfile
$eval echo \"\$$n\"
f* *
$ echo "$!n"
f* *

eval 不经常使用。在某些 shell 中,最常见的用途是获取名称直到运行时才知道的变量的值。在 bash 中,由于$!VAR 语法,这不是必需的。 eval 在您需要构造包含运算符、保留字等的较长命令时仍然有用。

【讨论】:

关于我上面的评论,eval 做了多少“通过”? @Konos5 什么评论? eval 接收一个字符串(它本身可能是解析和评估的结果),并将其解释为代码 sn-p。 在 Raze2dust 的回答下,我发表了评论。我现在倾向于相信 eval 主要用于解除引用目的。如果我输入 eval echo \$$n 我会得到一个。但是,如果我输入 echo \$$n,我会得到 \$1。我相信这是由于 eval 的“两遍”解析而发生的。我现在想知道如果我需要使用额外的 i=n 声明来三次取消引用会发生什么。在这种情况下,根据 Raze2dust,我只需要进行额外的评估。但是我相信应该有更好的方法......(它很容易变得混乱) @Konos5 我不会使用eval eval。我不记得曾经有过这种需要。如果你真的需要两个eval 传递,使用一个临时变量,它会更容易调试:eval tmp="\$$i"; eval x="\$$tmp" @Konos5 “解析两次”有点误导。由于难以在 Bash 中指定不受各种扩展影响的文字字符串参数,有些人可能会相信这一点。 eval 只是将代码放入字符串中并根据通常的规则对其进行评估。从技术上讲,它甚至是不正确的,因为在一些极端情况下,Bash 修改了解析,甚至不执行对 eval 参数的扩展——但这是一个非常晦涩的花絮,我怀疑有人知道。【参考方案3】:

eval 语句告诉 shell 将 eval 的参数作为命令并通过命令行运行它们。它在以下情况下很有用:

在你的脚本中,如果你将一个命令定义为一个变量,然后你想使用那个命令,那么你应该使用 eval:

/home/user1 > a="ls | more"
/home/user1 > $a
bash: command not found: ls | more
/home/user1 > # Above command didn't work as ls tried to list file with name pipe (|) and more. But these files are not there
/home/user1 > eval $a
file.txt
mailids
remote_cmd.sh
sample.txt
tmp
/home/user1 >

【讨论】:

【参考方案4】:

根据我的经验,eval 的“典型”用途是运行生成 shell 命令以设置环境变量的命令。

也许您有一个使用环境变量集合的系统,并且您有一个脚本或程序来确定应该设置哪些变量及其值。每当您运行脚本或程序时,它都会在分叉的进程中运行,因此它直接对环境变量执行的任何操作都会在退出时丢失。但该脚本或程序可以将导出命令发送到标准输出。

如果没有 eval,您需要将 stdout 重定向到临时文件,获取临时文件,然后将其删除。使用 eval,您可以:

eval "$(script-or-program)"

注意引号很重要。举这个(人为的)例子:

# activate.sh
echo 'I got activated!'

# test.py
print("export foo=bar/baz/womp")
print(". activate.sh")

$ eval $(python test.py)
bash: export: `.': not a valid identifier
bash: export: `activate.sh': not a valid identifier
$ eval "$(python test.py)"
I got activated!

【讨论】:

有任何常用工具的例子吗?该工具本身有一种方法可以生成一组可以传递给 eval 的 shell 命令? @Joakim 我不知道有什么开源工具可以做到这一点,但它被用于我工作过的公司的一些私人脚本中。我刚刚开始自己​​使用 xampp 再次使用这种技术。 Apache .conf 文件扩展环境变量写入$varname。我发现在几个不同的服务器上使用相同的 .conf 文件很方便,只需通过环境变量参数化一些东西。我编辑了 /opt/lampp/xampp(它启动了 apache),用一个脚本来执行这种 eval,该脚本在系统周围进行操作并输出 bash export 语句来定义 .conf 文件的变量。 @Joakim 另一种方法是使用脚本从模板生成每个受影响的 .conf 文件,基于相同的探索。我更喜欢我的方式的一件事是,在不通过 /opt/lampp/xampp 的情况下启动 apache 不会使用过时的输出脚本,而是因为环境变量扩展为空并创建无效指令而无法启动。 @Anthony Sottile 我看到您编辑了答案以在 $(script-or-program) 周围添加引号,说它们在运行多个命令时很重要。您能否提供一个示例 - 以下内容适用于 foo.sh 的标准输出中的分号分隔命令: echo '#!/bin/bash' > foo.sh; echo 'echo "echo -n a; echo -n b; echo -n c"' >>foo.sh; chmod 755 foo.sh;评估 $(./foo.sh)。这会在标准输出上产生 abc。运行 ./foo.sh 会产生: echo -n a;回声-n b;回声 -n c 有关使用 eval 的常用工具的示例,请参阅pyenv。 pyenv 让您可以轻松地在多个 Python 版本之间切换。您将eval "$(pyenv init -)" 放入您的.bash_profile(或类似的)shell 配置文件中。这会构建一个小 shell 脚本,然后在当前 shell 中对其进行评估。【参考方案5】:

我喜欢“在执行前再评估一次表达式”的答案,并想用另一个例子来澄清一下。

var="\"par1 par2\""
echo $var # prints nicely "par1 par2"

function cntpars() 
  echo "  > Count: $#"
  echo "  > Pars : $*"
  echo "  > par1 : $1"
  echo "  > par2 : $2"

  if [[ $# = 1 && $1 = "par1 par2" ]]; then
    echo "  > PASS"
  else
    echo "  > FAIL"
    return 1
  fi


# Option 1: Will Pass
echo "eval \"cntpars \$var\""
eval "cntpars $var"

# Option 2: Will Fail, with curious results
echo "cntpars \$var"
cntpars $var

选项 2 的奇怪结果是我们将传递 2 个参数,如下所示:

第一个参数:"value 第二个参数:content"

反直觉如何?额外的eval 将解决这个问题。

改编自https://***.com/a/40646371/744133

【讨论】:

【参考方案6】:

在问题中:

who | grep $(tty | sed s:/dev/::)

输出错误,声称文件 a 和 tty 不存在。我理解这意味着 tty 在执行 grep 之前没有被解释,而是 bash 将 tty 作为参数传递给 grep,grep 将其解释为文件名。

还有嵌套重定向的情况,应该通过匹配的括号来处理,应该指定一个子进程,但是bash原始是一个单词分隔符,创建参数发送给程序,因此括号不首先匹配,但解释为所见。

我使用 grep 进行了具体操作,并将文件指定为参数而不是使用管道。我还简化了基本命令,将命令的输出作为文件传递,这样 i/o 管道就不会被嵌套:

grep $(tty | sed s:/dev/::) <(who)

效果很好。

who | grep $(echo pts/3)

并不是真正想要的,但消除了嵌套管道并且效果很好。

总之,bash 似乎不喜欢嵌套的 pipping。重要的是要了解 bash 不是以递归方式编写的新浪潮程序。相反,bash 是一个旧的 1,2,3 程序,它已经附加了一些特性。为了确保向后兼容,最初的解释方式从未被修改过。如果 bash 被重写为首先匹配括号,那么多少个 bash 程序会引入多少错误?许多程序员喜欢神秘。

【讨论】:

【参考方案7】:

更新:有人说不应该 - 永远 - 使用 eval。我不同意。我认为当损坏的输入可以传递给eval 时,风险就会出现。但是,在许多常见情况下,这不是风险,因此在任何情况下都值得了解如何使用 eval。这个*** answer 解释了评估的风险和评估的替代方案。最终由用户决定是否/何时使用 eval 安全有效。


bash eval 语句允许您执行由 bash 脚本计算或获取的代码行。

也许最直接的例子是一个 bash 程序,它打开另一个 bash 脚本作为文本文件,读取每一行文本,并使用eval 按顺序执行它们。这与 bash source 语句的行为基本相同,除非有必要对导入脚本的内容执行某种转换(例如过滤或替换)。

我很少需要eval,但我发现读取或写入其名称 包含在分配给其他变量的字符串中的变量很有用。例如,对变量集执行操作,同时保持较小的代码占用空间并避免冗余。

eval 在概念上很简单。但是,bash 语言的严格语法和 bash 解释器的解析顺序可能会很微妙,使eval 显得晦涩难懂,难以使用或理解。以下是要点:

    传递给eval 的参数是一个在运行时计算的字符串表达式eval 将在您的脚本中将其参数的最终解析结果作为实际行代码执行。

    语法和解析顺序非常严格。如果结果不是可执行的 bash 代码行,在您的脚本范围内,程序将在尝试执行垃圾时在 eval 语句上崩溃。

    测试时,您可以将 eval 语句替换为 echo 并查看显示的内容。如果它是当前上下文中的合法代码,则通过eval 运行它会起作用。


以下示例可能有助于阐明 eval 的工作原理...

示例 1:

eval'normal' 代码前面的语句是 NOP

$ eval a=b
$ eval echo $a
b

在上面的例子中,第一个eval 语句没有任何用途,可以去掉。 eval 在第一行中毫无意义,因为代码没有动态方面,即它已经解析为 bash 代码的最后几行,因此它与 bash 脚本中的正常代码语句相同。第二个eval 也毫无意义,因为尽管有一个解析步骤将$a 转换为其等效的文字字符串,但没有间接性(例如,没有通过 actual bash 名词的字符串值引用或 bash 保存的脚本变量),因此它的行为与没有 eval 前缀的代码行相同。



示例 2:

使用作为字符串值传递的 var 名称执行 var 赋值。

$ key="mykey"
$ val="myval"
$ eval $key=$val
$ echo $mykey
myval

如果你是echo $key=$val,输出将是:

mykey=myval

那个,作为字符串解析的最终结果,就是eval会执行的,所以最后是echo语句的结果……



示例 3:

为示例 2 添加更多间接性

$ keyA="keyB"
$ valA="valB"
$ keyB="that"
$ valB="amazing"
$ eval eval \$$keyA=\$$valA
$ echo $that
amazing

上面的例子比前面的例子要复杂一些,更多地依赖于 bash 的解析顺序和特性。 eval 行将大致按以下顺序在内部进行解析 (请注意,以下语句是伪代码,不是真正的代码,只是为了说明语句如何在内部分解为步骤以达到最终结果).

 eval eval \$$keyA=\$$valA  # substitution of $keyA and $valA by interpreter
 eval eval \$keyB=\$valB    # convert '$' + name-strings to real vars by eval
 eval $keyB=$valB           # substitution of $keyB and $valB by interpreter
 eval that=amazing          # execute string literal 'that=amazing' by eval

如果假定的解析顺序不能充分解释 eval 的作用,则第三个示例可能会更详细地描述解析以帮助阐明正在发生的事情。



示例 4:

发现 names 包含在字符串中的 vars 本身是否包含字符串值。

a="User-provided"
b="Another user-provided optional value"
c=""

myvarname_a="a"
myvarname_b="b"
myvarname_c="c"

for varname in "myvarname_a" "myvarname_b" "myvarname_c"; do
    eval varval=\$$varname
    if [ -z "$varval" ]; then
        read -p "$varname? " $varname
    fi
done

在第一次迭代中:

varname="myvarname_a"

Bash 将参数解析为 eval,而 eval 在运行时看到字面意思:

eval varval=\$$myvarname_a

下面的伪代码试图说明bash如何解释上面的real行strong> 代码,得到eval 执行的最终值。 (以下几行是描述性的,不是确切的 bash 代码):

1. eval varval="\$" + "$varname"      # This substitution resolved in eval statement
2. .................. "$myvarname_a"  # $myvarname_a previously resolved by for-loop
3. .................. "a"             # ... to this value
4. eval "varval=$a"                   # This requires one more parsing step
5. eval varval="User-provided"        # Final result of parsing (eval executes this)

全部解析完成,结果就是执行,效果很明显,说明eval本身并没有什么特别神秘的地方,复杂的地方在于解析它的论点。

varval="User-provided"

上面示例中的其余代码只是测试分配给 $varval 的值是否为 null,如果是,则提示用户提供一个值。

【讨论】:

【参考方案8】:

我最近不得不使用eval 来强制按我需要的顺序评估多个大括号扩展。 Bash 从左到右进行多个大括号扩展,所以

xargs -I_ cat _/11..15/8..5.jpg

扩展到

xargs -I_ cat _/11/8.jpg _/11/7.jpg _/11/6.jpg _/11/5.jpg _/12/8.jpg _/12/7.jpg _/12/6.jpg _/12/5.jpg _/13/8.jpg _/13/7.jpg _/13/6.jpg _/13/5.jpg _/14/8.jpg _/14/7.jpg _/14/6.jpg _/14/5.jpg _/15/8.jpg _/15/7.jpg _/15/6.jpg _/15/5.jpg

但我需要先完成第二个大括号扩展,从而产生

xargs -I_ cat _/11/8.jpg _/12/8.jpg _/13/8.jpg _/14/8.jpg _/15/8.jpg _/11/7.jpg _/12/7.jpg _/13/7.jpg _/14/7.jpg _/15/7.jpg _/11/6.jpg _/12/6.jpg _/13/6.jpg _/14/6.jpg _/15/6.jpg _/11/5.jpg _/12/5.jpg _/13/5.jpg _/14/5.jpg _/15/5.jpg

我能想到的最好的办法是

xargs -I_ cat $(eval echo _/'11..15'/8..5.jpg)

这是因为单引号保护第一组大括号在解析eval 命令行期间不被扩展,让它们被eval 调用的子shell 扩展。

可能有一些涉及嵌套大括号扩展的狡猾方案,可以让这一步发生,但如果有的话,我太老太笨了,看不到它。

【讨论】:

【参考方案9】:

我本来是故意从不学习如何使用 eval 的,因为大多数人会建议像瘟疫一样远离它。然而,我最近发现了一个用例,让我因为没有早点识别它而脸红。

如果您有要以交互方式运行以测试的 cron 作业,您可以使用 cat 查看文件的内容,然后复制并粘贴 cron 作业以运行它。不幸的是,这涉及到触摸鼠标,这在我的书中是一种罪过。

假设您在 /etc/cron.d/repeatme 有一个 cron 作业,内容如下:

*/10 * * * * root program arg1 arg2

你不能把它作为一个脚本执行,前面有所有的垃圾,但是我们可以使用 cut 去掉所有的垃圾,将它包装在一个子shell中,然后用 eval 执行字符串

eval $( cut -d ' ' -f 6- /etc/cron.d/repeatme)

cut 命令只打印文件的第 6 个字段,由空格分隔。 eval 然后执行该命令。

我在这里使用了一个 cron 作业作为示例,但其概念是从标准输出格式化文本,然后评估该文本。

在这种情况下使用 eval 并不是不安全的,因为我们事先就知道要评估什么。

【讨论】:

【参考方案10】:

您询问了典型用途。

关于 shell 脚本的一个常见抱怨是您(据称)不能通过引用传递以从函数中取回值。

但实际上,通过“eval”,您可以通过引用传递。被调用者可以传回要由调用者评估的变量赋值列表。它是通过引用传递的,因为调用者可以指定结果变量的名称 - 请参见下面的示例。错误结果可以传回标准名称,如 errno 和 errstr。

这是一个在 bash 中通过引用传递的示例:

#!/bin/bash
isint()

    re='^[-]?[0-9]+$'
    [[ $1 =~ $re ]]


#args 1: name of result variable, 2: first addend, 3: second addend 
iadd()

    if isint $2 && isint $3 ; then
        echo "$1=$(($2+$3));errno=0"
        return 0
    else
        echo "errstr=\"Error: non-integer argument to iadd $*\" ; errno=329"
        return 1
    fi


var=1
echo "[1] var=$var"

eval $(iadd var A B)
if [[ $errno -ne 0 ]]; then
    echo "errstr=$errstr"
    echo "errno=$errno"
fi
echo "[2] var=$var (unchanged after error)"

eval $(iadd var $var 1)
if [[ $errno -ne 0 ]]; then
    echo "errstr=$errstr"
    echo "errno=$errno"
fi  
echo "[3] var=$var (successfully changed)"

输出如下所示:

[1] var=1
errstr=Error: non-integer argument to iadd var A B
errno=329
[2] var=1 (unchanged after error)
[3] var=2 (successfully changed)

该文本输出中几乎有无限带宽!如果使用多个输出行,还有更多的可能性:例如,第一行可以用于变量赋值,第二行用于连续的“思想流”,但这超出了本文的范围。

【讨论】:

说有“更多的可能性”至少可以说是微不足道的、琐碎的和多余的。

以上是关于Bash 中的 eval 命令及其典型用途的主要内容,如果未能解决你的问题,请参考以下文章

frame pointer及其用途

基于endomorphism优化scalar multiplication及其用途

linux下各文件夹的结构及其用途说明

Linux Bash

powershell 用于Web应用程序的Inventory SharePoint解决方案。这些文件中的每一个都搜索不同类型的解决方案及其用途。

这种 bash 文件名提取技术的用途?