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

在阅读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 

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

eval接受一个string作为它的参数,并且如同你在命令行上键入那个string一样评估它。 (如果你传递了几个参数,它们首先与它们之间的空格相连接。)

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

 echo ${!n} one 

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

eval echo \${$n}运行传递给eval的参数。 扩展之后,参数是echo${1} 。 所以eval echo \${$n}运行命令echo ${1}

请注意,大多数情况下,必须在variablesreplace和命令可疑(即任何时候存在$ )时使用双引号: "$foo", "$(foo)"总是把双引号放在variables和命令replace周围 ,除非你知道你需要把它们closures。 如果没有双引号,shell会执行字段拆分(即将variables的值或命令的输出拆分为单独的字),然后将每个字作为通配符模式处理。 例如:

 $ 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中,最常见的用途是获取名称在运行时才知道的variables的值。 在bash中,这是没有必要的感谢${!VAR}语法。 当你需要构造一个包含运算符,保留字等的更长的命令时, eval仍然有用。

简单地把eval想象成“在执行之前再额外一次评估你的表情”

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

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

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

所以eval <some command>将首先评估<some command> (通过在这里评估我的意思是replacevariables,用正确的replace转义字符等),然后再次运行结果expression式。

当你想要dynamic地创buildvariables或者从程序中读取输出时,使用eval就是为了像这样读取而devise的。 例子见http://mywiki.wooledge.org/BashFAQ/048 。 链接还包含使用eval一些典型方法,以及与之相关的风险。

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

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

如果没有eval,您需要将stdoutredirect到临时文件,获取临时文件,然后将其删除。 用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将eval的参数作为命令并通过命令行运行。 这在以下情况下非常有用:

在你的脚本中,如果你定义了一个命令到一个variables,然后你想使用该命令,那么你应该使用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 > 

更新:有人说,应该永远不要使用eval。 我不同意。 我认为,当腐败的投入可以传递给eval时,就会产生风险。 然而,在很多情况下,这并不是一个风险,因此在任何情况下都值得知道如何使用eval。 这个stackoverflow答案解释了eval和eval的替代品的风险。 最终,由用户决定是否/何时使用eval是安全和有效的。


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

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

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

eval在概念上是简单的。 但是,bash语言的严格语法,以及bash解释器的parsing顺序可以细微化,使得eval看起来很神秘,难以使用或理解。 这里是要领:

  1. 传递给eval的参数是在运行时计算的stringexpression式eval将执行其参数的最终parsing结果,作为脚本中的实际代码行。

  2. 语法和parsing顺序是严格的。 如果结果不是可执行的bash代码行,那么在您的脚本范围内,程序会在eval语句中崩溃,因为它试图执行垃圾。

  3. testing时,可以用echoreplaceeval语句,并查看显示内容。 如果它是当前上下文中的合法代码,则通过eval运行它将起作用。


下面的例子可能有助于澄清eval如何工作…

例1:

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

 $ eval a=b $ eval echo $a b 

在上面的例子中,第一个eval语句没有目的,可以被删除。 eval在第一行是没有意义的,因为代码中没有dynamic的方面,也就是说它已经被parsing成bash代码的最后一行,因此它和bash脚本中的正常代码语句是一样的。 第二个eval也是毫无意义的,因为虽然有一个parsing步骤将$a转换$a它的字面string等价,但是没有间接的(例如没有通过实际的 bash名词或bash持有的脚本variables的string值引用),所以它将行为与没有eval前缀的代码行相同。



例2:

使用作为string值传递的var名称执行var分配。

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

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

 mykey=myval 

是stringparsing的最终结果,是由eval执行的,因此最后的echo语句的结果是…



例3:

向示例2添加更多的间接性

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

上面的例子比前面的例子稍微复杂一些,更多地依赖于bash的parsing顺序和特性。 eval线大致按以下顺序在内部得到parsing(注意下面的语句是伪代码,而不是真正的代码,只是试图展示语句如何在内部被分解成步骤以得到最终结果)

  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 'this=amazing' by eval 

如果假定的parsing顺序不能解释eval在做什么,第三个例子可能会更详细地描述parsing,以帮助澄清正在发生的事情。



例4:

发现名称包含在string中的variablesvars是否包含string值。

 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将这个参数parsing为evaleval在运行时从字面上看这个:

 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) 

一旦完成了所有的parsing,结果就是执行了什么,其效果是显而易见的,说明eval本身并没有什么特别的神秘,复杂性在于它的论证的parsing

 varval="User-provided" 

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

我喜欢“在执行之前多加一次评估你的表情”的答案,并想用另一个例子来澄清。

 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

在这个问题上:

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

输出声明文件a和tty不存在的错误。 我明白这意味着tty在执行grep之前不会被解释,而是bash将tty作为parameter passing给grep,将其解释为文件名。

还有一个嵌套redirect的情况,应该由匹配的圆括号来处理,它应该指定一个subprocess,但是bash是原始的一个字分隔符,创build要发送给一个程序的参数,因此括号首先不匹配,但是被解释为看到。

我得到了具体的grep,并指定该文件作为参数,而不是使用pipe道。 我也简化了基本命令,将命令的输出作为文件传递,以便I / Opipe道不会被嵌套:

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

效果很好。

 who | grep $(echo pts/3) 

是不是真的想要,但消除了嵌套的pipe道,也很好。

总之,bash似乎不喜欢嵌套的小块。 理解bash不是以recursion方式编写的新浪程序很重要。 相反,bash是一个老的1,2,3程序,它已经被添加了特征。 为了确保向后兼容,最初的解释方式从未被修改过。 如果bash被重写为首先匹配括号,那么多less个bash程序会引入多less个错误? 许多程序员喜欢隐晦。