Unix 之书
☰ 目录

第 13 章...

使用 Shell:命令与定制 (Using the Shell: Commands and Customization)

在第 11 章里,我们泛泛地讨论了 Shell。在第 12 章中,我们在这个基础的 之上讨论了交互式 Shell、进程、环境变量、 Shell 变量和 Shell 选项。在本章,我们 要把其余的基本概念讲完。

你花在本章上的时间会很值得,原因有两个。 第一,我们要讲的知识对用好 Shell 至关 重要。第二,当你开始把关于 Shell 的知识 融会贯通时,你会发现,接下来要学的这些 技巧会在日常工作中为你省下大量时间。

元字符
(Metacharacters)

用你的键盘,你可以键入字母、数字以及各种 其他字符,例如标点符号和算术符号。 此外,你还可以使用 <Space>、 <Tab> 和 <Return> 键来 产生空格、制表符和换行符(见 第 7 章)。

我们把字母和数字统称为字母数字 (ALPHANUMERIC)字符。使用字母数字 字符很直接:你想打什么就打什么,看到什么 就是什么。然而,当你使用 Shell 时,许多 其他符号具有特殊的含义。我们把这类字符 称为元字符(METACHARACTERS),你需要 了解它们是如何工作的。

例如,如我们在第 10 章所讨论的,你可以 在同一行上输入多条命令,只要用分号把它们 分开即可。因为分号对 Shell 有特殊含义, 所以它是一个元字符。

 

从更抽象的角度说,当你按下 <Space>、 <Tab> 或 <Return> 时,你也在 使用元字符。空格和制表符作为空白字符,用来 分隔一条命令的各个部分(第 10 章), 而换行符用来标记一行的结束 (第 7 章)。

如果你现在就试图记住所有元字符的用途,会很 吃力,因为你要背的东西还不具备意义。既然每 个元字符都是用来实现 Shell 提供的某项 具体服务,那么在你学到某项服务时再学习 对应的元字符,才说得通。

例如,我前面解释过,$(美元)字符用于 你想引用某个变量的值时(比如 $TERM)。 因此 $ 是一个元字符。但如果你当时还 不理解什么是变量,我这样告诉你又能有什么 意义呢?

我会给你一份完整的元字符清单,但并不期望你 现在就把它们全部弄懂。以后你会一个一个地 了解它们。眼下我只想确保你见到它们时能 认出来,以免不小心用错。

例如,如果你输入下面这条命令,Shell 会看到 那个分号,并把你键入的内容解释为两条命令:

echo The search path is $PATH; the shell is $SHELL.

然而,如果你认出分号是元字符,就可以告诉 Shell 不要去动它:

echo "The search path is $PATH; the shell is $SHELL."

当我们用这种方式保护元字符时,我们说把它 "引用"(quote)起来了。

我们马上就会讨论引用。不过在此之前,我想先 让你认识一下元字符这个"完整阵容"。首先 看看图 13-1。它列出了我们在 Unix 中 使用的所有非字母数字字符,其中并非全都是 元字符。然后再看图 13-2,那里列出了 元字符以及对它们用途的简短说明。为了方便 查阅,我标注了讨论每个元字符的章节号。

在图 13-2 中你会注意到,有几个元字符 用于文件名扩展,它也被称为 "globbing"。 globbing 有一整套使用元字符的规则,我们将在 第 24 章详细讨论。

Figure 13-1: Unix 中使用的非字母数字字符

这里列出了我们在 Unix 中使用的所有 非字母数字字符。作为 Unix 传统的一部分, 大多数字符都有我们口头交流时使用的 昵称。例如,感叹号叫作 "bang"; $TERM 读作 "dollar term";如此 等等。如果你想听起来像个 Unix 老手,就用 这些昵称。

字符 英文名称 Unix 昵称
&ampersand(和号)
'apostrophe(撇号)quote、single quote(单引号)
*asterisk(星号)star(星)
@at sign(at 符号)at
`backquote(反引号)backtick(反撇号)
\backslash(反斜杠)
{ }brace brackets(花括号)braces、curly brackets(弯括号)
^circumflex(抑扬符)carat(尖符)
:colon(冒号)
,comma(逗号)
$dollar sign(美元符号)dollar(美元)
<Return>enter、return(回车)newline(换行)
=equal sign(等号)equals(等于)
!exclamation mark(感叹号)bang
>greater-than sign(大于号)greater-than(大于)
-hyphen、minus sign(连字符、减号)dash、minus(见提示)
<less-than sign(小于号)less-than(小于)
#number sign(数字号)hash、pound(见提示)
( )parentheses(圆括号)
%percent sign(百分号)percent(百分之)
.period(句号)dot(点)
+plus sign(加号)plus(加)
?question mark(问号)
"quotation mark(引号)double quote(双引号)
;semicolon(分号)
/slash(斜杠)forward slash(正斜杠)
<Space>space(空格)
[ ]square brackets(方括号)brackets(括号)
<Tab>tab(制表符)
~tilde(波浪号)
_underscore(下划线)
|vertical bar(竖线)pipe(管道)
字符 英文名称 Unix 昵称

— 提示 —

图 13-1 中有两个非字母数字字符,根据你 开始使用 Unix 的早晚不同,叫法也不同。这是 因为 20 世纪 90 年代中期随着 Linux 开始 流行,传统发生了变化。

连字符(-):年轻人把连字符叫 "dash" (破折号);年纪大的人叫它 "minus"(减号)。

数字号(#):年轻人叫它 "hash"; 年纪大的人叫它 "pound"(磅)。

如果你是学生,当你和三十岁以上的教授交谈时, 这一点就很重要。记得说 "minus" 和 "pound",这样会让老一辈的人觉得舒服。

Figure 13-2: Shell 使用的元字符

许多非字母数字字符在 Shell 内部都有 特殊含义。这些就是元字符。本表列出了 所有元字符以及它们的 Unix 昵称。最终你会 掌握每个元字符的确切使用规则。请注意, 花括号有两种不同的用法。

为了方便查阅,我标注了讨论每个元字符的 章节号。

字符 章节 名称 用途
{ }24花括号(braces)花括号扩展:生成一串字符模式
|15管道(pipe)命令行:创建一条管道
<15小于(less-than)命令行:重定向输入
>15大于(greater-than)命令行:重定向输出
( )15圆括号(parentheses)命令行:在子 Shell 中运行命令
#14hash、pound命令行:注释的开始,忽略本行剩余部分
;10分号(semicolon)命令行:分隔多条命令
`13反引号(backquote)命令行:命令替换
~24波浪号(tilde)文件名扩展:插入主目录名
?24问号(question mark)文件名扩展:匹配任意单个字符
[ ]24方括号(brackets)文件名扩展:匹配一组字符中的一个
*24星号(star)文件名扩展:匹配零个或多个字符
!13bang(感叹号)历史列表:事件标记
&26和号(ampersand)作业控制:在后台运行命令
\13反斜杠(backslash)引用:转义下一个字符
'13quote、single quote(单引号)引用:抑制所有替换
"13double quote(双引号)引用:抑制大多数替换
{ }13花括号(braces)变量:界定变量名
$13dollar(美元)变量:替换为变量的值
<Return>7newline(换行)空白字符:标记一行的结束
<Tab>10tab(制表符)空白字符:分隔命令行中的各个词
<Space>10space(空格)空白字符:分隔命令行中的各个词
字符 章节 名称 用途

引用与转义
(Quoting and Escaping)

有时候,你会想按字面意义使用某个元字符,而 不带任何特殊含义。例如,你可能想让分号就是 一个分号,而不是命令分隔符;或者你可能想用 |(竖线),但不希望它起管道的 作用(*)。这种情况下,你必须告诉 Shell 按字面解释这个字符。当你这样做时,我们说你把 这个字符"引用"(QUOTE)起来了。

* 脚注

正如弗洛伊德曾说过的那样,"有时候,一根 竖线就是一根普通的竖线。"

引用字符有三种方法:使用反斜杠、使用一对 单引号,或者使用一对双引号。

引用元字符最直接的方法,是在它前面加一个 反斜杠(\)。这告诉 Shell 忽略该字符 可能具有的任何特殊含义。例如:

echo It is warm and sunny\; come over and visit

在这个例子中,我们在 ;(分号)字符前 加了一个反斜杠。如果不加,Shell 就会把 ; 解释为元字符,从而认为你想输入 echocome 两条独立的命令。 这当然会产生错误。

— 提示 —

当你想引用单个字符时,要小心使用反斜杠 (\),而不是正斜杠(/)。

正斜杠的用途完全不同,它用于路径名中 (见第 23 章)。

当你用反斜杠引用单个字符时,我们把那个 反斜杠称为"转义字符"(escape character)。 这是一个重要的概念,所以我想花点时间讲讲 它。

你也许还记得,在第 6 章讨论运行级别时, 我们谈过"模式"的概念。具体地说,当一个 计算机系统、程序或设备可以处于若干状态中的 某一个时,我们用 MODE(模式)一词来指某个 特定状态。例如在第 6 章我解释过,你的 Unix 或 Linux 系统可以以单用户模式或多用户 模式启动。类似地,当我们在第 22 章讨论 vi 文本编辑器时,你会看到,vi 在任何时刻要么处于输入模式,要么处于 命令模式。

当程序处于某个特定模式,而我们做了某个动作 使其切换到另一个模式时,我们说我们从一种模式 "转义"(ESCAPE)到了另一种模式。当我们通过 按下某个键来切换模式时,我们把这个键称为 转义字符(*)。当你在 Shell 下键入一条命令时, 反斜杠就是一个转义字符,因为它告诉 Shell 从一种模式(注意元字符)切换到另一种模式 (不注意元字符)。我希望你记住转义字符这个 概念,因为你会一次又一次地遇到它,尤其 如果你是程序员的话。

* 脚注

现在你知道键盘上为什么有一个 <Esc> 或 <Escape> 键了。它的设计用途就是让 程序从一种模式切换到另一种模式。

在 Unix 中,"escape"一词有两种用法。最常见 的是说我们从一种模式转义到另一种模式。例如, 当你使用 vi 文本编辑器时,你按下 <Esc> 键,从插入模式转义到命令模式。

而在 Shell 里,我们用 "escape"这个词略有 不同,它是 quote(引用)的同义词。举例来说, 看看这个例子:

echo It is warm and sunny\; come over and visit

我们可以说,我们用反斜杠"转义"了那个分号。 这与说我们用反斜杠"引用"了那个分号是同一 回事(*)。

* 脚注

严格说来,当我们说从一种模式转义到另一种模式 时,是把 "escape"当作不及物动词使用,也就是 不带宾语的动词。而当我们说转义(引用)某个 字符时,是把 "escape"当作及物动词使用, 也就是带宾语的动词。

除非你是在披头士乐队走红之前就上了学,否则 你的语文老师也许忘了教你语法。如果是这样, 你不知道及物动词和不及物动词的区别,那么 这里再举个例子引起你的兴趣。

当你说 "Feel the fabric and tell me if it is soft"(摸摸这块布料,告诉我它是否柔软)时, 你用的是及物动词。动词是 "feel",宾语是 "fabric"。

当你说 "Do you feel lucky?"(你觉得自己很幸运 吗?)时,你用的是不及物动词。动词是 "feel", 没有宾语。(严格说来,"feel" 这里是系动词, "lucky" 是表语形容词,充当主语补足语。)

虽然你可能很难相信,但这类知识很重要,尤其是 当你想成为作家的时候。

如果只有反斜杠一个元字符引用手段,那也够用了, 因为同一行里可以多次使用反斜杠。想想下面这条 命令:

echo It is warm (and sunny); come over & visit

这条命令不能正常工作,因为里面有许多元字符: ();&。 要让这条命令能工作,我们必须转义这四个字符:

echo It is warm \(and sunny\)\; come over \& visit

现在我们得到了一条有效的命令,它能正常运行。 不过它实在太难读了。作为替代办法,Shell 允许 我们用单引号来引用一串字符。因此,上面那个 例子我们可以改写成:

echo 'It is warm (and sunny); come over & visit'

这样一来,我们把单引号之间的所有内容都引用 起来了。当然,这其中包括所有字符,不只是 元字符,但引用字母数字字符并没有什么坏处。 (停一下,想想为什么会这样。)

所以(到目前为止)我们有两种引用元字符的方法: 可以用反斜杠引用单个字符,也可以用单引号 引用一串字符。如果有需要,你可以在同一条命令 中把两种引用方式结合起来:

echo It is warm '(and sunny);' come over \& visit

大多数情况下,反斜杠和单引号就够你用了。然而, 有些情况下,使用第三种引用方式——双引号—— 会更方便。下面举个例子。

有时你会想在被引用的字符串里使用 $ 字符,通常是为了引用某个变量的值。例如,下面 这条命令会把你的用户识别名和终端类型显示在 尖括号中:

echo My userid is <$USER>; my terminal is <TERM>

这个形式是不行的,因为元字符 <;> 有特殊含义。 ($ 是好事;我们正是希望它作为 元字符起作用。)解决办法是只引用那些我们想要 按字面处理的元字符:

echo My userid is \<$USER\>\; my terminal is \<$TERM\>

