Special Parameters

官方原文

The shell treats several parameters specially.
These parameters may only be referenced; assignment to them is not allowed.

$* 和 $@

1
2
3
*      Expands to the positional parameters, starting from one.  When the expansion is not within double quotes, each positional parameter expands to a separate word.  In contexts where it is performed, those words are subject to further word splitting and
pathname expansion. When the expansion occurs within double quotes, it expands to a single word with the value of each parameter separated by the first character of the IFS special variable. That is, "$*" is equivalent to "$1c$2c...", where c is
the first character of the value of the IFS variable. If IFS is unset, the parameters are separated by spaces. If IFS is null, the parameters are joined without intervening separators.
1
2
3
@      Expands to the positional parameters, starting from one.  When the expansion occurs within double quotes, each parameter expands to a separate word.  That is, "$@" is equivalent to "$1" "$2" ...  If the double-quoted expansion occurs within a  word,
the expansion of the first parameter is joined with the beginning part of the original word, and the expansion of the last parameter is joined with the last part of the original word. When there are no positional parameters, "$@" and $@ expand to
nothing (i.e., they are removed).

$* 和 $@

在 Bash 中没有双引号时, 它们两个被扩展后, 结果是一样的, 都是表示外部输入的参数列表.
当有双引号时, 如 “$*”, “$@”, 这个时候, 前者表示的是用 IFS (Internal Field Separator) 分隔符连接起来的统一字符, 后者则表示的是输入的每个参数.

1
#      Expands to the number of positional parameters in decimal.
1
?      Expands to the exit status of the most recently executed foreground pipeline.
1
$      Expands to the process ID of the shell.  In a () subshell, it expands to the process ID of the current shell, not the subshell.
1
!      Expands to the process ID of the job most recently placed into the background, whether executed as an asynchronous command or using the bg builtin (see JOB CONTROL below).
1
0      Expands to the name of the shell or shell script.  This is set at shell initialization.  If bash is invoked with a file of commands, $0 is set to the name of that file.  If bash is started with the -c option, then $0 is set  to  the  first  argument after the string to be executed, if one is present.  Otherwise, it is set to the filename used to invoke bash, as given by argument zero.

$- 和 $_

1
-      Expands to the current option flags as specified upon invocation, by the set builtin command, or those set by the shell itself (such as the -i option).
1
2
_      At  shell startup, set to the absolute pathname used to invoke the shell or shell script being executed as passed in the environment or argument list.  Subsequently, expands to the last argument to the previous command, after expansion.  Also set to the full pathname used to invoke each command executed and placed in the environment exported to that command.  When checking mail, this parameter holds the name of the mail file currently being checked.

$- 是 set 命令的 –h 和 –B 的参数, 表示使用内置的 set 命令扩展解释之后的参数行,
具体分别表示为, 记住工作路径, 和允许使用 ! 历史扩展, 详细请参阅 set 命令.

$_ (下划线) 表示的是打印上一个输入参数行, 当这个命令在开头时, 打印输出文档的绝对路径名.

再谈 $* 和 $@ 在 Bash 中的表现

1
2
3
4
5
6
7
8
9
10
11
12
除非特别说明,本文中出现的 Shell 均指 Bash 4.3。
首先说一个基础知识:Shell 中的变量在展开成值(Parameter Expansion)之后,
这个值在某些上下文(Context)中,

还会进行分词操作(Word Splitting),但在另外一些上下文中,不会进行分词操作。

本文中
把会进行分词操作的上下文叫做列表上下文(List Context),

把不会进行分词的上下文叫做标量上下文(Scalar Context)。

还有一个基础知识再提一嘴,就是 Shell 在分词时会跳过那些被双引号包围的词。
1
2
3
因为 $* 和 $@ 这两个特殊变量在以上两种上下文中的展开结果不一样,
所以下面必须分两种情况讨论。

列表上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
列表上下文是我们最熟悉的情况,比如在简单命令的参数中,又比如在 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 对 $* 和 $@ 的讲解也仅限于列表上下文中的表现,下面我们讲讲它们俩在标量上下文中的表现。

标量上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
最常见的标量上下文就是赋值语句的右边,此外还有 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
1
2
3
4
5
6
7
8
在实际编码中没必要记忆这些区别,
你只需要记住一点,需要多个词的时候用 "$@",
需要一个词的时候用 "$*",是的,永远带着引号。

此外,由于 Posix 规范明确规定了“本规范不对 $@ 在标量上下文上的表现做任何定义”,
所以上面的一些代码示例在 Bash 以外的 Shell 上可能有不同的结果。

最后一句,$* 和 $@ 的所有表现都应该能推广到带 * 和 @ 下标的任意数组上。