第 5 章 理解 shell

本章内容

  • 探究shell的类型
  • 理解shell的父/子关系
  • 别出心裁的子shell用法
  • 探究内建的shell命令

现在你已经学到了一些shell的基础知识,例如如何进入shell以及初级的shell命令,是时候去一探shell进程的究竟了。要想理解shell,得先理解一些CLI。

shell不单单是一种CLI。它是一个时刻都在运行的复杂交互式程序。输入命令并利用shell来运行脚本会出现一些既有趣又令人困惑的问题。搞清楚shell进程以及它与系统之间的关系能够帮助你解决这些难题,或是完全避开它们。
本章将会带你全面学习shell进程。你会了解到如何创建子shell以及父shell与子shell之间的关系。探究各种用于创建子进程的命令和内建命令。另外还有一些shell的窍门和技巧等你一试。

5.1 shell 的类型

系统启动什么样的shell程序取决于你个人的用户ID配置。在/etc/passwd文件中,在用户ID记录的第7个字段中列出了默认的shell程序。只要用户登录到某个虚拟控制台终端或是在GUI中启动终端仿真器,默认的shell程序就会开始运行。
在下面的例子中,用户christine使用GNU bash shell作为自己的默认shell程序:

  1. $ cat /etc/passwd
  2. [...]
  3. Christine:x:501:501:Christine B:/home/Christine:/bin/bash
  4. $

bash shell程序位于/bin目录内。从长列表中可以看出/bin/bash(bash shell)是一个可执行程序:

$ ls -lF /bin/bash
-rwxr-xr-x.  1  root  root  938832  Jul  18   2013 /bin/bash*
$

本书所使用的CentOS发行版中还有其他一些shell程序。其中包括tcsh,它源自最初的C shell:

$ ls -lF /bin/tcsh
-rwxr-xr-x. 1 root root 387328 Feb 21   2013  /bin/tcsh*
$

另外还包括ash shell的Debian版:

$ ls -lF /bin/dash
-rwxr-xr-x. 1 root root 109672 Oct 17   2012 /bin/dash*
$

最后,C shell的软链接(参见第3章)指向的是tcsh shell:

$ ls -lF /bin/csh
lrwxrwxrwx.  1  root  root  4  Mar  18  15:16  /bin/csh  ->  tcsh*
$

这些shell程序各自都可以被设置成用户的默认shell。不过由于bash shell的广为流行,很少有人使用其他的shell作为默认shell。

说明 第1章对各种shell有一个简单的描述。如果你想进一步学习GNU bash shell之外的shell,第23章提供了更多的相关信息。

默认的交互shell会在用户登录某个虚拟控制台终端或在GUI中运行终端仿真器时启动。不过还有另外一个默认shell是/bin/sh,它作为默认的系统shell,用于那些需要在启动时使用的系统shell 脚本。
你经常会看到某些发行版使用软链接将默认的系统shell设置成bash shell,如本书所使用的CentOS发行版:

$ ls -l /bin/sh
lrwxrwxrwx. 1 root root 4 Mar 18 15:05 /bin/sh -> bash
$

但要注意的是在有些发行版上,默认的系统shell和默认的交互shell并不相同,例如在Ubuntu 发行版中:

$ cat /etc/passwd
[...]
christine:x:1000:1000:Christine,,,:/home/christine:/bin/bash
$
$ ls -l /bin/sh
lrwxrwxrwx 1 root root 4 Apr 22 12:33 /bin/sh -> dash
$

注意,用户christine默认的交互shell是/bin/bash,也就是bash shell。但是作为默认系统shell的/bin/sh被设置为dash shell。

窍门 对bash shell脚本来说,这两种不同的shell(默认的交互shell和默认的系统shell)会造成问题。一定要阅读第11章中有关bash shell脚本首行的语法要求,以避免这些麻烦。

并不是必须一直使用默认的交互shell。可以使用发行版中所有可用的shell,只需要输入对应的文件名就行了。例如,你可以直接输入命令/bin/dash来启动dash shell。

$ /bin/dash
$

除启动了dash shell程序之外,看起来似乎什么都没有发生。提示符$是dash shell的CLI提示符。可以输入exit来退出dash shell。

$ exit
exit
$

这一次好像还是什么都没有发生,但是dash shell程序已经退出了。为了理解这个过程,我们将在下一节中探究登录shell程序与新启动的shell程序之间的关系。