这样确实能工作,但实在太复杂了。我们当然可以 改用单引号来代替反斜杠:

echo 'My userid is <$USER>; my terminal is <$TERM>'

这样好读一些,但它把所有元字符都引用 了,包括 $。这意味着我们会原样看到 $USER$TERM,而不是变量的 值。对于这类情况,我们使用双引号,因为所有 $ 元字符在双引号中都保留其特殊含义。 例如:

echo "My userid is <$USER>; my terminal is <$TERM>"

因为我们用的是双引号,所以 ;> 字符都被引用了,而 $ 没有。(请自己动手试一试。)

除了 $ 之外,双引号还保留另外两个 元字符的含义,即 \(反斜杠)和 `(反引号)。反引号我们稍后再谈。 目前你只需要知道,它有特殊含义,这个含义在 双引号内被保留,而在单引号内则不被保留。

小结如下:

• 用反斜杠引用单个字符。 (这样做时,我们说你转义了那个字符。)

• 用单引号引用一串字符。

• 用双引号引用一串字符,但保留 $(美元)、`(反引号)或 \(反斜杠)的特殊含义。

强引用与弱引用
(Strong and Weak Quotes)

从上面的讨论可以看出,单引号比双引号更"强"。 因此,我们有时把单引号称为强引用 (STRONG QUOTES),把双引号称为弱引用 (WEAK QUOTES)。这里有个记住名字的好办法: 单引号强到只需要一个符号就够了;双引号弱一些, 所以需要双倍的符号。

实际上,反斜杠才是所有引用方式中最强的 (尽管我们没给它起专门的名字)。一个反斜杠 可以引用任何东西,所以如果你的单引号 偶尔不管用,就试试反斜杠。例如,总有一天你 可能需要转义一个单引号:

echo Don\'t let gravity get you down

反斜杠强大到甚至可以引用换行符。(花点时间 想想这意味着什么。)

假设你在一行的末尾键入 \<Return>。 这就产生了 \ 后面跟着一个换行符 (见第 7 章)。光标会移到下一行,但由于 换行符已经失去了它的特殊含义,它不再表示一行的 结束。这就是说你键入的内容将是上一行的继续。

试一试用下面这个例子,在每一行末尾都按 <Return>:

echo This is a very, very long \
line that goes on and on for a \
very, very long time.

如果你键入得完全正确,你会输入一条很长的命令, 并且看到一行很长的输出。

与反斜杠不同,单引号和双引号不会引用 换行符。既然如此,如果你在一个被单引号或双引号 引用的字符串中间按下 <Return>,你认为 会发生什么?例如,假设你键入了下面这一行:

echo 'This line ends without a second quote

当你在一行末尾按下 <Return> 时,换行符 没有被引用,所以它保留了特殊含义:也就是它标记 了这一行的结束。然而问题在于,这个单引号没有 配对。

如果你用的是 C-Shell 或 Tcsh,你会看到一条 错误消息,告诉你有一个 ' 字符没有配对。

如果你用的是 Bash 或 Korn Shell,Shell 会干脆 等你继续输入,指望你最终会键入第二个引号。 (Bourne Shell 比 C-Shell 更乐观。)一旦你键入 第二个引号,Shell 就会把所有内容合成一条极长 的命令,中间还夹着一个换行符。

作为练习,请键入下面两行,看看你的 Shell 会 怎么做:

echo 'This line ends without a matching quote
and here is the missing quote'

你能解释发生了什么以及为什么会这样吗?

Shell 内置的命令:type
(Commands That Are Built into the Shell: type)

当你输入一条命令时,Shell 会把命令行拆成 若干部分并加以分析。我们说 Shell 在"解析" (PARSE)这条命令。每条命令的第一部分是命令 名;其余部分是选项或参数(见第 10 章)。

解析完命令之后,Shell 决定如何处理它。这里 有两种可能。有些命令是 Shell 内部的,也就是 说 Shell 直接解释执行它们。这些就是内部命令 (INTERNAL COMMANDS),常称为内置命令 (BUILTIN COMMANDS),或更简单地称为 内置项(BUILTINS)。所有其他命令都是外部 命令(EXTERNAL COMMANDS),它们是各自独立的 程序,必须单独运行。

当你输入一个内置命令时,Shell 就在自己的进程 内部亲自执行这条命令。当你输入一个外部命令时, Shell 会搜索相应的程序,然后在另一个独立的 进程中运行它。打个比方,假设你拨打一家大公司 的客服热线。如果接电话的人能回答你的问题,他 自己就回答了(内部命令);否则他把你转接给 合适的人(外部命令)。(*)

* 脚注

更准确地说,外部命令的处理过程是这样的:Shell 通过 fork 创建一个子进程,然后等待。子进程 通过 exec 运行实际的程序。程序结束后,子进程 消亡。控制权返回父进程,父进程使子进程彻底 消失。然后父进程显示 Shell 提示符,邀请你输入 下一条命令。(更多细节,见第 12 章关于 进程的讨论。)

要判断一条命令是否内置在 Shell 中,有两种 办法。第一,你可以试着显示这条命令的 man 手册页。外部命令都有自己的 man 手册页;内置 命令则没有。内置命令的文档要么写在 Shell 本身的 man 手册页里,要么写在一个专门介绍 所有内置命令的 man 手册页里。

检查一条命令是否为内置命令,更快的办法是使用 type 命令。语法是:

type command...

例如:

type date time set

确切的输出取决于你使用的是哪个 Shell。例如, 下面是我在 Bash 中使用这条命令时看到的:

date is /bin/date
time is a shell keyword
set is a shell builtin

下面是在 Korn Shell、Tcsh 和 C-Shell 中看到的:

date is a tracked alias for /bin/date
time is a reserved shell keyword
set is a special builtin

虽然输出略有不同,但结论是一样的:date 是外部命令;其余都是内置命令。

到了这里,你还不必去理解 "tracked alias" (跟踪别名)是什么意思。这只是一个你可以忽略 的技术性区分。同样,我们也不必区分内置命令和 关键字:它们都是 Shell 内置的。(关键字是用于 编写 Shell 脚本的特殊内部命令。)重要的是要 认识到 date 是一个外部命令,存在于 自己的文件里(在某个系统是 /bin/date, 在另一个系统上是 /usr/bin/date)。 等我们在第 24 章讨论路径名之后,这些就会 更好懂了。

Unix 和 Linux 系统附带了成百上千条外部命令, 那内置命令有多少条呢?这取决于你使用的 Shell。 作为一份有趣的参考,图 13-3 给出了我们在 第 11 章讨论过的那些 Shell 的内置命令 数量。

Figure 13-3: 各种 Shell 的内置命令数量

Unix 命令有数百种之多,其中大多数是外部命令, 也就是说它们作为独立的程序存在。不过每个 Shell 都有一定数量的内部命令,即内置命令,可以直接 运行。本图中的表格列出了每个 Shell 的内置命令 数量,其中也包括关键字。为了做一个有趣的对比, 我还列出了早期 Bell Labs 的 Unix 用户所使用的 旧版 Bourne Shell。

Shell 内置命令
Bourne Shell18
Korn Shell47
C-Shell55
Bash69
Tcsh87
FreeBSD Shell97
Zsh129

了解内置命令
(Learning About Builtin Commands)

Unix 传统的一部分是:当某人创造了一个工具, 他就应该为其他用户写好这个工具的文档。具体 而言,人们期望程序员在写出一条新命令之后, 会为这条命令提供一份 man 手册页。由于联机 手册的格式早已约定俗成(见第 9 章),程序员 在编完程序之后创建一份 man 手册页并不困难。 事实上,几乎所有 Unix 程序在发布时都带有一份 作为正式文档的 man 手册页。

这套做法用于外部命令时很合适。因为每条外部 命令本身就是一个程序,所以它自带 man 手册页。 但内置命令怎么办呢?如我们所讨论的,内置命令 不是独立的程序;它们是 Shell 的一部分。既然 内置命令这么多(见图 13-3),指望那些开发 Shell 的程序员为每个内置命令都写一份单独的 man 手册页是不现实的。

作为替代,所有内置命令的文档都写在 Shell 本身 的 man 手册页里。例如,Korn Shell 的内置命令 记录在 Korn Shell 的 man 手册页中。因此,要想 了解某个特定 Shell 的内置命令,你需要查看 相应的 man 手册页。你可以使用下面几条命令 之一:

man bash
man ksh
man tcsh
man csh

不过请记住,Shell 的 man 手册页相当长,你可能 需要稍加搜索才能找到你想要的东西。

有些 Unix/Linux 系统为内置命令设置了单独的 man 手册页。要看看你的系统是否如此,你可以 使用 apropos 命令(见第 9 章):

apropos builtin

如果你的系统有这样的页面,那它就是快速查阅 所有内置命令清单的地方。在 Linux 和 FreeBSD 上,你可以使用:

man builtin

在 Solaris 上,使用:

man shell_builtins

Linux 还有一条 help 命令,你可以用它 以多种方式显示来自 builtin man 手册页 的信息。语法是:

help [-s] [command...]

其中 command 是命令名。

首先,单独键入 help 就可以显示所有内置 命令的一行摘要。如果输出太长,你可以把它 交给 less(第 21 章),一屏一屏地显示 这些信息:

help
help | less

当你想显示一份紧凑的内置命令清单时,就该用 这条命令,例如,当你在寻找某条特定命令的时候。

你也可以用 help 显示一条或多条具体 命令的信息,例如:

help set
help pwd history kill
help help

(你可以看到,help 本身就是一条内置 命令。)

最后,如果你只想看看某条命令的语法,可以使 用 -s(syntax,语法)选项:

help -s help
help -s pwd history kill

— 提示 —

当你编写 Shell 脚本时,你会使用一些特殊的 内置命令 — forifwhile 等等 — 来控制脚本的流程。 这些命令有时被称为关键字(KEYWORDS)。

当你在编写 Bash 脚本时,检查某个关键字语法的 最快办法是使用 help 命令。例如,要检查 所有 Bash 关键字的语法,使用:

help -s case for if select while until

如果你需要更多信息,就去掉 -s 选项:

help case for if select while until | less

请注意,我用了 less,以确保输出不会 滚出屏幕。

外部命令与搜索路径
(External Commands and the Search Path)

如果一条命令不是 Shell 内置的 — 而且 大多数命令都不是 — 那么 Shell 就必须 找到相应的程序来执行。例如,当你输入 date 命令时,Shell 必须找到 date 程序并替你运行它。因此, date 是外部命令的一个例子。

Shell 怎么知道该去哪里找外部命令呢?它检查 PATH 环境变量(见第 12 章)。 和所有变量一样,PATH 包含一串字符, 在这里是一系列目录名,我们把它们称为搜索 路径(SEARCH PATH)。

我们要到第 24 章才详细讨论目录。眼下我 只想让你知道:程序存储在文件里,而每个文件都 位于某个目录之中(*)。搜索路径就是包含所有 外部命令程序的目录清单。因此,搜索路径中的 某个目录里就有存放 date 程序的那个 文件。

* 脚注

为了和你的经验联系起来,你可以把 Unix 目录 想象成与 Windows 或 Macintosh 的文件夹类似。 不过,两者之间存在微妙但重要的差别。

如果你想看看自己的搜索路径,只要显示 PATH 变量的值即可:

echo $PATH

下面是一些典型的输出:

/bin:/usr/bin:/usr/ucb:/usr/local/bin:/home/harley/bin

在这个例子中,搜索路径由五个目录组成:

/bin
/usr/bin
/usr/ucb
/usr/local/bin
/home/harley/bin

你的搜索路径可能与这个例子稍有不同,但总体 来说,Unix 系统倾向于为存放外部命令的目录 使用标准名称。例如,我见过的每个 Unix 系统 都有 /bin/usr/bin 目录, 许多还有 /usr/ucb

等我们在第 24 章讨论完目录之后,这些 名称会更好懂。眼下我只提一句:名称中的 bin 用来表示该目录存放程序。

在我们的例子里,前三个目录 — /bin/usr/bin/usr/ucb — 存放的是系统中所有 用户都要使用的程序。头两个目录在每个 Unix 系统上都有,并且在安装 Unix 时自动建好。 /usr/ucb 目录只存在于某些系统上,它的 任务是存放源自 Berkeley Unix 的程序(见 第 2 章)。(ucb 这个名字是 University of California at Berkeley (加州大学伯克利分校)的缩写。)

后两个目录是为了自定义而设: /usr/local/bin 由系统管理员建立,用来 存放他专为本地使用而安装的程序; /home/harley/bin 指的是用户 harley 主目录中那个名为 bin 的 目录。你可以为自己建一个这样的目录,用它来 存放你自己的程序。

当 Shell 需要找到一条外部命令时,它会按搜索 路径中指定的顺序逐个检查每个目录。在我们的 例子中,Shell 会先从 /bin 开始查找。 如果找不到想要的东西,它再去 /usr/bin 查找,依此类推。

当 Shell 找到那条外部命令时,就停止搜索并执行 该程序。如果没有任何目录包含这条命令,Shell 就会放弃并显示一条错误消息。例如,如果你输入 命令 weedly,你会看到类似这样的消息:

weedly: command not found

