Bash 中的“ eval”命令及其典型用法

在阅读了 Bash 手册和关于 这篇文章之后,我仍然难以理解 eval命令到底是做什么的,以及它的典型用途是什么。

例如,如果我们这样做:

$ set -- one two three  # Sets $1 $2 $3
$ echo $1
one


$ n=1
$ echo ${$n}       ## First attempt to echo $1 using brackets fails
bash: ${$n}: bad substitution


$ echo $($n)       ## Second attempt to echo $1 using parentheses fails
bash: 1: command not found


$ eval echo \${$n} ## Third attempt to echo $1 using 'eval' succeeds
one

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

443789 次浏览

简单地把 Eval想象成“在执行之前多计算一次表达式”

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

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

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

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

eval用于动态创建变量,或读取专门设计成这样读取的程序的输出。有关示例,请参见 Eval 命令和安全问题。该链接还包含使用 eval的一些典型方式以及与之相关的风险。

eval接受一个字符串作为它的参数,并像在命令行中键入该字符串一样对其进行计算。(如果传递多个参数,则首先使用它们之间的空格将它们连接起来。)

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

echo ${!n}
one

$(…)运行子 shell 中括号内指定的命令(即在一个单独的进程中,该进程从当前 shell 继承所有设置,例如变量值) ,并收集其输出。因此,echo $($n)以 shell 命令的形式运行 $n,并显示其输出。由于 $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 语句告诉 shell 接受 eval 的参数作为命令,并通过命令行运行它们。它在下面这种情况下是有用的:

在您的脚本中,如果您正在将一个命令定义为一个变量,并且稍后您想使用该命令,那么您应该使用 Eval:

a="ls | more"
$a

产出:

命令未找到: ls | more

上面的命令不起作用,因为 是的试图列出名称管道(|)和更多的文件。但这些文件并不存在:

eval $a

产出:

File.txt
邮差
Remote _ cmd. sh
Sample.txt
TMP

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

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

如果没有 eval,则需要将标准输出重定向到一个临时文件,找到临时文件的源文件,然后删除它。有了 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!

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

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中的 好奇结果是,我们将传递以下两个参数:

  • 第一个参数: "par1
  • 第二个参数: par2"

这是如何反直觉? 额外的 eval将修复这一点。

它改编自 如何使用 Bash 引用变量文件?台的 另一个答案

问题是:

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

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

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

我使用 grep 获得了具体信息,并将该文件指定为一个参数,而不是使用管道。我还简化了 base 命令,将命令的输出作为文件传递,这样 i/o 管道就不会被嵌套:

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

效果很好。

who | grep $(echo pts/3)

并不是真正需要的,但是消除了嵌套管道,并且工作得很好。

总之,bash 似乎不喜欢嵌套的 pipping。重要的是要理解 bash 不是以递归方式编写的新 wave 程序。相反,bash 是一个旧的1,2,3程序,它附加了一些特性。为了确保向下兼容,最初的解释方式从未改变。如果 bash 被重写为第一个匹配括号,那么会在多少 bash 程序中引入多少 bug?许多程序员喜欢神秘兮兮的。

更新: 有些人说永远不要使用 eval。我不同意。我认为当损坏的输入可以传递给 eval时,风险就会出现。然而,在许多常见的情况下,这并不是一种风险,因此了解如何在任何情况下使用 eval 都是值得的。这个 堆栈溢出答案解释了 eval 和 eval 的替代方案的风险。最终由用户决定 eval 是否/何时可以安全有效地使用。


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

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

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

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

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

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

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


下面的示例可能有助于阐明 eval 是如何工作的..。

例一:

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

$ eval a=b
$ eval echo $a
b

在上面的示例中,第一个 eval语句没有任何用途,可以删除。eval在第一行没有意义,因为代码没有动态方面,也就是说,它已经解析成 bash 代码的最后几行,因此它将与 bash 脚本中的普通代码语句相同。第二个 eval也没有意义,因为尽管有一个解析步骤将 $a转换为字符串等价物,但是没有间接的(例如,没有通过 真的 bash 名词的字符串值或 bash 持有的脚本变量进行引用) ,所以它的行为将与没有 eval前缀的一行代码完全相同。



例二:

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

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

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

mykey=myval

作为字符串解析的最终结果, 将由 eval 执行,因此是最后的 echo 语句的结果..。



例三:

向示例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 做了什么,那么第三个示例可以更详细地描述解析,以帮助阐明正在发生的事情。



例子四:

了解包含在字符串中的 名字的 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

下面的 < em > 伪代码 试图说明 怎么做 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 的值是否为 null,如果为 null,则提示用户提供一个值。

我最近不得不使用 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 展开。

可能有一些巧妙的方案,包括嵌套的扩展支撑,允许这种情况发生在一个步骤,但如果有我太老和愚蠢,看到它。

我原来故意从来没有学过如何使用 eval,因为大多数人都会建议像瘟疫一样远离它。然而,我最近发现了一个用例,这个用例让我因为没有更快地识别它而感到不适。

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

假设您有一个 cron 作业,位于/etc/cron.d/repeat me,内容是:

*/10 * * * * root program arg1 arg2

您不能将它作为脚本执行,但是我们可以使用 cut 来清除所有的垃圾,将其包装在子 shell 中,并使用 eval 执行字符串

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

Cut 命令只打印出文件的第6个字段,该字段由空格分隔。

我在这里使用 cron 作业作为示例,但其概念是从 stdout 格式化文本,然后对该文本进行计算。

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

你问的是典型用途。

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

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

正如 clearlight 所说,“(p)也许最直接的例子是 bash 程序,它以文本文件的形式打开另一个 bash 脚本,读取每一行文本,并使用 eval按顺序执行它们”。我不是专家,但是我目前正在阅读的教科书(Jürgen Wolf 编写的 Shell-Programmierung)指出了一个特定的用法,我认为这对于这里收集的潜在用例集是一个有价值的补充。

出于调试目的,您可能希望逐行查看脚本(每个步骤按 Enter)。您可以使用 eval通过捕获 DEBUG 信号(我认为它在每一行之后发送)来执行每一行:

trap 'printf "$LINENO :-> " ; read line ; eval $line' DEBUG