我需要构造一个包含引用参数的复杂命令。碰巧它们是grep的参数,所以我将把它作为我的例子并深入简化命令以足以证明错误。
让我们从一个有效的例子开始:
> COMMAND='/usr/bin/grep _'
> echo $COMMAND
/usr/bin/grep _
> $COMMAND
foo <- I type this, and grep filters it out.
foo_ <- I type this, and.....
foo_ <- ... it matches, so grep emits it.
“foo”没有回显,因为它没有下划线,“foo_”有一个,所以它被返回了。让我们来看一下这个问题的演示:
> COMMAND='/usr/bin/grep "_ _"'
> echo -E $COMMAND
/usr/bin/grep "_ _"
> /usr/bin/grep "_ _" <- The exact same command line
foo <- fails to match
foo_ _ <- matches, so it gets echoed back
foo_ _
> $COMMAND <- But that command doesn't work from a variable
grep: _": No such file or directory
换句话说,当通过变量名调用此命令时,bash将下划线之间的空格作为参数分隔符 - 尽管有引号。
通常情况下,我会用反斜杠解决这个问题:
> COMMAND='/usr/bin/grep "_\ _"'
> $COMMAND
grep: trailing backslash (\)
好吧,也许我需要另一层逃避反斜杠:
> COMMAND='/usr/bin/grep "_\\ _"'
12:32 (master) /Users/ronbarry> $COMMAND
grep: _": No such file or directory
现在我们又回到原点 - 命令行仍然在空间被打破。当然,我可以通过一些调试来验证所有这些,它确定反斜杠是幸存的,未转义的,并且使用多个参数调用grep:
> set -x
> $COMMAND
+ /usr/bin/grep '"_\\' '_"' <- grep is being called with two args
我有一个利用数组的问题的解决方案,但是这种方式打包命令(在我的完整实现中,我会饶恕你)对于大多数读过我的代码的人来说都是不熟悉的。过度简化基于数组的命令的创建:
> declare -a COMMAND=('/usr/bin/grep' '-i' 'a b')
12:44 (master) /Users/ronbarry> ${COMMAND[*]}
foo <- Same old, same old
fooa B <- ...
fooa B <- Matches because of case-insensitive (-i) grep.
最后我们回答了这个问题。为什么bash在将它们解释为命令时会破坏字符串中的引用参数,为什么似乎没有字符串方式让它工作?如果我有一个打包在字符串变量中的命令,它违反了最小惊喜原则,使该字符串的解释与字符串本身不同。如果有人可以指出一些涵盖所有这些内容的文档,并且会让我安心,为什么我不得不求助于使用我所有命令构建数组的无限丑陋机制,我非常感激。< / p>
答案 0 :(得分:2)
免责声明:在写完以下内容之后,我几乎决定应该关闭这个问题,以鼓励基于意见的回复。这是一种基于意见的回应。自行承担风险。
为什么bash在将它们解释为命令
时会破坏字符串中的引用参数
因为它的作用。一个更有趣的问题可能是&#34;为什么bash会破坏字符串?&#34;,唯一可能的答案就是#34;当时这似乎是一个好主意&#34;。< / p>
或者,换句话说:一开始,没有人想过将空格放入文件名中。当你只有几个字母的文件名时,你并没有在空格上浪费任何字母。因此,将单词列表表示为空格分隔的单词列表似乎是合理的,这就是开发shell语言的基础。所以bash的默认行为,就像所有unix-y shell的默认行为一样,是考虑一个带有空格的字符串,它是一个以空格分隔的单词列表。
但是,当然,这会导致各种令人头疼的问题,因为字符串不是结构化数据。有时文件名的名称中确实有空格。并非所有实用程序参数都是文件名。有时你想给一个实用程序一个参数,例如一个句子。没有这种复杂性,贝壳能够避免让你输入引号,不像&#34;真正的&#34;需要引用字符串的编程语言。但是一旦你决定有时字符串中的空格只是另一个字符,你需要有某种引用系统。所以shell的语法添加了几个引用形式,每个形式都有略微不同的语义。最常见的是双引号,它将内容标记为单个单词,但仍允许变量扩展。
shell引用仍然是这样的,就像任何其他语言中的引号一样,只是语法结构。它们不是字符串的一部分,并且字符串中的特定字符用引号标记(或者,等效地,反斜杠)这一事实不会保留为字符串的一部分 - 再次,就像任何其他编程语言一样。字符串不是真正的单词列表;他们只是默认处理。
所有这些都不是很令人满意。 shell编程的本质是你真的想要一个数据结构,这是一个&#34;单词列表&#34; - 或者,更好的是,字符串列表。而且,最终,贝壳开始做到这一点。不幸的是,那时候shell语言中还没有多少句法空间;重要的是新功能不会改变现有shell脚本的行为。据我所知,数组的当前shell语法是由David Korn在1988年(或更早)创建的;最终,bash还使用基本相同的语法实现了数组。
语法中的一个好奇点是,有三种方法可以指定整个数组应该被替换:
${array[*]}
或${array[@]}
:将所有数组元素连接在一起,用$IFS
中的第一个字符分隔,然后将结果视为以空格分隔的单词列表
"${array[*]}"
:将所有数组元素连接在一起,用$IFS
中的第一个字符分隔,然后将结果视为一个单词。
"${array[@]}"
:每个数组元素都作为单独的单词插入。
其中,第一个基本没用;第二个偶尔是有用的,第三个 - 也是最难打字的 - 是你几乎总想要的。
在上面的简短讨论中,我遗漏了对glob字符和文件名扩展以及许多其他shell特性的任何考虑。因此,不要以任何方式将其作为完整的教程。
为什么似乎没有一种字符串方式让它发挥作用?
您始终可以使用eval
。不幸。如果你真的想让bash解释一个字符串,好像它是一个bash程序而不是一个字符串,如果你准备打开你的脚本到各种注入攻击,那么shell会愉快地给你足够的绳索。就个人而言,我绝不会允许使用eval
的脚本通过代码审查,因此我不打算在此处扩展其使用范围。但它有记录。
如果我有一个打包在字符串变量中的命令,它违反了最小惊喜原则,使该字符串的解释与字符串本身不同。
惊喜真的是旁观者的眼睛。可能有很多程序员认为换行符确实占用了两个字节,并且当发现在C中'\n'
[0]不是反斜杠时会感到惊讶。但我认为如果是的话,我们大多数人都会感到惊讶。 (我试图根据这种误解回答SO问题,这并不容易。)
Bash字符串,无论其他什么,都是字符串。它们不是bash程序。在我看来,将它们突然解释为bash程序,不仅会令人惊讶,而且会带来危险。至少如果你使用eval
,代码审查员就会有一个很大的红旗。