为什么命令替换会有这样的行为?

为什么命令替换会有这样的行为?
$ echo $(echo x; echo y)
x y
$ a='echo x; echo y'
$ echo $($a)  # expect 'x y'
x; echo y
  1. 为什么命令替换会有这样的行为?
  2. 如何在不使用evaland 的情况下对存储在变量中的命令列表执行命令替换bash -c

echo $(eval $a)实际上echo $(bash -c "$a")做了我想做的事情,但我听说使用 eval 通常是解决问题的错误方法,所以我想知道如何在没有这些命令的情况下进行管理(使用bash -c实际上是同一件事)

答案1

分词在命令的评估中发生得很晚。对你来说最重要的是,它发生了变量扩展和命令替换。

这意味着第二行

s="echo a; echo b"
echo $($s)

首先会$s扩展为echo a; echo b然后执行该命令没有将其拆分为由两个 s 组成的复合命令echo

(详细信息:$s 被分成四个单词,echoa;echob。封装$(...)将其作为一个命令执行,echo带有三个参数,a;echob,即,你本质上有echo $('echo' 'a;' 'echo' 'b'))。

因此,给予最外面的echo是字符串a; echo b(实际上是三个$(...)没有引号的单词),因为这是最里面的输出echo

比较一下

s1="echo a"
s2="echo b"
echo $($s1;$s2)

这会产生您所期望的结果。

是的,“eval是邪恶的”,大多数时候,sh -c如果你不知道自己在做什么,你可能会觉得笨拙并且脆弱。但是假设你有一些你信任的 shell 代码在一个字符串中在 shell 脚本中。在这种情况下,这些工具是使该代码正确执行的唯一(?)方法,因为这通常需要将字符串中的文本显式评估为 shell 代码(从开始到结束的所有评估阶段),特别是如果它是复合命令。


我认为这只是由于 Unix shell 与文本你很幸运能够echo $($s)执行某件事根本不

想想你必须采取哪些步骤才能让 C 程序执行一段以字符串形式给出的 C 代码......

答案2

经过一番阅读后,我尝试自己回答

为什么命令替换会有这样的行为?

$ a='echo x; echo y'
$ echo $($a)  # expect 'x y'

命令替换

让我们注意到变量的替换$a被执行在子外壳中在命令替换期间生成,而不是在当前 shell 中生成(1,2,3)。因此子shell执行命令$a,而不是echo x; echo y(变量的值a被继承)

所以现在我们只需要找出为什么 shell 会这样

$ a='echo x; echo y'
$ $a
x; echo y

分词

根据Bash 参考手册

展开式有七种……展开式的顺序是:大括号展开;波形符扩展、参数和变量扩展、算术扩展和命令替换(以从左到右的方式完成);分词;和文件名扩展

——摘自本节3.5 外壳扩展


shell 扫描参数扩展、命令替换和算术扩展的结果这不会发生在用于分词的双引号内。

shell 对待每一个$IFS 的字符作为分隔符,并将其他扩展的结果拆分为单词这些作为字段终止符的字符

——摘自本节3.5.7 分词

(默认值为IFS<space><tab><newline>


IFS 变量仅用于拆分扩展结果,而不是所有单词(请参阅单词拆分)

- 来自附录 B 与 Bourne Shell 的主要区别


元字符

一个字符,当不被引用时,分隔单词。元字符是空格、制表符、换行符或以下字符之一:|&;()<>

——摘自本节2. 定义


shell 读取和执行命令时的操作的简要描述。基本上,shell 执行以下操作:

  1. 读取其输入。
  2. 将输入分解为单词和运算符,遵守引用规则。这些标记由元字符分隔。
  3. 解析令牌分为简单命令和复合命令。
  4. 执行各种外壳扩展
  5. 执行任何必要的重定向。
  6. 执行命令

——摘自本节3.1.1 外壳操作

现在我们看到实际上有两种不同的“分词”— 初始分词(步骤 2)和属于 shell 扩展的分词(步骤 4)。

初始分词(步骤 2)将;, (,)等元字符视为分隔符;它的结果被解析(步骤 3),因此可以识别元字符和关键字。

随后 bash 执行各种 shell 扩展(步骤 4),其中包括“分词”——其中的一种。这种类型的分词仅将 的字符视为IFS分隔符。它不关心元字符。其结果未解析再次,以及任何其他扩展的结果。因此,无法识别元字符和关键字。

这就是;变量值内a不被视为命令分隔符的原因。$a变成'echo' 'x;' 'echo' 'y'.即使 的值为,该单词a也不会被视为元字符,因此命令将变为。'echo x ; echo y'';'$a'echo' 'x' ';' 'echo' 'y'

概括:

元字符和关键字(ifthenwhile)不能是扩展的结果,但程序名称、内置命令、函数和别名可以。它可能会带来一些混乱,因为它允许在变量中存储简单命令,而不允许使用复合命令执行相同的操作。

$ 'echo' 'a' ';' 'echo' 'b'  # ';' is a literal
a ; echo b

$ cmd=echo
$ "$cmd" a ; $(printf echo) b  # ';' is a metacharacter
a
b

$ cmd='echo a'  # simple command is executed properly
$ $cmd
a

$ cmd='echo a;'  # but metacharacters are not recognized
$ $cmd echo b
a; echo b

$ echo a $(echo ';') echo b  # ';' is a literal
a ; echo b

$ $(printf if) true; then echo a; fi  # parsing error
bash: syntax error near unexpected token 'then'

$ $(printf if) true; $(printf then) echo a; $(printf fi)  # looking for command "if"
bash: if: command not found
bash: then: command not found
bash: fi: command not found

如何在不使用 eval 和 bash -c 的情况下对存储在变量中的命令列表执行命令替换(因为 eval 是邪恶的)?

也许,正确的答案是“这也很邪恶”,所以你不需要避免 eval :)

变量保存数据。函数保存代码。不要将代码放入变量中!..

——摘自文章我试图将命令放入变量中,但复杂的情况总是失败!

链接

Unix 堆栈交换:

Bash shell 命令替换

为什么变量在子 shell 中可见?

括号真的将命令放入子 shell 中吗?

其他:

Bash 参考手册

我试图将命令放入变量中,但复杂的情况总是失败!

Debian bug 跟踪系统中的对话

相关内容