当你登录到系统,无论是全新的Mac OS X Terminal应用、干净的Linux安装,还是UNIX服务器,你得到的实际上都是Shell程序的全新副本。这个登录Shell维护着你所处的环境—一套每个用户各不相同的配置。
该环境从用户登录开始一直持续维护,到登出系统为止。
局部变量
在终端中给变量x任意赋值然后在通过脚本打印出该变量:
$ cat vartest
echo :$x:
$ x=100
$ vartest
::
在登录Shell
中被赋予100的变量x
称为局部变量。
子Shell
子Shell
实际上就是一个全新的Shell
,用于执行要求的程序。当登录Shell
执行程序时,它会启动一个新Shell
来执行该程序。只要新 Shell
一启动,就会拥有自己的环境以及一组局部变量。子Shell
并不知道由登录Shell
赋值(父Shell)的那些局部变量。而且,子Shell
无法修改父Shell
中变量的值。当程序执行完毕,子Shell
以及由程序所创建的所有变量都会被销毁。
导出变量
有种方法可以让子Shell
获知变量的值:使用export
命令将变量导出。该命令的格式很简单:
export variables
其中,variables
是要导出的变量名列表。已导出变量的值会传到export
命令之后的所有子Shell
中。
下面的程序帮助演示了局部变量和导出变量之间的差异:
$ cat vartest3
echo x = $x
echo y = $y
在登录Shell
中给变量x和y赋值,然后运行程序:
$ x=100
$ y=10
$ vartest3
x =
y =
由于x和y都是局部变量,因此它们的值并不会被传给运行程序的子Shell
。
现在导出变量y,然后运行该程序:
$ export y # 使子 Shell 获知变量 y
$ vartest3
x =
y = 10
子Shell
既不能改变局部变量的值,也不能改变导出变量的值,能够改变的仅仅是子Shell
启动时所实例化的变量副本。和局部变量一样,当子Shell
消失时,导出的变量值也会一并消失。实际上,只要导出变量进入到子Shell
中,它们就成为了局部变量。
如果Shell
程序调用了另一个Shell
程序,这个过程会重复下去:子Shell
的导出变量会被复制到新的子Shell
中。这些导出变量可以导出自登录Shell
或子Shell
。当变量被导出后,它会在之后出现的所有子Shell
中保持导出状态。
总结一下局部变量和导出变量的工作方式:
- 未被导出的变量都是局部变量,子
Shell
并不知道这些变量的存在。 - 导出的变量及其值会被复制到子
Shell
的环境中,在其中可以访问并修改这些导出变量。但是这些修改不会影响到父Shell
中的变量。 - 导出变量不仅保持在直接生成的子
Shell
中,对于由这些子Shell
所生成的子Shell
也不例外。 - 变量可以在赋值前后随时导出,但是只取其导出时的值,不再理会之后做出的改变。
export -p
如果你输入export -p
,会得到一个列表,其中包含了Shell
所导出的变量及其值:
$ export –p
export LOGNAME=steve
export PATH=/bin:/usr/bin:.
export TIMEOUT=600
export TZ=EST5EDT
export y=10
PS1
和PS2
作为命令行提示符的字符序列被Shell
保存在环境变量PS1
中。你可以将其修改成任何内容,修改结果立刻就能呈现出来。
$ echo :$PS1:
:$ :
$ PS1="==> "
==> pwd
/users/steve
==> PS1="I await your next command, master: "
I await your next command, master: date
Wed Sep 18 14:46:28 EDT 2002
I await your next command, master: PS1="$ "
当命令行上的输入长度多余一行的时候,需要用到辅命令行提示符,该提示符保存在变量PS2
中,默认为>
。你也可以根据需要来修改:
$ echo :$PS2:
:> :
$ PS2="=======> "
$ for x in 1 2 3
=======> do
=======> echo $x
=======> done
1
2
3
$
和其他Shell
变量一样,一旦登出系统,所有对于命令行提示符作出的改变就都失效了。如果你修改了PS1
,Shell
会在余下的会话过程中使用新的命令行提示符。但是等到下次再登录,一切就又回到了老样子,除非你将 PS1 的新值添加到了.profile
文件中。
HOME
主目录是用户登录系统后所处的位置。特殊的Shell
变量HOME
会在你登录时自动设置成该目录:
$ echo $HOME
/users/steve
程序中可以使用这个变量来定位主目录,很多UNIX程序也正是这么做的。如果你使用不带参数的cd
命令,那么该目录将作为默认的目的地:
$ pwd # 当前所处位置
/usr/src/lib/libc/port/stdio
$ cd
$ pwd
/users/steve # 进入主目录
你可以将HOME
变量修改成任意内容,但是要注意,这么做有可能会影响到依赖于该变量的程序:
$ HOME=/users/steve/book # 修改 HOME
$ pwd
/users/steve
$ cd
$ pwd # 看看造成的影响
/users/steve/book
你当然可以修改HOME
,但轻易别这么做。
PATH
当你输入程序名时,Shell
会在一个目录列表中搜索指定程序,直至找到为止。如果找到,则启动该程序。用来搜索用户命令的目录列表保存在Shell
变量PATH
中,在登录时会自动设置。可以使用echo
命令查看当前的目录列表:
$ echo $PATH
/bin:/usr/bin:.
要注意到目录之间是以冒号:
分隔的,Shell 会从左到右,依次在目录中查找指定的命令或程序。
用点号指定当前目录是可选的,但作为一种可见的提示,还是有帮助的。例如,以下的PATH
:
:/bin:/usr/bin
和之前的PATH
是等效的。
当前目录
$ cat cdtest
cd /users/steve/bin
pwd
该程序使用cd
命令切换到/users/steve/bin
,然后调用pwd
验证当前位置。
$ pwd # 当前目录
/users/steve
$ cdtest
/users/steve/bin
cd
命令不会对当前目录造成影响。这是因为当前目录是环境的一部分,当在子Shell
中执行cd
时,影响的只是子Shell
的目录。没有办法在子Shell
中改变父Shell
的当前目录。调用cd
时,除了会修改当前目录,还会将变量PWD
设置为新的当前目录的完整路径。因此,以下命令:
echo $PWD
和pwd
命令的效果是一样的。cd
还将变量OLDPWD
设置为前一个当前目录的完整路径,该变量在某些场景下也能发挥作用。
CDPATH
变量CDPATH
和PATH
类似:它指定了一个目录列表,当执行cd
命令时,由Shell
对其进行搜索。仅在没有给出目录的完整路径且 CDPATH
不为空的时候才会展开这个搜索过程。如果你输入:
cd /users/steve
Shell
会直接切换到/users/steve
,但如果输入的是:
cd memos
Shell
会查看变量CDPATH
,从中查找memos
目录。如果你的CDPATH
内容如下:
$ echo $CDPATH
.:/users/steve:/users/steve/docs
Shell
首先会在当前目录中查找memos目录,如果没有找到,再去/users/steve
中查找,要是还没找到,接着去/users/steve/docs
查找。如果找到的目录并不是相对于你的当前目录,cd
命令会打印出该目录的完整路径,好让你知道切换目录后的位置:
$ cd /users/steve
$ cd memos
/users/steve/docs/memos
$ cd bin
/users/steve/bin
$ pwd
/users/steve/bin
和PATH
一样,使用点号来指定当前目录是可选的,因此:
:/users/steve:/users/steve/docs
等同于
.:/users/steve:/users/steve/docs
CDPATH
并不会在登录时自动设置好,你得明确地将其设置为一系列目录,以便Shell
来搜索指定的目录名。
深入子Shell
子Shell
无法改变父Shell
中变量的值,也无法改变父Shell
的当前目录。
.
命令
该命令可以在当前Shell
中执行file的内容。也就是说,file中的命令就像是你直接输入的一样,由当前Shell
执行,而不是在子Shell
中。Shell
使用 PATH 变量查找file,就像在执行其他命令时一样。
$ . vars # 在当前 Shell 中执行 vars
$ echo $BOOK
/users/steve/book # 搞定!
因为程序并不是由生成的子Shell
执行的,即便是在程序执行结束后,赋值过的变量依然能够保留其值。
如果你做出了一些修改,也许会希望在子Shell
,而非当前Shell
中执行环境配置,否则当你完成工作后,留下的都是一些修改过的变量。最好的解决方法是在子Shell
中启动一个新Shell
,在其中修改变量以及更新环境配置。结束工作后,你可以按Ctrl+d
“登出”那个新 Shell
。
#
# 设置并导出与数据库相关的变量
#
HOME=/usr2/data
BIN=$HOME/bin
RPTS=$HOME/rpts
DATA=$HOME/rawdata
PATH=$PATH$BIN
CDPATH=:$HOME:$RPTS
PS1="DB: "
export HOME BIN RPTS DATA PATH CDPATH PS1
#
# 启动一个新 Shell
#
/bin/sh
exec
命令
exec
是使用新程序替换现有的程序,所以并不会有进程处于挂起状态,这有助于加快系统运行。由于UNIX系统执行进程的方式,exec
所替换的程序的启动时间也更快。
在上面程序中,一旦Shell
进程结束,你的程序也就结束了,因为程序中/bin/sh
之后没有任何命令。与其让程序等待子Shell
结束,不如使用exec
命令将当前程序替换成另一个新程序(/bin/sh)。
exec 的一般格式为:
exec program
exec
还可以用来关闭标准输入,然后使用其他你想要读取的文件重新打开它。要想将标准输入改成infile,可以使用下面的exec
命令:
exec < infile
随后任何要从标准输入中读取数据的命令都会转而从infile中读取。
也可以使用类似的方式来实现标准输出重定向。下列命令:
exec > report
会将随后所有写入标准输出的内容重定向到文件report中。注意,在上面的两个例子中,exec
并不是用来启动新程序的执行,而是用于重新分配标准输入或标准输出。
如果你用exec
重新分配了标准输入,随后想将其再重新分配到其他地方,这只需要重新调用exec
就行了。要将标准输入重新分配回终端,可以使用命令:
exec < /dev/tty
(...)
和{ ...; }
有时候你想将若干命令分组。例如,你想把sort
及plotdata
程序置入后台。两者之间不使用管道连接,就只是一前一后而已。你可以把它们放在一对小括号或花括号中,形成一个命令组。第一种形式会在子Shell
中运行组中的命令,而后一种形式则是在当前Shell
中。
$ x=50
$ (x=100) # 在子 Shell 中执行
$ echo $x
50 # 没有变化
$ { x=100; } # 在当前 Shell 中执行
$ echo $x
100
如果花括号中的命令全都写在同一行上,左花括号后一定要有一个空格,分号必须出现在最后一个命令的末尾。仔细观察上面例子中的语句:
{ cd bin; }
。
另一种将变量传给子Shell
的方法
如果你想将变量的值送入子Shell
,除了将其导出之外,还有另一种方法。在命令行上,把一个或多个变量的赋值放到命令的前面。例如:
DBHOME=/uxn2/data DBID=452 dbrun
它可以将变量DBHOME和DBID及其值放入dbrun的环境中,然后执行dbrun。这些变量不会被当前Shell
所知,因为它们仅存在于dbrun 的执行过程中。
实际上,上面的命令等同于:
(DBHOME=/uxn2/data; DBID=452; export DBHOME DBID; dbrun)
.profile
文件
登录Shell
会在系统中查找并读取两个特殊文件。
- 第一个文件是由系统管理员所设置的
/etc/profile
。该文件通常会检查你是否有新的邮件、设置默认的文件创建掩码、建立默认的PATH
以及其他管理员希望登录时完成的工作。 - 第二个自动执行的文件是用户主目录下的
.profile
。大多数UNIX系统在创建账户的时候就会设置一个默认的.profile
$ cat $HOME/.profile
PATH="/bin:/usr/bin:/usr/lbin:.:"
export PATH
这是一个很普通的.profile
文件,该文件仅是简单设置并导出了PATH
。你可以修改你自己的.profile
,使其包含在登录时要执行的命令,包括指明当前目录、检查已登录用户以及激活系统别名。甚至可以使用.profile
中的命令来覆盖/etc/profile
中建立好的设置(通常是环境变量)。
登录Shell
在实际执行这些文件时就好像是你在登录后立刻输入了:
$ . /etc/profile
$ . .profile
这也意味着在.profile
中对环境做出的修改会一直持续到登出Shell
。
TERM
变量
很多UNIX程序都是基于命令行的(如 ls 和 echo),还有一些全屏命令(如 vi 编辑器)需要知道终端设置及功能的详细信息。保存这些信息的环境变量是TERM
,你通常并不需要关心这个变量:Terminal
或SSH
程序一般都会自动将其设置为最优的工作值。但有一些老用户也许会发现需要将TERM
设置成一些如ansi
、vt100
或xterm
这样的特定值才能使全屏程序正常工作。在这种情况下,推荐在.profile
中完成这些设置。
TZ
变量
date
命令和一些标准C库函数使用TZ
变量决定当前时区。由于用户可以通过Internet远程登录,因此系统中不同的用户完全有可能位于不同的时区。最简单TZ
的设置是由时区名(长度至少为 3 个字符)和一个数字(指定了小时数)组成的,小时数必须和本地时间相加来形成世界协调时,又称为格林威治标准时间。这个数字可以是正数(本地时区在经度 0 以西),也可以是负数(本地时区在经度0以东)。例如,东部时间可以指定为:
TZ=EST5
date
命令会根据这些信息计算出正确的时间并使用时区名作为输出:
$ TZ=EST5 date
Wed Feb 17 15:24:09 EST 2016
$ TZ=xyz3 date
Wed Feb 17 17:46:28 xyz 2016
数字后可以跟上第二个时区名,如果指定的话,表示应用夏令时,意味着比标准时间早一个小时,此时date
自动调整时间。如果夏令时时区名后面还有另外一个数字,该值用来从世界协调时中计算夏令时,其计算方法和先前描述的一样。最常见的时区是EST5EDT
或 MST7MDT
,尽管有些地区其实并不使用夏令时,不过当然也可以指定。TZ
变量通常设置在/etc/profile
或.profile
中。如果没有设置,则使用系统特定的默认时区,一般是世界协调时。另外要注意的是,在很多现代Linux系统中,可以通过指定地理区域来设置时区,因此:
TZ="America/Tijuana" date
将会显示墨西哥提华纳市的当前时间。