为什么我不能双引号包含多个参数的变量?
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=$B
和A="$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 ^^^^
我正在编写一个 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=$B
和A="$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 ^^^^