在学习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展示了外部命令执行时的衍生过程。
当进程必须执行衍生操作时,它需要花费时间和精力来设置新子进程的环境。所以说,外部
命令多少还是有代价的。
说明
就算衍生出子进程或是创建了子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
[...]
25 ps --forest
26 history
27 ps --forest
28 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手册页面。
2. 命令别名
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、进程和衍生进程都会受到环境变量的影响。下一章,我们会探究环境变量的
影响方式以及如何在不同的上下文中使用环境变量。