5.2 shell 的父子关系

用于登录某个虚拟控制器终端或在GUI中运行终端仿真器时所启动的默认的交互shell,是一个父shell。本书到目前为止都是父shell提供CLI提示符,然后等待命令输入。
在CLI提示符后输入/bin/bash命令或其他等效的bash命令时,会创建一个新的shell程序。这个shell程序被称为子shell(child shell)。子shell也拥有CLI提示符,同样会等待命令输入。
当输入bash、生成子shell的时候,你是看不到任何相关的信息的,因此需要另一条命令帮助我们理清这一切。第4章中讲过的ps命令能够派上用场,在生成子shell的前后配合选项-f来使用。

$ ps -f
UID PID PPID C STIME TTY TIME CMD
501 1841 1840 0 11:50 pts/0 00:00:00 -bash
501 2429 1841 4 13:44 pts/0 00:00:00 ps -f
$
$ bash
$
$ ps -f
UID PID PPID C STIME TTY TIME CMD
501 1841 1840 0 11:50 pts/0 00:00:00 -bash
501 2430 1841 0 13:44 pts/0 00:00:00 bash
501 2444 2430 1 13:44 pts/0 00:00:00 ps -f
$

第一次使用ps -f的时候,显示出了两个进程。其中一个进程的进程ID是1841(第二列),运行的是bash shell程序(最后一列)。另一个进程(进程ID为2429)对应的是命令ps -f。

说明 进程就是正在运行的程序。bash shell是一个程序,当它运行的时候,就成为了一个进程。一个运行着的shell就是某种进程而已。因此,在说到运行一个bash shell的时候,你经常会看到“shell”和“进程”这两个词交换使用。

输入命令bash之后,一个子shell就出现了。第二个ps -f是在子shell中执行的。可以从显示结果中看到有两个bash shell程序在运行。第一个bash shell程序,也就是父shell进程,其原始进程ID是1814。第二个bash shell程序,即子shell进程,其PID是2430。注意,子shell的父进程ID(PPID) 是1841,指明了这个父shell进程就是该子shell的父进程。图5-1展示了这种关系。
5
第 5 章 理解 shell - 图1
图5-1 bash shell进程的父子关系
在生成子shell进程时,只有部分父进程的环境被复制到子shell环境中。这会对包括变量在内的一些东西造成影响,我们会在第6章中谈及相关的内容。
子shell(child shell,也叫subshell)可以从父shell中创建,也可以从另一个子shell中创建。

$ ps -f
UID PID PPID C STIME TTY TIME CMD
501 1841 1840 0 11:50 pts/0 00:00:00 -bash
501 2532 1841 1 14:22 pts/0 00:00:00 ps -f
$
$ bash
$
$ bash
$
$ bash
$
$ ps --forest
PID TTY TIME CMD
1841 pts/0 00:00:00 bash
2533 pts/0 00:00:00     \_ bash
2546 pts/0 00:00:00         \_ bash
2562 pts/0 00:00:00             \_ bash
2576 pts/0 00:00:00                 \_ ps
$

在上面的例子中,bash命令被输入了三次。这实际上创建了三个子shell。ps -forest命令展示了这些子shell间的嵌套结构。图5-2中也展示了这种关系。

第 5 章 理解 shell - 图2
图5-2 子shell的嵌套关系
ps -f命令也能够表现子shell的嵌套关系,因为它能够通过PPID列显示出谁是谁的父进程。

$ ps -f
UID PID PPID C STIME TTY TIME CMD
501 1841 1840 0 11:50 pts/0 00:00:00 -bash
501 2533 1841 0 14:22 pts/0 00:00:00 bash
501 2546 2533 0 14:22 pts/0 00:00:00 bash
501 2562 2546 0 14:24 pts/0 00:00:00 bash
501 2585 2562 1 14:29 pts/0 00:00:00 ps -f
$

bash shell程序可使用命令行参数修改shell启动方式。表5-1列举了bash中可用的命令行参数。
表5-1 bash命令行参数

参 数 描 述
-c string 从string中读取命令并进行处理
-i 启动一个能够接收用户输入的交互shell
-l 以登录shell的形式启动
-r 启动一个受限shell,用户会被限制在默认目录中
-s 从标准输入中读取命令

