当你登录到系统,无论是全新的Mac OS X Terminal应用、干净的Linux安装,还是UNIX服务器,你得到的实际上都是Shell程序的全新副本。这个登录Shell维护着你所处的环境—一套每个用户各不相同的配置。该环境从用户登录开始一直持续维护,到登出系统为止。

局部变量

在终端中给变量x任意赋值然后在通过脚本打印出该变量:

  1. $ cat vartest
  2. echo :$x:
  3. $ x=100
  4. $ vartest
  5. ::

在登录Shell中被赋予100的变量x称为局部变量。

Shell

Shell实际上就是一个全新的Shell,用于执行要求的程序。当登录Shell执行程序时,它会启动一个新Shell来执行该程序。只要新 Shell一启动,就会拥有自己的环境以及一组局部变量。子Shell并不知道由登录Shell赋值(父Shell)的那些局部变量。而且,子Shell无法修改父Shell中变量的值。当程序执行完毕,子Shell以及由程序所创建的所有变量都会被销毁。

导出变量

有种方法可以让子Shell获知变量的值:使用export命令将变量导出。该命令的格式很简单:

  1. export variables

其中,variables是要导出的变量名列表。已导出变量的值会传到export命令之后的所有子Shell中。
下面的程序帮助演示了局部变量和导出变量之间的差异:

  1. $ cat vartest3
  2. echo x = $x
  3. echo y = $y

在登录Shell中给变量x和y赋值,然后运行程序:

  1. $ x=100
  2. $ y=10
  3. $ vartest3
  4. x =
  5. y =

由于x和y都是局部变量,因此它们的值并不会被传给运行程序的子Shell
现在导出变量y,然后运行该程序:

  1. $ export y # 使子 Shell 获知变量 y
  2. $ vartest3
  3. x =
  4. y = 10

Shell既不能改变局部变量的值,也不能改变导出变量的值,能够改变的仅仅是子Shell启动时所实例化的变量副本。和局部变量一样,当子Shell消失时,导出的变量值也会一并消失。实际上,只要导出变量进入到子Shell中,它们就成为了局部变量。

a8529e31-4753-4746-885e-bba9363a7081.png
如果Shell程序调用了另一个Shell程序,这个过程会重复下去:子Shell的导出变量会被复制到新的子Shell中。这些导出变量可以导出自登录Shell或子Shell。当变量被导出后,它会在之后出现的所有子Shell中保持导出状态。
总结一下局部变量和导出变量的工作方式:

  • 未被导出的变量都是局部变量,子Shell并不知道这些变量的存在。
  • 导出的变量及其值会被复制到子Shell的环境中,在其中可以访问并修改这些导出变量。但是这些修改不会影响到父Shell中的变量。
  • 导出变量不仅保持在直接生成的子Shell中,对于由这些子Shell所生成的子Shell也不例外。
  • 变量可以在赋值前后随时导出,但是只取其导出时的值,不再理会之后做出的改变。

export -p

如果你输入export -p,会得到一个列表,其中包含了Shell所导出的变量及其值:

  1. $ export p
  2. export LOGNAME=steve
  3. export PATH=/bin:/usr/bin:.
  4. export TIMEOUT=600
  5. export TZ=EST5EDT
  6. export y=10

PS1PS2

作为命令行提示符的字符序列被Shell保存在环境变量PS1中。你可以将其修改成任何内容,修改结果立刻就能呈现出来。

  1. $ echo :$PS1:
  2. :$ :
  3. $ PS1="==> "
  4. ==> pwd
  5. /users/steve
  6. ==> PS1="I await your next command, master: "
  7. I await your next command, master: date
  8. Wed Sep 18 14:46:28 EDT 2002
  9. I await your next command, master: PS1="$ "

当命令行上的输入长度多余一行的时候,需要用到辅命令行提示符,该提示符保存在变量PS2中,默认为>。你也可以根据需要来修改:

  1. $ echo :$PS2:
  2. :> :
  3. $ PS2="=======> "
  4. $ for x in 1 2 3
  5. =======> do
  6. =======> echo $x
  7. =======> done
  8. 1
  9. 2
  10. 3
  11. $

和其他Shell变量一样,一旦登出系统,所有对于命令行提示符作出的改变就都失效了。如果你修改了PS1Shell会在余下的会话过程中使用新的命令行提示符。但是等到下次再登录,一切就又回到了老样子,除非你将 PS1 的新值添加到了.profile文件中。

HOME

主目录是用户登录系统后所处的位置。特殊的Shell变量HOME会在你登录时自动设置成该目录:

  1. $ echo $HOME
  2. /users/steve

程序中可以使用这个变量来定位主目录,很多UNIX程序也正是这么做的。如果你使用不带参数的cd命令,那么该目录将作为默认的目的地:

  1. $ pwd # 当前所处位置
  2. /usr/src/lib/libc/port/stdio
  3. $ cd
  4. $ pwd
  5. /users/steve # 进入主目录

