190

在阅读了 bash 手册页和关于这篇文章之后

我仍然无法理解该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

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

4

10 回答 10

223

eval接受一个字符串作为它的参数,并评估它,就好像你在命令行上输入了那个字符串一样。(如果你传递了几个参数,它们首先用它们之间的空格连接起来。)

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

echo ${!n}
one

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

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

请注意,大多数情况下,您必须在变量替换和命令替换周围使用双引号(即,只要有 a $"$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 中,最常见的用途是获取名称直到运行时才知道的变量的值。${!VAR}在 bash 中,由于语法原因,这不是必需的。eval当您需要构建包含运算符、保留字等的较长命令时,它仍然很有用。

于 2012-06-16T16:32:37.527 回答
45

简单地将 eval 视为“在执行前再评估一次表达式”

eval echo \${$n}echo $1第一轮评估后就变成了。需要注意的三个变化:

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

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

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

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

于 2012-06-16T16:19:06.237 回答
32

根据我的经验,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!
于 2014-10-22T17:29:13.357 回答
12

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 >
于 2014-09-23T09:12:59.513 回答
7

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


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

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

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

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

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

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

  3. 测试时,您可以将eval语句替换为echo并查看显示的内容。如果它是当前上下文中的合法代码,那么运行它就eval可以了。


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

示例 1:

eval“正常”代码前面的语句是 NOP

$ eval a=b
$ eval echo $a
b

在上面的示例中,第一个eval语句没有任何目的,可以删除。eval在第一行中没有意义,因为代码没有动态方面,即它已经解析为 bash 代码的最后几行,因此它与 bash 脚本中的正常代码语句相同。第二个eval也是没有意义的,因为虽然有一个解析步骤转换$a为它的文字字符串等价物,但没有间接性(例如,没有通过实际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:

发现名称包含在字符串中的变量本身是否包含字符串值。

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如何解释上面这行真实代码,以得出由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 的值是否为空,如果是,则提示用户提供一个值。

于 2017-10-26T00:28:29.063 回答
3

本来我是故意没学过 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 并不是不安全的,因为我们事先就知道我们将要评估什么。

于 2018-01-05T19:12:30.183 回答
2

我最近不得不使用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.

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

于 2018-01-05T09:16:32.770 回答
1

您询问了典型用途。

关于 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)

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

于 2018-03-18T10:34:00.893 回答
0

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

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://stackoverflow.com/a/40646371/744133

于 2016-11-17T04:38:03.187 回答
0

在问题中:

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 程序会引入多少错误?许多程序员喜欢神秘。

于 2017-09-28T04:46:38.950 回答