可以输入man bash获得关于bash命令的更多帮助信息,了解更多的命令行参数。bash —help命令也会提供一些额外的协助。
可以利用exit命令有条不紊地退出子shell。

$ exit
exit
$
$ ps --forest
PID TTY TIME CMD
1841 pts/0 00:00:00 bash
2533 pts/0 00:00:00     \_ bash
2546 pts/0 00:00:00         \_ bash
2602 pts/0 00:00:00             \_ ps
$
$ exit
exit
$
$ exit
exit
$
$ ps --forest
PID TTY TIME CMD
1841 pts/0 00:00:00 bash
2604 pts/0 00:00:00     \_ ps
$

exit命令不仅能退出子shell,还能用来登出当前的虚拟控制台终端或终端仿真器软件。只需要在父shell中输入exit,就能够从容退出CLI了。
运行shell脚本也能够创建出子shell。在第11章,你将会学习到相关话题的更多知识。
就算是不使用bash shell命令或是运行shell脚本,你也可以生成子shell。一种方法就是使用进程列表。

5.2.1 进程列表

你可以在一行中指定要依次运行的一系列命令。这可以通过命令列表来实现,只需要在命令之间加入分号(;)即可。

$ pwd ; ls ; cd /etc ; pwd ; cd ; pwd ; ls
/home/Christine
Desktop Downloads Music Public Videos
Documents junk.dat Pictures Templates
/etc
/home/Christine
Desktop Downloads Music Public Videos
Documents junk.dat Pictures Templates
$

在上面的例子中,所有的命令依次执行,不存在任何问题。不过这并不是进程列表。命令列表要想成为进程列表,这些命令必须包含在括号里。

$ (pwd ; ls ; cd /etc ; pwd ; cd ; pwd ; ls)
/home/Christine
Desktop Downloads Music Public Videos
Documents junk.dat Pictures Templates
/etc
/home/Christine
Desktop Downloads Music Public Videos
Documents junk.dat Pictures Templates
$

尽管多出来的括号看起来没有什么太大的不同,但起到的效果确是非同寻常。括号的加入使命令列表变成了进程列表,生成了一个子shell来执行对应的命令。

说明 进程列表是一种命令分组(command grouping)。另一种命令分组是将命令放入花括号中,并在命令列表尾部加上分号(;)。语法为{ command; }。使用花括号进行命令分组并不会像进程列表那样创建出子shell。

要想知道是否生成了子shell,得借助一个使用了环境变量的命令。(环境变量会在第6章中详述。)这个命令就是echo $BASH_SUBSHELL。如果该命令返回0,就表明没有子shell。如果返回1或者其他更大的数字,就表明存在子shell。
下面的例子中使用了一个命令列表,列表尾部是echo $BASH_SUBSHELL。

$ pwd ; ls ; cd /etc ; pwd ; cd ; pwd ; ls ; echo $BASH_SUBSHELL
/home/Christine
Desktop Downloads Music Public Videos
Documents junk.dat Pictures Templates
/etc
/home/Christine
Desktop Downloads Music Public Videos
Documents junk.dat Pictures Templates
0

在命令输出的最后,显示的是数字0。这就表明这些命令不是在子shell中运行的。
要是使用进程列表的话,结果就不一样了。在列表最后加入echo $BASH_SUBSHELL。

$ (pwd ; ls ; cd /etc ; pwd ; cd ; pwd ; ls ; echo $BASH_SUBSHELL)
/home/Christine
Desktop Downloads Music Public Videos
Documents junk.dat Pictures Templates
/etc
/home/Christine
Desktop Downloads Music Public Videos
Documents junk.dat Pictures Templates
1

这次在命令输入的最后显示出了数字1。这表明的确创建了子shell,并用于执行这些命令。所以说,命令列表就是使用括号包围起来的一组命令,它能够创建出子shell来执行这些命令。
你甚至可以在命令列表中嵌套括号来创建子shell的子shell。

$ ( pwd ; echo $BASH_SUBSHELL)
/home/Christine
1
$ ( pwd ; (echo $BASH_SUBSHELL))
/home/Christine
2

