Bash 为什么可以';我不能双引号引用一个包含多个参数的变量吗?

Bash 为什么可以';我不能双引号引用一个包含多个参数的变量吗?,bash,shell,whitespace,expansion,quoting,Bash,Shell,Whitespace,Expansion,Quoting,我正在编写一个bash脚本,它使用rsync来同步目录。根据报告: 始终引用包含变量、命令替换、空格或shell元字符的字符串,除非需要仔细的无引号扩展 使用“$@”,除非您有特定的理由使用$* 我编写了以下测试用例场景: #!/bin/bash __test1(){ echo stdbuf -i0 -o0 -e0 $@ stdbuf -i0 -o0 -e0 $@ } __test2(){ echo stdbuf -i0 -o0 -e0 "$@" stdbuf -i0 -

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

  • 始终引用包含变量、命令替换、空格或shell元字符的字符串,除非需要仔细的无引号扩展
  • 使用
    “$@”
    ,除非您有特定的理由使用
    $*
我编写了以下测试用例场景:

#!/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[@]}”
扩展与
“$@”
扩展具有相同的属性:它扩展为一个项目列表(数组/参数列表中的每个项目一个字),不会发生分词,就像每个项目都被引用一样。

它失败是因为
“$PARAM”
扩展为单个字符串,并且不会执行分词,尽管它包含命令应解释为多个参数的内容

一种非常有用的技术是使用数组而不是字符串。按如下方式构建阵列:

declare -a PARAM
PARAM+=(--dry-run)
PARAM+=(mirror.leaseweb.net::archlinux/)
PARAM+=(/tmp/test)
然后,使用阵列扩展来执行调用:

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

“${PARAM[@]}”
扩展与
“$@”
扩展具有相同的属性:它扩展为一个项目列表(数组/参数列表中的每个项目一个单词),不会发生分词,就像每个项目都被引用一样。

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

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

echo "$PARAM"
set|grep '^PARAM='
要实际显示原始测试中的
PARAM
是什么。
**
,它是:

PARAM=' --dry-run  mirror.leaseweb.net::archlinux/ /tmp/test'
也就是说,它是包含多个空格分隔的片段的单个字符串

根据经验(除了例外!
*
),除非您告诉bash不要拆分单词,否则它将拆分单词。在测试A和C中,
\uu test1
中未加引号的
$@
为bash提供了拆分
$PARAM
的机会。在测试B中,对u test2
的调用中未加引号的
$PARAM
具有相同的效果。因此,
rsync`将每个空格分隔的项视为测试a-C中的一个单独参数

在测试D中,由于引号的缘故,调用
\uu test2
时,传递给
\uu test2
“$PARAM”
不会被拆分。因此,
\uu test2
只能在
$@
中看到一个参数。然后,在
\uu test2
中,引用的
“$@”
将该参数保存在一起,因此它不会在空格处拆分。因此,
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

编辑
*
正如弗雷德指出的,一个例外是,在作业
A=$B
中,
B
不会展开。也就是说,
A=$B
A=“$B”
是相同的

**
正如ghoti所指出的,您可以使用
声明-p参数来代替
设置| grep'^PARAM='
。带有
-p
开关的将打印出一行,您可以将该行粘贴回shell以重新创建变量。在这种情况下,该输出为:

declare -a PARAM='([0]="--dry-run" [1]="mirror.leaseweb.net::archlinux/" [2]="/tmp/test")'
这是一个很好的选择。我个人更喜欢
set | grep
方法,因为
declare-p
为您提供了额外的引用级别,但两者都很好编辑正如@rici所指出的,如果数组中的元素可能包含换行符,请使用
declare-p

作为额外引用的一个例子,请考虑<代码>未设置的PARAM;声明-参数;PARAM+=(“Jim的”)

(一个包含一个元素的新数组)。然后你会得到:

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

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

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

echo "$PARAM"
set|grep '^PARAM='
要实际显示原始测试中的
PARAM
是什么。
**
,它是:

PARAM=' --dry-run  mirror.leaseweb.net::archlinux/ /tmp/test'
也就是说,它是包含多个空格分隔的片段的单个字符串

根据经验(除了例外!
*
),除非您告诉bash不要拆分单词,否则它将拆分单词。在测试A和C中,
\uu test1
中未加引号的
$@
为bash提供了拆分
$PARAM
的机会。在测试B中,对u test2
的调用中未加引号的
$PARAM
具有相同的效果。因此,
rsync`将每个空格分隔的项视为测试a-C中的一个单独参数

在测试D中,由于引号的缘故,调用
\uu test2
时,传递给
\uu test2
“$PARAM”
不会被拆分。因此,
\uu test2
只能在
$@
中看到一个参数。然后,在
\uu test2
中,引用的
“$@”
将该参数保存在一起,因此它不会在空格处拆分。因此,
rsync
认为整个
PARAM
就是主机名,所以失败了

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

PARAM=([0]="--dry-run" [1]="mirror.leaseweb.net::archlinux/" [2]="/tmp/test")
这是bash对数组的内部表示法:
PARAM[0]
“--dry run”
,等等。您可以单独查看每个单词<代码>回声$P