为什么shell在通过变量传递参数时忽略引号?

49

这些按照宣传所述运作正常:

grep -ir 'hello world' .
grep -ir hello\ world .

这些不行:

argumentString1="-ir 'hello world'"
argumentString2="-ir hello\\ world"
grep $argumentString1 .
grep $argumentString2 .

在第二个示例中,尽管'hello world'被引号括起来,但grep将'hello(和hello\)解释为一个参数,而将world'(和world)解释为另一个参数,这意味着,在这种情况下,'hello将成为搜索模式,world'将成为搜索路径。

同样,这只发生在从argumentString变量扩展参数时。在第一个示例中,grep正确地将'hello world'(和hello\ world)解释为单个参数。

有人能解释一下为什么吗?是否有一种正确的方法可以扩展字符串变量,以保留每个字符的语法,以便由shell命令正确解释?


6
我应该指出这与grep本身无关;这更多是一个bash问题(使用任何其他命令都会产生相同的效果)。 - nneonneo
grep $argumentString1 . 扩展为 grep -ir hello world . - undefined
grep $argumentString2 . 扩展为 grep -ir hello\ world .(即反斜杠是 grep 的第二个参数的一部分)。 - undefined
3个回答

50

为什么

当字符串被扩展时,它会被分成单词,但不会重新进行评估以查找引号、美元符号或...等特殊字符。这是自从1978年左右的Bourne shell以来,shell一直表现出来的方式。

解决方案

bash 中,使用数组来保存参数:

argumentArray=(-ir 'hello world')
grep "${argumentArray[@]}" .

或者,如果你勇敢/鲁莽,可以使用 eval

argumentString="-ir 'hello world'"
eval "grep $argumentString ."
另一方面,谨慎往往是勇气的好伴侣,在使用 eval 时更需要谨慎而非勇敢。 如果您无法完全控制被 eval 的字符串(如果命令字符串中存在未经严格验证的用户输入),则可能会导致严重的问题。
请注意,Bash 的扩展顺序在 GNU Bash 手册的Shell Expansions中有描述。特别是要注意第3.5.3节“Shell参数扩展”、第3.5.7节“单词分割”和第3.5.9节“引号去除”。

9
或者说,“那就别这样做。” http://mywiki.wooledge.org/BashFAQ/050 - tripleee
2
如果我们要展示使用eval,那么最好展示正确的使用方法。我认为正确使用eval始终包括传递一个单一字符串--否则,您将使用空格将其所有参数连接在一起,这会以令人惊讶的方式变得混乱。考虑eval printf '%s\n' "hello world"eval 'printf "%s\n" "hello world"'之间的比较,这是一个关于为什么传递多个参数给eval会导致混淆的例子。 - Charles Duffy
2
@natevw:大致上是因为"$@"产生的是参数列表而不是单个字符串(而"$*"则产生单个字符串,就像"${array[*]}"一样)。为什么"$@"会这样做呢?因为自古以来就是这样(或者至少是在1978年左右引入Bourne shell的第七版Unix中是这样的)。 - Jonathan Leffler
@thatotherguy:不要修改我的回答,如果你想要给出一个答案,请发布一个新的回答。如果它很好,随着时间的推移,它会上升到排名榜的前列。但是请保留我的回答,因为我不理解你试图做出的所有更改,所以如果你进行了更改,那就不再是我的回答了。 - Jonathan Leffler
我编辑了问题,使其更具普适性。我应该还原吗?这里的意图是使问题和答案更适用和有帮助性,让它更容易去重与这个话题无直接关联的问题,因为它们出现得非常频繁。如果你更喜欢,我很乐意重新发布一个自我回答且更通用的版本。 - that other guy
显示剩余2条评论

6
当您将引号字符放入变量中时,它们只成为普通文字(请参见http://mywiki.wooledge.org/BashFAQ/050;感谢@tripleee指出此链接)。
相反,尝试使用数组来传递参数:
argumentString=(-ir 'hello world')
grep "${argumentString[@]}" .

4

在查看这个问题及相关问题时,我惊讶地发现没有人提到使用显式子shell。对于bash和其他现代shell,您可以明确执行命令行。在bash中,需要使用-c选项。

argumentString="-ir 'hello world'"
bash -c "grep $argumentString ."

这种方法完全符合原问题的要求,但有两个限制:

  1. 命令或参数字符串中只能使用单引号。
  2. 只有导出的环境变量才会对该命令生效。

此外,这种技术可以处理重定向和管道,其它shell命令也可以正常工作。您还可以使用bash内部命令以及在命令行中可用的任何其他命令,因为您实际上是在请求一个子shell bash直接将其解释为命令行。以下是一个更复杂的示例,一个略显复杂的ls -l变体。

cmd="prefix=`pwd` && ls | xargs -n 1 echo \'In $prefix:\'"
bash -c "$cmd"

我曾经使用命令处理器和参数数组来构建。一般来说,这种方式更容易编写和调试,并且很容易回显你正在执行的命令。然而,当你真正拥有抽象参数数组时,而不仅仅是想要一个简单的命令变体时,参数数组会很好用。


命令中可以使用单引号和双引号,就像在命令行中输入一样。 cmd='x=" a "'\''$PATH'\''" b "'; echo "<$cmd>"; bash -c "$cmd; echo \"\$x\"" - jrw32982
“Subshell” 作为一个专业术语,指的是由 fork() 创建的没有 exec() 的 shell -- 它们会隐式地从管道、命令替换、进程扩展和许多其他结构中创建。相比之下,当你运行 bash -c '...' 时,那只是一个普通的子进程,恰好是一个 shell。 - Charles Duffy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接