为什么我不能双引号包含多个参数的变量?

Why can't I double-quote a variable with several parameters in it?

我正在编写一个 bash 脚本,它使用 rsync 来同步目录。根据 Google shell style guide:

  • Always quote strings containing variables, command substitutions, spaces or shell meta characters, unless careful unquoted expansion is required.
  • Use "$@" unless you have a specific reason to use $*.

我写了下面的测试用例场景:

#!/bin/bash

__test1(){
  echo stdbuf -i0 -o0 -e0 $@
  stdbuf -i0 -o0 -e0 $@
}

__test2(){
  echo stdbuf -i0 -o0 -e0 "$@"
  stdbuf -i0 -o0 -e0 "$@"
}


PARAM+=" --dry-run "
PARAM+=" mirror.leaseweb.net::archlinux/"
PARAM+=" /tmp/test"


echo "test A: ok"
__test1 nice -n 19 rsync $PARAM 

echo "test B: ok"
__test2 nice -n 19 rsync $PARAM

echo "test C: ok"
__test1 nice -n 19 rsync "$PARAM"

echo "test D: fails"
__test2 nice -n 19 rsync "$PARAM"

(我需要 stdbuf 立即观察我的较长脚本中的输出,我是 运行)

所以,我的问题是:为什么测试 D 失败并显示以下消息?

rsync: getaddrinfo:  --dry-run  mirror.leaseweb.net 873: Name or service not known

每个测试中的 echo 看起来都一样。如果我想引用所有变量,为什么在这种特定情况下会失败?

它失败了,因为 "$PARAM" 展开为单个字符串,并且没有执行分词,尽管它包含应该被命令解释为多个参数的内容。

一个非常有用的技巧是使用数组而不是字符串。像这样构建数组:

declare -a PARAM
PARAM+=(--dry-run)
PARAM+=(mirror.leaseweb.net::archlinux/)
PARAM+=(/tmp/test)

然后,使用数组扩展来执行您的调用:

__test2 nice -n 19 rsync "${PARAM[@]}"

"${PARAM[@]}" 扩展与 "$@" 扩展具有相同的 属性:它扩展为项目列表(array/argument 列表中每个项目一个词) , 没有出现分词,就好像每个项目都被引用了一样。

我同意@Fred 的观点——最好使用数组。这里有一些解释和一些调试技巧。

在 运行 测试之前,我添加了

echo "$PARAM"
set|grep '^PARAM='

实际显示 PARAM 是什么。** 在您的原始测试中,它是:

PARAM=' --dry-run  mirror.leaseweb.net::archlinux/ /tmp/test'

也就是说,它是一个包含多个 space 分隔的字符串。

根据经验(有例外!*),bash 会拆分单词,除非您告诉它不要这样做。在测试 A 和 C 中,__test1 中未加引号的 $@ 给了 bash 拆分 $PARAM 的机会。在测试 B 中,调用 __test2has the same effect. Therefore,rsync 时未加引号的 $PARAM 会将每个 space 分隔的项目视为测试 A-C 中的单独参数。

在测试 D 中,传递给 __test2"$PARAM" 在调用 __test2 时不会拆分,因为引号。因此,__test2$@中只看到一个参数。然后,在 __test2 中,引用的 "$@" 将该参数保持在一起,因此它不会在 space 处拆分。结果,rsync 认为整个 PARAM 是主机名,所以失败了。

如果使用 Fred 的解决方案,sed|grep '^PARAM=' 的输出是

PARAM=([0]="--dry-run" [1]="mirror.leaseweb.net::archlinux/" [2]="/tmp/test")

即bash对数组的内部表示法:PARAM[0]"--dry-run",等等。您可以单独查看每个单词。 echo $PARAM 对于数组不是很有帮助,因为它只输出第一个单词(这里是 --dry-run)。

编辑

* 正如 Fred 指出的那样,一个例外是,在赋值 A=$B 中,B 不会展开。即A=$BA="$B"是一样的

** 正如 ghoti 指出的那样,您可以使用 declare -p PARAM 而不是 set|grep '^PARAM='declare builtin-p 开关将打印出一行,您可以将其粘贴回 shell 以重新创建变量。在本例中,输出为:

declare -a PARAM='([0]="--dry-run" [1]="mirror.leaseweb.net::archlinux/" [2]="/tmp/test")'

这是一个不错的选择。我个人更喜欢 set|grep 方法,因为 declare -p 为您提供了额外的引用级别,但两者都可以正常工作。 编辑 正如@rici 指出的那样,如果数组的元素可能包含换行符,请使用declare -p

作为额外引用的示例,请考虑 unset PARAM ; declare -a PARAM ; PARAM+=("Jim's")(具有一个元素的新数组)。然后你得到:

set|grep:   PARAM=([0]="Jim's")
      # just an apostrophe ^
declare -p: declare -a PARAM='([0]="Jim'\''s")'
      #    a bit uglier, in my opinion ^^^^