除非特别说明,本文中出现的 Shell 均指 Bash 4.3。首先说一个基础知识:Shell 中的变量在展开成值(Parameter Expansion)之后,这个值在某些上下文(Context)中,还会进行分词操作(Word Splitting),但在另外一些上下文中,不会进行分词操作。本文中把会进行分词操作的上下文叫做列表上下文(List Context),把不会进行分词的上下文叫做标量上下文(Scalar Context)。还有一个基础知识再提一嘴,就是 Shell 在分词时会跳过那些被双引号包围的词。
因为 $* 和 $@ 这两个特殊变量在以上两种上下文中的展开结果不一样,所以下面必须分两种情况讨论。
列表上下文是我们最熟悉的情况,比如在简单命令的参数中,又比如在 for-in 语句的参数中,这些地方需要的都是多个词,所以 Shell 规定在这些地方要进行分词操作。
$* 在列表上下文中会展开成 $1 $2 $3 ... 多个词,而又因列表上下文存在分词操作,所以 $1 $2 等等都会再被 IFS 分割。
$ set a "b c" d
$ printf "%s/n" $* # $2 的值为 "b c",但由于 $2 本身没有被双引号包围,所以会被分成两个词 b c,所以一共就成了 a b c d 四个参数
a
b
c
d
"$*" 在列表上下文中会展开成 "$1c$2c$3...",c 是 IFS 的第一个字符,如果 IFS 为空,则 c 也是空,如果 IFS 不存在,则 c 为空格,虽然这里存在分词操作,但由于展开后的值仍处于双引号中,所以分词操作不会有任何效果。
$ set a "b c" d
$ IFS=:
$ printf "%s/n" "$*" # 所有位置参数连接成了一个参数
a:b c:d
$@ 在列表上下文中的表现和 $* 在列表上下文中的表现完全一样。
$ set a "b c" d
$ printf "%s/n" $@
a
b
c
d
"$@" 在列表上下文中会展开成 "$1" "$2" "$3" ...,由于展开后的每个值都处于双引号中,所以分词操作不会有任何效果。
$ set a "b c" d
$ printf "%s/n" "$@"
a
b c
d
列表上下文是我们最熟悉的,Bash manual 对 $* 和 $@ 的讲解也仅限于列表上下文中的表现,下面我们讲讲它们俩在标量上下文中的表现。
最常见的标量上下文就是赋值语句的右边,此外还有 case 关键字的后面,以及 [[ ]] 之间等等,这些地方需要的都是一个词,所以 Shell 规定在这些地方不进行分词操作。
$* 在标量上下文中展开成 $1c$2c$3...,c 是 IFS 的第一个字符,由于标量上下文没有分词操作,所以这就结束了,也就是说,$* 在标量上下文的效果等同于 "$*" 在列表上下文中的效果。
$ set a "b c" d
$ IFS=:
$ var=$* # var 的值成了 "a:b c:d"
$ echo "$var"
a:b c:d
"$*" 在标量上下文中展开成 "$1c$2c$3...",由于反正没有分词操作,所以和 $* 在标量上下文中的表现一样。所以也就是说 var=$* 和 var="$*" 完全一样。
$@ 在标量上下文展开成 $1空格$2空格$3...,这里用“空格”字样是为了说明展开后的值是一个词。也就是说,$@ 和 $* 在标量上下文下的区别仅仅是前者用空格做分隔符后者用 IFS 的第一个字符做分隔符这一个区别。
$ set a "b c" d
$ IFS=:
$ var=$* # var 的值成了 "a b c d",不使用 IFS
$ echo "$var"
a b c d
"$@" 在标量上下文中展开成 "$1空格$2空格$3..." 和不加引号效果一样,var=$@ 等效于 var="$@"。
再总结一下就是,在标量上下文中,$* 和 $@ 加不加引号都一样,它俩的区别就是分隔符的区别,它俩展开后的结果都是用一个分隔符把所有位置参数连接成了一个词。下面再用 [[ ]] 的代码示例巩固一下它俩的区别:
$ set a "b c" d
$ IFS=:
$ [[ "$*" == "a:b c:d" ]]; echo $?
0
$ [[ "$@" == "a b c d" ]]; echo $?
0
在实际编码中没必要记忆这些区别,你只需要记住一点,需要多个词的时候用 "$@",需要一个词的时候用 "$*",是的,永远带着引号。此外,由于 Posix 规范明确规定了“本规范不对 $@ 在标量上下文上的表现做任何定义”,所以上面的一些代码示例在 Bash 以外的 Shell 上可能有不同的结果。
最后一句,$* 和 $@ 的所有表现都应该能推广到带 * 和 @ 下标的任意数组上。