【问题标题】:eval command in Bash and its typical usesBash 中的 eval 命令及其典型用途
【发布时间】:2012-06-19 08:58:21
【问题描述】:

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

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

【问题讨论】:

  • 作为记录,第二次尝试有效。 $($n) 在子 shell 中运行 $n。它尝试运行不存在的命令1
  • @MartinWickman 但要求是最终运行echo $1,而不是1。我不认为它可以使用 subshel​​ls 来完成。
  • @Raze2dust:我不相信他是在暗示它可以使用 subshel​​l 运行,而是在解释为什么 OP 列出的第 5 个命令不起作用。

标签: linux bash shell scripting eval


【解决方案1】:

只需将 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 的典型方式以及与之相关的风险。

【讨论】:

  • 作为第一个项目符号的注释,${VAR} 语法 是允许的,并且在有任何歧义时首选($VAR == $V , 后跟 AR$VAR == $VA 后跟 R)。 ${VAR} 等价于 $VAR。其实它的变量名$n是不允许的。
  • eval eval echo \\\${\${$i}} 将执行三次取消引用。我不确定是否有更简单的方法可以做到这一点。此外,\${$n} 在我的机器上工作正常(打印one)..
  • @Konos5 echo \\\${\${$i}} 打印 \${${n}}eval echo \\\${\${$i}} 等价于echo \${${n}}`` and prints ${1}. eval eval echo \\\${\${$i}}` 等价于eval echo ${1} 并打印one
  • @Konos5 沿着相同的思路思考 - 第一个 ` escapes the second one, and the third ` 之后转义了 $。所以经过一轮评估后变成\${${n}}
  • @Konos5 从左到右是考虑引号和反斜杠解析的正确方式。首先\\ 产生一个反斜杠。然后\$ 产生一美元。以此类推。
【解决方案2】:

eval 将字符串作为其参数,并像在命令行上键入该字符串一样对其进行评估。 (如果你传递了几个参数,它们之间首先用空格连接。)

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

echo ${!n}
one

$(…) 在子 shell 中运行括号内指定的命令(即在从当前 shell 继承所有设置(例如变量值)的单独进程中),并收集其输出。所以echo $($n)$n 作为shell 命令运行,并显示其输出。由于$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 在您需要构造包含运算符、保留字等的较长命令时仍然有用。

【讨论】:

  • 关于我上面的评论,eval 做了多少“通过”?
  • @Konos5 什么评论? eval 接收一个字符串(它本身可能是解析和评估的结果),并将其解释为代码 sn-p。
  • 在 Raze2dust 的回答下,我发表了评论。我现在倾向于相信 eval 主要用于解除引用目的。如果我输入 eval echo \${$n} 我会得到一个。但是,如果我输入 echo \${$n},我会得到 \${1}。我相信这是由于 eval 的“两遍”解析而发生的。我现在想知道如果我需要使用额外的 i=n 声明来三次取消引用会发生什么。在这种情况下,根据 Raze2dust,我只需要进行额外的评估。但是我相信应该有更好的方法......(它很容易变得混乱)
  • @Konos5 我不会使用eval eval。我不记得曾经有过这种需要。如果你真的需要两个eval 传递,使用一个临时变量,它会更容易调试:eval tmp="\${$i}"; eval x="\${$tmp}"
  • @Konos5 “解析两次”有点误导。由于难以在 Bash 中指定不受各种扩展影响的文字字符串参数,有些人可能会相信这一点。 eval 只是将代码放入字符串中并根据通常的规则对其进行评估。从技术上讲,它甚至是不正确的,因为在一些极端情况下,Bash 修改了解析,甚至不执行对 eval 参数的扩展——但这是一个非常晦涩的花絮,我怀疑有人知道。
【解决方案3】:

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】:

    根据我的经验,eval 的“典型”用途是运行生成 shell 命令以设置环境变量的命令。

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

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

    【讨论】:

    • 有任何常用工具的例子吗?该工具本身有一种方法可以生成一组可以传递给 eval 的 shell 命令?
    • @Joakim 我不知道有什么开源工具可以做到这一点,但它被用于我工作过的公司的一些私人脚本中。我刚刚开始自己​​使用 xampp 再次使用这种技术。 Apache .conf 文件扩展环境变量写入${varname}。我发现在几个不同的服务器上使用相同的 .conf 文件很方便,只需通过环境变量参数化一些东西。我编辑了 /opt/lampp/xampp(它启动了 apache),用一个脚本来执行这种 eval,该脚本在系统周围进行操作并输出 bash export 语句来定义 .conf 文件的变量。
    • @Joakim 另一种方法是使用脚本从模板生成每个受影响的 .conf 文件,基于相同的探索。我更喜欢我的方式的一件事是,在不通过 /opt/lampp/xampp 的情况下启动 apache 不会使用过时的输出脚本,而是因为环境变量扩展为空并创建无效指令而无法启动。
    • @Anthony Sottile 我看到您编辑了答案以在 $(script-or-program) 周围添加引号,说它们在运行多个命令时很重要。您能否提供一个示例 - 以下内容适用于 foo.sh 的标准输出中的分号分隔命令: echo '#!/bin/bash' > foo.sh; echo 'echo "echo -n a; echo -n b; echo -n c"' >>foo.sh; chmod 755 foo.sh;评估 $(./foo.sh)。这会在标准输出上产生 abc。运行 ./foo.sh 会产生: echo -n a;回声-n b;回声 -n c
    • 有关使用 eval 的常用工具的示例,请参阅pyenv。 pyenv 让您可以轻松地在多个 Python 版本之间切换。您将eval "$(pyenv init -)" 放入您的.bash_profile(或类似的)shell 配置文件中。这会构建一个小 shell 脚本,然后在当前 shell 中对其进行评估。
    【解决方案5】:

    我喜欢“在执行前再评估一次表达式”的答案,并想用另一个例子来澄清一下。

    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

    【讨论】:

      【解决方案6】:

      在问题中:

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

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

      【讨论】:

        【解决方案7】:

        更新:有人说不应该 - 永远 - 使用 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 运行它会起作用。


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

        示例 1:

        eval'normal' 代码前面的语句是 NOP

        $ eval a=b
        $ eval echo $a
        b
        

        在上面的例子中,第一个eval 语句没有任何用途,可以去掉。 eval 在第一行中毫无意义,因为代码没有动态方面,即它已经解析为 bash 代码的最后几行,因此它与 bash 脚本中的正常代码语句相同。第二个eval 也毫无意义,因为尽管有一个解析步骤将$a 转换为其等效的文字字符串,但没有间接性(例如,没有通过 actual bash 名词的字符串值引用或 bash 保存的脚本变量),因此它的行为与没有 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:

        发现 names 包含在字符串中的 vars 本身是否包含字符串值。

        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如何解释上面的real行strong> 代码,得到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,如果是,则提示用户提供一个值。

        【讨论】:

          【解决方案8】:

          我最近不得不使用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】:

            我本来是故意从不学习如何使用 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 命令只打印文件的第 6 个字段,由空格分隔。 eval 然后执行该命令。

            我在这里使用了一个 cron 作业作为示例,但其概念是从标准输出格式化文本,然后评估该文本。

            在这种情况下使用 eval 并不是不安全的,因为我们事先就知道要评估什么。

            【讨论】:

              【解决方案10】:

              您询问了典型用途。

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

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

              该文本输出中几乎有无限带宽!如果使用多个输出行,还有更多的可能性:例如,第一行可以用于变量赋值,第二行用于连续的“思想流”,但这超出了本文的范围。

              【讨论】:

              • 说有“更多的可能性”至少可以说是微不足道的、琐碎的和多余的。
              猜你喜欢
              • 2014-10-17
              • 2015-01-09
              • 2011-06-26
              • 1970-01-01
              • 2015-07-23
              • 2014-09-09
              • 1970-01-01
              • 2023-03-12
              • 2012-08-17
              相关资源
              最近更新 更多