修改你的搜索路径
(Modifying Your Search Path)

在大多数系统上,你不必自己定义搜索路径,因为 PATH 变量已经替你设好了。然而,在某 些情况下(我们马上就会讨论),你可能想修改 搜索路径,所以我要教你怎么做。基本思路是:把 修改 PATH 变量的命令放进一个初始化 文件,这个文件会在你每次登录时自动执行。 (我们将在第 14 章讨论初始化文件。)

首先,谈谈如何把 PATH 变量设为某个 特定的值。Bourne Shell 家族和 C-Shell 家族 要分开处理,因为命令略有不同。

在 Bourne Shell 家族(Bash、Korn Shell)中, 你用 export 命令来设置 PATH (第 12 章)。使用 export 会把 PATH 变成环境变量,这意味着 Shell 及 随后所有进程都能使用它。下面是一条能完成这个 任务的典型命令:

export PATH="/bin:/usr/bin:/usr/ucb:/usr/local/bin"

这条命令本身很直截了当:它把 PATH 的 值设为一个字符串,内容是若干目录名组成的列表。 你可以看到,各名称之间用冒号分隔,而且等号两边 没有空格。

要自己设置 PATH 的值,你就把这条命令 (或类似的命令)放进你的"登录文件",那是一 个初始化文件,每次登录时都会自动执行。要改动 搜索路径,你只要修改登录文件即可。(这些内容 在第 14 章都有解释。)

在 C-Shell 家族(C-Shell、Tcsh)中,我们使用 稍有不同的命令,因为我们设置的是 path 这个 Shell 变量,而不是 PATH 环境 变量:

set path=(/bin /usr/bin /usr/ucb /usr/local/bin)

你也许还记得第 12 章讲过的:每当你改动 path,Shell 就会自动重设 PATH。 因此,这条命令得到的 PATH 设置与前面 那条 Bourne Shell 命令相同。

不过请注意语法上的差别。在 Bourne Shell 命令 中,我们把一个环境变量(PATH)的值设为 一长串字符。在 C-Shell 命令中,我们把一个 Shell 变量(path)的值设为一个名称 列表。按 C-Shell 的语法,列表由一个或多个 元素组成,元素之间用空格分隔,并且整个列表 放在圆括号内。

刚才我提到,设置 PATH 的 Bourne Shell 命令应放进你的登录文件。而设置 path 的 C-Shell 命令则放进你的"环境文件",那是另一 个初始化文件,每次启动新的 Shell 时都会自动 执行。(同样,这些内容都在第 14 章解释, 那里还有示例。)

在大多数系统上,定义 PATH 变量的命令 已经替你设置好了,所以你不必使用我们一直在 讨论的那类命令。不过,你可能想修改默认的搜索 路径以适合自己的使用。例如,如果你编写自己的 Shell 脚本和程序,并把它们放在自己的个人 bin 目录($HOME/bin)中,你就 会想把该目录的名称加到搜索路径里。

下面两条命令告诉你该怎么做。第一条适用于 Bourne Shell 家族;第二条适用于 C-Shell 家族。

export PATH="$PATH:$HOME/bin"
set path = ($path $HOME/bin)

请注意这里的语法。它的意思是"把搜索路径的 值改为旧值后面再加上 $HOME/bin。" 换句话说,我们把名称 $HOME/bin 追加到 现有搜索路径的末尾。

另外,你也可能想把新目录的名称插入搜索路径的 开头。命令是:

export PATH="$HOME/bin:$PATH"
set path = ($HOME/bin $path)

正如这些例子所示,我们可以依据变量的当前值来 修改它。这是一个重要的观念,所以不要死记格式。 花一点时间,确保你完全理解到底发生了什么。

现在你知道如何把自己的目录加进搜索路径了,于是 问题来了:应该把自己的目录放在列表的开头还是 末尾?这完全取决于你想要什么。

如果你把个人目录放在搜索路径的末尾,Shell 会 最后才检查你的目录。如果你把目录放在搜索路径的 开头,Shell 会首先检查它。举例来说,假设你写了 一个名为 date 的程序,放在 $HOME/bin 里。然后你输入:

date

如果你把自己的目录放在搜索路径的开头,Shell 就会运行你的 date 程序,而不是标准 版本。这样,你就可以用你自己的程序有效地替换 任何外部命令。反之,如果你把目录放在搜索路径 的末尾,Shell 就会运行标准的 date 程序,而不是你的版本。这可以防止你在不知不觉 中用同名文件替换掉某个程序。哪种做法最适合你, 由你自己决定。

作为程序员,还有一个目录名称你可能也想加到 搜索路径中。如果你指定一个点(.)字符, 它就把你的"工作目录"加入搜索路径。(工作 目录就是你当前正在使用的那个目录。我们将在 第 24 章讨论它。)

下面这个例子可以帮助你弄清这个概念。以下命令 把 $HOME/bin 和你的工作目录都追加到 当前路径的末尾(按此顺序)。第一条命令适用于 Bourne Shell 家族;第二条适用于 C-Shell 家族:

export PATH="$PATH:$HOME/bin:."
set path = ($path $HOME/bin .)

这是告诉 Shell:当它查找程序时,最后才检查 你当前正在工作的那个目录。

关于搜索路径的详细讨论超出了本书的范围 (也没有太大必要)。通常,你可以直接接受 系统默认替你设好的搜索路径,至多做些细微 改动。

— 提示 —

除非你是专家,否则为了稳妥起见,请把你的 个人目录放在搜索路径的末尾

黑客如何利用搜索路径
(How a Hacker Can Use the Search Path)

把工作目录(.)放进你自己的搜索路径 没什么问题,但绝不要为超级用户(root) 或任何其他有特殊权限的用户识别名这样做。这样 做会造成安全隐患。举例来说...

你是一名系统管理员,为了方便,你把工作目录放 在了 root 搜索路径的开头。你的某个 用户 — 其实是个黑客(*) — 向你 求助,于是你以 root 身份登录,并切换到 该用户的主目录(这是很常见的事)。然后你输入 ls 命令,列出该用户目录的内容(这也是 很常见的事)。

* 脚注

当然,我说的是坏黑客 — 也就是破解者 (cracker) — 而不是好消息客。关于 好黑客与坏黑客的讨论,见第 4 章。

你不知道的是,那个黑客已经做了一个 ls 的替换版本,放在他的主目录里。这个 假冒的 ls 表现得和真的一模一样,但当 root 运行它时,它有一个副作用:创建一个 带有特殊权限的秘密文件。之后,黑客就可以用那个 文件(称为后门,BACK DOOR)为自己取得超级 用户权限。

过程是这样的:你切换到该用户主目录的那一刻,它 就成了你的工作目录。当你输入 ls 命令 时,Shell 在工作目录中查找,发现了黑客版本的 ls 并运行它。等你反应过来,黑客已经 接管了你的系统,而你的人生也被揭穿成一场彻底 的骗局。

实际上,这是一个老掉牙的黑客把戏:想方设法以 root 身份运行一个被动手脚的程序, 留下一个可以日后使用的后门。这个故事的教训 是什么?在修改任何用于系统管理的用户识别名的 搜索路径之前,请务必三思。

— 提示 —

请确保所有用于系统管理的用户识别名(包括 root)使用的搜索路径中不包含工作目录, 也不包含任何用户可能有权访问的目录。

Shell 提示符
(The Shell Prompt)

如你所知,每当 Shell 准备好接受你输入命令时, 它就会显示一个提示符。如果你愿意,这个提示符 是可以改的。事实上,提示符的显示方式有很大的 自由度,有些人设计了很讲究的提示符,不仅显示 颜色,还显示各种信息。我们先从简单的开始, 再逐步走向更复杂的自定义。

最初,所有 Shell 的提示符都是两个字符:一个 字符加一个空格。Bourne Shell 用 $ (美元)字符;C-Shell 用 %(百分号) 字符。这一传统沿用至今。因此,如果你使用 Bourne Shell 家族的成员(Bash、Korn Shell), 你能看到的最简单的 Shell 提示符是:

$ date

我在提示符后面键入了 date 命令,这样 你就能看到 $ 后面的那个空格。这个空格 是提示符的一部分。

如果你使用 C-Shell 或 Tcsh,最简单的 Shell 提示符看起来像这样:

% date

虽然按传统 C-Shell 家族应当使用 % 字符,但许多 Tcsh 用户改用 > (大于号)字符,以提醒自己他们用的是扩展过的 C-Shell:

> date

最后一条传统与超级用户有关。当你以 root 身份登录时,无论你使用哪个 Shell,提示符永远是一个 #(井号)字符。 其用意是让你时刻记住自己是超级用户,从而格外 小心:

# date

在继续之前,请看一下图 13-4,那里把基本 提示符做了小结。其中的约定只有几条,我希望你 记住它们的含义,这样无论何时看到 Shell 提示符,你都能立刻回答两个问题:(1)你是否以 超级用户身份登录?(2)如果不是,你用的是哪 一种 Shell?

Figure 13-4: 标准的 Shell 提示符

按约定,标准 Shell 提示符由一个字符加上一个 空格组成。Bourne Shell 家族使用 $ (美元)字符;C-Shell 家族使用 % (百分号)字符。唯一的例外是 Tcsh 有时使用 >(大于号)字符。当你以超级用户 身份登录时,无论使用哪个 Shell,你都会看到 # (井号)字符。

Shell 提示符字符
Bash$
Korn Shell$
C-Shell%
Tcsh%>
超级用户#

修改 Shell 提示符
(Modifying the Shell Prompt)

正如我在上一节所说,通过改变某个变量的值,你 可以修改自己的 Shell 提示符。在 Bourne Shell 家族中,你改变的是一个名为 PS1 的环境 变量(*)。在 C-Shell 家族中,你改变的是一个 名为 prompt 的 Shell 变量。

* 脚注

PS1 这个名字的意思是 "prompt for the shell, number 1"(Shell 的 1 号提示符)。还有 另外三个同类变量 — PS2PS3PS4 — 但你永远 不需要改动它们,所以不必理会。如果你好奇其中 的细节,请查看你的 Shell 的 man 手册页。

我们从一个简单的例子开始。下面是一条适用于 Bourne Shell 家族的命令,它把提示符设为 $(美元)字符后跟一个空格:

export PS1="$ "

同样,下面是一条适用于 C-Shell 家族的命令, 使用标准的 %(百分号)字符:

set prompt = "% "

如果你是 Tcsh 用户,应当改用习惯上使用的 >(大于号)字符:

set prompt = "> "

在继续之前,我想通过复习第 12 章讲过的 基本概念,确保你真正理解这些命令。

变量有两种:全局变量和局部变量。全局变量称为 "环境变量"。局部变量称为"Shell 变量"。

所有 Bourne Shell 都把提示符的值存放在一个 名为 PS1 的环境变量中。要改变环境变量 的值,你使用 export。所以上面用了 export 命令。请注意这里的语法。特别 要说明的是,=(等号)字符前后绝不能 加空格。

所有 C-Shell 都把提示符的值存放在一个名为 prompt 的 Shell 变量中。在 C-Shell 中, 你用 set 来修改 Shell 变量。所以那两条 命令用了 set

到这里你可能会想:Bourne Shell 用一个环境 (全局)变量来存放提示符,而 C-Shell 用一个 Shell(局部)变量,这有什么要紧吗? 一般而言,这并不十分重要,只要你确保在想修改 Shell 提示符时使用合适的命令(exportset)就行。不过,当我们谈到初始化 文件时,这个区别就很重要了,初始化文件能帮助 我们每次登录时自动设好提示符。这些我们在 第 14 章再讲。

继续往下说,到目前为止我们只对 Shell 提示符 做了简单的改动。然而,通过操作合适的变量,你 可以把 Shell 提示符设成任何你想要的样子。 例如,你可以把提示符设成显示一句可爱的话:

export PS1="Enter a command please, Harley$ "
set prompt = "Enter a command please Harley% "

实际上,可爱的 Shell 提示符很快就让人腻烦。 这里有个更有用的做法:用一个提示符显示 Shell 的名字,作为提醒。(请选择适合你所用 Shell 的 那条命令。)

export PS1="bash$ "
export PS1="ksh$ "
set prompt = "csh% "
set prompt = "tcsh> "

这些命令所产生的提示符如下:

bash$
ksh$
csh%
tcsh>

这种提示符对超级用户特别方便。举例来说,假设 你的 root 用户识别名默认使用 Bash。 如果你把提示符设成:

export PS1="bash# "

那么提示符就是:

bash#

其中的 # 会提醒你身为超级用户,而名字 会提醒你正在使用哪个 Shell。

除了使用文字和字符之外,还有三种方法可以增强 你的 Shell 提示符。你可以:

• 把一个变量的值插入提示符。

• 使用转义字符来利用各种特殊代码。

• 把一条命令的结果插入提示符。 (这称为命令替换。)

这些技巧各有各的重要性,其用处远远超出修改 Shell 提示符本身。因此,我要把每个概念分开 讨论,以便你理解其中的一般原理。

使用变量的值
(Using the Value of a Variable)

如我们在第 12 章所讨论的,要使用一个变量 的值,你要键入一个 $(美元)字符,后面 跟上用花括号括起来的变量名。例如:

