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

时间:2012-06-16 16:13:41

标签: linux bash shell scripting 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

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

10 个答案:

答案 0 :(得分:178)

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仍然有用。

答案 1 :(得分:33)

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

第一轮评估后,

eval echo \${$n}变为echo $1。需要注意三点变化:

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

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

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

当您想要动态创建变量或从专门设计为可读的程序中读取输出时,使用

eval。有关示例,请参阅http://mywiki.wooledge.org/BashFAQ/048。该链接还包含一些使用eval的典型方式以及与之相关的风险。

答案 2 :(得分:22)

根据我的经验,一个典型的&#34;使用eval用于运行生成shell命令以设置环境变量的命令。

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

如果没有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!

答案 3 :(得分:7)

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 >

答案 4 :(得分:2)

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

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

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

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

eval在概念上很简单。但是,bash语言的严格语法和bash解释器的解析顺序可能会有细微差别,使eval看起来很神秘,难以使用或理解。以下是要点:

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

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

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

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

    示例1:

    在&#39; normal&#39;前面的

    eval声明代码是NOP

    $ eval a=b
    $ eval echo $a
    b
    

    在上面的示例中,第一个eval语句没有任何意义,可以删除。 eval在第一行中没有意义,因为代码没有动态方面,即它已经解析为bash代码的最后一行,因此它与bash脚本中的正常代码语句完全相同。第二个eval也没有意义,因为虽然有一个解析步骤将$a转换为它的文字字符串等价,但没有间接(例如,没有通过实际的字符串值引用 bash noun或bash-held脚本变量),因此它的行为与没有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的值是否为null,如果是,则提示用户提供值。

答案 5 :(得分:1)

我原本故意从未学过如何使用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)

剪切命令仅打印出文件的第6个字段,用空格分隔。然后Eval执行该命令。

我在这里以cron作为例子,但概念是从stdout格式化文本,然后评估该文本。

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

答案 6 :(得分: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

答案 7 :(得分:0)

在问题中:

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

输出声称文件a和tty不存在的错误。我理解这意味着在执行grep之前没有解释tty,而是bash将tty作为参数传递给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程序?许多程序员喜欢神秘。

答案 8 :(得分:0)

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

可能有一些涉及嵌套大括号扩展的狡猾方案允许这一步骤发生,但是如果我有太老和愚蠢的看法。

答案 9 :(得分:0)

您询问了典型用途。

关于shell脚本的一个常见抱怨是,您(据称)无法通过引用传递以从函数中取回值。

但实际上,通过&#34; eval&#34;,您可以通过引用传递。被调用者可以传回一个由调用者评估的变量赋值列表。它是通过引用传递的,因为调用者可以允许指定结果变量的名称 - 请参阅下面的示例。错误结果可以传回标准名称,如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)

文本输出中几乎有无限带宽!如果使用多个输出线,则有更多的可能性:例如,第一行可用于变量分配,第二行用于连续思考流,但这超出了范围这篇文章。