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


165

在阅读了bash手册页和关于此帖子之后

我仍然很难理解该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

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


1
为了记录,第二次尝试有效。在子外壳中$($n)运行$n。它试图运行1不存在的命令。
Martin Wickman 2012年

1
@MartinWickman但是要求echo $1最终要运行,而不是1。我不认为可以使用subshel​​l来完成。
哈里·梅农

5
您应该了解使用的安全隐患eval
暂停,直到另行通知。

1
@ Raze2dust:我不相信他暗示它可以与子壳一起运行,而是解释为什么列出的OP的第5条命令不起作用。
jedwards

Answers:


196

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

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

echo ${!n}
one

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

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

请注意,大部分的时间,你必须用双引号周围的变量替换和命令替换(即随时有一个$"$foo", "$(foo)"始终在变量和命令替换两边加上双引号,除非您知道需要将其省略。如果没有双引号,则外壳程序将执行字段拆分(即,它将变量的值或命令的输出拆分为单独的单词),然后将每个单词视为通配符模式。例如:

$ 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当您需要构建包含运算符,保留字等的较长命令时,它仍然很有用。


关于我上面的评论,评估进行了多少次“通过”?
kstratis 2012年

@ Konos5有何评论?eval接收一个字符串(它本身可能是解析和求值的结果),并将其解释为代码段。
吉尔斯(Gillles)“所以别再作恶了”

在Raze2dust的回答下,我发表了评论。我现在倾向于认为eval主要用于取消引用目的。如果输入eval echo \ $ {$ n},我得到一个。但是,如果键入echo \ $ {$ n},则会得到\ $ {1}。我相信这是由于eval的“两次通过”解析而发生的。我现在想知道如果我需要使用额外的i = n声明进行三重取消引用会发生什么情况。在这种情况下,根据Raze2dust,我只需要添加一个额外的评估。但是我相信应该有更好的方法...(它很容易变得混乱)
kstratis 2012年

@ Konos5我不会用eval eval。我不记得曾经有过这种需要。如果您确实需要两次eval通过,请使用一个临时变量,它会更容易调试:eval tmp="\${$i}"; eval x="\${$tmp}"
吉尔斯(Gillles)“所以别再作恶了”

1
@ Konos5“两次解析”有点误导。由于难以在Bash中指定字面量字符串参数(避免受到各种扩展的保护),可能使某些人相信这一点。eval只是将代码存储在字符串中,然后根据通常的规则对其进行求值。从技术上讲,这甚至是不正确的,因为在一些特殊情况下,Bash修改了解析,甚至没有对eval的参数进行扩展-但这是一个非常晦涩的小窍门,我怀疑任何人都不会知道。
ormaaj 2012年

39

只需将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用法以及与之相关的风险。


3
作为第一个项目符号的注释,${VAR}语法允许的,并且在存在歧义时(does $VAR == $V,后跟AR$VAR == $VA后跟R)是首选的语法。 ${VAR}等同于$VAR。实际上,它的变量名$n是不允许的。
jedwards 2012年

2
eval eval echo \\\${\${$i}}将进行三重取消引用。我不确定是否有更简单的方法可以做到这一点。此外,在我的机器上也\${$n}可以正常工作(打印one
。– Hari Menon

2
@ Konos5 echo \\\${\${$i}}打印\${${n}}eval echo \\\${\${$i}}等价于echo \${${n}}`` and prints $ {1} . eval eval echo \\\ $ {\ $ {$ i}}`等价于eval echo ${1}and print one
吉尔斯(Gillles)“所以-别再邪恶了”

2
@ Konos5遵循相同的思路-第一个` escapes the second one, and the third `会在$后面跳过。因此,\${${n}}经过一轮评估后,它变成了
Hari Menon 2012年

2
@ Konos5从左到右是思考报价和反斜杠解析的正确方法。首先\\ 产生一个反斜杠。然后\$产生一美元。等等。
吉尔斯(Gillles)“所以-别再邪恶了”

25

以我的经验,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!

有执行此操作的常用工具的示例吗?该工具本身具有产生一组可以传递给eval的shell命令的方法。
2014年

@Joakim我不知道有任何开源工具可以执行此操作,但是在我工作过的公司的一些私有脚本中使用了它。我刚刚通过xampp再次开始使用此技术。Apache .conf文件扩展了编写的环境变量${varname}。我发现在几台不同的服务器上使用相同的.conf文件非常方便,而只需通过环境变量对几件事进行参数设置即可。我编辑了/ opt / lampp / xampp(以apache开头),通过脚本在系统中进行搜索并输出bash export语句来定义.conf文件的变量,以进行这种评估。
sootsnoot

@Joakim替代方法是有一个脚本,该脚本可以基于相同的戳记从模板生成每个受影响的.conf文件。我对自己的方式更满意的一件事是,不通过/ opt / lampp / xampp来启动apache不会使用陈旧的输出脚本,而是无法启动,因为环境变量扩展为零并创建了无效的指令。
sootsnoot

@Anthony Sottile我看到您编辑了答案,在$(script-or-program)周围添加了引号,并说它们在运行多个命令时很重要。您能否提供一个示例-下列命令在foo.sh的标准输出中使用分号分隔的命令可以正常工作:echo'#!/ bin / bash'> foo.sh; echo'echo“ echo -na; echo -nb; echo -n c”'>> foo.sh; chmod 755 foo.sh; 评估$(./ foo.sh)。这将在stdout上生成abc。运行./foo.sh会产生:echo -na; 回声-nb; echo -nc
sootsnoot

1
有关使用eval的常用工具的示例,请参见pyenv。pyenv使您可以轻松地在多个版本的Python之间切换。您放入eval "$(pyenv init -)".bash_profile(或类似的)shell配置文件。那会构造一个小的shell脚本,然后在当前shell中对其进行评估。
Jerry101 '18

10

eval语句告诉外壳程序将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。我不同意。我认为,如果可以将腐败的输入传递给,则会产生风险eval。但是,在很多情况下这都不是风险,因此在任何情况下都应该了解如何使用eval是值得的。这个stackoverflow答案解释了评估的风险以及评估的替代方法。最终,由用户来确定eval是否/何时安全有效地使用。


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

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

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

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

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

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

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


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

范例1:

eval “正常”代码前面的语句是NOP

$ eval a=b
$ eval echo $a
b

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



范例2:

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

$ 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

如果假定的解析顺序不能解释评估的作用,则第三个示例可能会更详细地描述解析,以帮助阐明正在发生的事情。



范例4:

发现是否有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 如何解释上述代码行,以得出由所执行的最终值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,如果是,则提示用户提供一个值。


3

最初,我有意从未学习过如何使用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并不是不安全的,因为我们事先知道我们将要评估的内容。


2

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

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


1

您询问了典型用途。

关于shell脚本的一个普遍抱怨是(据说)您不能通过引用来从函数中获取值。

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

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


至少可以说,存在“更多可能性”是微不足道的,琐碎的和多余的。
dotbit

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中的Curious结果是我们将通过以下两个参数:

  • 第一个参数: "value
  • 第二个参数: content"

柜台如何直观?附加功能eval将解决此问题。

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


0

在问题中:

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

输出错误,声称文件a和tty不存在。我理解这意味着tty不会在执行grep之前被解释,而是bash将tty作为参数传递给grep,后者将其解释为文件名。

还有嵌套重定向的情况,应由匹配的括号处理,该括号应指定一个子进程,但bash最初是一个单词分隔符,创建要发送给程序的参数,因此括号不首先匹配,而是解释为看过。

我特定于grep,并将文件指定为参数而不是使用管道。我还简化了基本命令,将命令的输出作为文件传递,这样就不会嵌套I / O管道:

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

效果很好。

who | grep $(echo pts/3)

并不是真正想要的,但是它消除了嵌套管道,并且效果很好。

总之,bash似乎不喜欢嵌套管道。重要的是要了解bash并不是以递归方式编写的新程序。相反,bash是一个旧的1,2,3程序,它已附加了功能。为了确保向后兼容,从未修改过初始的解释方式。如果将bash重写为第一个匹配括号,那么将有多少个bug引入到多少个bash程序中?许多程序员喜欢保密。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.