echo "My userid is ${USER}."

为了方便,如果花括号并不是用来把变量名与其他 字符分隔开的,你可以省略它们。例如:

echo "My userid is $USER."

— 提示 —

当你使用变量的值时,即使并非必要,也最好 养成使用花括号的好习惯。

这样做能提高命令的可读性,尤其是在 Shell 脚本中。此外,由于花括号把你的变量隔离开来, 它们有助于避免那些本来可能让你困惑不解的 神秘语法问题。

在 Shell 提示符中使用变量的值很简单。例如, 要把你的用户识别名插入提示符,你可以用:

export PS1="${USER}$ "
set prompt = "${USER}% "

(第一条命令用于 Bourne Shell;第二条用于 C-Shell。)

如果你的用户识别名是 harley(那可就太 酷了),这些命令会产生如下提示符:

harley$
harley%

哪些环境变量适合用在 Shell 提示符中呢?你可以 在第 12 章的图 12-2 中找到最重要 的那些环境变量的列表。原则上,你想用哪个变量 都行。不过,其中大多数并不适合用于 Shell 提示符。为了帮你缩小选择范围,图 13-5 列出了 我认为对 Shell 提示符最有用或最有趣的变量。 要想试一试,就从下面两条命令中选一条适合你 Shell 的,并把其中的变量换成图 13-5 里的 某个变量。

export PS1="${VARIABLE}$ "
set prompt = "${VARIABLE}% "

Figure 13-5: 适合在 Shell 提示符中使用的环境变量

增强 Shell 提示符的一种方法,是把某个有用或 有趣的变量的值包含进去。第 12 章的 图 12-2 列出了所有重要的环境变量。本表 则显示其中适合用于 Shell 提示符的那些。

最左边一列显示每个变量被哪些 Shell 支持: B = Bash;K = Korn Shell;C = C-Shell;T = Tcsh。圆点表示该 Shell 不支持 这个选项。

Shell 变量 含义
B K C THOME你的主目录
• • • THOST你的计算机名称
B • • •HOSTNAME你的计算机名称
B • • THOSTTYPE主机的类型
B K C TLOGNAME当前的用户识别名
B K C TPWD你的工作[当前]目录
B K • •RANDOM0 到 32,767 之间的随机数
B K • •SECONDS当前 Shell 启动至今的秒数
B K C TSHELL你登录所用 Shell 的路径名
B K C TUSER当前的用户识别名

大多数人喜欢使用 LOGNAMEPWDSHELLUSER。不过在我看来, 最值得反复查看的有趣变量是 RANDOMSECONDS。可惜它们只在 Bash 和 Korn Shell 中可用。如果你想试一试,命令如下:

export PS1='Your lucky number is ${RANDOM} $ '
export PS1='Working time: ${SECONDS} seconds $ '

引用变量时该用哪种引号
(Which Quotes to Use When Quoting Variables)

请仔细看看上一节中的两个例子:

export PS1='Your lucky number is ${RANDOM} $ '
export PS1="${USER}$ "

你注意到其中一条命令用单引号,另一条用双引号 了吗?这个差别说明了一个微妙但重要的道理,我 希望你能理解,尤其是当你想编写 Shell 脚本的 时候。

我们之所以使用两种不同的引号,是因为在这两个 变量中,一个会变,另一个不会变。准确地说, RANDOM 的值是一个随机数,每次查看时都 不一样。USER 的值是你的用户识别名, 它始终不变。

我们用双引号引用 ${USER},以便让 $ 字符被解释为元字符。这就是说, USER 的值在处理这条命令的那一刻就确定 了,这正合我们所需,因为 USER 的值 从不改变。

我们用单引号引用 ${RANDOM},这样就能把 $ 字符的含义保留下来供以后使用。这个 技巧确保 RANDOM 的值直到真正创建提示符 时才被求值。这样一来,到该显示新提示符的时候, Shell 会使用 RANDOM 在那一刻所具有的 任何值。

说到这里,回想一下本章前面关于强引用和弱引用的 讨论可能会有帮助。小结如下:

• 单引号('...'),又称 强引用,什么都引用。在单引号之内,没有任何 字符具有特殊含义。

• 双引号("..."),又称 弱引用,除了 $(美元)、` (反引号)和 \(反斜杠)这三个元字符 之外,其他都引用。在双引号之内,这三个字符 保留其特殊含义。

因此,当你在命令中使用 '${VARIABLE}' 时,所有字符都按字面处理,$ 的含义被 保留下来供以后使用。而当你在命令中使用 "${VARIABLE}" 时,$ 被解释为 元字符,整个表达式在当时就被替换为 VARIABLE 的值。

所以,当你需要引用一个变量时,只要问自己: "在我使用这个变量之前,它的值会变吗?"如果 答案是会变,就用强引用(也就是单引号),让 $ 字符在你需要它之前不被解释。否则, 就用弱引用(双引号),让 $ 字符立即 被解释。

使用转义字符的特殊代码
(Special Codes That Use an Escape Character)

到目前为止,我已经说明你的 Shell 提示符中可以 包含任何你想要的字符,也可以包含一个或多个 变量的值。本节我们要讨论如何使用特殊代码来 增强你的提示符。

在我们一直讨论的四个 Shell 中,只有 Bash 和 Tcsh 允许你使用这类代码。这些代码允许你在 提示符中插入各种信息:你的工作目录名、你的 用户识别名、你计算机的主机名,等等。如果你 愿意花心思搞点创意,你甚至可以加入颜色、下 划线和粗体,尽管大多数人懒得这么做。

作为参考,图 13-6 列出了最有用的那些代码。 完整清单记录在你所用 Shell 的 man 手册页中。 如果你非常有抱负又恰好时间充裕,你可能想学学 如何在提示符中使用颜色之类的效果。如果是这样, 你可以在网上搜索到所需的帮助。(提示:使用这类 代码相当复杂,所以如果你不能马上理解,不必 着急。)

Figure 13-6: 可在 Shell 提示符中使用的特殊代码、命令和变量

Bash 和 Tcsh 允许你使用特殊代码,把信息插入 Shell 提示符。本表列出了最有用的代码。完整 清单请见 Bash 或 Tcsh 的 man 手册页。圆点 表示该 Shell 不支持某个功能。

Korn Shell 和 C-Shell 不支持这类代码。不过, 为了完整起见,本表也给出了几种做法:利用变量 可以模拟出其中少数几个代码的效果。

含义 Bash Tcsh Korn Shell C-Shell
工作目录:~ 记法\w%~
工作目录:仅基础名\W
工作目录:完整路径名%/$PWD
你的计算机的主机名\h%m`hostname` `hostname`
当前的用户识别名\u%n$LOGNAME $LOGNAME
Shell 的名称\s`basename $SHELL``basename $SHELL`
时间:AM/PM 记法\@%@
时间:24 小时记法\A%T
日期\d
星期几%d
几号(月内日)%D
月份%w
年份%Y
历史列表:事件编号\!%!!!

你会注意到,图 13-6 中每个 Bash 和 Tcsh 代码都是由一个转义字符后跟另一个字符组成。 (正如本章前面讨论的,转义字符告诉 Shell 从一种模式切换到另一种模式。)在 Bash 中, Shell 提示符的转义字符是 \(反斜杠); 在 Tcsh 中,它是 %(百分号)。

如我所说,只有 Bash 和 Tcsh 使用特殊代码。 但对于其他 Shell,人们仍然有把这些信息放进 Shell 提示符的需要。有少数几种情况可以通过 命令和变量来做到。为了完整,图 13-6 中 也给出了这些做法。(与图 13-5 对照一下。) 你会注意到,其中一些变量是用在含有反引号的 表达式里。这种语法我们在后面讲命令替换的一节 中解释。

大多数 Shell 提示符代码都容易理解。不过我要 说明一点:工作目录的代码要等你理解了目录 (第 24 章)之后才会觉得好懂;而历史事件 编号的代码要等你学会了使用历史列表(本章后面) 之后才会明白。

为了向你展示这一切如何工作,这里举两个例子。 要在提示符中插入你的用户识别名,Bash 用 \u,Tcsh 用 %n。因此,下面两条 Bash 命令效果相同:

export PS1="\u$ "
export PS1="${USER}$ "

如果你的用户识别名是 harley,你的提示符 就是:

harley$

同样,下面两条 Tcsh 命令效果相同:

set prompt = "%n> "
set prompt = "${USER}> "

图 13-6 中的代码都很简单,所以你有空的 时候不妨随意试一试。如果愿意,你可以 同时使用多个代码。例如,要在 Bash 提示符中 显示日期和时间,你用:

export PS1="\d \@$ "

在 Tcsh 提示符中,你用:

set prompt = "%d %w %D %@> "

命令替换
(Command Substitution)

本节我们要讨论 Shell 提供的最迷人、最强大的 功能之一:命令替换。命令替换(COMMAND SUBSTITUTION)允许你把一条命令嵌入另一条 命令之中。Shell 先执行被嵌入的命令,并用它的 输出替换它本身;然后 Shell 再执行整条命令。

显然,我们面对的是一个复杂的概念,所以我先 举几个例子。然后再向你展示一个实际应用:如何 在 Shell 提示符中使用命令替换,显示一些否则 无法作为提示符内容的有用信息。

先从基本语法开始。你把一条命令嵌入另一条命令 之中,办法是用 `(反引号)字符把第一条 命令括起来。例如:

echo "The time and date are `date`."

在这个例子中,date 命令被反引号括了 起来。这是告诉 Shell 分两个阶段执行整条命令: 先对 date 命令求值,并把它的输出 替换到那条较大的命令中;然后执行这条 较大的命令(本例中是 echo)。

假设现在是 2008 年 12 月 21 日星期一上午 10:30,你身处太平洋时区。那么 date 命令 的输出会是:

Mon Dec 21 10:30:00 PST 2008

在第一阶段,Shell 用这个值替换 date, 于是原来的命令变成:

echo "The time and date are Mon Dec 21 10:30:00 PST 2008."

在第二阶段,Shell 执行这条被改写的 echo 命令,产生如下输出:

The time and date are Mon Dec 21 10:30:00 PST 2008.

虽然我把解释分成了两部分,但这一切发生得极快, 在你看来,就好像 Shell 是瞬间显示了最终输出。

再举一个例子。环境变量 $SHELL 存放的 是你那个 Shell 程序的路径名。例如,假设你用 的是 Bash 并且你输入:

echo $SHELL

你会看到如下输出(或与之类似的内容):

/bin/bash

这意味着你的 Shell 是 bash 程序,它位于 /bin 目录中。(我们将在第 24 章 讨论目录和路径名。)

完整的写法 /bin/bash 称为路径名。不过 在这种情况下,我们并不关心整个路径名,只关心 最后那一部分(bash)。要取出任何路径名 的最后部分,我们用 basename,这条命令的 作用就是读入一个完整路径名并输出它的最后一 部分。例如,下面这条命令的输出是 bash:

basename /bin/bash

更一般地说,要显示你的 Shell 程序的名字而不 带路径名的其余部分,你可以用:

basename $SHELL

现在想想,利用命令替换,这一点如何用在 Shell 提示符中。假设你想在提示符中显示你的 Shell 的 名字。你要做的只是把 basename 命令的 输出插入设置提示符的那条命令之中:

export PS1="`basename ${SHELL}`$ "
set prompt = "`basename ${SHELL}`% "
set prompt = "`basename ${SHELL}`> "

第一条命令用于 Bash 或 Korn Shell;第二条用于 C-Shell;第三条用于 Tcsh。

你可以看到,命令替换创造出了本来并不存在的 功能。例如,在上一节中我们讨论了用特殊代码把 信息插入 Shell 提示符。然而这些代码只有 Bash 和 Tcsh 才有。那其他 Shell 怎么办呢?解决办法 就是使用命令替换。

例如,在图 13-6 中你可以看到,把主机名 (你计算机的名字)插入 Shell 提示符的代码, Bash 是 \h,Tcsh 是 %m。对于 其他 Shell,我们可以改用命令替换。

基本思路总是一样的。先问自己:哪条命令能完成 工作的第一部分?然后再想出最好的办法,把这条 命令的输出替换进另一条命令,由它完成工作的 第二部分。

在这个例子里,你会问自己:哪条命令能显示你 计算机的名字?答案是 hostname(见 第 8 章)。更具体地说,取决于你的 Unix 或 Linux 版本,你可能需要也可能不需要 -s 选项。看看哪种写法在你的系统上 效果最好:

hostname
hostname -s

现在你要做的,就是把 hostname 的输出 替换进设置提示符的那条命令。(如果你不需要 -s,就把它去掉。)

export PS1="`hostname -s`$ "
set prompt = "`hostname -s`% "

第一条命令可用于 Bourne Shell(Bash、Korn Shell);第二条可用于 C-Shell(C-Shell、Tcsh)。

最后一个例子。正如 Bash 和 Tcsh 有在 Shell 提示符中显示主机名的代码,它们也有显示你的 用户识别名的代码(分别是 \u%n)。但如你所知,其他 Shell 没有这类 代码。

一种解决办法是在 Shell 提示符中使用 $USER 变量,就像本章前面所做的那样。 另一种办法是使用命令替换配合 whoami 命令(第 8 章):

