令人惊讶的数组扩展行为

12

在以下示例中,我对标记为(!!)的行感到惊讶:

log1 () { echo $@; }
log2 () { echo "$@"; }

X=(a b)
IFS='|'

echo ${X[@]}   # prints a b
echo "${X[@]}" # prints a b
echo ${X[*]}   # prints a b
echo "${X[*]}" # prints a|b
echo "---"
log1 ${X[@]}   # prints a b
log1 "${X[@]}" # prints a b
log1 ${X[*]}   # prints a b
log1 "${X[*]}" # prints a b (!!)
echo "---"
log2 ${X[@]}   # prints a b
log2 "${X[@]}" # prints a b
log2 ${X[*]}   # prints a b
log2 "${X[*]}" # prints a|b

以下是我的理解:

  • ${X[*]}${X[@]} 都扩展为 a b
  • "${X[*]}" 扩展为 "a|b"
  • "${X[@]}" 扩展为 "a" "b"
  • $*$@ 的行为与 ${X[*]}${X[@]} 相同,只是它们的内容为程序或函数的参数

这似乎得到了Bash手册的确认。

在行log1 "${X[*]}"中,因此我期望引用的表达式会扩展为"a|b",然后传递给log1函数。该函数有一个字符串参数,它会显示该参数。为什么会发生其他事情呢?

如果您的答案能够得到手册/标准参考支持,那就太酷了!

3个回答

7

IFS 不仅用于连接元素 ${X[*]},也用于拆分未加引号的扩展 $@。对于 log1 "${X[*]}",会发生以下情况:

  1. "${X[*]}" 正如预期的那样扩展为 a|b,因此 log1 中的 $1 被设置为 a|b
  2. 当展开未加引号的 $@ 时,得到的字符串是 a|b
  3. 未加引号的扩展按照分隔符(由于全局变量 IFS 的值而确定)进行单词拆分,因此 echo 接收到两个参数,即 ab

哦,对了。有没有办法实现我想要的行为(即使用分隔符进行格式化,但不要在其上拆分单词)?我认为 printf 可能是最简单的选项(例如:https://dev59.com/12cs5IYBdhLWcg3wRB7h)。 - Norswap
2
始终使用引用扩展。这就是答案。您的“log1”函数是错误的。正确的形式是“log2”。 - Etan Reisner
2
更准确地说,始终引用$@。(有一些特殊情况下,您可能会故意不引用$*或其他参数,但$@存在是为了被引用;它与$*完全相同。) - chepner

5
那是因为$IFS被设置为|
(X='a|b' ; IFS='|' ; echo $X)

输出:

a b

man bash说:

IFS是在扩展后用于单词拆分的内部字段分隔符...


3
在POSIX规范的[特殊参数]部分(http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_05_02),我们找到以下内容。
“@”:从1开始,扩展为位置参数。当在双引号内进行扩展并执行字段分割(请参阅字段拆分)时,每个位置参数都应扩展为单独的字段,前提是第一个参数的扩展仍应与原始单词的开头部分连接(假设嵌入了扩展参数在单词中),并且最后一个参数的扩展仍应与原始单词的最后一部分连接。如果没有位置参数,“@” 的扩展将生成零个字段,即使“@”被双引号括起来也是如此。
“*”:从1开始,扩展为位置参数。当在双引号字符串(请参阅双引号)中进行扩展时,它将扩展为一个单独的字段,其中每个参数的值由IFS变量的第一个字符或者如果未设置IFS,则由换行符分隔。如果IFS设置为空字符串,则这不等同于取消设置;其第一个字符不存在,因此参数值被连接。
因此,从引用的变体开始(它们更简单):
我们看到,* 扩展“扩展为一个单独的字段,其中每个参数的值由IFS变量的第一个字符分隔”。这就是为什么您从echo "${X[*]"和log2 "${X[*]}"得到a|b的原因。
我们还可以看到,@ 扩展为“每个位置参数都应扩展为单独的字段”。这就是为什么您从echo "${X[@]}"和log2 "${X[@]}"得到a b的原因。
您是否看到规范文本中有关字段拆分的注释?“在执行字段拆分(请参阅字段拆分)的情况下”?这就是谜团的关键。
在引号外,扩展的行为是相同的。区别在于之后发生了什么。具体来说,是字段/单词拆分。
显示问题最简单的方法是启用set -x运行代码。
这将得到:
+ X=(a b)
+ IFS='|'
+ echo a b
a b
+ echo a b
a b
+ echo a b
a b
+ echo 'a|b'
a|b
+ echo ---
---
+ log1 a b
+ echo a b
a b
+ log1 a b
+ echo a b
a b
+ log1 a b
+ echo a b
a b
+ log1 'a|b'
+ echo a b
a b
+ echo ---
---
+ log2 a b
+ echo a b
a b
+ log2 a b
+ echo a b
a b
+ log2 a b
+ echo a b
a b
+ log2 'a|b'
+ echo 'a|b'
a|b

注意到这里的一件事情是,在除了最后一种情况之外,当调用log1时,|已经消失了。之所以它已经消失,是因为没有引号,变量扩展(在这种情况下是*扩展)的结果被分为字段/单词。由于IFS用于组合正在扩展的字段,然后再次将它们拆分,所以|被字段拆分吞噬了。
对于实际问题的情况,即使在调用中使用了扩展的带引号版本(即log1 "${X[*]}"扩展为正确的log1 "a|b"),log1自身也没有使用带引号的扩展@,因此函数中@的扩展本身就被拆分成单词(可以通过log1中的echo a b以及所有其他log1情况看出)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接