第 12 章...
使用 Shell:变量与选项 (Using the Shell: Variables and Options)
- 交互式 Shell 与非交互式 Shell (Interactive and Non-Interactive Shells)
- 环境、进程与变量 (The Environment, Processes and Variables)
- 一路 Fork 到底 (Forking Till You Die)
- 环境变量与 Shell 变量 (Environment Variables and Shell Variables)
- 显示环境变量: env, printenv
- 显示 Shell 变量: set
- 显示并使用变量的值: echo, print
- Bourne Shell 家族:使用变量: export, unset
- C-Shell 家族:使用变量: setenv, unsetenv, set, unset
- Shell 选项: set -o, set +o
- 显示 Shell 选项 (Displaying Shell Options)
- 机器可读,人可读 (Machine-readable, Human-readable)
- 练习 (Exercises)
很多人不肯花时间学会好好使用 shell。这是个错误。没错,shell 和所有复杂的 Unix 程序一样,有许多 你并不需要真正理解的功能。然而, 有一些根本性的概念具有 极大的实用价值。下面是我列出的清单:
• 交互式 Shell
• 进程
• 环境变量
• Shell 变量
• Shell 选项
• 元字符
• 引用
• 外部命令
• 内置命令
• 搜索路径
• 命令替换
• 历史列表
• 自动补全
• 命令行编辑
• 别名
• 初始化文件
• 注释
如果你看了这份清单觉得喘不过气来, 我完全理解。你甚至可能想问: "这些东西我真的全都要学吗?"
答案是:是的,而且这要花上一些时间, 但别担心。首先,我会把这些内容 分散在三章里讲,所以你不会一下子 学太多。其次,我会确保每个主题 都能自然地引出下一个,这样你 就不会觉得迷惑。(事实上,我们 就会按你在上面清单里看到的顺序 来讲这些主题。)最后,当你开始领会 shell 的精妙之处,当一切都说得通了, 你会发现自己在一个概念到另一个概念的 推进过程中乐在其中。
如你所知,shell 有两个家族: Bourne 家族(Bash、Korn shell)和 C-Shell 家族(C-Shell、Tcsh)。学习 如何使用 shell 时,有些细节会因为 你使用的 shell 不同而有所差别, 我在教你的时候也会体现出这一点。 话虽如此,理解每个 shell 家族 如何处理某些问题、并能欣赏这些 差异,本身就是 Unix 基本功的一部分。
因此,在你学习后面三章时,我希望你 阅读所有章节、查看所有示例,不管你 用的是哪个 shell。有些人只学自己 眼下正在用的那个 shell,但这是个错误。 我的目标是让你对所有主流 shell 都能应付自如。做到这一点的方法, 是用心体会基本原理,而不是去死记 硬背某个特定 shell 那些晦涩的细节。
阅读本章时,你需要知道自己当前 用的是哪个 shell。如果你不确定, 可以用下面这条命令显示你登录时 启动的那个 shell 的名字 (读到本章后面你就会明白它的意思):
echo $SHELL
如果你暂时切换到了另一个 shell (见第 11 章),那你当然知道自己 正在用哪个 shell。
开始之前最后一点。第 11 章讲的 是 shell 的概况。如果你还没读过 那一章,请先花点时间看看,再继续往下读。
交互式与非交互式 Shell
(Interactive and Non-interactive Shells)
交互式(INTERACTIVE)程序是指与人 交流的程序。当你运行一个交互式 程序时,输入来自你的键盘或鼠标, 输出则送到你的显示器上。例如, 当你使用文字处理软件或 Web 浏览器时, 你用的就是交互式程序。
非交互式(NON-INTERACTIVE)程序 独立于人运行。通常它从文件读取输入, 并把输出写入另一个文件。例如,当你 编译一个程序时(处理它以便能够运行), 你用的就是非交互式程序 — 编译器。
有时候,交互式程序和非交互式程序之间 的界限会有点模糊。比如,一个交互式 程序可能把输出送到文件或打印机。 同样,一个非交互式程序可能要求你 在键盘上输入一些数据,或者在发生 重要事情时在你的显示器上显示一条消息。
不过就实际目的而言,把一个程序归类为 交互式(与你配合工作)还是非交互式 (自己独立工作)通常很简单。一般而言, 交互式程序从人那里获取输入(键盘、鼠标), 并把输出送给人(显示器、扬声器)。 非交互式程序使用的输入来自非人的来源 (比如文件),输出也送到非人的来源 (另一个文件)。那么问题就来了: shell 是哪一种?交互式还是非交互式?
答案是:两种都可以。你应该还记得 第 11 章讲过,shell 既可以充当 用户界面,也可以充当脚本解释器。要使用 CLI(命令行界面),你要打开一个终端 窗口,或者使用一个虚拟终端(见第 6 章)。 看到 shell 提示符后,你输入一条命令, shell 处理你的命令,然后再显示出下一个 提示符。当你这样工作时,shell 就是你的 用户界面,我们说你用的是一个交互式 SHELL(INTERACTIVE SHELL)。
另外,你也可以创建一组命令,称之为 SHELL 脚本(SHELL SCRIPT),并把它 保存在一个文件里。当你运行这个脚本时, shell 从文件中读取命令,不需要你输入 任何东西,逐条把它们处理完。这种情况 下,我们说你用的是一个非交互式 SHELL(NON-INTERACTIVE SHELL)。
重要的是要理解:在这两种情况下, 你用的是同一种 shell。之所以可以这样, 是因为 shell 在设计上就既能交互式工作, 也能非交互式工作。
当你登录时,系统会替你启动一个 shell, 并设置为交互方式。同样,当你在命令行 手工启动一个新的 shell 时(比如输入 bash 或 tcsh), 这个新 shell 也会被设置为交互方式。
另一方面,当你运行一个 shell 脚本时, 系统会自动启动一个新的 shell,并交给它 解释你脚本的任务。这个新 shell 会被 设置为非交互方式。一旦活干完 —— 也就是 脚本执行结束 —— 这个非交互式 shell 就被终止了。
那么 shell 怎么知道自己该交互式还是 非交互式工作呢?这完全取决于它启动时 拿到哪些选项。我们稍后会在本章讨论 shell 选项。
名称的由来(What's in a Name?)
Shell
日常生活中,"shell"(外壳)这个词我们 有两种用法,容易让人困惑。我们既可以谈 shell 这个概念本身,也可以指一个 正在运行的 shell 实例。
举个例子。你是个小伙子,受邀参加一个 女生联谊会的派对,那儿有很多啦啦队员。 有人把房间里最漂亮的女孩介绍给你, 为了打破尴尬,你问她:"你用哪个 shell?"跟你聊了几分钟之后,她说: "橄榄球运动员让我觉得无聊。我喜欢 懂 shell 的男人。你何不到我那儿去, 帮我调一调我的 kernel(内核)呢?" 在这个例子里,你和女孩谈的是 概念上的 shell。
第二天,你坐在 Unix 课的讲堂里, 教授说:"……登录之后,系统会启动 一个 shell 来充当你的用户界面。 如果你输入 bash 命令,就会启动 一个新的 shell。当你运行 shell 脚本时, 又会启动另一个 shell……"这一次, 教授谈的不是 shell 这个抽象概念, 而是实际正在运行的 shell。
为了确保你理解这种区别,看看下面这句话 对你是否讲得通:"一旦你学会了如何使用 shell,你就可以在需要的时候随时启动 一个新的 shell。"
环境、进程与变量
(The Environment, Processes and Variables)
在第 6 章讨论多任务时,我提出过这样一个 概念:在 Unix 系统内部,每一个对象 要么用文件表示,要么用进程表示。 简单地说,文件用来保存数据或提供对 资源的访问;进程则是正在执行的程序。 因此,一个正在运行的 shell 就是一个 进程。同样,在 shell 内部启动的任何 程序也是一个进程。
一个进程在运行时,可以访问所谓的 环境(ENVIRONMENT),它是一张变量表, 表中的每个变量都用来存放信息。要把 这个概念弄明白,我们得先从一个基本 问题开始:变量是什么?我们能拿它 做什么?
先给出定义。变量(VARIABLE)是用来 存储数据的实体。每个变量都有一个名字 和一个值。名字(NAME)是我们用来指代 该变量的标识符;值(VALUE)则是存放在 变量内部的数据。
举一个例子。正如我们在第 7 章讨论过的, Unix 使用一个名为 TERM 的变量 来存放你所用终端类型的名字。这样做的 想法是:任何需要知道你终端类型的程序, 只要查看 TERM 的值就可以了。 TERM 最常见的取值是 xterm、linux、vt100 和 ansi。
当你给自己的变量起名时,自由度很大: 只有两条简单的规则。第一,变量名必须由 大写字母(A-Z)、 小写字母(a-z)、数字 (0-9)或下划线字符 (_)组成。第二,变量名必须以 字母或下划线开头;不能以数字开头。 因此,变量名 TERM、path 和 TIME_ZONE 是合法的; 名字 2HARLEY 则不合法。
当你使用 Unix shell 时,变量有 两种不同类型,分别称为"shell 变量" 和"环境变量",整章我们都会 谈到它们。一般来说,你对变量只能 做四种事情:创建它、查看它的值、 改变它的值,或者销毁它。
不严格地说,把变量想象成一个小 盒子会有助于理解。变量的名字 写在盒子上,变量的值就是盒子里 装的东西。比如,你可以想象一个 名为 TERM 的盒子,里面装着 xterm 这个词。这种情况下, 我们说变量 TERM 的值是 xterm。
对大多数编程语言来说,变量可以 存放各种不同类型的数据:字符、 字符串、整数、浮点数、数组、集合 等等。但对 shell 来说,变量几乎 总是只存放一种数据,也就是 字符串(CHARACTER STRING),即一串 纯文本字符。例如在我们的例子里, TERM 变量存放的字符串由 5 个 字符组成:x、t、e、 r 和 m。
创建变量时,你通常会在同时给它一个 值,但这并不是必须的。如果你不给值, 我们说这个变量的值是空(NULL),也就是 说它没有值。这就像做了一个贴着名字的 盒子,却什么也没往里放。如果一个变量 的值是空的,以后有需要时你随时 可以给它赋值。
现在我们来看看变量、环境和进程 是怎样相互关联的。设想这样 一个场景:你在 shell 提示符下启动 vi 文本编辑器(第 22 章)。 用专业术语说,就是一个进程(shell) 启动了另一个进程(vi)。 (细节我们下一节再讲。)
发生这种情况时,第一个进程称为 父进程(PARENT PROCESS),或简称 父(PARENT);第二个进程是子进程 (CHILD PROCESS)或简称子(CHILD)。 在这个例子里,父进程是 shell, 子进程是 vi。
在子进程被创建的那一刻,它会得到 一份环境,这份环境是父进程环境的 副本。我们说子进程继承(INHERITS) 了父进程的环境。这就是说,凡是 父进程可以访问的环境(ENVIRONMENT) 变量,现在子进程也都能访问。
例如在我们的场景里,当 vi (子进程)被创建时,它继承了 shell (它的父进程)的环境。具体说, vi 现在就能查看 TERM 变量的值,从而知道你用的是哪种 终端。这让 vi 能够针对你所用的 终端正确格式化它的输出。
一路 Fork 到底
(Forking Till You Die)
(下面这一节解释父进程创建子进程时 涉及的一些细节。如果你对 Unix 编程有兴趣,特别是想自己编写 shell 脚本,那么下面的内容对你理解很重要。 如果编程对你毫无吸引力,你可以跳过 这一节。不过,即便你不想编程,这一节 也挺有意思,你不妨纯粹当作乐趣 读一读。)
当一个进程需要请求内核执行某项 服务时,它使用系统调用(SYSTEM CALL)。 在 Unix 和 Linux 内部有很多不同的 系统调用,而成为程序员的一部分功课 就是学会使用最常见的那些。三个最 重要的系统调用,就是用来创建和使用 新进程的那三个,它们分别是 fork、exec 和 wait。
fork 系统调用创建当前进程的一份 副本。(想象一条徒步小路,其中一条 岔成两条。)wait 系统调用让 原来的进程暂停下来,直到新进程停止。 最后,exec 系统调用改变一个 进程正在运行的程序。令人惊叹的是, 仅仅使用这三个基本系统调用(再加上 几个我们可以忽略的小变体),Unix 进程就能协调好你、shell 以及你选择 运行的各个程序之间那套复杂的互动。
为了方便讨论创建进程这件事,我们常常 把 FORK、EXEC 和 WAIT 当作动词来用。 比如,我可能会告诉你:进程每 fork 一次,原有的进程就成为父进程, 新进程就成为子进程。
当一个进程最终停止时(不论出于什么 原因),我们说它死亡(DIES)或 终止(TERMINATES)。事实上,正如我们将在 第 26 章讨论的,每当我们选择停掉 一个进程,我们就说把它杀掉(KILL)。 当一个子进程死亡时,一直在等待这个 子进程的父进程会被自动唤醒。就在这一 瞬间,死去的子进程永远消失了。 (Unix 编程不适合胆小的人。)
换句话说,为了运行一个程序,一个进程 先 fork 出一个子进程,然后等待这个 子进程死亡。子进程一旦创建,就 exec 去运行那个程序。程序死亡时,父进程被 唤醒,于是子进程消失。
举个例子,下面是你输入启动 vi 文本编辑器命令时所发生的事。你一输入 命令,父进程(你的 shell)就 fork 出一个与自己完全相同的子进程。接着 父进程暂停,等待子进程死亡。与此同时, 子进程 exec,从运行 shell 变成运行 vi。你注意到的现象是:输入 vi 命令的一刹那之后,shell 提示符就被 vi 程序取代了。
当你用完 vi,你就退出这个程序, 这等于杀掉了子进程。子进程的死亡唤醒 父进程,于是子进程消失。你注意到的 现象是:停掉 vi 程序的一刹那 之后,它就被一个 shell 提示符取代了。
你可能会问:如果父进程 fork 之后 意外死亡,把孩子孤零零留下怎么办? 这种情况下,子进程被称为孤儿 (ORPHAN)。可孤儿死亡时会发生什么? 因为没有父进程可以被唤醒,死去的 子进程就无法消失。
当一个子进程死亡(终止)时,它被称为 僵尸(ZOMBIE)。等到父进程醒来,它才 不再是僵尸并消失。如果没有父进程 可以唤醒,这个进程就一直停留在僵尸 状态。当程序有个 bug,让它创建子进程 却不等待时,就可能出现这种情况。如果 你无意中造出了一个僵尸进程,你没有任何 办法把它清掉。(一个已经死掉的东西, 你怎么杀?)
在较老的 Unix 系统上,僵尸进程会 永远存在(或者直到系统重启,看哪个 先发生)。在现代 Unix 系统上,僵尸 进程会被 1 号进程 — init 进程(引导过程中留下的)收养,然后 这个进程毫不留情地把它们销毁。
Unix 编程酷不酷?
环境变量与 Shell 变量
(Environment Variables and Shell Variables)
如果你是程序员,你会理解全局变量和 局部变量的区别。在编程中,局部变量 (LOCAL VARIABLE)只存在于它被创建时 所在的作用域内。例如,假设你在写一个 程序,创建了一个变量 count, 只在名为 calculate 的函数内部 使用。我们会说 count 是一个 局部变量。更准确地说,我们会说 count 对于函数 calculate 是局部的(LOCAL)。这意味着,当 calculate 正在运行时,变量 count 就存在;一旦 calculate 停止运行,变量 count 就不复存在了。(*)
* 脚注
在本章中,我们讨论的是简单变量, 它们一次只存放一个值。如果你打算 编写 shell 脚本,你应当知道 Bash 和 Korn Shell 也允许你使用一维数组。 (数组是一种包含一串值的变量。) 更多细节请查看 Bash 的 man 手册页 (看"Arrays"一节)或 Korn shell 的 man 手册页(看"Parameters"一节)。
而全局变量(GLOBAL VARIABLE)则在 程序的各个部分都可用。例如,假设你在 写一个程序,对一长串数字做统计运算。 如果你把这串数做成一个全局变量,程序 的所有部分都能使用它。这就是说,如果 某个函数或过程修改了这个列表,程序的 其他部分也会看到这个改动。
于是一个问题产生了:当你使用 Unix shell 时,有没有类似程序员用的那种 全局变量和局部变量?
答案是:有。所有 shell 都使用全局 变量和局部变量,你需要了解它们是 怎么工作的。首先是我们已经讨论过的 环境变量。由于环境变量对所有进程都 可用,它们就是全局变量,事实上我们 常常就用这个名字来称呼它们。(*)
* 脚注
从严格的编程意义上讲,环境变量并非 完全全局,因为子进程所做的修改不会 回传给父进程。
这个限制是有充分理由的。如果允许 子进程修改父进程的环境变量,那将 造成大量的混乱、bug 和安全漏洞。
其次是 SHELL 变量(SHELL VARIABLES), 它们只在某个特定的 shell 内部使用, 不属于环境的一部分。因此它们不会从 父进程传给子进程,基于这个原因,我们 称它们为局部变量。
总的来说,局部(shell)变量有两种 用法。第一,它们可以存放对 shell 自身有意义的信息。例如,在 C-Shell 和 Tcsh 中,ignoreeof 这个 shell 变量用来控制当你按下 ^D 时, shell 是否应当忽略 eof 信号 (见第 7 章)。
第二,shell 变量在 shell 脚本中的用法, 与局部变量在普通程序中的用法相同: 当作临时的存储容器。因此,当你编写 shell 脚本时,你可以根据需要创建 shell 变量来临时存放数据。
到目前为止,这一切都很直白:shell 变量对于创建它的那个 shell 是局部的; 环境变量是全局的,因为任何使用同一 环境的进程都能访问它们。
然而在实际操作中,这里有一个问题。 因为在 shell 这里,局部变量和全局变量 之间的界限是模糊的。所以我想花几分钟 准确说明 shell 究竟如何处理变量。 此外,Bourne 家族和 C-Shell 家族之间 存在重大差异,所以我们得分开讲。 不过这些概念非常重要,所以无论你现在 用的是哪个 shell,我都希望你务必理解 变量在这两个家族中是如何工作的。
开始之前,让我先花点时间解释变量的 命名。有些编程语言有一种传统:全局变量 用大写名字,局部变量用小写名字。 C-Shell 家族(C-Shell、Tcsh)就沿用 这个传统。环境变量用大写名字,比如 HARLEY;shell 变量用小写名字, 比如 harley。
Bourne shell 家族(Bash、Korn shell) 则不同:shell 变量和环境变量传统上 都用大写名字。之所以如此, 一会儿就会清楚。
在多数编程语言里,一个变量要么是局部 的,要么是全局的。但在 shell 这里有个 古怪的问题:有些变量作为局部变量 和全局变量都很有意义。换句话说, 有些变量对 shell 本身有用(这意味着它们 应当是 shell 变量),同时对 shell 启动的 进程也有用(这意味着它们应当是环境 变量)。
Bourne shell 家族处理这个问题的办法, 是强制规定每个变量要么只是局部的, 要么既是局部又是全局的。不存在纯粹的 全局变量。例如,你可能有两个变量 A 和 B,其中 A 是 shell 变量,而 B 既是 shell 变量又是环境变量。但你无法拥有一个 只是环境变量、却不是 shell 变量的 变量。(请花点时间想一想这件事。)
那么创建变量时会发生什么?在 Bourne shell 家族中,你只被允许创建局部变量。 也就是说,每个新变量自动就是一个 shell 变量。如果你希望一个变量同时 也是环境变量,你必须使用一条名为 export 的特殊命令。export 命令把一个 shell 变量变成"shell+环境" 变量。当你这样做时,我们说你把这个 变量导出(EXPORT)到了环境中。
举一个例子。(细节先别管,我们本章 后面会讲到。)首先,我们创建一个名为 HARLEY 的变量,并给它赋值 cool:
HARLEY=cool
此时,HARLEY 只是一个 shell 变量。如果我们启动一个新的 shell 或 运行一条命令,新进程将无法访问 HARLEY,因为它不属于环境。 现在让我们把 HARLEY 导出到环境:
export HARLEY
HARLEY 现在既是一个 shell 变量, 又是一个环境变量。如果我们启动 新的 shell 或运行命令,它们将能够 访问 HARLEY。
所以现在你明白了,为什么 Bourne shell 家族对 shell 变量和环境变量都只用 大写字母。用大写让名字更显眼,而且 由于不存在纯粹的环境变量,也没有 简单的办法区分局部和全局。(再花点 时间把这件事想透。)
你可以看到,Bourne shell 家族处理变量 的方式让人眼花缭乱,对初学者尤其如此。 事实上,这些 shell 所用的这套机制可以 追溯到第一个 Bourne shell,它是 1976 年 Steve Bourne 在贝尔实验室开发的(见 第 11 章)。两年后的 1978 年, Bill Joy 在加州大学伯克利分校开发 C-Shell 时(同样见第 11 章),他决定 改进变量的组织方式。为此,他创建了一个 简单得多的体系,其中环境变量和 shell 变量有着清晰的区分。
在 C-Shell 家族中,环境变量由 setenv 命令创建(稍后介绍), 并使用大写名字,比如 TERM。 shell 变量由 set 命令创建(同样 稍后介绍),并使用小写名字,比如 user。(*)就实际目的而言, 事情就是这么简单。
* 脚注
长久以来,贬低 C-Shell 一度成为一种 风尚,尤其是在把它和现代 Bourne shell (比如 Bash)相比较的时候。在第 11 章 讨论 Tom Christiansen 那篇Csh Programming Considered Harmful (论 Csh 编程之害)时,我谈到过这一 文化现象。
然而,Bourne shell 继承了不少严重的 设计缺陷,而为了保持向后兼容,这些 缺陷无法更改。想想看,Bourne shell 处理局部变量和全局变量那种令人困惑的 方式就知道了。C-Shell 虽然也有自己的 毛病,但它体现了 Bill Joy 的洞见 —— 他是一位杰出的程序员,年轻时在设计 高质量工具方面有着惊人的天赋。
在挑选你自己的 shell 时,别让别人 过度影响你。C-Shell 的现代版本(Tcsh) 是一款非常出色的工具,用于交互式 使用时,它完全能与 Bash 和 Korn shell 平分秋色。(也许该有人写一篇新文章, 题目叫Don't Bash the C-Shell (别踩 C-Shell)了。)
不过,C-Shell 这套简单机制留下了一个 令人纠结的问题。如我所说,有某些变量 在 shell 内部和在所有子进程内部都 有意义。Bourne shell 家族通过允许你使用 既是局部又是全局的变量来避开这个 问题。C-Shell 家族却不允许这样做。
取而代之的是,C-Shell 家族承认少数几个 需要同时作为局部和全局的特殊量。 解决办法是设置几个特殊的 shell 变量, 它们与环境变量绑定在一起。只要其中 一个变量改变,shell 就会自动更新 另一个。
例如,有一个名为 home 的 shell 变量,对应名为 HOME 的环境变量。 如果你改动 home,shell 会对 HOME 做同样的改动。如果你改动 HOME,shell 就会改动 home。
在所有这些双名变量中,日常使用重要的 只有五个,我列在了 Figure 12-1 中。TERM 和 USER 变量 现在你应该能理解了。PATH 变量 将在本章后面解释。PWD 和 HOME 要等我们讨论完 Unix 文件系统(第 23 章)和目录 (第 24 章)之后才会明白。
Figure 12-1: C-Shell 家族:相互关联的 shell/环境变量
在 C-Shell 家族中,有几个 shell 变量 被认为与对应的环境变量是同一个东西。 当这一对变量中有一个被改动时,shell 会自动改动另一个。例如,当 home 被改动时,shell 会自动改动 HOME, 反之亦然。详见正文。
| Shell 变量 |
环境 变量 |
含义 |
|---|---|---|
| cwd | PWD | 你当前的/工作目录 |
| home | HOME | 你的主目录 |
| path | PATH | 查找程序时要搜索的目录 |
| term | TERM | 你所用的终端类型 |
| user | USER | 当前的用户 ID |
名称的由来(What's in a Name?)
cwd, PWD
Figure 12-1 列出了 C-Shell 中 彼此关联的变量对。你可以看到,除了一例 之外,每个 shell 变量的名字都与对应的 环境变量相同(不区分大小写)。 唯一的例外是 cwd 和 PWD。
这些变量存放的是你工作目录的名字, 工作目录有时也称为当前目录 (见第 24 章)。因此才有 cwd这个名字:current/working directory(当前/工作目录)。
PWD 变量的名字来自 pwd 命令,该命令显示工作目录的名字。有意思 的是,pwd 是最原始的 Unix 命令之一。它代表"print working directory"(打印工作目录),它源自 计算机输出真的用纸打印的年代 (见第 3 章和第 7 章)。
显示环境变量:
env, printenv
(Displaying Environment Variables)
虽然你也可以创建自己的环境变量和 shell 变量,但除非你编写程序,否则 并不需要这么做。大多数时候,你会 使用默认的变量。
可默认变量到底是哪些?要显示它们, 你使用 env 命令:
env
在许多系统上,还有另一条命令同样 可以用,那就是 printenv:
printenv
当你使用 env 或 printenv 时, 环境变量可能多到从屏幕上滚过去。 如果是这样,用 less 一次显示 一页输出:
env | less
printenv | less
显示环境变量时,你会发现它们没有 按字母顺序排列。要对输出排序,可以 使用 sort 命令(见第 19 章), 如下所示:
env | sort | less
printenv | sort | less
这种构造称为"管道"(pipeline)。 我们将在第 15 章和第 16 章讨论它。
作为参考,Figure 12-2 列出了 最重要的环境变量及其含义。你实际 看到的变量会因你使用的操作系统和 shell 而异。不过,表中大部分变量 你都应该有。如果你有些地方看不懂, 不必担心:等到你学到足以关心 如何使用某个环境变量的程度时, 你就会明白它的用途了。
Figure 12-2: 最重要的环境变量
默认情况下,Unix 系统会使用大量 环境变量。你在自己系统上会看到哪些, 取决于你使用的操作系统和 shell。
最左边一列显示哪些 shell 支持每个 变量:B = Bash;K = Korn shell; C = C-Shell;T = Tcsh。圆点表示某个 shell 不支持该选项。
| Shell | 变量 | 含义 |
|---|---|---|
| B K • • | CDPATH | cd 命令搜索的目录 |
| B K • T | COLUMNS | 你的屏幕或窗口的宽度(以字符计) |
| B K C T | EDITOR | 默认文本编辑器 |
| B K • • | ENV | 环境文件的名称 |
| B K • • | FCEDIT | 历史列表:fc 命令要使用的编辑器 |
| B K • • | HISTFILE | 历史列表:用于存放命令历史的文件名 |
| B K • • | HISTSIZE | 历史列表:最多存放的行数 |
| B K C T | HOME | 你的主目录 |
| • • • T | HOST | 你的计算机的名字 |
| B • • • | HOSTNAME | 你的计算机的名字 |
| B • • T | HOSTTYPE | 主计算机的类型 |
| B • • • | IGNOREEOF | 结束 shell 之前忽略的 eof 信号(^D)个数 |
| B K C T | LOGNAME | 当前用户 ID |
| B • • T | MACHTYPE | 系统的描述 |
| B K C T | 检查新邮件的文件 | |
| B K C T | MAILCHECK | shell 检查新邮件的频率(以秒计) |
| B K • • | MAILPATH | 检查新邮件的若干文件 |
| B K • • | OLDPWD | 你上一个工作目录 |
| B • • T | OSTYPE | 操作系统的描述 |
| B K C T | PAGER | 显示数据的默认程序(应当是 less) |
| B K C T | PATH | 查找程序时要搜索的目录 |
| B K • • | PS1 | 你的 shell 提示符(改动这个变量即可定制) |
| B K • • | PS2 | 续行时使用的特殊 shell 提示符 |
| B K C T | PWD | 你的工作[当前]目录 |
| B K • • | RANDOM | 0 到 32,767 之间的随机数 |
| B K • • | SECONDS | shell 被调用以来经过的时间(以秒计) |
| B K C T | SHELL | 你登录 shell 的路径名 |
| B K C T | TERM | 你所用的终端类型 |
| B K • • | TMOUT | 若你不键入命令,多少秒之后自动登出 |
| • K C T | TZ | 时区信息 |
| B K C T | USER | 当前用户 ID |
| B K C T | VISUAL | 默认文本编辑器(覆盖 EDITOR) |
| Shell | 变量 | 含义 |
显示 Shell 变量:set
(Displaying Shell Variables: set)
要显示所有 shell 变量及其取值, 你使用 set 命令,不带任何选项 或参数:
set
这条命令很简单,对所有 shell 都 有效。不过,有一个要点你需要 记住。
在 C-Shell 家族中,你看到的 shell 变量的名字都是小写的。按定义,它们 就是局部变量。
在 Bourne shell 家族中,shell 变量的 名字都是大写的。然而,光看名字你 无法判断某个变量是局部还是全局变量。 如果它只是 shell 变量,那它是局部的; 如果它既是 shell 变量又是环境变量, 那它就既是局部又是全局。 (记住,Bourne shell 家族里没有 纯粹的全局变量。)这意味着当你用 set 列出变量时,没有办法 轻易看出哪些变量尚未导出到环境。
— 提示 —
奇怪但属实:要判断哪些 Bourne shell 变量没有导出,唯一的办法是把 set 的输出与 env 的输出做 比较。如果某个变量出现在 set 里 却没出现在 env 里,它就是一个 shell 变量。如果它既出现在 set 里又出现在 env 里,它就既是 shell 变量又是环境变量。
很显然,这令人困惑。不过这关系不大, 因为在 Bourne shell 家族中,shell 变量用得并不多。不错,当你编写 shell 脚本时,你会按需创建局部 (shell)变量。但在日常的交互式 工作中,重要的是环境变量,而不是 shell 变量。
在 C-Shell 家族中,情况不一样。那里 有大量的 shell 变量,其中许多用于 控制 shell 的行为。前面我提到过其中 几个变量:cwd、home、 term 和 user。作为参考, Figure 12-3 除了这四个之外,还列出了 我认为最重要的其他变量。完整的清单 见附录 G。(事实上,你现在不妨偷偷 瞄一眼附录 G,看看 C-Shell 家族 到底用了多少个 shell 变量。)
这就留下最后一个问题。如果 C-Shell 家族用 shell 变量来控制 shell 的行为, 那么 Bourne shell 家族用什么?答案是: 一套复杂的机制,称为"shell 选项", 我们本章后面会讲到。不过在此之前, 我们还需要先把与使用变量有关的几个 基本概念讲完。
Figure 12-3: C-Shell 家族:最重要的 shell 变量
在 C-Shell 家族中,有很多 shell 变量 被 shell 用于特殊目的。这里列出我 认为最有用的那些。更完整的清单 可在附录 G 中找到。
最左边一列显示哪些 shell 支持每个 选项:C = C-Shell;T = Tcsh。 圆点表示某个 shell 不支持该选项。
| Shell | 变量 | 含义 |
|---|---|---|
| • T | autologout | 如果你不键入命令,多少分钟之后自动登出 |
| C T | cdpath | cd、chdir、popd 要搜索的目录 |
| • T | color | 让 ls-F 命令使用颜色 |
| C T | cwd | 你的工作[当前]目录(与 owd 对照) |
| C T | filec | 自动补全:启用 |
| C T | history | 历史列表:最多存放的行数 |
| C T | home | 你的主目录 |
| C T | ignoreeof | 收到 eof 信号(^D)时不退出 shell |
| • T | implicitcd | 单独键入目录名即表示切换到该目录 |
| • T | listjobs | 作业控制:每当有作业被暂停时列出所有作业;long = 长格式 |
| • T | loginsh | 设置此变量用于表明这是一个登录 shell |
| C T | 检查新邮件的文件列表 | |
| C T | noclobber | 不允许重定向的输出替换已有文件 |
| C T | notify | 作业控制:后台作业一完成立即通知 |
| • T | owd | 你最近一次[原先]的工作目录(与 cwd 对照) |
| C T | path | 查找程序时要搜索的目录 |
| C T | prompt | 你的 shell 提示符(改动这个变量即可定制) |
| • T | pushdsilent | 目录栈:pushd 和 popd 不列出目录栈 |
| • T | pushdtohome | 目录栈:不带参数的 pushd 默认假定为主目录(与 cd 相同) |
| • T | rmstar | 执行 rm *(删除所有文件)前强制用户确认 |
| • T | rprompt | 屏幕右侧的特殊提示符(提示:设为 %~ 或 %/) |
| • T | savedirs | 目录栈:登出前保存目录栈 |
| C T | savehist | 历史列表:登出前保存这么多行 |
| C T | shell | 你登录 shell 的路径名 |
| C T | term | 你所用的终端类型 |
| C T | user | 当前用户 ID |
| C T | verbose | 调试:回显每条命令,仅在历史替换之后 |
| • T | visiblebell | 用屏幕闪烁代替声音提示 |
| Shell | 变量 | 含义 |
显示并使用变量的值:
echo, print
(Displaying and Using the Value of a Variable)
如果你想一次显示所有环境变量的值, 可以使用 env 或 printenv 命令。如果你想显示所有 shell 变量, 可以使用 set。然而很多时候, 你只想显示单个变量的值。这种情况 下,你使用 echo 命令。
echo 命令的职责是显示你给它的 任何东西的值。例如,如果你输入:
echo I love Unix
你会看到:
I love Unix
(事到如今,这话应该已经是真的了。)
要显示某个变量的值,你用一个 $(美元符号)字符,后面跟上 用花括号(通常简称大括号)括起来的 变量名。例如,要显示 TERM 的值, 你可以输入:
echo ${TERM}
在你的系统上试一试,看看会得到什么。
如果没有歧义,你可以省去花括号:
echo $TERM
多数情况下都可以这样,但一会儿我会 举一个需要用花括号的例子。
当我们谈论以这种方式使用变量时, $ 字符读作"dollar"(美元)。 所以你可能听人这样说:"如果你想显示 TERM 变量的值,就用 echo-dollar-term 这条命令。"
$NAME 这个记法很重要, 我希望你记住它。当你只输入一个变量名 时,它只是一个名字;当你输入一个 $ 后面跟一个名字(比如 $TERM)时,它指的是该名字变量 的值。因此,上面那条 echo 命令的意思是"显示变量 TERM 的值"。
看下面这个例子,它与上一个类似,但 没有 $ 字符。这种情况下, echo 命令只会显示 TERM 这几个字母,而不是 TERM 变量的值:
echo TERM
你可以用 echo 按任意你想要 的方式显示变量和文本。例如,下面是 一条关于终端类型、信息更完整的消息:
echo The terminal type is $TERM
如果你的终端类型是(比如说)xterm, 你会看到:
The terminal type is xterm
在 shell 内部,有些标点字符 —— 它们 称为"元字符"(metacharacters)—— 有特殊含义(我们将在第 13 章讨论 这一点)。为了阻止 shell 解释元字符, 你可以把它们放在双引号里。这是告诉 shell 按字面意思理解这些字符。例如, 假设你想在尖括号里显示 TERM 的 值。你可能会这样试:
echo The terminal type is <$TERM>.
然而,< 和 > 字符是用于 "重定向"的元字符(见第 15 章), 所以这条命令无法正常工作。(试试看。) 相反,你需要使用:
echo "The terminal type is <$TERM>."
— 提示 —
当你用 echo 命令显示标点符号时, 用双引号告诉 shell 不要把标点当作 元字符来解释。
使用 echo 命令时,你可以很 灵活。例如,你可以一次显示多个变量:
echo $HOME $TERM $PATH $SHELL
如果某个变量与相邻内容没有分隔开, 你必须用花括号把它界定出来。例如, 假设变量 ACTIVITY 的值是 surf。命令:
echo "My favorite sport is ${ACTIVITY}ing."
将显示:
My favorite sport is surfing.
— 提示 —
如果你编写 shell 脚本,你会发现自己 大量使用 echo 命令。花点时间 看看它的手册页(man echo), 那里有各种选项和功能,可以用来控制 输出的格式和内容。
— 给 Korn shell 用户的提示 —
所有 shell 都允许你用 echo 命令 显示文本和变量。在 Korn shell 中,你 还可以使用 print 命令:
print "The terminal type is $TERM."
Korn shell 的开发者 David Korn (见第 11 章)设计 print 是为了 取代 echo。他这样做是因为当时 Unix 的两个主要版本 —— System V 和 BSD(见第 2 章)—— 的 echo 命令功能略有差异。这意味着使用 echo 的 shell 脚本并不总能 在不同系统之间移植。
为了解决这个问题,Korn 把 print 设计成在所有系统上行为 一致。如今这已经不像 Korn 那个年代 那么严重了。不过,如果你写的 Korn shell 脚本确定要在多台计算机上运行, 那么用 print 代替 echo 仍是稳妥之举。
Bourne Shell 家族:
使用变量:export, unset
(Bourne Shell Family: Using Variables)
在 Bourne shell 家族中,创建一个变量 很容易。你只要输入一个名字,后面跟上 一个 =(等号)字符,再跟上 一个值。值必须是字符串。语法是:
NAME=value
如我前面所述,变量名可以使用字母、 数字或下划线(_)。但是变量名 不能以数字开头。
下面是一个你可以试的例子(愿意的话 可以用你自己的名字)。注意等号两边 不要加空格:
HARLEY=cool
当你以这种方式创建一个变量时,我们说 你"设置"(SET)了它。因此我们可以说, 上面这个例子设置了变量 HARLEY, 并给它一个值 cool。
如果你想使用的值中含有空白字符 (空格或制表符;见第 10 章), 就把值放在双引号里:
WEEDLY="a cool cat"
变量一旦存在,你就可以用同样的语法 改变它的值。例如,一旦你创建了 HARLEY,你可以用下面这条命令把 它的值从 cool 改为 smart:
HARLEY=smart
在 Bourne shell 家族中,每个新变量 自动就是一个 shell 变量。(见本章 前面的讨论。)要把变量导出到环境, 你使用 export 命令。输入 export,后面跟上一个或多个 变量的名字。下面这个例子导出了 HARLEY 和 WEEDLY:
export HARLEY WEEDLY
HARLEY 和 WEEDLY 现在都 已从 shell 变量变成了"shell+环境"变量。
正如我们在第 10 章讨论的,你可以 用分号分隔,在同一行上输入多条命令。 由于创建一个变量之后马上导出是常见 做法,所以把这两条命令写在一起是 顺理成章的,例如:
PAGER=less; export PAGER
这比输入两条独立命令要快,你会看到 这种写法被大量使用,尤其是在 Unix 文档和 shell 脚本中。不过,还有 更好的办法。export 命令实际上 允许你在设置变量的同时把它导出。 语法是:
export NAME[=value]...
这里有一个简单的例子:
export PAGER=less
如果你仔细看看这个语法,就会发现 export 允许你指定一个或多个 变量名,每个都可以带一个值。因此, 一条命令可以做很多事:
export HARLEY WEEDLY LITTLENIPPER
export PAGER=less EDITOR=vi PATH="/usr/local/bin:/usr/bin:/bin"
— 提示 —
一般来说,最出色的 Unix 用户往往 思路很快。因此,他们偏爱尽可能 好敲的命令。基于这个原因,设置并 导出一个变量的首选方式是用一条 命令:
export PAGER=less
虽然很多人用两条命令来设置并导出 一个变量,但用一条命令干两件事, 会让你显得聪慧过人、不同凡响。
如我所说,当你创建变量时,我们说你 "设置"(set)了它;当你删除一个变量时, 我们说你"取消设置"(UNSET)它。你很少 需要取消设置变量,但如果有这个需要, 你可以用 unset 命令做到。语法 很简单:
unset NAME...
举一个例子:
unset HARLEY WEEDLY
— 提示 —
有意思的是,在 Bourne shell 家族中, 没有办法方便地把一个变量从环境中 移除。变量一旦导出,想取消导出的 唯一办法就是取消设置它。
换句话说,要把一个 Bourne shell 变量 从环境中移除,唯一的办法就是销毁 它。(*)
* 脚注
谜语:一个 Bourne shell 变量,和 斑点猫头鹰有什么相似之处?
C-Shell 家族:
使用变量:setenv, unsetenv, set, unset
(C-Shell Family: Using Variables)
正如我们讨论过的,C-Shell 家族 —— 与 Bourne shell 家族不同 —— 在环境变量 和 shell 变量之间有清晰的划分。 换句话说,C-Shell 明确区分全局变量和 局部变量。因此你会发现,在 C-Shell 家族中处理变量比在 Bourne shell 家族 中更容易。
要设置(创建)和取消设置(删除)环境 变量,你使用 setenv 和 unsetenv。要设置和取消设置 shell 变量,你使用 set 和 unset。
setenv 命令的语法如下:
setenv NAME [value]
其中 NAME 是变量的名字; value 是你要给该变量设的值。 注意这里不使用 =(等号)字符。
下面是几个创建环境变量的例子。如果 你想动手试试,请记住:环境变量一旦 创建,你就可以用 env 或 printenv 命令显示它们。
setenv PATH /usr/local/bin:/usr/bin:/bin
setenv HARLEY cool
setenv WEEDLY "a cool cat"
setenv LITTLENIPPER
前三条命令设置一个变量并给它指定的 值。第三个例子中,我们用双引号来 包含空白字符(那两个空格)。最后一个 例子中,我们只给出变量名 (LITTLENIPPER)而不给值。这样 创建的变量值是空的,也就是说没有 值。当我们只关心一个变量是否存在、 而不关心它的值时,就这样做。
要取消设置一个环境变量,你使用 unsetenv 命令。语法是:
unsetenv NAME
其中 NAME 是变量的名字。
例如,要取消设置(删除)变量 HARLEY,你可以使用:
unsetenv HARLEY
要设置一个 shell 变量,你使用 set 命令,语法如下:
set name[=value]
其中 name 是 shell 变量的名字; value 是你要给该变量设的值。
下面是几个例子:
set term=vt100
set path=(/usr/bin /bin /usr/ucb)
set ignoreeof
第一个例子很直白。我们把 shell 变量 term 的值设为 vt100。
第二个例子说明了一个要点。在 C-Shell 家族中使用变量时,有时你会 把一组字符串用圆括号括起来,而不是 用双引号。这样做时,你定义的是可以 逐个访问的一组字符串。在这个例子里, path 的值被设为括号内的三个 字符串。
在最后一个例子中,我们只给出 shell 变量而不给值,这使该变量的值为空。 这种情况下,ignoreeof 这个变量 的存在本身就告诉 shell 去忽略 eof 信号。这就要求我们使用 logout 命令来结束 shell。 (见第 7 章。)
shell 变量一旦存在,你可以用 unset 命令删除它。语法是:
unset variable
其中 variable 是变量的名字。
举个例子,如果你想让 shell 关闭 ignoreeof 功能,你可以使用:
unset ignoreeof
务必要理解"把变量设为空"和 "删除变量"之间的区别。想想下面 三条命令:
set harley=cool
set harley
unset harley
第一条命令创建一个名为 harley 的 shell 变量,并把它的值设为 cool。第二条命令把 harley 的值设为空。最后一条 命令把该变量彻底删除。
Shell 选项:set -o, set +o
(Shell Options)
如我们前面所讨论的,在 C-Shell 家族中, 我们可以用 shell 变量来控制 shell 行为的方方面面。在 Bourne shell 家族 中,我们使用所谓的 SHELL 选项(SHELL OPTIONS)。例如,正是 shell 选项在控制 一个 shell 是交互式还是非交互式。
Shell 选项就像开/关开关。当你打开一个 选项时,我们说你"设置"(SET)了它, 这是告诉 shell 按某种方式行事。当你 关闭一个选项时,我们说你"取消设置" (UNSET)了它,这是告诉 shell 停止那样 行事。
例如,shell 支持一种叫"作业控制" (job control)的机制,让你可以在后台 运行程序。(我们将在第 26 章讨论 这个。)要打开作业控制,你设置 monitor 选项。如果你想关闭 作业控制,你就取消设置 monitor 选项。默认情况下,交互式 shell 的 monitor 是打开的。
— 提示 —
"set"和"unset"这两个词,在谈论 shell 选项还是在谈论变量时,含义 是不同的。
Shell 选项要么关、要么开;它们不 需要被创建。因此,当我们设置一个 shell 选项时,是把它打开;当我们 取消设置一个选项时,是把它关闭。
变量则不同。当我们设置一个变量时, 我们是真的在创建它;当我们取消设置 一个变量时,我们是把它永久删除了。
设置或取消设置 shell 选项有两种途径。 第一,在 shell 启动时,可以按通常的 方式,在命令后面指定一个或多个选项 来设定(见第 10 章)。例如,下面 这条命令启动一个 Korn shell,并设置 (打开)monitor 选项:
ksh -m
除了标准的命令行选项之外,还有另一种 办法可以打开和关闭 shell 选项,那就是 使用 set 命令的一个变体。语法 如下。要设置一个选项,使用:
set -o option
要取消设置一个选项,使用:
set +o option
其中 option 是选项的"长名字" (见 Figure 12-4)。
例如,假设 shell 正在运行,你想设置 monitor 选项。使用:
set -o monitor
要取消设置 monitor 选项,使用:
set +o monitor
注意你输入的是 o,即小写字母 "o",不是数字零。(只要记住, o 代表"option"(选项)。)
一开始,用 -o 打开选项、用 +o 关闭选项,看起来会很别扭。 但我向你保证,总有一天你会明白 它的道理(*)。
* 脚注
这里给出简短的解释。你从 第 10 章已经知道,选项的标准形式 是一个 -(连字符)字符后跟一个 字母。事实是,大多数时候你修改 shell 选项时,你是想设置它们 —— 也就是 打开它们 —— 而不是取消设置。基于这个 原因,常用的写法(-o)用来表示 "set",而不常用的写法(+o) 用来表示"unset"。
随着时间推移,当你积累经验,这类 设计思路就会开始让你觉得顺理成章。 当这一天到来,你脑子里会发生某种 变化,使用 Unix 就变得轻松多了。 (遗憾的是,同样的变化也会让你更难 在女生联谊派对上遇到啦啦队员。)
shell 每次启动时,各类选项都会按默认 情况被设置或取消设置,具体取决于该 shell 是交互式还是非交互式。设计 shell 的程序员心里有数,所以在大多数情况下, shell 选项保持默认就好。这意味着你 很少需要改动 shell 选项。
不过,如果你确实要改,可以参考 Figure 12-4:它列出了交互式 shell 中最有用的那些 shell 选项。和前面 讨论的环境变量一样,看不懂也不必 担心。这份清单是供查阅用的。等你 学到足以关心某个选项如何使用的 程度时,你就会明白它的用途了。
Figure 12-4: Bourne shell 家族:交互式 shell 选项一览
本表汇总了在交互式 shell 中有用的 shell 选项。更多信息请查看你所用 shell 的手册页。
最左边一列显示哪些 shell 支持每个 选项:B = Bash;K = Korn shell。 圆点表示某个 shell 不支持该选项。 注意有些选项(比如 history) 只有长名字,没有短选项名。
注:(1) 虽然 Bash 支持 emacs 和 vi 选项,但它不使用 -E 和 -V。 (2) Korn shell 使用 -h, 但不支持长名字 hashall。
| Shell | 选项 | 长名字 | 含义 |
|---|---|---|---|
| B K | -a | allexport | 导出随后定义的所有变量和函数 |
| B • | -B | braceexpand | 启用花括号展开(生成字符模式) |
| B K | -E | emacs | 命令行编辑器:Emacs 模式;关闭 vi 模式 |
| B K | -h | hashall | 为找到的命令建立哈希(记住)其位置 |
| B • | -H | histexpand | 历史列表:启用 ! 风格的替换 |
| B • | history | 历史列表:启用 | |
| B K | -I | ignoreeof | 忽略 eof 信号 ^D;用 exit 退出 shell(见第 7 章) |
| • K | markdirs | 使用通配时,在目录名后追加 / | |
| B K | -m | monitor | 作业控制:启用 |
| B K | -C | noclobber | 不允许重定向的输出替换已有文件 |
| • K | nolog | 历史列表:不保存函数定义 | |
| B K | -b | notify | 作业控制:后台作业一完成立即通知 |
| • K | trackall | 别名:为命令替换成完整路径名 | |
| B K | -V | vi | 命令行编辑器:vi 模式;关闭 Emacs 模式 |
| • K | viraw | 在 vi 模式下:每键入一个字符就立即处理 |
除了 Figure 12-4 中列出的这些,还有 许多其他 shell 选项,其中大多数对 非交互式 shell 有用(也就是说,当你 编写 shell 脚本时用得上)。此外,如果你 使用 Bash,还有一条名为 shopt ("shell options")的特殊命令,让你能 访问更多选项。
我在附录 G 中收集了完整的 shell 选项清单,外加一些关于 shopt 的 提示。虽然你现在不必理解所有这些 内容,但我希望你花点时间看看 附录 G,了解一下都有些什么可用。
— 提示 —
关于 shell 选项的权威信息,请查看 你那个 shell 的 man 手册页:
man bash
man ksh
在 Bash 的手册页中,搜索"SHELL BUILTIN COMMANDS"。在 Korn shell 的手册页中, 搜索"Built-in Commands"或 "Special Commands"。
显示 Shell 选项
(Displaying Shell Options)
Bourne shell 家族使用 shell 选项来 控制 shell 的运行。要显示你当前各 shell 选项的取值,单独使用 set -o 或 set +o 即可:
set -o
set +o
使用 set -o 会以易于阅读的 方式显示所有选项的当前状态。使用 set +o 则以紧凑的格式显示 同样的信息,适合作为 shell 脚本或 程序的数据使用。
如果输出太长,屏幕放不下,就把 它交给 less,它会一屏一屏 地显示:
set -o | less
set +o | less
如果你想练习设置和取消设置选项, 可以拿 ignoreeof 选项来试。 正如我们在第 7 章讨论的,你可以 通过按 ^D(eof 键)来 终止一个 shell。但如果这个 shell 恰好是你的登录 shell,你就会 被登出。
遗憾的是,一不小心按到 ^D 就把自己登出,这种事太容易发生了。 为了防范这种情况,你可以设置 ignoreeof 选项。这是告诉 shell, 当你按 ^D 时不要结束。你必须 输入 exit 或 logout 才行。 要设置这个选项,使用下面这条命令:
set -o ignoreeof
要取消设置这个选项,使用:
set +o ignoreeof
动手试一试:设置选项、取消设置选项、 显示选项。每改一次,就显示一下选项的 当前状态,然后按 ^D,看看 会发生什么。
— 提示 —
除非你是高级用户,否则你需要关心的 选项只有 ignoreeof、 monitor 和 noclobber, 以及 emacs 或 vi 中的一个。
monitor 选项启用作业控制, 我在第 26 章讨论。noclobber 选项可以防止你在重定向标准输出时 意外删掉一个文件(见第 15 章)。 emacs 和 vi 选项用来指定 你想用哪个内置编辑器来调出并编辑 先前输入过的命令。本章后面会解释。
这些选项最好在你的环境文件中设置, 那是一个初始化文件,每次启动新 shell 时都会自动执行。我们将在 第 14 章讨论这个文件。
名称的由来(What's in a Name?)
set
你会注意到,仅仅在这一章里,我们就 已经用好几种不同的方式使用 set 命令,每种都有自己的语法。 我们用 set 显示 shell 变量、 创建 shell 变量、打开和关闭 shell 选项,以及显示 shell 选项。
如果你仔细观察,就会发现我们其实 面对的是四条不同的命令,只不过它们 恰好同名。看来,set 这个名字 对程序员有种特别的吸引力,让他们 忍不住反复使用。
这并不像听起来那么奇怪。你知道吗, 在英语里,"set"这个词的义项比 任何其他单词都多?去查查看词典吧。 我保证你会大吃一惊。
机器可读,人可读
(Machine-readable, Human-readable)
当一个程序以可作为另一个程序数据的 方式显示复杂输出时,我们说这个输出 是机器可读的(MACHINE-READABLE)。 虽然这个词会让人联想到机器人像人 一样阅读的画面,但它的意思只是说 输出被格式化成便于程序处理的形式。 例如,你可能有一张人口普查数据表, 它被组织成用逗号分隔的数字串, 而不是排成各列。虽然你我读起来 很别扭,但它对程序来说是合适的输入。
当输出被设计得特别好读时,我们有时 说它是人可读的(HUMAN-READABLE)。 这个术语用得不多,但你会在 GNU 工具集的 man 手册页里遇到它。正如 我们在第 2 章讨论的,GNU 工具集 用于许多种类的 Unix,包括 Linux。 事实上,许多命令都有专门用来产生 人可读输出的选项。
举个例子,在上一节我提到, set -o 命令以易于阅读的 方式显示输出,而 set +o 显示的输出适合当作 shell 脚本的数据。 换一种说法就是:set -o 产生人可读的输出,而 set +o 产生机器可读的输出。
当然,每个人的喜好并不相同。某样 东西号称是人可读的,并不意味着你 个人就会比机器可读的版本更喜欢它。 例如,我就恰好更喜欢 set +o 的输出,而不是 set -o 的输出。(*)
* 脚注
不过话说回来,我也喜欢在牛油果上 抹花生酱。
练习
(Exercises)
复习问题 #1:
交互式 shell 和非交互式 shell 有什么 区别?
复习问题 #2:
环境是一张变量表,shell 以及该 shell 启动的任何程序都可以使用其中的变量。 环境中存放的是哪一类变量?请举三个 例子。
哪一类变量不属于环境?
复习问题 #3:
在 Bourne shell 家族(Bash、Korn shell)中,要让一个 shell 变量成为 环境的一部分,你使用哪条命令?
复习问题 #4:
怎样显示你所有环境变量的值?所有 shell 变量的值?单个变量的值?
复习问题 #5:
解释"machine-readable"(机器可读)和 "human-readable"(人可读)这两个术语。
应用你的知识 #1:
环境变量 USER 存放的是当前用户 ID 的名字。请给出显示该变量值的三种 不同方法。
应用你的知识 #2:
创建一个名为 SPORT 的环境变量, 并把它的值设为 "surfing"。显示该 变量的值。
启动一个新的 shell。再次显示该变量, 以说明它作为环境的一部分被传给了 新 shell。
把 SPORT 的值改为 "running", 并显示新的值。
现在退出这个 shell,回到原来的 shell。显示 SPORT 的值。你看到 了什么?为什么?
应用你的知识 #3:
在 Bourne shell 家族(Bash、Korn shell)中,ignoreeof 选项告诉 shell:当你按 ^D(eof 键) 时不要把你登出。
启动 Bash 或 Korn shell。检查 ignoreeof 是开着还是关着。如果 是关着的,把它打开。
按 ^D,确认你不会被登出。然后 关闭 ignoreeof。再按一次 ^D,确认你确实被登出了。
进一步思考 #1:
环境变量并不是真正的全局变量,因为 子进程所做的修改不会回传给父进程。 假设情况不是这样。请说明一个入侵者 如何利用这个漏洞,在一个他拥有账号 的多用户系统上制造麻烦。
进一步思考 #2:
C-Shell 家族在环境变量和 shell 变量 之间有清晰的划分。Bourne shell 家族 就没那么清楚,因为新创建的变量默认 是 shell 变量。如果它被导出,就同时 成为局部变量和环境变量。
这两种体系各有什么优点和缺点?
你认为哪种体系更好?为什么?
你认为 Bourne shell 家族处理变量的 方式为什么会如此令人困惑?
© 本书全部内容,2026 年版权所有,Harley Hahn
完整的商标与版权信息