在 Bash 中引用 $(命令替换)

Cou*_*ine 173 shell bash quoting command-substitution

在我的 Bash 环境中,我使用包含空格的变量,并在命令替换中使用这些变量。

引用我的变量的正确方法是什么?如果这些是嵌套的,我该怎么做?

DIRNAME=$(dirname "$FILE")
Run Code Online (Sandbox Code Playgroud)

或者我在替换之外引用?

DIRNAME="$(dirname $FILE)"
Run Code Online (Sandbox Code Playgroud)

或两者?

DIRNAME="$(dirname "$FILE")"
Run Code Online (Sandbox Code Playgroud)

还是我使用反引号?

DIRNAME=`dirname "$FILE"`
Run Code Online (Sandbox Code Playgroud)

这样做的正确方法是什么?以及如何轻松检查报价是否正确?

l0b*_*0b0 239

按照从最差到最好的顺序:

  • DIRNAME="$(dirname $FILE)" 如果$FILE包含空格(或$IFS当前包含的任何字符)或通配符,则不会执行您想要的操作\[?*
  • DIRNAME=`dirname "$FILE"`从技术上讲是正确的,但不建议将反引号用于命令扩展,因为嵌套它们时会增加额外的复杂性,并且会在其中进行额外的反斜杠处理。
  • DIRNAME=$(dirname "$FILE")是正确的,但仅仅是因为这是对标量(不是数组)变量的赋值。如果您在任何其他上下文中使用命令替换,例如export DIRNAME=$(dirname "$FILE")du $(dirname -- "$FILE"),如果扩展结果包含空格或通配符,则缺少引号会导致问题。
  • DIRNAME="$(dirname "$FILE")"(除了缺少的--,见下文)是推荐的方式。您可以DIRNAME=在不更改任何其他内容的情况下用命令和空格替换,并dirname接收正确的字符串。

进一步改进:

  • DIRNAME="$(dirname -- "$FILE")"如果$FILE以破折号开头则有效。
  • DIRNAME="$(dirname -- "$FILE" && printf x)" && DIRNAME="${DIRNAME%?x}" || exit即使$FILE的 dirname 以换行符结尾也能工作,因为$()在输出结束时切断换行符,包括添加的换行符dirname和可能是实际数据一部分的换行符。

您可以随心所欲地嵌套命令扩展。随着$()您始终创建一个新的引用上下文,因此您可以执行以下操作:

foo "$(bar "$(baz "$(ban "bla")")")"
Run Code Online (Sandbox Code Playgroud)

没有想尝试与反引号。

  • @AmadeusDrZaius“使用`$()`,你总是会创建一个新的引用上下文”,所以它就像在外部引号之外。据我所知,没有更多的事情了。 (15认同)
  • @l0b0 谢谢,是的,我发现你的解释非常清楚。我只是想知道它是否也在某个地方的手册中。[我确实在 walledge 找到了它(尽管是非正式的)](http://mywiki.wooledge.org/CommandSubstitution#line-33)。我想如果您*仔细*阅读有关替换顺序的信息,您可以得出这个事实。 (7认同)
  • 是否有参考/资源详细说明引号内命令替换中的引号行为? (4认同)
  • 明确回答我的问题。当我嵌套这些变量时,我可以像现在一样继续引用吗? (3认同)

Gra*_*eme 20

您始终可以使用 显示变量引用的效果printf

分词完成于var1

$ var1="hello     world"
$ printf '[%s]\n' $var1
[hello]
[world]
Run Code Online (Sandbox Code Playgroud)

var1 引用,所以没有分词:

$ printf '[%s]\n' "$var1"
[hello     world]
Run Code Online (Sandbox Code Playgroud)

分词在var1inside $(),相当于echo "hello" "world"

$ var2=$(echo $var1)
$ printf '[%s]\n' "$var2"
[hello world]
Run Code Online (Sandbox Code Playgroud)

没有分词var1,不引用没有问题$()

$ var2=$(echo "$var1")
$ printf '[%s]\n' "$var2"
[hello     world]
Run Code Online (Sandbox Code Playgroud)

var1再次分词:

$ var2="$(echo $var1)"
$ printf '[%s]\n' "$var2"
[hello world]
Run Code Online (Sandbox Code Playgroud)

引用两者,最简单的方法来确定。

$ var2="$(echo "$var1")"
$ printf '[%s]\n' "$var2"
[hello     world]
Run Code Online (Sandbox Code Playgroud)

通配问题

不引用变量也会导致其内容的全局扩展:

$ mkdir test; cd test; touch file1 file2
$ var="*"
$ printf '[%s]\n' $var
[file1]
[file2]
$ printf '[%s]\n' "$var"
[*]
Run Code Online (Sandbox Code Playgroud)

请注意,这仅在变量展开后发生。在赋值期间没有必要引用 glob:

$ var=*
$ printf '[%s]\n' $var
[file1]
[file2]
$ printf '[%s]\n' "$var"
[*]
Run Code Online (Sandbox Code Playgroud)

使用set -f禁用此行为:

$ set -f
$ var=*
$ printf '[%s]\n' $var
[*]
Run Code Online (Sandbox Code Playgroud)

set +f重新启用它:

$ set +f
$ printf '[%s]\n' $var
[file1]
[file2]
Run Code Online (Sandbox Code Playgroud)

  • 人们往往会忘记分词不是唯一的问题,您可能想要更改您的示例以使用 `var1='hello * world'` 来说明 globbing 问题。 (8认同)

小智 13

除了已接受的答案:

虽然我普遍同意@l0b0在这里的回答,但我怀疑在“最差到最好”列表中放置裸反引号至少部分是由于$(...)随处可用的假设。我意识到这个问题指定了 Bash,但有很多时候 Bash 的意思是/bin/sh,这实际上可能并不总是完整的 Bourne Again shell。

特别是,普通的 Bourne shell 不知道如何处理$(...),因此声称与其兼容的脚本(例如,通过#!/bin/shshebang 行)如果它们实际上是由“真实”运行的,则可能会出现错误行为/bin/sh——这是例如,在生成 init 脚本或打包前后脚本时特别感兴趣,并且可以在安装基本系统期间将其放置在一个令人惊讶的地方。

如果其中任何一个听起来像是您计划用这个变量做的事情,那么嵌套可能比让脚本实际、可预测地运行更重要。当它是一个足够简单的案例并且可移植性是一个问题时,即使我希望脚本通常/bin/shBash 的系统上运行,我也经常出于这个原因倾向于使用反引号,多个分配而不是嵌套。

话虽如此,实现$(...)(Bash、Dash 等)的 shell 无处不在,这让我们在大多数情况下可以坚持使用更漂亮、更易于嵌套且最近更喜欢的 POSIX 语法,例如@l0b0 提到的所有原因。

旁白:这也偶尔出现在 StackOverflow 上——