注意,在第一个进程列表中,数字1表明了一个子shell,这个结果和预期的一样。但是在第二个进程列表中,在命令echo $BASH_SUBSHELL外面又多出了一对括号。这对括号在子shell中产生了另一个子shell来执行命令。因此数字2表明的就是这个子shell。
在shell脚本中,经常使用子shell进行多进程处理。但是采用子shell的成本不菲,会明显拖慢处理速度。在交互式的CLI shell会话中,子shell同样存在问题。它并非真正的多进程处理,因为终端控制着子shell的I/O。

5.2.3 别出心裁的子 shell 用法

在交互式的shell CLI中,还有很多更富有成效的子shell用法。进程列表、协程和管道(第11章会讲到)都利用了子shell。它们都可以有效地在交互式shell中使用。
在交互式shell中,一个高效的子shell用法就是使用后台模式。在讨论如果将后台模式与子shell 搭配使用之前,你得先搞明白什么是后台模式。

  1. 探索后台模式

5
在后台模式中运行命令可以在处理命令的同时让出CLI,以供他用。演示后台模式的一个经典命令就是sleep。
sleep命令接受一个参数,该参数是你希望进程等待(睡眠)的秒数。这个命令在脚本中常用于引入一段时间的暂停。命令sleep 10会将会话暂停10秒钟,然后返回shell CLI提示符。

$ sleep 10
$

要想将命令置入后台模式,可以在命令末尾加上字符&。把sleep命令置入后台模式可以让我们利用ps命令来小窥一番。

$ sleep 3000&
[1] 2396
$ ps -f
UID PID PPID C STIME TTY TIME CMD
christi+ 2338 2337 0 10:13 pts/9 00:00:00 -bash
christi+ 2396 2338 0 10:17 pts/9 00:00:00 sleep 3000
christi+ 2397 2338 0 10:17 pts/9 00:00:00 ps -f
$

sleep命令会在后台(&)睡眠3000秒(50分钟)。当它被置入后台,在shell CLI提示符返回之前,会出现两条信息。第一条信息是显示在方括号中的后台作业(background job)号(1)。第二条是后台作业的进程ID(2396)。
ps命令用来显示各种进程。我们可以注意到命令sleep 3000已经被列出来了。在第二列显示的进程ID(PID)和命令进入后台时所显示的PID是一样的,都是2396。
除了ps命令,你也可以使用jobs命令来显示后台作业信息。jobs命令可以显示出当前运行在后台模式中的所有用户的进程(作业)。

$ jobs
[1]+    Running    sleep  3000  &
$

jobs命令在方括号中显示出作业号(1)。它还显示了作业的当前状态(running)以及对应的命令(sleep 3000 &)。
利用jobs命令的-l(字母L的小写形式)选项,你还能够看到更多的相关信息。除了默认信息之外,-l选项还能够显示出命令的PID。

$ jobs -l
[1]+    2396 Running    sleep  3000  &
$

一旦后台作业完成,就会显示出结束状态。

[1]+    Done    sleep  3000  &
$

窍门 需要提醒的是:后台作业的结束状态可未必会一直等待到合适的时候才现身。当作业结束状态突然出现在屏幕上的时候,你可别吃惊啊。

后台模式非常方便,它可以让我们在CLI中创建出有实用价值的子shell。

  1. 将进程列表置入后台

之前说过,进程列表是运行在子shell中的一条或多条命令。使用包含了sleep命令的进程列表,并显示出变量BASH_SUBSHELL,结果和期望的一样。

$ (sleep 2 ; echo $BASH_SUBSHELL ; sleep 2)
1
$

在上面的例子中,有一个2秒钟的暂停,显示出的数字1表明只有一个子shell,在返回提示符之前又经历了另一个2秒钟的暂停。没什么大事。
将相同的进程列表置入后台模式会在命令输出上表现出些许不同。

$ (sleep 2 ; echo $BASH_SUBSHELL ; sleep 2)&
[2] 2401
$ 1
[2]+ Done ( sleep 2; echo $BASH_SUBSHELL; sleep 2 )
$

把进程列表置入后台会产生一个作业号和进程ID,然后返回到提示符。不过奇怪的是表明单一级子shell的数字1显示在了提示符的旁边!不要不知所措,只需要按一下回车键,就会得到另一个提示符。
在CLI中运用子shell的创造性方法之一就是将进程列表置入后台模式。你既可以在子shell中进行繁重的处理工作,同时也不会让子shell的I/O受制于终端。
当然了,sleep和echo命令的进程列表只是作为一个示例而已。使用tar(参见第4章)创建备份文件是有效利用后台进程列表的一个更实用的例子。

