从一开始,Unix 命令行就具备一些特殊的东西,使其区别与其他操作系统,即所谓的的 Unix工具箱:每种 Unix 和 Linux 系统都拥有的大量程序。本章将解释 Unix 工具箱之后隐藏的设计准则,然后师范如何将基本的构建块组合成适合于自己的功能强大的工具。
Unix 设计准则
每个程序都应该是一个单独的工具,或许还有几个基本的选项。一个程序应该只做一件事,但必须出色地完成这个事情。四需要执行一项复杂的任务,应该通过对现有工具的组合来完成(当存在可能性时),而不是再去编写一个新程序。
more 程序(输出显示工具):每次一屏地显示数据,按键就会显示下一屏幕数据;按q键就可以退出。
因此,Unix 的设计准则可以用两句话概括:
- 每个程序或者命令应该是一个工具,它只完成一件事情,但一定要完成好这件事情;
- 当需要新工具时,最好对现有的工具进行组合,而不是编写一个新工具;
有时候将这一设计准则描述为:
- “Small is beautiful(小的就是完美的)” 或者 “Less is more(少的就更好)”;
Unix 新设计准则
CLI(命令行界面)是基于文本的,它无法处理图形和图像,或者没有包含存文本的文件。大多数命令行程序读取和写入文本,这就是为什么这样的程序可以继承在一起工作的原因:他们都使用相同类型的数据。但是当希望处理非文本数据时,必须使用其他类型的程序,所以需要学习 CLI 和 GUI。
常用屏幕显示工具:
- more:使用简单,所有系统上都有这个程序
- pg
- less:许多系统上,less 是默认的程序,而且 less 程序更加出色
Unix 新设计准则:
- “除非程序无法更小,否则小的就是完美的”
标准输入、标准输出和标准错误
Unix 的开发人员设计了一种读取数据的通用方法(称之为标准输入)和两种写出数据的通用方法(称之为标准输出和标准错误),综合起来,我们称这些功能为标准I/O(standard I/O)。
重定向标准输出
在登录时,shell 会自动地将标准输入设置为键盘,将标准输出和标准错误设置为屏幕。这意味着,默认情况下,大多数程序从键盘读取输入,并将输出写入到屏幕。
但是,每次输入命令时,都可以告诉 shell 在此命令执行期间重置标准输入、标准输出或标准错误。
如果希望命令的输出写到屏幕上,则无需做任何事情,这是自动的。
如果想将命令的输出写入到文件中,需要在命令后面键入 >(大于号)字符,后面跟文件名:
sort>names# 该命令将它的输出写入到一个叫 names 的文件中写入到的目的文件可以存在,也可以不存在。如果不存在,shell 将自动创建这个文件;如果存在,那么它的内容将被替换。如果希望将新数据添加到原有的数据后面,需要用 >> 字符,即两个连续的大于号字符:
sort>>names# 将新数据追加在已有文件的尾部将标准输出发送给文件时,即为重定向标准输出。
如果希望 shell 永远不替换现有文件的内容,可以通过设置 noclobber 选项(Bash、Korn shell)或者 shell 变量 noclobber(C-Shell、Tcsh)。
set-o noclobber如果设置了此选项,并且重定向标准输出到一个已经存在的文件,那么 Bash 将拒绝执行命令,并给出如下消息:
bash: names: cannot overwrite existing file
重定向标准输入
默认情况下,标准输入被设置成键盘。这意味着,当运行需要读取数据的程序时,程序期望用户通过键盘输入数据,每次一行。但结束输入数据时,可以按 ^D(组合键)发送 eof 信号。
重定向标准输入,使命令从文件中读取数据而不是键盘,只需要在命令的最后键入 <(小于号),后面跟着文件的名称即可:
sort<names# 从 names 文件中读取内容sort</etc/passwd# 从 /etc/passwd 文件中读取内容标准输入和标准输出可以同时重定向:
sort<rawdata>report重定向标准错误、文件描述符
shell 提供两种不同的输出目标:标准输出和标准错误。标准输出用于正常输出,标准错误用于错误消息。两者皆可重定向。但重定向标准错误的语法不相同。
在 Unix 进程中,每个输入源和每个输出目标都由一个唯一的数字标识,这个数字称为文件描述符(file descriptor)。
文件描述符的本质代表着进程对 I/O 资源的引用。在 Linux/Unix 系统中,文件描述符是一个非负整数,它代表了进程与一个文件、管道、套接字(Socket)或其他 I/O 资源之间的连接。关注如下的命令:
calculate8>results此命令在执行之前,shell 解析后会为这个进程创建值为8的文件描述符,并使其与 results 文件关联。其中的 > 表明这个文件是用于写入的,即打开文件的模式是只读还是只写的,另外,如果要创建一个可读可写的文件描述符,可以使用 <>:
calculate8<>results在 Bourne shell 家族中,重定向输入或输出的正式语法是在文件描述符数字之后使用 <(小于号)或 >(大于号)。默认情况下,Unix 为每个进程提供3个预定义的文件描述符:0 代表标准输入;1 代表标准输出;2 代表标准错误;
因此,重定向标准IO 的写法如下:
command0<inputfile# 重定向标准输入,同 command < inputfilecommand1>outputfile# 重定向标准输出,同 command > outputfilecommand2>outputfile# 重定向标准错误为了方便起见,如果重定向输入时省略了 0,那么 shell 假定指的就是标准输入;如果重定向输入时省略 1,那么 shell 假定指的就是标准输出。
同一个条命令可以指定多个重定向:
sort0<rawdata1>results2>errorssort<rawdata>results2>errors以上重定向只适用于 Bourne shell,在 C-Shell 家族中重定向标准错误比较复杂。
子 shell
进程就是加载到内存中并且准备运行的程序,以及程序的数据和跟踪程序所需的信息。当进程需要启动另一个进程时,这个进程就创建一个副本进程。原始的进程称为父进程,副本进程称为子进程。
子进程开始运行后,父进程等待子进程死亡(也就是结束)。一旦子进程死亡,父进程就会被唤醒,重新获得控制权并再次开始运行,此时子进程消失。
shell 在解析命令时,判断命令是内部命令还是外部命令,内部命令 shell 就会在自己的进程中直接解释执行。而外部命令 shell 需要查找合适的程序,然后以一个新进程运行这个程序,当该程序终止时,shell 重新获得控制权,此时shell 就是父进程,shell 运行的程序就是子进程。
当一个 shell 启动另一个 shell 时,我们称第二个 shell 为 子shell。当创建子 shell 时,它集成父shell 的环境,但是,子shell 对环境的任何改变都不会传递回父shell。
除了直接输入命令启动一个新的 shell,还有一个方法让命令在 子shell 中运行:将命令括在圆括号中。这样就可以告诉 shell 在子shell 中运行命令:
(date)# 在子shell 中运行 date 命令重定向标准输出和标准错误到同一个文件
对于 Bourne Shell 其基本思想就是将一种类型的输出重定向到一个文件,然后再将另一种类型的输出重定向到同一个位置:
commandx>outputfile y>&xsort1>output2>&1# 重定向标准输出和标准错误到文件 outputsort&>output# bash 4.0 以上,可以使用此写法,也可以使用 sort >& output# 错误!在一条命令中重定向同一个文件两次的话,一个重定向会覆盖另一个重定向sort>output2>output其中 &1 标识文件描述符1当前指向的地址。
抛弃输出
当需要抛弃输出时,只需要重定向输出到一个特殊的文件:/dev/null;对于此文件,发送给它的任何东西都会永远消失。
另外,特殊文件 /dev/tty 表示终端,数据发送给次文件时,输出就显示在显示器上。
Bourne Shell 家族的重定向总结
这里只提供 Bourne shell 家族的重定向总结,C-Shell 不再给出。以下为 Bourne shell 家族的标准IO的重定向:
| 元字符 | 动作 |
|---|---|
| < | 重定向标准输入(同 0<) |
| > | 重定向标准输出(同 1>) |
| >| | 重定向标准输出:强制重写 |
| >> | 追加标准输出(同 1>>) |
| 2> | 重定向标准错误 |
| 2>> | 追加标准错误 |
| 2>&1(原书有错误) | 将标准错误重定向到标准输出 |
| >& 或者 &> | 重定向标准输出 + 标准错误(只适用于 bash) |
| | | 将标准输出通过管道传送给另一条命令 |
| 2>&1 | | 将标准输出+标准错误通过管道传送给另一条命令 |
大多数命令行程序使用标准IO进行输入和输出,输入来源于标准输入(stdin)、正常输出发送到标准输出(stdout)、错误消息则发送给标准错误(stderr)。
对于 Bourne shell 家族,可以使用文件描述符(stdin=0、stdout=1,stderr=2)及各种元字符控制标准IO。当没有歧义性时,可以省略文件描述符,为了防止不小心重写了已有的文件,可以设置 shell 选项 noclobber。如果设置了 noclobber 选项,则可以使用 >| 强制进行重写。
- ls(list,列举)命令显示文件的信息,使用 -l(long)选项,ls可以显示文件的信息。
- touch 命令可以用来创建一个空文件。
- cat 命令可以用于查看某个文件的内容。
- rm(remove,移除)命令用于删除文件。
管道线
Unix 开发人员的目标就是构建小的工具,每个工具只做一件事情,当一个工具无法解决的问题时,能够使用一组工具来完成这个任务。
例如,假设有3个文件,每个文件中的每一行都是一个人的信息,现在要找出有多少人叫 Harley。
首先,使用 cat(catenate,连接)命令将这几个文件组合在一起;然后使用 grep 命令抽取所有包含单词 Harley 的行;最后,使用 wc(word count,单词统计)命令和 -l(line count,行统计)选项统计行的数量,即:
catfile1 file2 file3>tempfile1# combine filesgrepHarley<tempfile1>rempfile2# extract lineswc-l<tempfile2# count linesrmtempfile1 tempfile2# delete temp files注意,这些命令使用重定向在临时文件中存储中间结果,当工作完成时再将临时文件删除。
上面的命令序列可以完成任务,但是有一个缺陷:将所有事情连接在一起粘合剂是使用临时文件进行重定向,这种方式难以理解,且复杂。
为了使这样的解决方法简单些,shell 允许创建一序列命令,在这一序列命令中,一个程序的标准输出可以自动地发送给下一个程序的标准输入。当这样做时,两个程序之间的连接就是管道(pipe),而命令序列本身称为管道线(pipeline)。创建管道线,只需要将命令之间使用 | 字符(管道符号)分隔开即可。
例如,上面的命令序列可以使用下列命令替代:
catfile1 file2 file3|grepHarley|wc-l在 Unix/Linux 命令行中,管道符(|)的作用是将一个命令的标准输出(Standard Output, stdout)连接到另一个命令的标准输入(Standard Input, stdin)。在上面的命令中,三个程序是在各自独立的进程中执行的。
对于 Bourne shell 家族来说,可以将标准输出和标准错误组合在一起,然后一起发送给另一个程序:
command12>&1|command2# command1 和 command2 都是命令当创建管道线时,必须使用能够从标准输入读取文本,并向标准输出写入文本的程序。我们称这样的程序为 过滤器,许多程序都是过滤器。
实际中,大多数管道线只连续使用2条或者3条命令。到目前为止,管道线最常见的应用就是将一些命令的输出传递给 less,从而可以每次一屏地显示命令的输出:
cal2008|less管道线分流:tee
有时候,可能希望将程序的输出同时发送到两个地方。例如希望将一个输出保存在文件中,同时还发送到另一个程序。
使用 tee 命令可以实现这目的。tee 命令的作用就是从标准输入读取数据,并向标准输出和一个文件各发送一份数据,其语法为:
tee[-a]file...# file 是希望将数据发送到的文件的名称例如上面的问题中:
catnames1 names2 names3|grepHarley# 显示三个文件中包含Harley 的所有行# 加入 tee 进行分流catnames1 names2 names3|teemasterlist|grepHarley# 此时 cat 的输出保存在 masterlist 中,同时还将传送给 grep使用 tee 时,通过指定更多个文件名可以为输出保存不止一份副本:
# 在此管道线中,tee 将 cat 的输出复制到两个文件 d1 和 d2 中catnames1 names2 names3|teed1 d2|grepHarley如果在 tee 命令中指定的文件不存在,那么 tee 命令会创建这个文件。但是,使用时必须小心,如果文件存在,那么 tee 命令将会重写这个文件,原始内容就会丢失。如果希望 tee 命令在文件的末尾追加数据,而不是替换文件,则可以使用 -a(append,追加)选项,例如:
# 在此管道线中,tee 将 cat 的输出保存在 backup 文件中,如果该文件存在,则追加到文件的末尾catnames1 names2 names3|tee-a backup|grepHarley注意下面的这个模式:
command|teefile# 将 command 的输出保存在文件中并输出到屏幕上who|teestatus# 将 who 的输出保存在 status 文件中并显示在屏幕上在此模式下,不必在 tee 之后使用另一个程序。因为 tee 将它的输出发送给标准输出,默认情况下就是屏幕。
管道线的重要性
实际上,管道线的实现是 Unix 设计准则提升的催化剂。在很大程序上,正式管道线和标准IO使Unix 的命令行界面功能如此强大。
条件执行
有时候,希望在前一条命令成功执行的条件下执行另一条命令,语法为:
command1&&command2有时候,希望在前一条命令没有成功执行的条件下执行另一条命令,语法为:
command1||command2这里就是当前一条命令成功执行或失败时才执行另一条命令,这就是所谓的条件执行。
观察如下命令:
# 查找 people 文件中的 Harley 行,如果存在,就对 people 内容进行排序并输出到 contacts 文件中grepHarley people&&sortpeople>contacts# 与上一个命令相同,不同的是此命令不显示 grep 的输出grepHarley people>/dev/null&&sortpeople>contacts# 如果 update 命令执行失败,就显示提示update|echo"The update program failed."命令的成功或失败由其退出状态码决定,0 表示成功,非 0 表示失败。command1 && command2 依赖 command1 的退出码为 0 来决定是否执行 command2。你可以通过 echo $? 检查上一个命令的退出码,或参考命令的文档了解具体退出码含义。