你可以将HOME变量修改成任意内容,但是要注意,这么做有可能会影响到依赖于该变量的程序:

  1. $ HOME=/users/steve/book # 修改 HOME
  2. $ pwd
  3. /users/steve
  4. $ cd
  5. $ pwd # 看看造成的影响
  6. /users/steve/book

你当然可以修改HOME,但轻易别这么做。

PATH

当你输入程序名时,Shell会在一个目录列表中搜索指定程序,直至找到为止。如果找到,则启动该程序。用来搜索用户命令的目录列表保存在Shell变量PATH中,在登录时会自动设置。可以使用echo命令查看当前的目录列表:

  1. $ echo $PATH
  2. /bin:/usr/bin:.

要注意到目录之间是以冒号:分隔的,Shell 会从左到右,依次在目录中查找指定的命令或程序。
用点号指定当前目录是可选的,但作为一种可见的提示,还是有帮助的。例如,以下的PATH

  1. :/bin:/usr/bin

和之前的PATH是等效的。

当前目录

  1. $ cat cdtest
  2. cd /users/steve/bin
  3. pwd

该程序使用cd命令切换到/users/steve/bin,然后调用pwd验证当前位置。

  1. $ pwd # 当前目录
  2. /users/steve
  3. $ cdtest
  4. /users/steve/bin

cd命令不会对当前目录造成影响。这是因为当前目录是环境的一部分,当在子Shell中执行cd时,影响的只是子Shell的目录。没有办法在子Shell中改变父Shell的当前目录。调用cd时,除了会修改当前目录,还会将变量PWD设置为新的当前目录的完整路径。因此,以下命令:

  1. echo $PWD

pwd命令的效果是一样的。
cd还将变量OLDPWD设置为前一个当前目录的完整路径,该变量在某些场景下也能发挥作用。

CDPATH

变量CDPATHPATH类似:它指定了一个目录列表,当执行cd命令时,由Shell对其进行搜索。仅在没有给出目录的完整路径且 CDPATH不为空的时候才会展开这个搜索过程。如果你输入:

  1. cd /users/steve

Shell会直接切换到/users/steve,但如果输入的是:

  1. cd memos

Shell会查看变量CDPATH,从中查找memos目录。如果你的CDPATH内容如下:

  1. $ echo $CDPATH
  2. .:/users/steve:/users/steve/docs

Shell首先会在当前目录中查找memos目录,如果没有找到,再去/users/steve中查找,要是还没找到,接着去/users/steve/docs查找。如果找到的目录并不是相对于你的当前目录,cd命令会打印出该目录的完整路径,好让你知道切换目录后的位置:

  1. $ cd /users/steve
  2. $ cd memos
  3. /users/steve/docs/memos
  4. $ cd bin
  5. /users/steve/bin
  6. $ pwd
  7. /users/steve/bin

PATH一样,使用点号来指定当前目录是可选的,因此:

  1. :/users/steve:/users/steve/docs

等同于

  1. .:/users/steve:/users/steve/docs

CDPATH并不会在登录时自动设置好,你得明确地将其设置为一系列目录,以便Shell来搜索指定的目录名。

深入子Shell

Shell无法改变父Shell中变量的值,也无法改变父Shell的当前目录。

.命令

该命令可以在当前Shell中执行file的内容。也就是说,file中的命令就像是你直接输入的一样,由当前Shell执行,而不是在子Shell中。Shell使用 PATH 变量查找file,就像在执行其他命令时一样。

  1. $ . vars # 在当前 Shell 中执行 vars
  2. $ echo $BOOK
  3. /users/steve/book # 搞定!

因为程序并不是由生成的子Shell执行的,即便是在程序执行结束后,赋值过的变量依然能够保留其值。
如果你做出了一些修改,也许会希望在子Shell,而非当前Shell中执行环境配置,否则当你完成工作后,留下的都是一些修改过的变量。最好的解决方法是在子Shell中启动一个新Shell,在其中修改变量以及更新环境配置。结束工作后,你可以按Ctrl+d“登出”那个新 Shell

  1. #
  2. # 设置并导出与数据库相关的变量
  3. #
  4. HOME=/usr2/data
  5. BIN=$HOME/bin
  6. RPTS=$HOME/rpts
  7. DATA=$HOME/rawdata
  8. PATH=$PATH$BIN
  9. CDPATH=:$HOME:$RPTS
  10. PS1="DB: "
  11. export HOME BIN RPTS DATA PATH CDPATH PS1
  12. #
  13. # 启动一个新 Shell
  14. #
  15. /bin/sh

exec命令

exec是使用新程序替换现有的程序,所以并不会有进程处于挂起状态,这有助于加快系统运行。由于UNIX系统执行进程的方式,exec 所替换的程序的启动时间也更快。
在上面程序中,一旦Shell进程结束,你的程序也就结束了,因为程序中/bin/sh之后没有任何命令。与其让程序等待子Shell结束,不如使用exec命令将当前程序替换成另一个新程序(/bin/sh)。
exec 的一般格式为:

  1. exec program

exec还可以用来关闭标准输入,然后使用其他你想要读取的文件重新打开它。要想将标准输入改成infile,可以使用下面的exec命令:

  1. exec < infile