$ (tar -cf Rich.tar /home/rich ; tar -cf My.tar /home/christine)&
[3] 2423
$

将进程列表置入后台模式并不是子shell在CLI中仅有的创造性用法。协程就是另一种方法。

  1. 协程

协程可以同时做两件事。它在后台生成一个子shell,并在这个子shell中执行命令。要进行协程处理,得使用coproc命令,还有要在子shell中执行的命令。

$ coproc sleep 10
[1] 2544
$

除了会创建子shell之外,协程基本上就是将命令置入后台模式。当输入coproc命令及其参 数之后,你会发现启用了一个后台作业。屏幕上会显示出后台作业号(1)以及进程ID(2544)。
jobs命令能够显示出协程的处理状态。

$ jobs
[1]+    Running    coproc COPROC sleep 10 &
$

在上面的例子中可以看到在子shell中执行的后台命令是coproc COPROC sleep 10。COPROC是coproc命令给进程起的名字。你可以使用命令的扩展语法自己设置这个名字。

$ coproc My_Job { sleep 10; }
[1] 2570
$
$ jobs
[1]+    Running    coproc  My_Job  {  sleep  10;  }  &
$

通过使用扩展语法,协程的名字被设置成My_Job。这里要注意的是,扩展语法写起来有点麻烦。必须确保在第一个花括号({)和命令名之间有一个空格。还必须保证命令以分号(;)结尾。另外,分号和闭花括号(})之间也得有一个空格。

说明 协程能够让你尽情发挥想象力,发送或接收来自子shell中进程的信息。只有在拥有多个协程的时候才需要对协程进行命名,因为你得和它们进行通信。否则的话,让coproc命令将其设置成默认的名字COPROC就行了。

你可以发挥才智,将协程与进程列表结合起来产生嵌套的子shell。只需要输入进程列表,然后把命令coproc放在前面就行了。

$ coproc ( sleep 10; sleep 2 )
[1] 2574
$
$ jobs
[1]+   Running    coproc COPROC ( sleep 10; sleep 2 ) &
$
$  ps  --forest
PID TTY    TIME CMD
2483 pts/12    00:00:00 bash
2574 pts/12    00:00:00    \_ bash
2575 pts/12    00:00:00    |    \_  sleep

2576 pts/12    00:00:00    \_  ps
$

记住,生成子shell的成本不低,而且速度还慢。创建嵌套子shell更是火上浇油!
在命令行中使用子shell能够获得灵活性和便利。要想获得这些优势,重要的是理解子shell的行为方式。对于命令也是如此。在下一节中,我们将研究内建命令与外部命令之间的行为差异。

5.3 理解 shell 的内建命令

在学习GNU bash shell期间,你可能听到过“内建命令”这个术语。搞明白shell的内建命令和非内建(外部)命令非常重要。内建命令和非内建命令的操作方式大不相同。

5.3.1 外部命令

外部命令,有时候也被称为文件系统命令,是存在于bash shell之外的程序。它们并不是shell程序的一部分。外部命令程序通常位于/bin、/usr/bin、/sbin或/usr/sbin中。
ps就是一个外部命令。你可以使用which和type命令找到它。

$ which ps
/bin/ps
$
$ type -a ps
ps is /bin/ps
$
$ ls -l /bin/ps
-rwxr-xr-x 1 root root 93232 Jan   6  18:32  /bin/ps
$

当外部命令执行时,会创建出一个子进程。这种操作被称为衍生(forking)。外部命令ps很方便显示出它的父进程以及自己所对应的衍生子进程。

$ ps -f
UID PID PPID C STIME TTY TIME CMD
christi+ 2743 2742 0 17:09 pts/9 00:00:00 -bash
christi+ 2801 2743 0 17:16 pts/9 00:00:00 ps -f
$

作为外部命令,ps命令执行时会创建出一个子进程。在这里,ps命令的PID是2801,父PID 是2743。作为父进程的bash shell的PID是2743。图5-3展示了外部命令执行时的衍生过程。
第 5 章 理解 shell - 图3
图5-3 外部命令的衍生

当进程必须执行衍生操作时,它需要花费时间和精力来设置新子进程的环境。所以说,外部命令多少还是有代价的。