export PS1="`whoami`$ "
set prompt = "`whoami`% "

在结束本节之前,让我提醒你前面讨论过的一点。 当你使用单引号(强引用)时,两个引号之间的 任何东西都不再保留特殊含义。当你使用双引号 (弱引用)时,有三个元字符确实保留了特殊 含义:$(美元)、`(反引号)和 \(反斜杠)。现在你明白为什么反引号 也在这个名单里了。

— 提示 —

反引号字符只用于命令替换。要小心,不要把它 与单引号或双引号弄混。尽管名字和外形如此, 反引号与"引用"毫无关系。这大概就是为什么 许多 Unix 用户宁可叫它 "backtick" 而不叫 "backquote"。

键入命令并做出修改
(Typing Commands and Making Changes)

一旦你用了 Unix 一段时间,你就会明白,只因为 要做一处小小的改动就不得不把整条命令重新打 一遍,是多么令人恼火。如果你像我一样容易犯 拼写错误,这件事就格外烦人。为了方便用户, Shell 提供了几个便于输入命令的功能:历史列表、 命令行编辑、自动补全和别名。在本章余下的部分, 我们会一次讲一个功能。各项细节在不同 Shell 之间有所不同,所以当你需要帮助时,记住你所用 Shell 的 man 手册页永远是最权威参考。

自从有了 Shell 以来,人们一直在争论哪个 Shell 最好。依我之见,这个功能那个功能你可以随便 议论,但那些让键入(以及重新键入)命令变得容易 的 Shell 会赢得我的一票。大体而言,最好的那些 功能只有 Bash 和 Tcsh 才有,这正是有经验的用户 偏爱使用这两个 Shell 之一的原因。

首先,让我回顾一下我们在第 7 章讨论过的、 关于改正错误和编辑命令行的那些概念。然后再 进入新的内容。

• 要删掉你刚键入的最后一个字符,你按下 <Backspace>。这会发出 erase 信号。(在某些计算机上,例如 Macintosh,你要 用 <Delete> 来代替 <Backspace>。)

• 要删掉你刚键入的最后一个词,你按下 ^W(<Ctrl-W>),发出 werase 信号。

• 要删掉整行,你按下 ^X^U(取决于你的系统),发出 kill 信号。

• 要显示你系统上所有的键位映射,你使用 stty 命令。

在大多数(但并非所有)Shell 中,你还可以使用 <Left> 和 <Right> 光标控制键在 命令行中移动。举例来说,假设你想输入 date 命令,结果却打成了:

dakte

你的光标在行的末尾。只要按两次 <Left> 就向左移动两个位置,然后按 <Backspace> 删掉那个 k。现在你可以按 <Return> 来输入这条命令。请注意,你 可以在该行的任意位置按 <Return>,不必 非得在行末。

除了修改当前这一行之外,你还可以按 <Up> 来调出一条先前的命令,然后修改它 并重新输入。你可以多次按 <Up> 去查找 较早的命令,也可以按 <Down> 回到较新的 命令。

Bash 和 Tcsh 支持这样使用 <Left>、 <Right>、<Up> 和 <Down>, 而 Korn Shell 和 C-Shell 不支持。用 Bash 你还会 得到一份额外的馈赠:你不仅可以用 <Backspace> 删除左边的字符,还可以用 <Delete> 删除右边的字符。等你习惯了, 你会觉得这非常要紧。(注意:如果 <Delete> 在你的系统上不起作用,你可以 改用 ^D。)

— 提示 —

用 <Left> 和 <Right> 在命令行内 移动,用 <Up> 和 <Down> 调出先前 的命令,这实在方便得很,所以我劝你多加练习, 直到你发现自己不假思索就在用这些键为止。

在输入命令这件事上,你的座右铭应该是:复用, 而不是重打。

历史列表:fchistory
(The History List: fc, history)

当你输入命令时,Shell 会把它们保存在所谓的 历史列表(HISTORY LIST)中。你可以用多种方法 访问历史列表来调出先前的命令,然后修改并 重新输入。例如,当你按 <Up> 和 <Down> 来调出命令时,你实际上是在历史 列表中向前和向后移动。

不过,<Up> 和 <Down> 一次只能让 你看到一条命令。有一个更强大的功能,能让你 显示历史列表的全部或部分,然后挑出某条特定 命令。具体如何工作取决于你的 Shell。大体而言, Bourne Shell 家族(Bash、Korn Shell)使用 fc 命令,而 C-Shell 家族(C-Shell、 Tcsh)使用 history! 命令。 多数人觉得 C-Shell 那套更容易掌握,因此 Bash 允许你使用其中任何一套。以下是细节。

在历史列表中,每条命令称为一个事件(EVENT), 每个事件都被赋予一个内部编号,称为事件编号 (EVENT NUMBER)。历史列表的强大之处在于,很 容易根据事件编号调出某条命令。例如,你可以让 Shell 调出第 24 号命令。

在 Bourne Shell 家族中,你使用 fc 命令 加上 -l(list,列表)选项来显示历史 列表。(我马上就会解释 fc 这个名字的 含义。)

fc -l

在 C-Shell 家族中,你使用 history 命令:

history

这些命令的输出是每行一个事件,行首是它的 事件编号。事件编号并不是命令的一部分;显示 它们只是为了方便你查看。例如:

20 cp tempfile backup
21 ls
22 who
23 datq
24 date
25 vi tempfile
26 history

如果你的历史列表长到滚出屏幕,可以使用 less 命令:

history | less

请注意,你输入的每条命令都会被加入历史列表, 包括写错的命令,也包括 historyfc 命令本身。

你可以通过引用事件编号来调出并执行某条特定 命令。在 Bourne Shell 中,你键入 fc 加上 -s(substitute,替换)选项,后面 跟上编号。例如,要重新执行第 24 号命令,用:

fc -s 24

在 C-Shell 中更简单。只要键入一个 ! (感叹号)字符,后面跟上编号。注意 ! 之后不要加空格:

!24

有一种特殊情况:你想重复刚才输入的最后一条 命令。在 Bourne Shell 中,你用不带编号的 fc -s 来重新执行上一条命令:

fc -s

在 C-Shell 中,你连续键入两个 ! 字符:

!!

两类 Shell 都允许你在重新执行命令之前做些 小改动。用 fc 时,语法是:

fc -s pattern=replacement number

在 C-Shell 家族中,语法是:

!number:s/pattern/replacement/

两种情况下,patternreplacement 指的都是一串字符,而 number 指的是事件编号。

例如,在前面那个例子里,第 25 号事件是一条用 名为 tempfile 的文件启动 vi 编辑器的命令:

25 vi tempfile

假设你想再次运行这条命令,但改用名为 data 的文件。也就是说,你想调出第 25 号事件,把 tempfile 改成 data, 然后重新执行该命令。在 Bourne Shell 中你会用:

fc -s tempfile=data 25

在 C-Shell 中你会用:

!25:s/tempfile/data/

同样,如果你想用的是最近一条命令,命令会 更简单。例如,假设你想运行 date 命令, 但不小心输入了 datq,于是显示了一条 错误消息:

$ datq
datq: Command not found.

你想把 q 改成 e,然后重新执行 该命令。用 fc -s 时,如果你省去 事件编号,默认就是上一条命令:

fc -s q=e

在 C-Shell 中,你使用的语法是:

^pattern^replacement

例如:

^q^e

我知道这看起来有点怪,但它快捷方便,而且你会 经常用到它,尤其当你有一条很长的命令只需做 一点小改动的时候。例如,假设你想把名为 masterdata 的文件复制一份,命名为 backup。你用 cp 命令(第 25 章) 键入:

cp masterxata backup

你会得到一条错误消息,因为你在键入第一个文件 名时,不小心按到了 x 而不是 d。 要改正这个错误并重新运行该命令,只要输入:

^x^d

— 提示 —

当你使用 Bash 时,你比其他 Shell 得到两个 重要优势。

第一,在历史列表命令方面,你兼得两者之长。 你可以使用 fc 命令,也可以使用 history/! 那一套,看你更喜欢 哪个。

如果你不确定该用哪一个,先从 history/! 那一套开始。

第二,Bash 支持一个使用 ^R 的额外 功能。(可以把它想成"recall"(调回)键。) 按下 ^R,然后开始键入一个模式。Bash 会调出包含该模式的最近一条命令。例如,要调出 最近的 ls 命令,先按 ^R,再键入 ls

如果这不是你要的命令,只要再按一次 ^R, 就能得到包含该模式的下一条最近命令。在我们的 例子里,你再按一次 ^R 就会得到另一个 ls 命令。

当你看到想要的命令时,可以按 <Return> 运行它,也可以先做个改动再按 <Return>。

名称的由来(What's in a Name?)

fc


Bourne Shell 家族(Bash、Korn Shell)使用 fc 命令来显示和修改历史列表中的命令。 fc 是一条功能强大、语法复杂的命令, 可能需要一段时间才能掌握。

fc 这个名字代表 "fix command"(修正 命令)。这是因为,当你键入错误时,你可以用 fc 修正那条命令,然后重新执行它。

历史列表:设置大小
(History List: Setting the Size)

Shell 把历史列表存放在一个文件里。这个文件可以 在你注销时自动保存,并在你登录时恢复。这一点 很重要,因为这意味着你所做之事的记录可以在 一次又一次工作会话之间保存下来。

在 Bourne Shell 家族中,历史文件会自动保存和 恢复。在 C-Shell 家族中,除非你设置了 savehist Shell 变量(见下文),否则你的 文件不会被保存。

要认识到的一件重要事情是:当你查看历史列表时, 你实际上是在回顾过去的时间,甚至可能跨越多个 独立的工作会话。和人生一样,你会发现记得太多 反而有害无益。因此,Shell 允许你通过设置一个 变量来限制历史列表的大小。

在 Bourne Shell 家族中,你设置 HISTSIZE 环境变量。例如,要指定你的历史列表容纳 50 条 命令(对大多数人已经足够),用:

export HISTSIZE=50

在 C-Shell 家族中,你设置 history Shell 变量:

set history = 50

如果你想保留更长的工作记录,只要把这个变量设成 一个更大的数字即可。如果你不设置大小也没关系, Shell 会使用一个默认值,大概已经够好了。

如前所述,如果你想让 C-Shell 或 Tcsh 在你注销 时保存历史列表,你必须设置 savehist 这个 Shell 变量。和 history 一样,你 必须指定你想保存多少条命令。例如,要在两次 工作会话之间保存最后 30 条命令,用:

set savehist = 30

— 提示 —

如果你想设置历史列表的大小,应该把这条命令 放进初始化文件,这样每次登录时该变量就会自动 设好。我们将在第 14 章讨论具体做法。

历史列表示例:
避免删错文件
(History List Example: Avoid Deleting the Wrong Files)

正如我们将在第 25 章讨论的,你用 rm(remove,删除)命令来删除文件。使用 rm 时,你可以指定模式来表示一批文件。 例如,模式 temp* 代表任何以 temp 开头、后面跟着零个或多个字符的 文件名;模式 extra? 指任何以 extra 开头、后面跟着单个字符的文件名。

rm 的危险在于:一旦你删除了一个文件, 它就永远消失了。如果你发现自己犯了错、删掉了 不该删的文件 — 哪怕就在你按下 <Return> 之后的一刹那 — 也没有 任何办法可以恢复该文件。(我们现在稍作停顿, 让 Macintosh 用户恢复一下镇定。)(*)

* 脚注

信不信由你,Unix 的 rm 命令会永久删除 文件这件事,其实反而是件好事。有经验的 Unix 用户很少因意外而丢失文件,因为他们已经 训练自己在动手之前仔细思考。此外,他们学会了 为自己的行为负责,因为他们不能指望操作系统来 弥补自己的疏忽。你可以想象,这样的明智与自立 会影响生活的方方面面,这就是 Unix 用户作为一个 整体在成就与人生圆满方面都如此之高的原因。

(我们再次稍作停顿,让 Macintosh 用户恢复一下 镇定。)

假设你想删除一批文件,它们的名称是 temptemp_backupextra1extra2。你正在考虑 输入这条命令:

rm temp* extra?

然而,你忘记了自己还有一个重要文件叫 temp.important。如果你输入前面那条 命令,这个文件也会被删掉。

更好的策略是:先使用 ls(list files, 列出文件)命令,用你打算交给 rm 的那些 模式:

ls temp* extra?

这会列出所有匹配这些模式的文件名。如果这个 清单里有一个你忘记的文件,比如 temp.important,你就不会按计划输入那条 rm 命令。但如果文件清单正是你所预期的, 你就可以放心地把这些文件删除:把 ls 改成 rm,然后重新执行这条 命令。在 Bourne Shell 中你会用:

fc -s ls=rm

在 C-Shell 中:

^ls^rm

你可能会问,为什么要复用上一条命令?一旦我 确认这些模式匹配的正是我要的文件,为什么不 干脆用同样的模式重新键入一条 rm 命令 呢?

你当然可以,但当你复用那条 ls 命令时, 你保证得到的正是你想要的。如果你重新键入这些 模式,就可能打错字,结果尽管你处处小心,最后 还是删掉了不该删的文件。而且在许多情况下, 修改上一条命令比从零键入一条新命令更快。

如果你喜欢这个主意,你还可以用别名让这个过程 更省事。本章后面我会告诉你怎么做。

在 Shell 提示符中显示
事件编号与工作目录
(Displaying Event Number & Working Directory in Your Shell Prompt)

本章前面我们讨论过如何把各种信息放进你的 Shell 提示符:你的用户识别名、你的 Shell 名字, 等等。本节我要向你展示如何显示两件会不时变化的 东西:事件编号和你工作目录的名称。把这些内容 放在 Shell 提示符里,有助于你掌握自己正在 做什么。

要显示事件编号的当前值,你使用一个特殊代码。 这些代码各个 Shell 互不相同,所以我把它们小结 在图 13-7 中。

Figure 13-7: 在 Shell 提示符中显示历史列表的事件编号

要在 Shell 提示符中显示历史列表事件编号的 当前值,请使用适合你 Shell 的那个代码。示例 见正文。

Shell 代码
Bash\!
Korn Shell!
C-Shell!
Tcsh%!

下面一些例子展示了四个主要 Shell 分别该怎么 做。在这些例子中,我把事件编号放在方括号里(*) 这样显示出来比较好看。

export PS1="bash[\!]$ "
export PS1="ksh[!]$ "
set prompt = "csh[\!]% "
set prompt = "tcsh[%\!]> "

* 脚注

恰好,C-Shell 和 Tcsh 中,! 字符后面 紧跟一个右方括号会引起问题。因此在这些例子里, 我用反斜杠引用了那个 !。其中的缘由 含混不清,所以你不必纠结。

举例来说,假设当前的事件编号是 57。按上面的 定义,四个提示符看起来会是这样:

bash[57]$
ksh[57]$
csh[57]%
tcsh[57]>

正如你可能想到的,事件编号可以与其他代码或 变量组合起来,构造出一个更丰富的提示符。例如, 下面几个提示符同时显示了 Shell 的名称、工作 目录(见第 24 章)和事件编号。(关于在 Shell 提示符中显示工作目录名的信息,见 图 13-6。)

为了便于阅读,我插入了一些空格,把工作目录名 放在圆括号里,把事件编号放在方括号里。

首先看 Bash 的提示符。我们用 \w 显示 工作目录,用 \! 显示事件编号:

export PS1="(\w) bash[\!]$ "

同样的提示符,对 Korn Shell 来说要更费点劲。 因为 Korn Shell 没有显示工作目录名的代码,我们 就用 PWD 变量。但由于 PWD 会不时 变化,我们必须使用强引用而不是弱引用。(见本章 前面的讨论。)

export PS1='($PWD) ksh[!]$ '

另一种做法是使用弱引用,只要确保用反斜杠引用 那个 $ 字符:

export PS1="(\$PWD) ksh[!]$ "

最后,这里是 Tcsh 使用的命令。我们用 %~ 显示工作目录,用 %! 显示 事件编号。

set prompt = "(%~) tcsh[%\!]> "

那 C-Shell 呢?从图 13-6 你可以看到, 没有办法简单地 C-Shell 提示符中显示工作目录的 名称,所以我没有例子可以给你看。不过,有一种 更复杂的办法可以做到,那就是使用所谓的"别名"。 这个概念我们在本章后面再讲。

自动补全
(Autocompletion)

Shell 让输入命令变得容易的方法之一,是在你 键入时帮你把单词补全。这个功能称为自动补全 (AUTOCOMPLETION)。

例如,你正在输入一条命令,需要键入一个很长的 文件的名字,比如:

harley-1.057-386i.rpm

如果没有其他名字与之相近的文件,你凭什么要 把整个名称都打出来?你应当只键入几个字母, 让 Shell 替你完成剩下的部分。

有了自动补全,Shell 会仔细关注你键入的一切。 在任何时候,你都可以按下某个特殊的按键组合, Shell 会尽它所能替你补全当前的单词。如果它 无法补全,Shell 就会鸣叫一声。我先举个例子 让你了解这个概念,然后再讲细节。

假设你有四个文件:

haberdashery
hardcopy
harley-1.057-i386.rpm
hedgehog

如果你键入 harl 然后按下自动补全的按键 组合,没有任何歧义。因为只有一个文件以 harl 开头,Shell 会替你把名称的其余 部分填上。

然而,如果你键入 har 然后再按自动补全 键,会发生什么?有两个文件名以 har 开头,所以 Shell 鸣叫,表示你键入的内容存在 歧义。

这时候你有两种选择。你可以再多键入一点,然后 重试。或者,如果你不确定该键入什么,你可以按 第二个自动补全按键组合,让 Shell 显示所有 可能匹配项的列表。

在我们的例子中,如果你键入 har 并按下 第二个按键组合,Shell 会显示:

hardcopy
harley-1.057-i386.rpm

然后你可以再键入一个 d 或一个 l,让 Shell 把剩下的活干完。

你可以看到,要使用基本的自动补全功能,你只需 记住两个不同的按键组合。由于历史原因,这些键 在各个 Shell 之间有所不同,所以我把它们列在 图 13-8 中。第一个按键组合告诉 Shell 试着 补全当前的单词。第二个按键组合告诉 Shell 显示 所有与你已经键入内容相匹配的可能补全项。

Figure 13-8: 自动补全键

基本的自动补全功能使用两个不同的按键组合。 第一个告诉 Shell 试着补全你当前正在键入的 单词。如果这不管用,你可以用第二个按键组合, 让 Shell 显示所有与你模式匹配的可能补全项。

Shell 补全单词 显示所有可能项
Bash<Tab><Tab><Tab>
Korn Shell<Esc><Esc><Esc>=
C-Shell<Esc>^D
Tcsh<Tab>^D

为了让你看到自动补全如何工作,我给你演示一些 可以在你自己的计算机上尝试的例子。不过在开始 之前,我想先确保你的 Shell 已经打开了自动补全 功能。对 Bash、Korn Shell 和 Tcsh 来说,这 总是如此。但对 C-Shell 而言,你需要通过设置 filec 变量来打开自动补全。做到这一点的 命令是:

set filec

这条命令最好的存放位置是初始化文件,这样每次 你启动新 Shell 时该变量就会自动设好。我会在 第 14 章教你怎么做。

回到我们的例子。为了试用自动补全,我们需要 几个名称相似的文件。为了创建它们,我们使用 touch 命令。这些文件是 xaaxxabxxacxxccx(*)。 命令如下:

touch xaax xabx xacx xccx

(我们将在第 25 章讨论 touch 命令。 眼下你只需要知道,这是创建空文件最简单的 办法。)

* 脚注

如果你好奇的话,我这些文件的命名取自我的四位 前女友。

我们现在用 ls -l 命令来演示自动补全, 这条命令在列出文件名的同时还会列出其他信息。 首先,让我给你展示一下补全一个文件名时会发生什么。 键入以下内容,然后停下来,不要按 <Return>:

ls -l xc

现在,看一下 Figure 13-8,按下你所用 Shell 对应的"补全单词"按键组合。也就是说,用 Bash 或 Tcsh 时按 <Tab>;用 C-Shell 时按 <Esc>; 用 Korn Shell 时按两下 <Esc>。

由于不存在歧义,Shell 会替你补全这个文件名。 你会看到:

ls -l xccx

你现在可以按 <Return> 来执行这条命令。

这种自动补全称为文件名补全(FILENAME COMPLETION)。 还有其他几种类型,我们稍后再讨论。

现在,让我们看看 Shell 无法补全文件名时会发生什么。 键入以下内容,然后停下来,不要按 <Return>:

ls -l xa

再一次,按下你所用 Shell 的"补全单词"按键组合 (<Tab>、<Esc> 或 <Esc><Esc>)。 这一次 Shell 会鸣响一声,因为与你键入的内容匹配的 文件名不是唯一的(事实上,有三个)。键入字母 b,然后停下来,不要按 <Return>。 你会看到:

ls -l xab

现在再按一次那个按键组合。这一次,Shell 就能替你 完成补全,因为与 xab 匹配的文件名只有一个 (xabx)。按 <Return> 执行这条命令。

最后一个例子。键入以下内容,然后停下来,不要按 <Return>:

ls -l xa

这一次,看一下 Figure 13-8,按下你所用 Shell 对应的"显示所有可能项"按键组合。也就是说,用 Bash 时按两下 <Tab>;用 Korn Shell 时按 <Esc>=;用 C-Shell 或 Tcsh 时按 ^D(Ctrl-D)。这是在告诉 Shell 列出所有可能的 匹配项。

Shell 会显示出匹配项,然后替你重新键入你的命令, 好让你把它补完。你会看到:

xaax xabx xacx
ls -l xa

你现在可以按自己的意愿把命令补完,然后按 <Return>。

最后,当你试用完毕,你需要自己收拾一下,把这四个 练习用的文件删除:

rm xaax xabx xacx xccx

这里还有最后一个例子。在任何时刻,你当前正在其中 工作的目录都称为你的"工作目录"(见第 24 章)。 你不时会想改变自己的工作目录,为此你会键入 cd(change directory,改变目录)命令,后面 跟一个目录名。有些时候你会发现自己要键入很长的目录 名,尤其是如果你是系统管理员的话。这种情况出现时, 最好使用自动补全。

例如,假设你在使用一台 Linux 系统,而你想切换到 下面这个目录:

/usr/lib/ImageMagick-5.5.7/modules-Q16/filters

你可以键入 cd,后面跟上这个很长的目录名。 不过,只键入最少数量的字符然后使用自动补全要容易 得多。这种情况下,用 Bash 你可以键入:

cd /us<Tab>/li<Tab>/Im<Tab>/mo<Tab>/fi<Tab><Return>

如果你用的是别的 Shell,自动补全的键会有所不同, 但道理是一样的:既然 Shell 会替你键入,为什么还要 自己敲那么长的名字呢?

自动补全:超出基本用法
(Autocompletion: Beyond the Basics)

在第 11 章,我解释 C-Shell 和 Tcsh 这两个名字 是怎么来的时候,提到过最初 Tcsh 的作者 Ken Greer 曾在一台 PDP-10 计算机上工作,用的是 TENEX 命令 解释器(类似于 Shell)。

TENEX 使用很长的命令名,因为这些名字容易理解, 但要键入完整的命令却麻烦得很。于是一种称为 "命令补全"(command completion)的设施就被用来 分担大部分工作。你只需要键入几个字母,然后按下 <Esc> 键。命令解释器就会把你键入的内容展开成 完整的命令。Greer 把这个特性加进了他正在编写的 新 C-Shell,到了给这个 Shell 起名字的时候,他把它 叫作 Tcsh,其中的"T"指的就是 TENEX。

在上一节里,我们用自动补全来帮助我们键入文件名, 而这也将是你大多数时候的用法。不过,从 Tcsh 的 故事你可以看到,自动补全是一个很老的想法。而且, 它可以补全的不只是文件名。事实上,现代的 Shell 为各种不同类型的名字都提供了自动补全。

具体细节因 Shell 而异,而且相当复杂。事实上, 大多数 Shell,特别是 Bash 和 Zsh(第 11 章提到过), 给你的自动补全功能多到你三辈子也用不完。大多数 时候,上一节我们讨论的那些技巧就够你用的了。本节 里我再多做些解释。如果你想了解更多信息,显示你 所用 Shell 的 man 手册页,然后搜索"completion" 这个词。

一般而言,自动补全有五种类型。并非所有 Shell 都 支持每一种类型,尽管所有现代的 Shell 都提供文件名 补全,而这是最重要的一种自动补全。作为参考, Figure 13-9 列出了各种 Shell 支持哪些类型的 自动补全。

Figure 13-9: 自动补全的类型

自动补全让你键入名字的一部分,然后由 Shell 替你 补全。一般而言,自动补全有五种不同的类型,每一种 补全不同类型的名字。细节见正文。

最左一列显示哪些 Shell 支持哪种自动补全:B = Bash; K = Korn Shell;C = C-Shell;T = Tcsh。一个圆点表示 该 Shell 不支持那个特性。

Shell 自动补全 补全的名字类型
B K C T文件名补全文件和目录
B • • T命令补全命令,包括路径名

文件名补全我们已经讨论过了。命令补全(COMMAND COMPLETION,仅 Bash、Tcsh)与此类似。当你键入一行 命令的开头时,你可以用自动补全来帮你键入命令名。 这可以是外部命令、内置命令、别名或函数。

例如,假设你在用 Bash 或 Tcsh,你想输入 whoami 命令来显示你的用户 ID(见 第 8 章)。你开始键入:

whoa

然后你按下 <Tab>,Shell 就会替你补全这条命令。

变量补全(VARIABLE COMPLETION,Bash、Tcsh)在你用 一个 $ 字符开始一个单词时就会派上用场。 Shell 假定你正要键入一个变量的名字。

例如,你在用 Bash,你想显示一个环境变量的值, 却忘了它的名字。你只记得它以字母 H 开头。 键入以下内容:

echo $H<Tab><Tab>

如果用 Tcsh,你会键入 $H,后面跟 ^D(Ctrl-D):

echo $H^D

Shell 会列出所有名字以 H 开头的变量。例如:

HISTCMD   HISTFILESIZE  HOME      HOSTTYPE
HISTFILE  HISTSIZE      HOSTNAME

你认出你要的那个变量是 HOSTTYPE,于是你键入 名字中足够多的部分,让它能被识别出来,然后(用 Bash)按下 <Tab> 把剩下的事做完:

echo $HOSTT<Tab>

如果用 Tcsh,你会用:

echo $HOSTT<Esc><Esc>

用户 ID 补全(USERID COMPLETION,也称为用户名补全 USER NAME COMPLETION)在 Bash、C-Shell 和 Tcsh 上都 可用。它的工作方式与变量补全相同,只不过你必须用一 个 ~(波浪号)字符来开始这个单词。这是因为 ~userid 这种语法是 userid 主目 录的简写形式。

最后是主机名补全(HOSTNAME COMPLETION),只有 Bash 提供,它会补全你本地网络上的计算机的名字。当你用 @(at 符号)字符开始一个单词时,就会用到主机 名补全。例如,你在键入一个电子邮件地址时就会这么做。

— 提示 —

当你心里清楚自己要输入什么,却记不住完整的名字时, 自动补全特别有用。

例如,如果你在用 Bash,想输入一条以 lp 开头 的命令,却不记得是哪一条,那就键入 lp<Tab><Tab>。(用 Tcsh 时, 则用 lp^D。)

同样,你可以键入 $ <Tab><Tab> (或 $^D)来列出你所有变量的名字。试一试。

用自动补全获取乐趣与实惠
(Using Autocompletion for Fun and Profit)

你可能还记得第 7 章里的程序员与公主的 寓言。在那个故事里,一位英俊的年轻程序员不必 按 <Return> 键或 ^M,就能输入一条命令, 从而救出了一位美丽的公主。(他用的是 ^J。) 这里有个更酷的招数:如何用自动补全同时赚点小钱, 并且(假设你是男的)打动女性。

下次你参加 Linux 用户聚会时,环顾一下满场的极客, 找一个看起来手里有点钱的。既然此人是 Linux 极客, 你就知道他一定在用 Bash。跟他打个小赌(比如说五 美元),赌你能在不按 <Return> 键的情况下列出 他所有环境变量的名字。当他接受赌注时,输入:

env^M

这时他就会看出你是怎么戏弄他的,于是你提议赌注 翻倍。这一次,你保证不用 <Return> 或 ^M。当他接受赌注时,输入:

env^J

现在你已经准备好给出致命一击了。提议赌注翻三倍, 并且这一次你保证不用 <Return>、^M^J。而且,为了让事情更难,你连一条命令都不 键入。

到这时候,你已经吸引了许多其他 Linux 极客的注意, 他们都想掺和进来。让他们把钱放到桌上,等你把能收 的赌注都收齐之后,键入:

$<Tab><Tab>

你捧起钱走人,回头对那帮极客说一句:"你们难道没 听说过 RTFM 吗?"

命令行编辑:bindkey
(Command Line Editing: bindkey)

在前面几节里,我们谈到了几个彼此相关的主题:边 键入边修改命令、使用历史列表,以及使用自动补全。 在读这些内容的时候,你可能已经注意到两件事。第一, 三个较新的 Shell — Bash、Korn Shell 和 Tcsh — 提供的功能比老的 C-Shell 多得多。第二,似乎有一条 贯穿其中的主线把所有这些串在一起。

事实确实如此。这里起作用的一般原理称为命令行编辑 (COMMAND LINE EDITING),它只在较新的 Shell 中可用, C-Shell 没有。命令行编辑是一种强大的设施,让你可以 使用大量不同的命令来加工你在命令行上键入的内容, 其中包括使用历史列表和自动补全的能力。

你应该还记得我说过好几次:Unix 有两个主要的文本 编辑器,即 vi(读作"vee-eye")和 Emacs。 迟早你必须学会熟练使用其中至少一个。事实上,本书 用了整整一章来讲 vi(第 22 章)。

vi 和 Emacs 都提供数量众多、功能强大的命令, 让你可以查看和修改文本文件。这些命令设计得极为出色, 以至于适合在任何情况下编辑任何类型的文本。具体说, Shell 让你可以使用 vi 命令或 Emacs 命令 (由你选择)来查看和修改你在命令行上键入的内容以及 你的历史列表。

恰好 vi 的命令与 Emacs 的命令非常不同,所以 Shell 让你一次只能使用其中一套。默认情况下,Shell 假定你想使用 Emacs 命令。我们把这个状态称为 Emacs 模式(EMACS MODE)。不过,如果你愿意,你可以切换到 vi。若如此,我们就说 Shell 处于 vi 模式 (vi MODE)。

从一个命令编辑模式切换到另一个模式的方法取决于你 所用的 Shell。在 Bash 和 Korn Shell 中,你设置一个 Shell 选项,要么是 emacs,要么是 vi。 (Shell 选项在第 12 章解释。)因此,你会使用下面 两条命令之一:

set -o emacs
set -o vi

在 Tcsh 中,你使用 bindkey 命令。你可以带上 -e(Emacs)或 -v(vi)选项:

bindkey -e
bindkey -v

这两条命令最好的存放位置是初始化文件,这样每次你 登录时该命令就会自动执行。第 14 章我会教你怎么 做。

说到编辑普通的文本文件,vi 是初学者的最佳 选择。这是因为,虽然 vi 很难学,但 Emacs 是 非常难学。所以,如果你是初学者,到了该学怎么 编辑文件的时候,我建议你先从 vi 入手,而不是 Emacs。

然而,说到编辑历史列表和命令行,Emacs 其实比 vi 更容易用。原因是,大多数时候你只需要在 历史列表里上下移动,或者对命令行做一些小改动。 用 Emacs 做这些很直接。vi 编辑器更复杂, 因为它有两种不同的模式 — 命令模式和插入模式 — 而在使用 vi 之前,你需要学会如何在 两种模式之间来回切换。(我们将在第 22 章细讲。) 出于这个原因,所有 Shell 都默认使用 Emacs 模式。

本章前面我教你用 <Up> 和 <Down> 在历史 列表里移动,以及如何对命令行做基本修改时,其实展示 的就是简单的 Emacs 命令。所以,你其实早就在用 Emacs 做命令行编辑了,只是当时自己没意识到。事实上,如果 你的 Shell 当时处于 vi 模式,你会发现光标移动 键并不像你预期的那样工作。

vi 和 Emacs 都提供了极多的方法来加工你的历史 列表和命令行。然而,在你学会使用其中一个编辑器之前, 这些对你毫无帮助。出于这个原因,我不打算讲解高级命令 行编辑的细节。如果你有兴趣,在你学会使用 vi 或 Emacs 命令之后可以来试一试。到那时,再回到本章,读完 本节剩下的内容。(对 vi 来说,那是在你读完第 22 章之后。)

要自学命令行编辑,先用 setbindkey 把你的 Shell 设为 vi 模式或 Emacs 模式。现在, 想象你正在处理一个包含历史列表的隐形文件。在任何时刻, 你都可以从这个文件里复制一行到你的命令线上,在那里你 可以按自己的意愿修改文本。每当你按 <Return> 运行 一条命令时,命令线上的内容就会被添加在这个隐形文件的 底部(也就是添加到历史列表里)。

记住这个意象,做试验就很容易了。你要做的只是以合理的 方式使用 vi 或 Emacs 命令。先从基本动作开始练 习:在隐形文件里移动、搜索模式、做替换,等等。你会 发现,一旦你用熟了 vi 或 Emacs,命令行编辑就很 直接、很符合直觉。

如果你需要一份参考,看一下你所用 Shell 的 man 手册页, 搜索有关命令编辑的信息。你可能会觉得这些说明有点让人 摸不着头脑,但请耐心一点。重组你的脑细胞是需要时间的。

别名:aliasunalias
(Aliases: alias, unalias)

别名(ALIAS)是你给一条命令或一串命令起的名字。 你可以把别名用作简写,也可以用它们在现有命令的 基础上做出按你心意改造的变体。例如,假设你常常 需要输入这条命令:

ls -l temp*

如果你给它起个别名叫 lt,你就可以更省事地 键入:

lt

要创建一个别名,你使用 alias 命令。语法因你 所用的 Shell 而略有不同。对于 Bourne Shell 家族 (Bash、Korn Shell),语法是:

alias [name=commands]

注意不要在等号两边留下空格(这与你创建变量时一样)。

对于 C-Shell 家族(C-Shell、Tcsh),语法几乎相同。 唯一的区别是你要去掉等号:

alias [name commands]

两种情况下,name 都是你要创建的别名的名字, 而 commands 是一条或多条命令组成的列表。

举个例子,我们来创建我上面提到的那个别名。第一条 命令用于 Bourne Shell;第二条命令(省掉了等号)用于 C-Shell:

alias lt='ls -l temp*'
alias lt 'ls -l temp*'

注意我把命令包在了单引号里。这是因为这条命令既包含 空格又包含元字符(*)。一般而言,强引用 (单引号)比弱引用(双引号)更好用,因为它们会把 元字符的含义保留下来,直到别名被执行时为止。

下面这个例子为两条命令组成的列表创建一个别名。同样, 第一条命令用于 Bourne Shell;第二条用于 C-Shell:

alias info='date; who'
alias info 'date; who'

一旦创建了这个别名,只要你想运行这两条命令,就可以 输入 info

这是我最喜欢的那个别名。它给 alias 命令本身 创建了一个简写:

alias a=alias
alias a alias

一旦创建了这个别名,你就可以用 a 来创建更多 别名,而不必键入 alias 这个完整的词。例如, 一旦定义了这个别名,你可以输入:

a info='date; who'
a info 'date; who'

如果你想改变一个别名的含义,重新定义它就行。例如, 如果 info 是个别名,你随时都可以改变它,只用 再写一条 alias 命令:

alias info='date; uname; who'
alias info 'date; uname; who'

要查看一个别名的当前值,输入 alias 命令,后面 只跟一个名字。例如,要显示别名 info 的含义, 用:

alias info

要一次性显示你所有的别名,输入 alias 命令, 不带任何参数:

alias

要删除一个别名,你使用 unalias 命令。语法是:

unalias name

其中 name 是某个别名的名字。例如,要删除我们 刚定义的那个别名,你会用:

unalias info

如果你想一次性删除所有别名(比如说你在做实验),用 unalias 命令,要么带上 -a 选项(用于 Bourne Shell),要么带一个 * 字符(用于 C-Shell):

unalias -a
unalias *

还记得本章前面我们讨论过的 type 命令吗?(你 指定一个命令的名字,type 就告诉你它是哪一类 命令。)你可以用 type 来查明某条命令是不是一个 别名。例如,假设你按上面所示定义了 info 别名。 然后你输入:

type info

你会看到类似下面这样的一条消息:

info is aliased to 'date; who'

你可以想象得到,你很可能会积累出一整套自己随时都在用 的别名。然而,每次登录都要重新键入这些 alias 命令实在 麻烦。相反,你可以把你所有心爱的别名定义放进一个初始 化文件,这样每次你启动新 Shell 时它们就会被自动执行。 第 14 章我会教你怎么做。

暂时停用别名
(Suspending an Alias Temporarily)

别名一个非常常见的用途,是让你每次运行某条命令时都能 方便地带上同样的选项。

例如,ls 命令(我们将在第 24 章讨论它)列出一个 目录的内容。单独使用 ls 时,你得到一份"简短" 列表;使用带 -l 选项的 ls 时,你得到一份 "详细"列表。

假设你发现自己几乎总是给 ls 带上 -l 选项。 为了省下每次键入选项的工夫,你定义了下面这个别名:

alias ls="ls -l"
alias ls "ls -l"

(第一个定义用于 Bourne Shell 家族;第二个用于 C-Shell 家族。)

现在,你可以只输入命令本身,不必键入那个选项:

ls

这会输出一份详细列表,就如同你输入了:

ls -l

使用这类别名时,你会发现,自己不时会想运行原来的命令, 而不是别名。例如,你可能想在不带 -l 选项的情况 下运行 ls

要暂时停用一个别名 — 只针对一条命令 — 你在命令名 开头键入一个 \(反斜杠)字符:

\ls

这是在告诉 Shell 运行真正的命令,而不是别名。在我们这个 例子里,Shell 会忽略 ls 别名,于是你会得到(默认 的)简短列表。

别名示例:
避免删错文件
(Alias Example: Avoid Deleting the Wrong Files)

本节里,我要向你展示如何把一个别名与一条从历史列表 召回的命令结合起来,做出一个极为顺手的工具。

本章前面我们讨论过一个例子:我们正考虑使用 rm(remove,删除)命令来删除所有名字与某个 特定模式匹配的文件。我们讨论的那个例子是:

rm temp* extra?

为了确定我们不出错,应该在真正执行删除之前, 先检查一下我们使用的模式。做法是对同一个模式使用 ls 命令:

ls temp* extra?

如果 ls 列出的正是我们想要的文件,我们就继续 删除。否则,我们可以换一个模式再试,直到得到我们想要 的结果。这样,我们就保证了 rm 做的正是我们要它 做的事。这一点很重要,因为 Unix 一旦删除了一个文件, 它就永远消失了。

那么,假设 ls 命令找到了我们要删除的文件。我们 本可以直接输入 rm 命令,带上同样的模式。可是, 万一我们键错了字呢?我们到头来也许还是删错了文件。 更好的办法是让 Shell 替我们干活。为此,我们从历史列表 里召回那条 ls 命令,把 ls 改成 rm, 然后执行这条改过的命令。

对于 Bourne Shell 家族的成员(Bash、Korn Shell),我们 使用:

fc -s ls=rm

为了让这条命令好用,我们定义一个叫 del 的别名:

alias del='fc -s ls=rm'

对于 C-Shell 家族的成员(C-Shell、Tcsh),我们通常会 使用:

^ls^rm

然而,出于一些我不打算展开的技术原因,这在别名里行 不通。我们必须改用下面这条命令:

rm !ls:*

显然,这有点绕。(Unix 里满是绕人的命令。)宽松一点 说,我们是在要求 Shell 从最近一条 ls 命令里提取 参数,并用它们来运行一条 rm 命令。其效果是用与 ls 命令相同的参数来运行 rm 命令。

为了让这条命令好用,我们定义一个别名。注意我们要给 ! 加上引号,以保留它的含义。(这对你来说说得通 吗?)

alias del 'rm \!ls:*'

一旦定义了 del 别名,我们就可以用下面的步骤来 删除与某个特定模式匹配的文件。妙处在于,无论你用的是 哪个 Shell,这个步骤都是一样的。

首先,输入一条 ls 命令,带上那个描述你要删除哪些 文件的模式。例如:

ls temp* extra?

如果这个模式显示出的名字正如我们所料,我们就输入:

del

就这么简单。

如果这个模式不对,我们就换一个模式重新输入 ls 命令,直到得到想要的结果为止。然后再用 del。 这样一来,我们就不可能因为模式不匹配而删错文件。

如果你养成用这种 lsdel 别名的习惯, 我向你保证,总有一天,它会让你免于一场灾难。事实上, 我有数学证明 — 用数学归纳法和超几何函数 —ios 说,单是这一招就值回这本书的价钱。 (遗憾的是,这个证明的讲解超出了本书的范围。)

别名示例:
复用历史列表中的命令
(Alias Example: Reusing Commands From the History List)

本章前面我解释过,Bourne Shell 家族和 C-Shell 家族 使用不同的命令来访问历史列表。具体说,Bourne Shell (Bash、Korn Shell)使用 fc 命令,而 C-Shell (C-Shell、Tcsh)使用 history 命令和 ! 命令。

最初的历史列表设施是为 C-Shell 写的。在当时这是一项 突破,事实上,它到今天仍然好用、易用。后来,Korn Shell 引入了一套强大得多的系统,使用 fc 命令。 遗憾的是,fc 的语法设计得很糟,命令本身的细节 既难记住又难使用。不过,借助别名,我们可以让 fc 看起来就像 C-Shell 那一套。

首先,我们定义一个名为 history 的别名,它给 fc 带上 -l(list,列表)选项来显示历史 列表中的行:

alias history="fc -l"

要更省事,还可以把 history 简写成 h:

alias h=history

这是我最喜欢的一个别名,我在每个 Shell 里都用它, 连 C-Shell 和 Tcsh 也不例外。毕竟,谁想一遍又一遍地 键入 history 这个词呢?(*)

* 脚注

说到这个,谁又想一遍又一遍地键入 alias 这个 词呢?这就是我建议你给 alias 命令本身也创建 一个别名的原因:

alias a=alias

接下来,我们定义一个别名 r(recall,召回)来 代替 fc -s,后者是从历史列表中召回并重新执行一 行的命令:

alias r="fc -s"

现在,只要愿意,重新执行我们刚输入的最后一条命令就很 容易。我们只要用 r 这个别名:

r

如果我们想做点改动,只要指定一个旧模式和一个新模式。 例如,假设我们刚键入了这条命令:

vi tempfile

这会启动 vi 来编辑一个名为 tempfile 的文件。 我们决定再运行一次这条命令,去编辑一个名为 data 的文件。我们只需要键入:

r tempfile=data

处理历史列表中某一具体行同样容易。只要指明事件编号 (行号)即可。例如,假设你的历史列表长这样:

20 cp tempfile backup
21 diff backup backup1
22 whoami
23 date
24 vi tempfile
25 vi data

你想知道现在几点了,所以你想重新执行 date 命令, 也就是 23 号事件:

r 23

接着,你想重新执行第 20 条命令。不过,首先你想把 tempfile 换成 data:

r 20 tempfile=data

如果你指定了一个或多个字符,Shell 会重新开头包含这些 字符的最近一条命令。例如,要重新执行最后一条以 di 开头的命令(在这个例子里是第 21 条, diff 命令),用:

r di

如果你想重新执行 date 命令,你可以指定最后一条 以 d 开头的命令:

r d

稍加练习,这类替换就能为你省下大量的时间和精力。

为了给本节收尾,我就在你所用 Shell 上使用 historyhr 这几个别名的 问题,给你一些具体的建议。

Bash:正如我在本章前面解释的,Bash 自带 fc 命令,也自带 history!! 命令。不过,你还是应该自己动手创建 hr 这两个别名:

alias h=history
alias r="fc -s"

Korn Shell:Korn Shell 使用 fc,而且它 自带已经定义好的 historyr 别名,所以 你不必再创建它们。不过为了顺手,你应该加上 h 这个别名:

alias h=history

C-Shell 和 Tcsh:正如我在本章前面解释的,这两个 Shell 都自带 history 命令,也自带修改并复用历史 列表中命令的便捷方法。为了顺手,你需要加的只有 h 这个别名:

alias h history

这些别名之美有两点:第一,它们让使用历史列表变得容易; 第二,它们让你无论你用的是哪个 Shell,都能以同一种方式 访问历史列表。

别名示例:在 Shell 提示符中显示
工作目录名
(Alias Example: Displaying Name of Working Directory in Shell Prompt)

本节的目标是解决一个只与 C-Shell 有关的具体问题。 不过,我们会涉及几个普遍有用的概念,所以无论你 恰好用的是哪个 Shell,我都希望你读一读本节,想想 讨论中浮现出的那些思路。

本章前面我们谈过如何让你的 Shell 提示符显示工作 目录的名字。在那次讨论的结尾,我提到 C-Shell 没有 简单的办法做到这一点。然而,有一种复杂的办法,它 要用到别名,那正是我们这里要讨论的。

这场讨论会涉及目录,我们将在第 24 章讲它。 眼下你只需要知道,目录用来存放文件,而你的"工作 目录"就是你当前正在其中工作的那个目录。你可以随时 改变你的工作目录,而每次改变时,如果新目录的名字 能出现在你的 Shell 提示符里就很方便,这样你就能 随时掌握自己在哪。

用这种方式在提示符中显示工作目录的名字,在 Bash (用 \w)、Korn Shell(用 $PWD)和 Tcsh(用 %~)里都很容易。下面是几条能办成 这件事的示例命令。为了便于阅读,它们把工作目录的 名字显示在圆括号里:

export PS1="(\w) bash$ "
export PS1='($PWD) ksh$ '
set prompt = "(%~) tcsh> "

这些命令之所以管用,是因为每当你的工作目录改变时, 这些 Shell 会自动更新提示符中的那个代码或变量。

诚然,C-Shell 也有一个 PWD 变量。然而,如果 你把它放进 Shell 提示符,你会发现这个变量不会被自动 更新。这是因为 C-Shell 比其他三个 Shell 都要老,它 没有这个能力。

解决这个问题的思路是:使用一个别名,每当你改变工作 目录时,它都重新定义 Shell 提示符。首先,我们需要 回答一个问题:我们用哪条命令来改变工作目录?答案是 cd(change directory,改变目录)命令。

我们将在第 24 章详细讨论 cd。这里我先告诉 你:要切换到一个新目录,你键入 cd,后面跟上那个 目录的名字。例如,如果你想切换到 bin 目录,你会 输入:

cd bin

你可能还记得第 12 章讲过,C-Shell 在任何时刻都用两 个不同的变量来保存你的工作目录名:cwd(一个 Shell 变量)和 PWD(一个环境变量)。每当你用 cd 改变工作目录时,这两个变量都会被更新。

因此,我们的计划是创建一个别名来重新定义 cd,让 它做两件事:(1)按你指定的方式改变工作目录,然后 (2)用 cwdPWD 变量重新定义 Shell 提示 符,让它反映新的工作目录。下面这个别名就能办成这件事:

alias cd 'cd \!* && set prompt="($PWD)% "'

要理解它是怎么工作的,你需要知道 && 分隔两条 命令。&& 的含义是先运行第一条命令,如果它正常 结束,再运行第二条命令。如果第一条命令失败了,第二条 命令就不会执行。换句话说,如果 cd 命令出于某种 原因失败了,那更新提示符也就没有意义。

我们的 cd 别名开头先执行这条命令:

cd \!*

记号 \!* 指的是你在原来那条命令行上键入的任何 参数。这样,命令行上原来的参数就被传给了别名内部的 cd 命令。(这是个编程门道的事,所以如果这对你 来说说不通,也不必放在心上。)

如果第一条命令正常结束,PWD 变量就已经被 Shell 自动更新了。接着我们就可以运行第二条命令:

set prompt="($PWD)% "

这条命令改变 Shell 提示符,让它在圆括号里显示工作目录 的名字,后面跟一个 % 字符,再跟一个空格。就这么 回事。

整件事之所以行得通,是因为别名的展开是在 Shell 解析并 解释命令行之前完成的。例如,假设我们定义了上面 那个 cd 别名,然后我们输入:

cd bin

Shell 做的第一件事就是展开这个别名。在内部,命令行变成 了:

cd bin && set prompt="($PWD)% "

然后执行 cd 命令,紧接着执行 set 命令。

一旦定义了基本的别名,你还可以把它做得更讲究。例如, 为什么不让提示符显示的不只是工作目录和一个 % 字符呢?

下面这个别名定义了一个更复杂的提示符:我们在圆括号里 显示工作目录,然后是一个空格、Shell 的名字、方括号里的 事件编号、一个 % 字符和一个空格:

alias cd 'cd \!* && set prompt = "($PWD) csh[\\!]% "'

用这种方式定义出来的一个典型提示符看起来会像这样:

(/export/home/harley) csh[57]%

你会把这类别名放进初始化文件,这样提示符就会自动为你 更新。我们将在第 14 章讲解初始化文件。

最后再补充一句。你会注意到在最后一个例子里,! 字符被引用了两次(用两个反斜杠)。第一个反斜杠在 alias 命令第一次被解析时引用 !。第二个 反斜杠在 set 命令第二次被解析时引用 !

这是一个我要确保你理解的概念:当某个东西要被解析多次 时,你可能需要把它引用多次。请花一点时间想想这一点, 直到你觉得说得通为止。

练习
(Exercises)

复习问题 #1:

什么是字母数字字符?

什么是元字符?

说出三个元字符的名字,并解释它们用来做什么。

复习问题 #2:

在 Unix 的世界里,一些字符有外号。例如,撇号 (apostrophe)通常被称作"quote"或"single quote"。 Unix 人士对下面这些字符用的外号分别是什么:

• 星号(asterisk)
• 回车/换行(enter/return)
• 感叹号(exclamation mark)
• 句号(period)
• 引号(quotation mark)
• 竖线(vertical bar)

复习问题 #3:

引用字符的三种不同方式是什么?它们有何区别?

复习问题 #4:

什么是内置命令?

你在哪里找到内置命令的文档?

复习问题 #5:

什么是搜索路径?

你怎么显示你的搜索路径?

复习问题 #6:

什么是历史列表?

历史列表最简单、最常见的用途是重新执行上一条命令。 你怎么做?

用 Bash 或 Tcsh,你如何召回、编辑并执行上一条命令?

复习问题 #7:

什么是自动补全?

自动补全有多少种不同的类型?

简要说明每种自动补全各做什么。

哪一种自动补全最重要?

应用你的知识 #1:

你怎么修改 Bash 的 Shell 提示符,让它显示你的用户 ID、 工作目录和当前时间?

对 Tcsh 的 Shell 提示符,你怎么做同样的事?

应用你的知识 #2:

什么是命令替换?

用命令替换创建一条命令,它显示 "These userids are logged in right now:",随后列出 这些用户 ID。

应用你的知识 #3:

输入命令:

echo "I am a very smary person."

使用一个历史列表工具,把这条命令改过来,修正 "smart" 的拼写。

应用你的知识 #4:

你的工作目录里只有下面这些文件:

datanew dataold important phonenumbers platypus

使用自动补全,要指到每个文件,你最少需要键入多少个 字符?

进一步思考 #1:

本章里,我们讨论了几种帮你快速输入命令的工具:历史 列表、自动补全和别名。这些工具相当复杂,需要花时间 才能掌握。有些人不愿费那个工夫,因为在他们看来不值 得。有些人则欣然投入这类工具的怀抱。

哪一类人非常需要尽可能快地输入命令?

进一步思考 #2:

创建大量专用别名有什么好处?又有什么坏处?