[翻译]-Shell Variables you should know

Shell 变量你应该知道的(包括 \$* 和 \$@)

翻译自 Shell Variables you should know (including $* and $@)

Shell 变量你应该知道的(包括 $* 和 $@)

Unix shell所做的事情之一是使你能够定义可以插入命令行或脚本中的、包含文本或数字的变量。
除非你想使用单引号或反斜线来保护你的命令行, shell 总会通过寻找$符号来查找变量,即使是在双引号的字符串中:

$ x=hello
$ echo "the variable x contains $x"
the variable x contains hello
$ echo "protect the dollar using backslash \$x"
protect the dollar using backslash $x
$ echo 'protect the dollar using single quotes $x'
protect the dollar using single quotes $x

如果你的 shell 或 shell 脚本不在-u标志下运行,未定义的变量不会引发错误并且仅扩展为空:

$ echo "this is $nosuchvariable expanding"
this is  expanding
$ /bin/bash -u
bash$ echo "this is $nosuchvariable expanding"
bash: nosuchvariable: unbound variable
bash$ exit

在你的shell脚本中最好总是使用-u标志:

#!/bin/bash -u

变量声明可以在当前shell进程中本地完成,或者可以被引入到子进程的环境中(通过内置的export命令):

$ x=foo # define a local variable named x containing foo
$ /bin/bash # start a child process (another shell)
bash$ echo "see x $x"
see x # variable x was not exported to child environment
bash$ exit
$ export x # export the variable
$ /bin/bash # start a child process (another shell)
bash$ echo "see x $x"
see x foo # variable x was exported; value is inherited

引入的变量也被称为环境变量。因为它们是一个新子进程启动环境的一部分。

与当前进程的任何本地事物一样(同样包括umask和当前目录),在子进程中设置一个变量不会影响任何父进程。父进程不能从子进程中继承shell变量。

当你登录进一个Unix系统中时,你的登录shell已经准备好了很多变量。一些是你登录shell的本地变量,很多变量已经导入到你从该shell启动的子进程中。输入没有参数列表的set命令来列出所有变量(本地的和引入的)以及它们当前的值。输入无参数的printenvenv命令来列出环境变量(引入的):

$ set | wc
    85    108    1697
$ printenv | wc
    40    45    876

一旦子进程将所有导出变量的副本放入其环境中,父进程就不再起作用。更改父进程中的变量不会更改已运行的子进程中的值。(新子进程将在启动新进程时获得新值的副本。)

双引号你的变量

要记得给任何可能包含 shell 元字符的 shell 变量加上双引号,否则元字符将被 shell 扩展,结果可能不是你想要的:

$ x='*' # x contains an asterisk (GLOB character)

$ echo "$x" # remember to double-quote the variable
* # GLOB character does not expand (good!)

$ echo $x # unquoted variable is not safe!
a b file gar.h # GLOB character expands (bad bad bad!)

$ test "$x" = "*" # double-quoted variable is safe to use
$ echo $?
0

$ test $x = "*" # unquoted variable causes errors!
test: too many arguments # GLOB character expands (bad bad bad!)

$ msg='* warning *' # set a prefix for a warning message
$ echo "$msg test" # double-quoted variable is safe to use
* warning * test # GLOB characters do not expand (good!)
$ echo $msg test # unquoted variable is not safe!
a b file gar.h warning a b file gar.h test # GLOB characters expand (bad!)

