【问题标题】:In bash, how do I execute the contents of a variable, VERBATIM, as though they were a command line?在 bash 中,如何执行变量 VERBATIM 的内容,就好像它们是命令行一样?
【发布时间】:2015-04-23 10:42:37
【问题描述】:

我需要构造一个包含引用参数的复杂命令。碰巧的是,它们是 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 在将字符串解释为命令时会分解字符串中的引用参数,为什么似乎没有一种字符串方式来使其工作?如果我将一个命令打包在一个字符串变量中,那么将该字符串的解释方式与字符串本身的解释方式不同,就违反了最小意外原则。如果有人可以向我指出一些涵盖所有这些的文档,并且会让我对为什么我必须诉诸于使用我的所有命令构建数组的无限丑陋机制感到平静,我将非常感激。

【问题讨论】:

    标签: string bash variables command interpolation


    【解决方案1】:

    免责声明:写完以下内容后,我几乎决定关闭该问题以鼓励基于意见的回答。这是基于意见的回应。风险自负。

    为什么 bash 在将字符串解释为命令时会分解带引号的参数

    因为它就是这样做的。一个更有趣的问题可能是“为什么 bash 会分解字符串?”,唯一可能的答案是“当时这似乎是个好主意”。

    或者,换一种说法:一开始,没有人想过在文件名中加入空格。当文件名只有几个字母时,您不会在空格上浪费任何字母。因此,将单词列表表示为以空格分隔的单词列表似乎是合理的,这就是开发 shell 语言的基础。所以 bash 的默认行为,就像所有 unix-y shell 一样,将一个带有空格的字符串视为一个以空格分隔的单词列表。

    但是,当然,这会导致各种令人头疼的问题,因为字符串不是结构化数据。有时文件名的名称中确实包含空格。并非所有实用程序参数都是文件名。有时你想给一个实用程序提供一个参数,例如一个句子。没有这种复杂性,shell 能够避免让你输入引号,这与需要引用字符串的“真实”编程语言不同。但是一旦你决定有时字符串中的空格只是另一个字符,你就需要某种引用系统。因此,shell 的语法添加了几种引用形式,每种形式的语义略有不同。最常见的是双引号,将内容标记为单个单词,但仍允许变量扩展。

    shell 引号与任何其他语言中的引号一样,仍然是简单的句法结构。它们不是字符串的一部分,并且字符串中的特定字符用引号(或等效的反斜杠)标记的事实不会保留为字符串的一部分——再次,就像任何其他编程语言一样。字符串真的不是单词列表;默认情况下,它们只是被这样对待。

    所有这些都不是很令人满意。 shell 编程的本质是你真的想要一个数据结构,它是一个“单词”列表——或者,更好的是,一个字符串列表。而且,最终,shell 开始这样做了。不幸的是,到那时,shell 语言已经没有多少语法空间了。重要的是新功能不会改变现有 shell 脚本的行为。据我所知,当前的数组 shell 语法是由 David Korn 在 1988 年(或更早)创建的;最终,bash 也实现了语法基本相同的数组。

    语法中的一个令人好奇的地方是,有三种方法可以指定应该替换整个数组:

    1. ${array[*]} 或${array[@]}:将所有数组元素连接在一起,用$IFS 中的第一个字符分隔,然后将结果视为以空格分隔的单词列表。

    2. "${array[*]}":将所有数组元素连接在一起,用$IFS中的第一个字符分隔,然后将结果视为一个单词。

    3. "${array[@]}":每个数组元素都作为一个单独的单词插入。

    其中,第一个基本上没用;第二个偶尔有用,而第三个——也是最难输入的——是你几乎总是想要的。

    在上面的简短讨论中,我没有考虑 glob 字符和文件名扩展,以及许多其他 shell 特性。所以无论如何都不要把它当作一个完整的教程。

    为什么似乎没有一种字符串方式来让它工作?

    您始终可以使用eval。很遗憾。如果您真的想让 bash 将字符串解释为 bash 程序而不是字符串,并且如果您准备打开脚本以应对各种注入攻击,那么 shell 会很乐意为您提供足够的绳索.就个人而言,我永远不会允许使用 eval 的脚本通过代码审查,所以我不会在这里扩展它的使用。但它已记录在案。

    如果我将一个命令打包在一个字符串变量中,那么将该字符串的解释方式与字符串本身的解释方式不同就违反了最小意外原则。

    惊喜真的在旁观者的眼中。可能有很多程序员认为换行符确实占用了两个字节,当发现在 C 中 '\n'[0] 不是反斜杠时,他们会感到惊讶。但我想如果是这样的话,我们大多数人都会感到惊讶。 (基于这种误解,我试图回答 SO 问题,这并不容易。)

    Bash 字符串,不管其他什么,都是字符串。它们不是 bash 程序。在我看来,将它们突然解释为 bash 程序不仅令人惊讶,而且很危险。至少如果你使用eval,代码审查者有一个很大的危险信号。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-09-22
      • 2014-08-27
      • 2018-08-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多