eval command in Bash and its typical uses
在阅读了bash手册页并针对此帖子之后。
我仍然很难理解
1 2 3 4 5 6 7 8 9 10 | 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 |
这里到底发生了什么,美元符号和反斜杠如何与问题联系在一起?
1 2 | echo ${!n} one |
请注意,在大多数情况下,必须在变量替换和命令替换周围使用双引号(即在任何存在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | $ 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视为"在执行之前再评估一次表达式"
在第一轮评估后,
-
\$ 变为$ (需要反斜杠,否则它将尝试计算${$n} ,这意味着名为{$n} 的变量,不允许使用) -
$n 被评估为1 -
eval 消失了
在第二轮中,基本上
因此,
当您要动态创建变量或从专门设计用于读取的程序中读取输出时,使用
以我的经验,"典型"使用eval来运行生成外壳程序命令以设置环境变量的命令。
也许您有一个使用环境变量集合的系统,并且有一个脚本或程序来确定应设置的变量及其值。每当您运行脚本或程序时,它都会在分支过程中运行,因此直接退出环境变量时,所做的任何操作都会丢失。但是该脚本或程序可以将导出命令发送到stdout。
如果没有eval,则需要将stdout重定向到一个临时文件,提供该临时文件的源,然后将其删除。使用eval,您可以:
1 | eval"$(script-or-program)" |
注意引号很重要。举这个(人为的)例子:
1 2 3 4 5 6 7 8 9 10 11 12 | # 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语句告诉外壳程序将eval的参数作为命令并通过命令行运行它们。在以下情况下很有用:
在脚本中,如果要将命令定义为变量,以后再使用该命令,则应使用eval:
1 2 3 4 5 6 7 8 9 10 11 | /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。我不同意。我认为当损坏的输入可以传递给
好。
bash
好。
也许最直接的示例是bash程序,该程序打开另一个bash脚本作为文本文件,读取文本的每一行,并使用
好。
我很少需要
好。
好。
传递给
好。
语法和解析顺序很严格。如果结果不是可执行的bash代码行,则在脚本范围内,该程序将在尝试执行垃圾时在
好。
测试时,可以将
好。
以下示例可以帮助阐明eval的工作原理...
范例1:
好。
普通代码前面的
1 2 3 | $ eval a=b $ eval echo $a b |
在上面的示例中,前一个
好。
范例2:
好。
使用作为字符串值传递的变量名称执行变量分配。
1 2 3 4 5 | $ key="mykey" $ val="myval" $ eval $key=$val $ echo $mykey myval |
如果您要
好。
1 | mykey=myval |
作为字符串解析的最终结果,这将由eval执行,因此在最后执行echo语句的结果...
好。
范例3:
好。
向示例2添加更多间接访问
1 2 3 4 5 6 7 | $ keyA="keyB" $ valA="valB" $ keyB="that" $ valB="amazing" $ eval eval \$$keyA=\$$valA $ echo $that amazing |
上面的示例比前面的示例要复杂一些,它更多地依赖于bash的解析顺序和特性。
好。
1 2 3 4 | 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:
好。
发现其名称包含在字符串中的变量本身是否包含字符串值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 |
在第一次迭代中:
好。
1 | varname="myvarname_a" |
Bash将参数解析为
好。
1 | eval varval=\$$myvarname_a |
以下伪代码试图说明bash如何解释上述实代码行,以得出
好。
1 2 3 4 5 | 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) |
一旦完成所有解析,结果就是执行的结果,其效果是显而易见的,这表明
好。
1 | varval="User-provided" |
上面示例中的其余代码只是简单地测试以查看分配给$ varval的值是否为空,如果是,则提示用户提供一个值。
好。
好。
最初,我有意从未学习过如何使用eval,因为大多数人会建议您像瘟疫一样远离它。但是我最近发现了一个用例,使我因无法尽快识别它而变得面目全非。
如果您有要交互式运行的cron作业以进行测试,则可以使用cat查看文件的内容,然后复制并粘贴cron作业以运行它。不幸的是,这涉及到触摸鼠标,这在我的书中是一个罪过。
假设您在/etc/cron.d/repeatme中有一份cron作业,内容如下:
您不能将其作为脚本来执行,但所有垃圾都在其前面,但是我们可以使用cut摆脱所有垃圾,将其包装在子shell中,并使用eval执行字符串
cut命令仅打印出文件的第六个字段,并用空格分隔。然后,Eval执行该命令。
我在这里以cron作业为例,但是概念是格式化来自stdout的文本,然后评估该文本。
在这种情况下使用eval并不是不安全的,因为我们确切地知道我们将要事先评估的内容。
您询问了典型用途。
关于shell脚本的一个普遍抱怨是(据说)您不能通过引用来从函数中获取值。
但是实际上,您可以通过"评估"来引用。 被调用方可以传回要由调用方评估的变量分配列表。 它通过引用传递,因为调用者可以允许指定结果变量的名称-请参见下面的示例。 错误结果可以传递回标准名称,例如errno和errstr。
这是在bash中通过引用传递的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | #!/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 2 3 4 5 | [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) |
该文本输出中几乎没有带宽! 而且,如果使用多个输出行,则存在更多的可能性:例如,第一行可以用于变量分配,第二行可以用于连续的"思想流",但这超出了本文的范围。
最近,我不得不使用
1 | xargs -I_ cat _/{11..15}/{8..5}.jpg |
扩展到
1 | 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 |
但是我需要先完成第二个支撑扩展,
1 | 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 |
我能想到的最好的办法是
1 | xargs -I_ cat $(eval echo _/'{11..15}'/{8..5}.jpg) |
之所以行之有效,是因为单引号在
可能存在一些涉及嵌套括号扩展的狡猾方案,使它可以一步完成,但是如果有的话,我太老太笨了,看不到它。
在问题中:
1 | who | grep $(tty | sed s:/dev/::) |
输出错误,声称文件a和tty不存在。我理解这意味着tty不会在执行grep之前被解释,而是bash将tty作为参数传递给grep,后者将其解释为文件名。
还有嵌套重定向的情况,应由匹配的括号处理,该括号应指定一个子进程,但bash最初是一个单词分隔符,创建要发送给程序的参数,因此括号不首先匹配,而是解释为看过。
我特定于grep,并将文件指定为参数而不是使用管道。我还简化了基本命令,将命令的输出作为文件传递,这样就不会嵌套I / O管道:
1 | grep $(tty | sed s:/dev/::) <(who) |
效果很好。
1 | who | grep $(echo pts/3) |
并不是真正想要的,但是它消除了嵌套管道,并且效果很好。
总之,bash似乎不喜欢嵌套管道。重要的是要了解bash并不是以递归方式编写的新程序。相反,bash是一个旧的1,2,3程序,它已附加了功能。为了确保向后兼容,从未修改过初始的解释方式。如果将bash重写为第一个匹配括号,那么将有多少个bug引入多少个bash程序中?许多程序员喜欢保密。
我喜欢"在执行前再评估一次表达式"的答案,并想举另一个例子来说明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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中的Curious结果是,我们将通过以下两个参数:
-
第一个参数:
"value -
第二个参数:
content"
对于柜台而言,这是如何直观的?附加的
改编自https://stackoverflow.com/a/40646371/744133