说明 就算衍生出子进程或是创建了子shell,你仍然可以通过发送信号与其沟通,这一点无论是在命令行还是在脚本编写中都是极其有用的。发送信号(signaling)使得进程间可以通过信号进行通信。信号及其发送会在第16章中讲到。

5.3.2 内建命令

内建命令和外部命令的区别在于前者不需要使用子进程来执行。它们已经和shell编译成了一体,作为shell工具的组成部分存在。不需要借助外部程序文件来运行。

cd和exit命令都内建于bash shell。可以利用type命令来了解某个命令是否是内建的。

$ type cd
cd is a shell builtin
$
$ type exit
exit is a shell builtin
$

因为既不需要通过衍生出子进程来执行,也不需要打开程序文件,内建命令的执行速度要更快,效率也更高。附录A给出了GNU bash shell的内建命令列表。
要注意,有些命令有多种实现。例如echo和pwd既有内建命令也有外部命令。两种实现略有不同。要查看命令的不同实现,使用type命令的-a选项。

$ type -a echo
echo is a shell builtin echo is /bin/echo
$
$ which echo
/bin/echo
$
$ type -a pwd
pwd is a shell builtin pwd is /bin/pwd
$
$ which  pwd
/bin/pwd
$

命令type -a显示出了每个命令的两种实现。注意,which命令只显示出了外部命令文件。

窍门 对于有多种实现的命令,如果想要使用其外部命令实现,直接指明对应的文件就可以了。例如,要使用外部命令pwd,可以输入/bin/pwd。

  1. 使用history命令

一个有用的内建命令是history命令。bash shell会跟踪你用过的命令。你可以唤回这些命令并重新使用。
要查看最近用过的命令列表,可以输入不带选项的history命令。

$ history
1 ps -f
2 pwd
3 ls
4 coproc ( sleep 10; sleep 2 )
5 jobs
6 ps --forest
7 ls
8 ps -f
9 pwd
10 ls -l /bin/ps
11 history
12 cd /etc
13 pwd
14 ls
15 cd
16 type pwd
17 which pwd
18 type echo
19 which echo
20 type -a pwd
21 type -a echo
22 pwd
23 history

在这个例子中,只显示了最近的23条命令。通常历史记录中会保存最近的1000条命令。这个数量可是不少的!

窍门 你可以设置保存在bash历史记录中的命令数。要想实现这一点,你需要修改名为HISTSIZE 的环境变量(参见第6章)。

你可以唤回并重用历史列表中最近的命令。这样能够节省时间和击键量。输入!!,然后按回车键就能够唤出刚刚用过的那条命令来使用。

$  ps  --forest
PID TTY    TIME CMD
2089 pts/0    00:00:00 bash
2744 pts/0    00:00:00    \_ ps
$
$ !!
ps --forest
PID TTY    TIME CMD
2089 pts/0    00:00:00 bash
2745 pts/0    00:00:00    \_ ps
$

当输入!!时,bash首先会显示出从shell的历史记录中唤回的命令。然后执行该命令。
命令历史记录被保存在隐藏文件.bash_history中,它位于用户的主目录中。这里要注意的是,bash命令的历史记录是先存放在内存中,当shell退出时才被写入到历史文件中。

$ history
[...]
ps --forest
history
ps --forest
history
$
$ cat .bash_history
pwd ls
history exit
$

注意,当history命令运行时,列出了28条命令。出于简洁性的考虑,上面的例子中只摘取了一部分列表内容。但是文件.bash_history的内容被显示出来时,其中只有4条命令,与history 命令的输出并不匹配。
可以在退出shell会话之前强制将命令历史记录写入.bash_history文件。要实现强制写入,需要使用history命令的-a选项。

$ history -a
$
$ history
[...]
25 ps --forest
26 history
27 ps --forest
28 history
29 ls -a
30 cat .bash_history
31 history -a
32 history
$
$ cat .bash_history
[...]
ps --forest
history
ps --forest
history
ls -a
cat .bash_history
history -a

由于两处输出内容都太长,因此都做了删减。注意,history命令和.bash_history文件的输 入是一样的,除了最近的那条history命令,因为它是在history -a命令之后出现的。

说明 如果你打开了多个终端会话,仍然可以使用history -a 命令在打开的会话中向.bash_history文件中添加记录。但是对于其他打开的终端会话,历史记录并不会自动更新。这是因为.bash_history文件只有在打开首个终端会话时才会被读取。要想强制重新读取.bash_history文件,更新终端会话的历史记录,可以使用history -n命令。

