在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
看起来很神秘,难以使用或理解。 这里是要领:
-
传递给
eval
的参数是在运行时计算的stringexpression式 。eval
将执行其参数的最终parsing结果,作为脚本中的实际代码行。 -
语法和parsing顺序是严格的。 如果结果不是可执行的bash代码行,那么在您的脚本范围内,程序会在
eval
语句中崩溃,因为它试图执行垃圾。 -
testing时,可以用
echo
replaceeval
语句,并查看显示内容。 如果它是当前上下文中的合法代码,则通过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为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)
一旦完成了所有的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
将解决这个问题。
在这个问题上:
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个错误? 许多程序员喜欢隐晦。