随后任何要从标准输入中读取数据的命令都会转而从infile中读取。
也可以使用类似的方式来实现标准输出重定向。下列命令:

  1. exec > report

会将随后所有写入标准输出的内容重定向到文件report中。注意,在上面的两个例子中,exec并不是用来启动新程序的执行,而是用于重新分配标准输入或标准输出。
如果你用exec重新分配了标准输入,随后想将其再重新分配到其他地方,这只需要重新调用exec就行了。要将标准输入重新分配回终端,可以使用命令:

  1. exec < /dev/tty

(...){ ...; }

有时候你想将若干命令分组。例如,你想把sortplotdata程序置入后台。两者之间不使用管道连接,就只是一前一后而已。你可以把它们放在一对小括号或花括号中,形成一个命令组。第一种形式会在子Shell中运行组中的命令,而后一种形式则是在当前Shell中。

  1. $ x=50
  2. $ (x=100) # 在子 Shell 中执行
  3. $ echo $x
  4. 50 # 没有变化
  5. $ { x=100; } # 在当前 Shell 中执行
  6. $ echo $x
  7. 100

如果花括号中的命令全都写在同一行上,左花括号后一定要有一个空格,分号必须出现在最后一个命令的末尾。仔细观察上面例子中的语句:{ cd bin; }

另一种将变量传给子Shell的方法

如果你想将变量的值送入子Shell,除了将其导出之外,还有另一种方法。在命令行上,把一个或多个变量的赋值放到命令的前面。例如:

  1. DBHOME=/uxn2/data DBID=452 dbrun

它可以将变量DBHOME和DBID及其值放入dbrun的环境中,然后执行dbrun。这些变量不会被当前Shell所知,因为它们仅存在于dbrun 的执行过程中。
实际上,上面的命令等同于:

  1. (DBHOME=/uxn2/data; DBID=452; export DBHOME DBID; dbrun)

.profile文件

登录Shell会在系统中查找并读取两个特殊文件。

  • 第一个文件是由系统管理员所设置的/etc/profile。该文件通常会检查你是否有新的邮件、设置默认的文件创建掩码、建立默认的PATH以及其他管理员希望登录时完成的工作。
  • 第二个自动执行的文件是用户主目录下的.profile。大多数UNIX系统在创建账户的时候就会设置一个默认的.profile
  1. $ cat $HOME/.profile
  2. PATH="/bin:/usr/bin:/usr/lbin:.:"
  3. export PATH

这是一个很普通的.profile文件,该文件仅是简单设置并导出了PATH。你可以修改你自己的.profile,使其包含在登录时要执行的命令,包括指明当前目录、检查已登录用户以及激活系统别名。甚至可以使用.profile中的命令来覆盖/etc/profile中建立好的设置(通常是环境变量)。
登录Shell在实际执行这些文件时就好像是你在登录后立刻输入了:

  1. $ . /etc/profile
  2. $ . .profile

这也意味着在.profile中对环境做出的修改会一直持续到登出Shell

TERM变量

很多UNIX程序都是基于命令行的(如 ls 和 echo),还有一些全屏命令(如 vi 编辑器)需要知道终端设置及功能的详细信息。保存这些信息的环境变量是TERM,你通常并不需要关心这个变量:TerminalSSH程序一般都会自动将其设置为最优的工作值。但有一些老用户也许会发现需要将TERM设置成一些如ansivt100xterm这样的特定值才能使全屏程序正常工作。在这种情况下,推荐在.profile中完成这些设置。

TZ变量

date命令和一些标准C库函数使用TZ变量决定当前时区。由于用户可以通过Internet远程登录,因此系统中不同的用户完全有可能位于不同的时区。最简单TZ的设置是由时区名(长度至少为 3 个字符)和一个数字(指定了小时数)组成的,小时数必须和本地时间相加来形成世界协调时,又称为格林威治标准时间。这个数字可以是正数(本地时区在经度 0 以西),也可以是负数(本地时区在经度0以东)。例如,东部时间可以指定为:

  1. TZ=EST5

date命令会根据这些信息计算出正确的时间并使用时区名作为输出:

  1. $ TZ=EST5 date
  2. Wed Feb 17 15:24:09 EST 2016
  3. $ TZ=xyz3 date
  4. Wed Feb 17 17:46:28 xyz 2016

数字后可以跟上第二个时区名,如果指定的话,表示应用夏令时,意味着比标准时间早一个小时,此时date自动调整时间。如果夏令时时区名后面还有另外一个数字,该值用来从世界协调时中计算夏令时,其计算方法和先前描述的一样。最常见的时区是EST5EDTMST7MDT,尽管有些地区其实并不使用夏令时,不过当然也可以指定。TZ变量通常设置在/etc/profile.profile中。如果没有设置,则使用系统特定的默认时区,一般是世界协调时。另外要注意的是,在很多现代Linux系统中,可以通过指定地理区域来设置时区,因此:

  1. TZ="America/Tijuana" date

将会显示墨西哥提华纳市的当前时间。