本节书摘来自异步社区《UNIX编程环境》一书中的第1章,第1.4节,作者:【美】Brian W. Kernighan , Rob Pike著,更多章节内容可以访问云栖社区“异步社区”公众号查看
1.4 shell
当系统印出提示符$,你键入命令并得到了执行时,此时并不是内核在与读者对话,而是与一个称为命令解释器或外壳shell的在对话。shell是同date或who一样的普通程序,尽管它可以处理一些不同寻常的事。shell存在于用户和内核机制之间的事实对用户是有帮助的,有些会在这里说明。下面是三个要点。
文件名简写:可以通过指定文件名的模式来选取一套文件名作为程序的变量—shell会找出匹配该模式的文件名。
输入输出重定向:可以把任何程序的输出送到一个文件中而不是终端上,并当作来自文件的而不是终端的输入。输入和输出甚至可以连接到其他程序上。
环境个性化:可以定义自己的命令和简写。
1.4.1 文件名简写
现在讨论文件名模式。假如你正在键入一个像书一样大的文档。逻辑上,这个文档会被分成许多小部分,类似于章、节。从物理上看,也应该被划分开,因为编辑一个大文件是很麻烦的,所以你应该把文本作为一批文件键入。你可以每章分为一个文件,称为ch1、ch2等。或者每一章拆分成节,可以建成如下的文件:
这也是用于本书的结构。基于系统的命名惯例,你只要看一眼就可以说出某一特定文件在整个系统中的位置。
如果打算印出全本书,那么该怎么办呢?可以进行
但这样很快就令人厌烦,而且容易出现错误。这就是文件名简写的原因所在。如果说
shell认为的含义是“任何字符串”,所以ch是一个匹配当前目录中的所有以ch开头的文件的一个模式。shell会按字母顺序1建立清单,并把清单传递给pr。pr命令绝不会看到*,shell在当前目录中所做的匹配模式生成一个传递给pr的字符串清单。
关键之处在于,文件名简写并不是pr命令的特性,而是shell的一项服务。所以读者可以用它为任何命令生成文件名序列。例如,要计算第一章的字数:
有一个名为echo的程序对于同简写字符共同试验非常有价值。正如你所猜测的,echo除了照抄其变量之外,什么也不做:
但是该变量可以由模式匹配所生成:
它列出第1章中的所有文件名,而
以字母顺序列出在当前目录中的所有文件名,而
显示按字母顺序排列的所有的文件,而
则把当前目录中的文件全部删除(最好确认你确实打算这么做)。
*并不是只能用在文件的最后位置,它可以放在任何位置,并可以使用多次。例如:
是把所有结尾为save的文件删除。
注意,文件名按字母顺序排序同按数字排序是不一样的。如果一本书有10章,次序可能不是你所想要的,因为ch10在ch2之前:
*不是shell提供的唯一模式匹配特性,尽管它被频繁地使用。模式[...]可匹配括号内的任何字符,连续的字母或数字范围可以缩写为:
?模式匹配任何单个字符:
注意,模式只匹配已存在的文件。特别强调的是不能用模式建立新的文件名。例如,如果打算把每一章的ch扩展为chapter,不能采用如下方法:
因为没有与chapter.*匹配的已存在文件。
模式字符如之类既可用于单个文件名也可用于路径名,模式按含有特殊字符路径的每个分量进行。这样/usr/mary/完成在/usr/mary中的匹配,而/usr/*/calendar生成所有用户calendar文件的路径名清单。
如果打算关闭*、?等的特定作用,可以把整个变量放在单引号内,如下所示:
也可以在特殊字符前加上一个反斜杠:
(注意,由于?不是删除符也不是消行符,该括号由shell解释,而不是由内核解释。)有关引号的讨论在第3章中进行。
练习1-4 下面这些命令有何不同?
1.4.2 I/O重定向
到目前为止所讨论的命令多数可在终端上产生输出,如编辑器,也从终端上取其输入。而终端可以被一个文件所替代供输入或输出,举例来说:
在你的终端上列出文件名。但是,如果
那么同样的文件名清单会被filelist文件所替代。符号>的含义为“把输出放到如下文件中,而不是在终端上”。如果该文件原先不存在,则会把它创建出来;如果原先有,则该文件会被重写,在终端上什么也没创建。另一个例子是,通过把cat的输出都抓进一个文件的途径,可以把若干个文件组合成一个文件:
符号>>的操作与>类似,但它的含义是“加在某文件的尾部”。也就是说:
其意思是把f1、f2以及f3的文件内容复制到temp文件原有文件的结尾之后,而不是重写原有的内容。同使用>时一样,如果temp原先不存在,它会创建一个新的空文件。
相似地,<的含义是程序从下列的文件中取输入,而不是从终端上取。这样,读者可以在文件let中准备一封信函,然后用下列方法发送给若干人:
在所有这些例子中,在>或<两边的空格是可选的,但本书中使用的格式是传统格式。
用>获得了重定向输出的能力,就有可能把命令组合起来以实现原先不可能达到的效果。例如,要印出用户的字母顺序清单:
由于who为每一个登录用户印出一行输出,而wc-l计算行数(不使用计算字数和字符数功能),于是就可以计算用户数,这可用如下方法:
也可用下列方式统计当前目录中文件数:
用这个方法,计数中包含temp自身。用下列方法可以3列形式印出文件名:
而通过组合运用who以及grep,可以看到登录的某一特定用户:
在所有这些例子中,在使用诸如*一类文件名模式匹配符时,请记住很重要的一点,>和<是由shell解释的,而不是由各个程序来解释的。功能的集中化使得shell的这一输入和输出重定向功能可供任何程序使用,而这些程序自身并不知晓发生了什么不寻常的事。
这样就引出了一项重要的惯例。命令
对文件temp的内容进行排序,而
做类似的工作,但有所差别。因为字符串
但是,如果没有指定文件名,它就对其标准输入进行排序。这是大多数命令的一个基本特性:如果没有指定文件名,就对标准输入进行处理。这里,你可以直接键入命令,观看它们如何工作。例如:
在下一节中,将讨论这一原理如何应用。
练习1-5 解释为什么如下命令:使ls.out被包括在文件名单中。
练习1-6 解释来自如下命令的输出:
如果拼错了命令名,比如:
会发生什么?
1.4.3 管道
上一节的所有例子中都依赖于相同的技巧:把一个程序的输出,通过一个临时文件送到另一个文件的输入,而该临时文件并没有其他的作用。实际上,使用这样一个文件是很笨拙的。这样一种观念导致了UNIX系统的基础性的贡献之一—管道的想法的诞生。管道是不使用临时文件而把一个程序的输出连接到另一个程序的输入的途径,管道线是通过管道对两个或两个以上的程序的连接。
下面通过管道而不是临时文件重新检查前面的一些例子。垂直线字符(¦)通知shell建立一条管道线:
任何从终端读取的程序都可改由管道读取,任何写到终端上的程序都可以改为写向管道。当没有指定文件名时,任何读取标准输入的程序均可使用管道线。grep、pr、sort以及wc都以上述方式使用管道线。
在管道线中可以有任意多个程序:
此行命令,在行式打印机上输出三列的文件名清单,而
统计Mary登录的次数。
在管道线上的程序实际是同时在运行,而不是一个接一个运行的。这意味着,在管道线中的程序彼此之间可以相互交互;内核处理有关调度和同步,以使它们全部运行。
当需用管道时,shell作出安排,显然各个程序要进行重定向。当然,在做这些工作时,程序必须反应敏捷。大多数命令遵循一个公共格式,所以它们多半能在任何位置上适用管道的要求。通常,命令按如下方式实施:
如果没有指定文件名,则命令读进标准输入,其默认设备是终端(便于进行实践),但输入可以被重定向为来自文件或者管道。同时,在输出一方,多数命令把输出写到标准设备,其默认设备也是终端,但它也可以重定向到文件或者管道。
到现在为止,所讨论过的命令几乎都符合这一模式;唯一例外的是类似data和who这种命令,它们没有输入,还有一些如cmp及diff,它们有固定数量的文件输入。(请注意它们的“-”选项。)
练习1-7 解释如下两者的不同:
1.4.4 进程
shell除了设置管道之外,它还处理大量的工作。现在花点时间看看同时运行多于一个程序的基本情况,我们已经看到了有管道时的一些情况。例如,可以在一个命令行中用分号分隔两个命令,shell识别分号并把命令分成两个命令:
在shell返回提示符之前,两个命令都(顺序地)执行了。
如果你希望的话,也可以同时运行多于一个的程序。例如,如果需要做一些费时的工作,如计算书本中的字数,但在做其他事之前不想等待wc完成工作,那么可以这样做:
在命令行结尾的&通知shell“开始该命令的运行,然后立即从终端取下一个命令”,这也就是说,不等到其完成,就取下一个命令。这样一来,命令开始工作,当它运行时,用户就可以去做其他的事。把输出导向至wc.out,用户同时做其他的事时就不会受到它的打扰。
一个正在运行的程序的实例称为进程。由&初始化的命令,经由shell显示出的数字称为进程标识符;该进程标识符可以在其他命令中使用,用以引用某一个运行着的程序。
区分程序和进程的概念很重要,wc是一个程序;每次运行程序wc时,就创建了一个新进程。如果同一个程序同时有若干个实例在运行,那么每一个进程都是分隔的,且有一个不同的进程标识符。
如果管道线被&初始化了,如下所示:
如果忘了进程ID,可以用命令ps显示正在运行的一切内容。如果着急的话,kill 0会终止除了所登录的shell之外的全部进程。如果想知道别的用户在做什么,ps-ag会展示当前在运行的所有进程。下面是一些输出的例子:
PID是进程ID;TTY是同进程相关的终端(如在who中一般);TIME是以分秒计的处理器时间;其余的是在运行的命令。ps是一个在系统的不同版本中均有所不同的命令,所以你得到的输出格式可能会与本书的不同。变量甚至也会有所不同——请参见手册的ps(1)页。
进程和文件有相同的层次化结构;每个进程有一父进程,也可能还有子进程。用户shell由同终端连接到系统相关的进程创建。当运行命令时,这些进程就是shell的直接子进程。如果运行其中之一的程序,例如,从ed中退出的!命令,ed创建自己的子进程,这样就有了shell的孙进程。
有时一个进程花费的时间是如此之长,以至于你打算在这个进程运行后,关闭终端回家,而不是等待它完成。但是在关闭终端或断开连接时,即使使用了&,进程通常也会被终止。命令nohup(no hangup)是用来处理这种情形的。如果操作如下:
那么在用户退出登录后,命令仍会继续运行。命令产生的任何输出会保存在名为nohup.out的文件之中,但不能使用nohup追溯一条命令。
如果进程耗费大量的处理机资源,比较合适的是赋予该进程低于正常的优先级别。这可以通过另一个名为nice的程序来完成:
nohup自动调用nice,用户一旦退出了登录,便可以以较长的时间来执行该命令。
最后,可以直接通知系统,在清早别人还在睡觉而不从事计算的时侯,启动用户的进程。该命令名为at(1):
时间可以是24小时制,如2130,也可以是12小时制,如930pm。
1.4.5 剪裁环境
UNIX系统的优点之一是,可通过多种方式调整系统,使之更接近个人习惯或适合本地计算环境。例如,我们曾讨论过删字符和消行符,它们通常约定为#和@。可用下列操作在任何时间修改它们:
这里e是打算用于删除的字符,而k是用于行删除的字符。但是每次登录时却要键入它们,也是件烦人的事。
shell可以弥补这一不足。如果在用户登录目录中有一个名为.profile的文件,那么登录时shell就会执行其中的命令,然后显示第一个提示符。所以可以把命令放进.profile中,从而建立所希望的环境,这样每次登录时,它们都会被执行。
多数人会把下列操作首先放进.profile:
这里使用←是为了读者可以看见它,可以在.prifile文件放进一退格符。stty也理解表示Ctrl + x的^x,所以用下列的处理可以得到相同的效果:
因为Ctrl+ h也表示Backspace。(^字符是管道操作符¦的一个作废的同义词,所以必须用引号保护。)
如果你的终端没有设置Tab键,你可以用stty和–tabs加入:
如果希望在登录时看看系统是否忙碌,可以加入:
以便计算用户数。如果要知道有什么新闻,可以加入news。某些人还喜欢试试自己的运气:
如果觉得登录时间太长了,可以在.profile中删掉一些内容,只剩下最必要的。
shell的某些特性实际上由所谓的shell变量控制,其值可被访问,也可被设置。例如,如前所用的提示符$实际上存放在shell变量PS1中,可以设置为任何用户希望的提示符,操作如下:
由于提示符串中有空格,所以引号是必需的。在本构造中,=旁不允许有空格。
shell也特别把HOME及MAIL视作变量。HOME是用户主目录的名称,它通常不必用.profile文件就会正常地设置。变量MAIL命名了保存mail的标准文件,如果shell对它做了定义,就可以在每一命令之后得到是否有新邮件到达的通知2:
(你的系统中,mail系统可能与这里描述的不同,/usr/mail/you也是常见的形式。)
也许shell变量中最有用的是,shell用来控制搜寻命令的变量。请回忆,当键入命令名称时,shell通常首先查询当前目录,而后是/bin,再是/usr/bin。这一目录序列称为查询路径,并存放在名为PATH的shell变量中。如果默认的查询路径不是所希望的,可以修改它,而且通常也是在.profile中修改。例如,下面的操作把标准路径及/usr/games设置为路径:
其语法有一点奇怪:目录名序列由冒号分割。请记住“.”是当前目录,可以略去“.”;在PATH中的空的成分意指当前目录。
在这种特定情况中,一种替代的PATH设置,是把变量设定为先前的值:
可以在变量名前缀以$,从而得到shell变量的值。在上例中,用表达式$PATH提取PATH当前值,并被加入新值,然后将结果赋给PATH。这一点可以用echo进行验证:
如果用户有自用的命令,也许打算把它们收集进自用的目录中,并把它们加入进查询目录中。在这种情形下,PARH语句如下:
在第3章中将讨论如何编写用户自己的命令。
另一种变量是TERM,通常由文本编辑使用,且比ed更新潮,其名称是用户所使用的终端的一种。它可以使程序更有效地管理屏幕,可以如下例添加到用户的.profile中去。
也有可能使用变量缩写。如果你有常用的、名称较长的目录,那么有必要添加如下一行命令到.profile文件中去:
这样就可用下列语句:
类似于d的、私用的变量通常用小写方式拼写,以便同shell自身使用的变量相区别,如PATH。
最后,有必要把用户打算在其他的程序中使用的变量通知shell,这可以通过export命令实现,有关内容将在第3章中进一步讨论:
现在小结一下,典型的.profile文件可能如下例所示:
本书没有必要穷尽shell所提供的服务。最有用的功能之一,是把已有的命令包装在shell处理的文件中,从而创建一条新命令。这种方法简单却能产生显著的效果。