你可以唤回历史列表中任意一条命令。只需输入惊叹号和命令在历史列表中的编号即可。

$ history
[...]
13 pwd
14 ls
15 cd
16 type pwd
17 which pwd
18 type echo
19 which echo
20 type -a pwd
21 type -a echo
[...]
32 history -a
33 history
34 cat .bash_history
35 history
$
$ !20
type -a pwd
pwd is a shell builtin
pwd is /bin/pwd
$

编号为20的命令从命令历史记录中被取出。和执行最近的命令一样,bash shell首先显示出从shell历史记录中唤回的命令,然后执行该命令。
使用bash shell命令历史记录能够大大地节省时间。利用内建的history命令能够做到的事情远不止这里所描述的。可以通过输入man history来查看history命令的bash手册页面。

  1. 命令别名

alias命令是另一个shell的内建命令。命令别名允许你为常用的命令(及其参数)创建另一个名称,从而将输入量减少到最低。
你所使用的Linux发行版很有可能已经为你设置好了一些常用命令的别名。要查看当前可用的别名,使用alias命令以及选项-p。

$ alias -p
[...]
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
alias grep='grep --color=auto'
alias l='ls -CF'
alias la='ls -A'
alias ll='ls -alF'
alias ls='ls --color=auto'
$

注意,在该Ubuntu Linux发行版中,有一个别名取代了标准命令ls。它自动加入了—color选项,表明终端支持彩色模式的列表。
可以使用alias命令创建属于自己的别名。

$ alias li='ls -li'
$
$ li
total 36
529581 drwxr-xr-x. 2 Christine Christine 4096 May 19 18:17 Desktop
529585 drwxr-xr-x. 2 Christine Christine 4096 Apr 25 16:59 Documents
529582 drwxr-xr-x. 2 Christine Christine 4096 Apr 25 16:59 Downloads
529586  drwxr-xr-x.  2  Christine  Christine  4096  Apr  25  16:59  Music
529587 drwxr-xr-x. 2 Christine Christine 4096 Apr 25 16:59 Pictures
529584  drwxr-xr-x.  2  Christine  Christine  4096  Apr  25  16:59  Public
529583  drwxr-xr-x.  2  Christine  Christine  4096  Apr  25  16:59  Templates
532891  -rwxrw-r--.  1  Christine  Christine    36 May 30 07:21 test.sh
529588  drwxr-xr-x.  2  Christine  Christine  4096  Apr  25  16:59  Videos
$

在定义好别名之后,你随时都可以在shell中使用它,就算在shell脚本中也没问题。要注意, 因为命令别名属于内部命令,一个别名仅在它所被定义的shell进程中才有效。

$ alias li='ls -li'
$
$ bash
$
$ li
bash: li: command not found
$
$ exit
exit
$

不过好在有办法能够让别名在不同的子shell中都奏效。下一章中就会讲到具体的做法,另外还会介绍环境变量。

5.4 小结

本章讨论了复杂的交互式程序:GNU bash shell。其中包括理解shell进程及其关系,如何生成子shell,以及子shell与父shell的关系。还探究了那些能够创建子进程的命令和不能创建子进程的命令。
当用户登录终端的时候,通常会启动一个默认的交互式shell。系统究竟启动哪个shell,这取决于用户ID配置。一般这个shell都是/bin/bash。默认的系统shell(/bin/sh)用于系统shell脚本,如那些需要在系统启动时运行的脚本。

子shell可以利用bash命令来生成。当使用进程列表或coproc命令时也会产生子shell。将子shell运用在命令行中使得我们能够创造性地高效使用CLI。子shell还可以嵌套,生成子shell的子shell,子shell的子shell的子shell。创建子shell的代价可不低,因为还必须为子shell创建出一个全新的环境。
在最后,我们学习了两种不同类型的命令:内建命令和外部命令。外部命令会创建出一个包含全新环境的子进程,而内建命令则不会。相比之下,外部命令的使用成本更高。内建命令因为不需要创建新环境,所以更高效,不会受到环境变化的影响。
shell、子shell、进程和衍生进程都会受到环境变量的影响。下一章,我们会探究环境变量的影响方式以及如何在不同的上下文中使用环境变量。