一定要总在变量周围加上双引号。(实际上,你不需要在始终为数字的变量(例如 $#$$$?)周围加上双引号,因为数字不会导致 shell 扩展问题;但是,如果你始终用双引号括住所有变量,就不会忘记。)

你应该知道的变量

你应该知道下列shell变量的含义:

$TERM
$HOME
$PATH
$SHELL
$USER
$$
$#
$0
$?
$1, $2, ...
$*
$@
  • $TERM: 登录时设置以包含终端类型的 Unix 名称。常用值为“vt100”、“xterm”、“ansi”和“linux”。如果不确定所用终端的类型,请将 TERM 设置为“vt100”。不要在没有正确设置 $TERM的情况下使用 vi 或 vim。

  • $HOME: 登录时设置为你的主目录的绝对路径。如果在路径名开头使用,shell 别名“~”是 $HOME 的同义词,例如 ~/foo 与 $HOME/foo 相同(注意:路径名开头的 ~userid 被 shell 扩展为用户 ID“userid”的主目录,例如 ~idallen 。)

  • $PATH: 在登录期间设置为以冒号分隔的目录列表,shell 将在这些目录中查找与命令名称匹配的可执行文件。如果你输入的命令名称已包含任何斜杠,则不使用 $PATH。which命令会在你的 $PATH 中查找命令名称。

  • $SHELL: 在登录期间设置为密码文件 (/etc/passwd) 中指定的 Unix Shell 的路径名。(这可能是也可能不是你当前正在运行的 shell,因为登录后你可以自由启动其他 shell。

  • $USER: 登录时设置为你的帐户名,例如abcd0001。有些系统设置$LOGNAME而不是$USER。

  • $$: 当前shell的进程 ID。通常在创建唯一的临时文件时使用,例如tmp=/tmp/tempfile$$

  • $#: 给予当前执行的 shell 的命令行参数的数量(通常在 shell 脚本中使用)。

  • $0: 当前执行的shell脚本的名称。

  • $?: 最后一个执行的命令的退出状态。

  • $1, $2, ...: 给予当前执行的 shell 脚本的各个命令行参数。

  • $*: 当前执行的 shell 脚本的所有命令行参数。每个参数之间用一个空格隔开。

  • $@: 当前正在执行的 shell 脚本的所有命令行参数。当此变量在双引号内使用时,它将成为所有命令行参数的列表,每个参数都单独用双引号引起来 - 这是双引号字符串生成多个参数的唯一情况。有关其工作原理的更多详细信息,请参阅下一节。

$*$@中的命令行参数

变量$*$@都包含当前 shell 脚本的所有命令行参数。
被双引号包括的字符串"$*"总被当作一个参数,类似大部分被双引号包括的字符串。它包括所有被单空格分割的命令行参数文本,将整体作为一个单独的参数。
被双引号包括的字符串"$@"是个例外和特殊情况。即使被双引号包括,"$@"仍会扩展为多个独立的被双引号包括的参数,每一个都是当前脚本的命令行参数。这是在被双引号包括的字符串包括的字符串扩展为多个参数的唯一的例外。
我们将使用显示其自身命令行参数的“argv”程序来说明 shell 脚本中“$*”和“$@”之间的区别。考虑这个名为“foo”的测试脚本,它将自己的参数传递给 argv 程序:

#!/bin/sh -u 
# "$*" bundles all the command-line arguments into a single string
argv "$*"

当在foo脚本内执行时,argv程序会展示它收到来多少个参数:

$ ./foo a b c
Argument 0 is [/home/idallen/bin/argv]
Argument 1 is [a b c]

$*字符串扩展为一个包括所有命令行参数的单独的参数。\

现在考虑这个使用$@的名为“bar”的测试脚本:

#!/bin/sh -u
# "$@" keeps all the command-line arguments as separate strings
argv "$@"

当在bar脚本内执行时,argv程序会展示使用$@$*的不同输出:

$ ./bar a b c
Argument 0 is [/home/idallen/bin/argv]
Argument 1 is [a]
Argument 2 is [b]
Argument 3 is [c]

这次,argv程序显示双引号包括的$@扩展为三个独立的参数。
脚本编写者使用$@来传递单独的参数到同一shell脚本的其他命令中。举个例子,如果你想写一个名为“mycopy.sh”的shell脚本,它工作起来类似Unix的“cp”命令,但是总会打开“-p”标志(保留修改时间),你可以试试这个:

#!/bin/sh -u
# call copy with the -p option
cp -p "$*" # WRONG WRONG WRONG USE of $*

当执行时:

$ ./mycopy.sh a b
cp: missing destination file

问题在于,当你使用双引号引起的"$*"符号作为“cp”命令的参数时,“cp”只收到来一个单独的字符串,一个参数。脚本中不正确的行为:

cp -p "$*" # WRONG WRONG WRONG USE of $*

使得变量$*被shell扩展为:

cp -p "a b"

这个复制命令只得到了一个单独的路径名称参数。而复制命令需要两个参数:一个源地址和一个目的地址。以下是修正,使用双引号的"$@"

#!/bin/sh -u
# call copy with the -p option
cp -p "$@" # RIGHT USE of $@

以上可以正确运行,不产生错误信息:

$ ./mycopy.sh a b
$

使用$@符号时脚本中的cp命令可以通过$@的特殊扩展,收到独立的的命令行参数。脚本中的命令行:

cp -p "$@" # RIGHT USE of $@

使得变量$@被shell特殊扩展为:

cp -p "a" "b"

复制命令正确收到两个路径名称。与$*不同,被双引号包括的$@会被扩展为多个双引号引起的参数。$@是唯一一个会被shell扩展为多个参数的双引号引起的字符串。其他所有的被引的字符串都只扩展为一个单独的参数。

当你希望将单个命令行参数传递给 shell 脚本中的命令时,请始终使用$@。仅当你希望将所有命令行参数捆绑到单个字符串中时才使用$*(就像你可能在错误消息中所做的那样)。

注意:一些 shell 程序员认为他们可以通过删除 $* 周围的双引号并在 shell 脚本中使用它来解决"$*"问题。如果没有双引号,$*$@ 都会扩展为单独的命令行参数;但是,如果没有双引号进行保护,shell 也会看到并处理这些参数中包含的任何 GLOB 通配符或空格,从而导致混乱:

#!/bin/sh -u
# call copy with the -p option
cp -p $* # WRONG WRONG WRONG USE of $* without quotes!

当使用包含空格的命令行参数执行时,脚本内部的空格不受保护(因为缺少引号),并且脚本失败:

$ ./mycopy.sh  "my file"  newfile
cp: copying multiple files, but last argument `newfile' is not a directory

脚本中的不正确行:

cp -p $* # WRONG WRONG WRONG USE of $* without quotes!

使得变量$*被shell扩展为:

cp -p my file newfile

copy 命令错误地接收了 三个 参数,而不是两个。如果任何命令行参数包含 GLOB 字符,它们也会被扩展,从而造成更多混乱。

在脚本中使用变量的唯一安全方法是用双引号括起来,以防止其内容被 shell 进一步扩展。在 shell 脚本中传递单个命令行参数的唯一安全方法是使用双引号“$@”,该参数会扩展为单独的双引号参数。

无参数使用 $*$@

如果你是一位细心的 shell 脚本编写者,你总是会使用“-u”(检查未定义的变量)标志编写要执行的 shell 脚本。如果没有将命令行参数传递给脚本,那么在使用扩展为命令行参数列表的 $*$@ 变量时,这可能会产生意想不到的效果。

让我们看一个两行的名为enter.sh的脚本:

#!/bin/sh -u
echo "You entered: '$*'"

如果你没有向脚本传递命令行参数,变量 $*$@ 将没有值。bash shell(Linux 上的标准)将变量 $*$@ 视为已定义但为空。使用不带参数的 $*$@ 只会导致空字符串:

$ ./enter.sh
You entered: ''

在大多数专有 Unix 系统上,默认 shell 不是开源 bash shell。如果没有命令行参数,其他 Bourne 风格的 shell 会将变量 $*$@ 视为 未定义,这会导致在“-u”下运行脚本时出错;

$ ./enter.sh
./enter.sh: *: parameter not set

如果执行脚本时不使用“-u”选项,则可以避免出现错误消息;因为未定义的变量不再被 shell 检测为错误:

$ /bin/sh enter.sh
You entered: ''

我们不想停止使用“-u”来检查未定义的变量。为了使脚本无需修改即可“随处”工作,我们需要添加一些 shell 语法来应对变量 $*$@ 可能被 shell 视为未定义的情况:

#!/bin/sh -u
echo "You entered: '${*-}'"

变量语法 ${*-} 告诉 shell 如果变量 $* 未定义,则用空字符串代替。使用此语法将允许你的脚本在所有版本的 Unix 上使用“-u”运行,而不仅仅是在 Linux 上。

(你必须在脚本中使用 -u,以便 shell 能够捕获你的输入错误;否则,拼写错误的变量将无法被检测到。)

留言