关于Bash中的linux:eval命令及其典型用法

eval command in Bash and its typical uses

在阅读了bash手册页并针对此帖子之后。

我仍然很难理解eval命令的确切功能以及这将是其典型用法。 例如,如果我们这样做:

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

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


eval将字符串作为其参数,并对其进行评估,就像在命令行上键入该字符串一样。 (如果您传递几个参数,则它们首先会以空格隔开。)

${$n}是bash中的语法错误。在大括号内,您只能具有一个变量名,并带有一些可能的前缀和后缀,但是您不能具有任意的bash语法,尤其是不能使用变量扩展。尽管有这样一种说法:"名称在此变量中的变量的值":

1
2
echo ${!n}
one

$(…)在子外壳程序中(即在一个单独的过程中,在括号内指定的命令运行),并从当前外壳程序继承变量值之类的所有设置),并收集其输出。因此echo $($n)$n作为shell命令运行,并显示其输出。由于$n的值为1,因此$($n)尝试运行命令1,该命令不存在。

eval echo \${$n}运行传递给eval的参数。扩展后,参数为echo${1}。因此eval echo \${$n}运行命令echo ${1}

请注意,在大多数情况下,必须在变量替换和命令替换周围使用双引号(即在任何存在$的地方):"$foo","$(foo)"。始终在变量和命令替换两边加上双引号,除非您知道需要将其保留。如果不使用双引号,则Shell会执行字段拆分(即,它将变量的值或命令的输出拆分为单独的单词),然后将每个单词视为通配符模式。例如:

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很少使用。在某些Shell中,最常见的用法是获取变量的值,该变量的名称直到运行时才知道。在bash中,由于${!VAR}语法,因此没有必要。当需要构造包含运算符,保留字等的较长命令时,eval仍然有用。


只需将eval视为"在执行之前再评估一次表达式"

在第一轮评估后,eval echo \${$n}变为echo $1。注意三点变化:

  • \$变为$(需要反斜杠,否则它将尝试计算${$n},这意味着名为{$n}的变量,不允许使用)
  • $n被评估为1
  • eval消失了

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

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

当您要动态创建变量或从专门设计用于读取的程序中读取输出时,使用eval。有关示例,请参见http://mywiki.wooledge.org/BashFAQ/048。该链接还包含使用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。我不同意。我认为当损坏的输入可以传递给eval时会产生风险。但是,在很多情况下这都不是风险,因此在任何情况下都应了解如何使用eval。这个stackoverflow答案说明了评估的风险以及评估的替代方法。最终,由用户来确定eval是否/何时安全有效地使用。

好。

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

好。

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

好。

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

好。

eval在概念上很简单。但是,bash语言的严格语法以及bash解释器的解析顺序可能会发生细微差别,并使eval显得晦涩难懂且难以使用或理解。这里是要领:

好。

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

    好。

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

    好。

  • 测试时,可以将eval语句替换为echo,然后查看显示的内容。如果在当前上下文中它是合法代码,则可以通过eval运行它。

    好。

  • 以下示例可以帮助阐明eval的工作原理...

    范例1:

    好。

    普通代码前面的eval语句是NOP

    1
    2
    3
    $ eval a=b
    $ eval echo $a
    b

    在上面的示例中,前一个eval语句没有任何目的,可以删除。 eval在第一行是没有意义的,因为代码没有动态方面,即它已经被解析为bash代码的最后几行,因此它与bash脚本中的常规代码语句相同。第二个eval也是毫无意义的,因为尽管有一个解析步骤将$a转换为其等效的文字字符串,但没有间接寻址(例如,不通过实际bash名词或bash持有的脚本变量的字符串值进行引用) ,因此它的行为与没有eval前缀的代码行相同。

    好。

    范例2:

    好。

    使用作为字符串值传递的变量名称执行变量分配。

    1
    2
    3
    4
    5
    $ key="mykey"
    $ val="myval"
    $ eval $key=$val
    $ echo $mykey
    myval

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

    好。

    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的解析顺序和特性。 eval行将大致按以下顺序在内部进行解析(请注意,以下语句是伪代码,而不是实际代码,只是为了试图说明该语句如何在内部分解为多个步骤以得出最终结果)。

    好。

    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将参数解析为eval,而eval实际上在运行时看到了这一点:

    好。

    1
    eval varval=\$$myvarname_a

    以下伪代码试图说明bash如何解释上述实代码行,以得出eval执行的最终值。 (以下几行是描述性的,而不是确切的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)

    一旦完成所有解析,结果就是执行的结果,其效果是显而易见的,这表明eval本身没有什么特别神秘的地方,而复杂性在于其参数的解析。

    好。

    1
    varval="User-provided"

    上面示例中的其余代码只是简单地测试以查看分配给$ varval的值是否为空,如果是,则提示用户提供一个值。

    好。

    好。


    最初,我有意从未学习过如何使用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)

    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)

    该文本输出中几乎没有带宽! 而且,如果使用多个输出行,则存在更多的可能性:例如,第一行可以用于变量分配,第二行可以用于连续的"思想流",但这超出了本文的范围。


    最近,我不得不使用eval来强制按照需要的顺序评估多个大括号扩展。 Bash从左到右执行了多个括号扩展,因此

    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)

    之所以行之有效,是因为单引号在eval命令行的解析过程中保护了第一组花括号免于扩展,而使它们由eval调用的子shell扩展。

    可能存在一些涉及嵌套括号扩展的狡猾方案,使它可以一步完成,但是如果有的话,我太老太笨了,看不到它。


    在问题中:

    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"

    对于柜台而言,这是如何直观的?附加的eval将解决此问题。

    改编自https://stackoverflow.com/a/40646371/744133