UNIX编程环境

978-7-115-33835-8
作者: 【美】Brian W. Kernighan Rob Pike
译者: 陈向群等
编辑: 杨海玲

图书目录:

详情

本书系Uinx编程领域内的经典畅销书。作者本着“授之以渔”的态度,不仅向读者介绍了Unix系统编程的基本技巧及编程规范,更是将Unix的编程哲学融入其中,以帮助读者更加深刻地理解Unix系统。本书的主要内容包括Uinx系统基本操作、文件系统、shell编程、过滤器、标准I/O库编程、系统调用、程序开发工具、文档准备工具等。   

图书摘要

UNIX编程环境

[美]Brian W.Kernighan Rob Pike 著

陈向群 等 译

人民邮电出版社

北京

图书在版编目(CIP)数据

UNIX编程环境/(美)克尼汉(Kernighan,B.W.),(美)派克(Pike,R.)著;陈向群等译.--北京:人民邮电出版社,2014.4

ISBN 978-7-115-33835-8

Ⅰ.①U… Ⅱ.①克…②派…③陈… Ⅲ.①UNIX操作系统一程序设计 Ⅳ.①TP316.81

中国版本图书馆CIP数据核字(2013)第283960号

内容提要

本书是关于在UNIX环境下进行程序设计的一本经典教科书。书中引用了大量编程实例,由浅入深地讲解了如何使用UNIX及其各种工具,以及如何用C语言在UNIX环境下写出高质量的程序。

本书共9章3个附录,第1章为系统基础入门,第2章讨论UNIX文件系统,第3章讲述怎样按要求使用shell,第4章介绍过滤程序,第5章讨论如何使用shell编写程序,第6章讲述运用标准I/O库编写程序,第7章涉及系统调用,第8章讨论有关程序开发的工具,第9章讨论文档准备工具,附录A概括了标准编辑器ed,附录B列出了一个编程实例——计算器的语言参考手册,附录C是编程实例——计算器程序的最后源代码版本。

本书适合作为大学院校相关专业的教科书,对于想深入掌握UNIX和C语言的程序设计人员是一本很好的参考书。本书也适合想学习和掌握Linux的人员阅读。

◆著 [美]Brian W.Kernighan Rob Pike

译 陈向群 等

责任编辑 杨海玲

责任印制 程彦红 焦志炜

◆人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

三河市海波印务有限公司印刷

◆开本:800×1000 1/16

印张:20.75

字数:446千字  2014年4月第1版

印数:1-3000册  2014年4月河北第1次印刷

著作权合同登记号 图字:01-2012-8863号

定价:59.00元

读者服务热线:(010)81055410 印装质量热线:(010)81055316

反盗版热线:(010)81055315

广告经营许可证:京崇工商广字第0021号

版权声明

Authorized translation from the English language edition,entitled The UNIX Programming Environment,0-13-937681-x by Brian W.Kernighan and Rob Pike,published by Pearson Education,Inc.,publishing as Prentice Hall,Copyright©1984 by Pearson Education,Inc.

All rights reserved.No part of this book may be reproduced or transmitted in any form or by any means,electronic or mechanical,including photocopying,recording or by any information storage retrieval system,without permission from Pearson Education,Inc.

CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD.and POSTS & TELECOM PRESS Copyright©2014.

本书中文简体字版由Pearson Education Asia Ltd.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签,无标签者不得销售。

版权所有,侵权必究。

“UNIX安装的数量已经增加了10倍,预期还将更多。”

——UNIX程序员手册,1972年6月第2版

UNIX[1]操作系统是1969年首次在贝尔实验室的一台丢弃的DEC PDP-7计算机上启用的。当时Ken Thompson从Rudd Canaday、Doug McIlroy、Joe Ossanna和Dennis Ritchie那里获得理念和支持,编写了小型通用分时系统,其适用性能良好,足以吸引热心的用户,并最终为一台较大的计算机——PDP-11/20 的购买提供了充分的可靠性。系统早期的用户之一是Ritchie,他在1970年曾帮助将该系统转到PDP-11计算机上。Ritchie也曾设计和编写了C程序语言用的编译程序。1973年,Ritchie和Thompson用C语言重写了UNIX核心,打破了系统软件要用汇编语言编写的传统。经过此次重写,该系统最终成为今天必不可少的操作系统。

大约在1974年,大学获得了“用于教育目的”的UNIX使用许可,而且,几年后UNIX有效地开拓了商业用途。在此期间,UNIX 系统在贝尔实验室取得了成功,从而找到了进入实验室、软件开发项目、字处理中心和电话公司运行支持系统的途径。从那时起,UNIX 已遍布整个世界,从微型计算机到最大的主机,已安装了数以万计的UNIX系统。

是什么促使UNIX系统如此成功呢?可以阐明几个原因。首先,由于是用C语言编写,因此它是可移植的,UNIX 系统的运行范围可以从微处理系统到最大的主机。这是一个强大的商业优势。其次,源代码非常有效并且是用高级语言编写的,从而使系统容易适应特殊的需求。最后,也是最重要的一点,它是一个良好的操作系统,特别是对程序员而言,UNIX程序环境通常是丰富而有成果的。

尽管UNIX系统引入了很多创新的程序和技术,但是一个单独的程序或想法并不能使工作完美无缺。相反,是程序设计的处理方式才使系统有成效,这也是应用计算机的一个基本原则。虽然这个基本原则不能用一句话概括,但其核心就是,系统的能力更多地来自程序之间的关系,而不是程序本身。许多UNIX程序虽然单独做相当简单的工作,但是,在与其他程序结合起来使用时,它们就成为全面而有益的工具。

本书的目的是传播UNIX程序设计的基本原则,由于这个基本原则是以程序间的关系为基础的,因此我们必须用大量的篇幅专门对单个的工具进行讨论,但同时贯穿了程序组合的主题和运用程序进行创建程序的内容。要想很好地使用UNIX系统及其组成部件,不仅必须了解如何使用程序,还要了解怎样使其与环境相匹配。

随着UNIX系统的广泛应用,能够熟练掌握UNIX系统的用户越来越难找到。好几次,我们发现富有经验的用户,包括我们自己在内,只能笨拙地找到问题的解法,或者只会写程序去做现有的工具易于处理的工作。当然,如果没有一定的经验和理解,就不易了解精巧的解法。我们希望无论是新手还是老用户,通过阅读本书,都能够开发理解力,从而使系统的使用更加有效和有趣。希望读者更好地使用UNIX。

我们把目光对准程序员们,希望通过本书使其工作更有效,从而也可以使程序组的工作更有成果。尽管我们的主要对象是程序员,但前4章或前5章并不需要具有编程经验就可以理解,因此,对于其他用户来说,本书也是很有帮助的。

无论如何,我们已经尽可能尝试使用真实的、而不是人为的例子来表述我们的观点。尽管有一些程序在本书中是作为例子开始的,但它们就此而成为常用程序的一部分。所有实例都经过了机器可读格式文本的直接测试。

本书的编排如下:第1章为系统使用的基础入门。其中包括登录、邮件、文件系统、常用命令和命令解释的基本原理。有经验的用户可以略过此章。

第2章讨论UNIX文件系统。文件系统是系统操作和使用的核心,因此必须充分理解才能更好地使用。这一章讲述文件和目录,容许权限和文件模式,以及 i 接点。其中包括文件系统层次的浏览和设备文件的解释。

命令解释程序,即shell,是一个基本工具,不仅用于执行程序,也用于写程序。第3章讲述怎样按要求使用shell:创建新命令,使用命令变量、shell变量、基本控制流和输入输出再定向。

第4章介绍过滤程序:当数据流通过时,对数据执行简单转换的程序。第一部分涉及grep模式搜索命令和其对应程序;接下来讨论诸如sort的一些更多的常用过滤程序;其余部分重点讨论两个通用数据转换程序,称为sed和awk。sed是流编辑程序,是当数据流通过时,做出编辑更改的一个程序。awk 是用于简单信息检索和报告生成任务的一种程序设计语言。使用这些程序,有时再加上与shell的配合,常常就能完全避免流于常规的程序设计。

第5章讨论如何使用shell进行程序编写,并能够经得起其他人的使用。主要内容包括更高级的控制流以及变量、陷阱和中断处理。本章的实例充分利用了sed、awk和shell。

人们最终会达到运用shell和其他现有的程序进行工作所能达到的极限。第6章讲述有关运用标准I/O库写新程序的内容。这些程序用C语言编写,我们假设读者了解或至少最近学过C语言。我们会展示用于设计和组织新程序的实用策略,以及怎样用易于管理的步骤创建程序和如何利用现有的工具。

第7章涉及UNIX系统调用,这是其他所有软件层的基础,主要内容包括输入/输出、文件创建、出错处理、目录、i接点、进程和信号。

第8章讨论有关程序开发的工具:yacc,一种语法分析程序生成器;make,可以控制编译大程序的过程;lex,它可以生成词法分析程序。程序评注是以一个大程序的开发为基础的,该大程序是一种类似C语言的可编程计算器。

第9章讨论文档处理工具,可用一种用户级的叙述以及手册的一张页面来说明第8章的计算器。本章可以独立于其他章节进行阅读。

附录A概括了标准编辑器ed。尽管许多读者在日常工作中喜欢运用其他编辑器,但ed是非常通用、有益而且高效的。它的正则表达式是其他的程序,如grep和sed的核心,而且只为此就值得一读。

附录B包括用于第8章计算器语言的参考手册。

附录C是计算器程序最后版本的目录,在同一处列示了所有的编码以便于阅读。

另外,还有一些实用的内容。首先,UNIX 系统的使用已经很普遍,有多种广泛应用的版本。例如,第7版出自UNIX系统的发源地——贝尔实验室计算机科学研究中心。系统III和系统V是贝尔实验室支持的正式版本。Berkeley是加利福尼亚大学提供的系统,源自第7版,通常称为UCB 4.xBSD。另外,特别是对于小型计算机,有大量变种UNIX系统,它们也是从第7版本派生的。

我们尽可能地把完全一致的内容放在一起,来对付这些版本差异。尽管我们将要讲授的课程是独立于任何特定版本的,但对于我们所选定的细节内容而言,会和第7版中有关内容一致,因为它是以广泛应用的绝大多数UNIX系统为基础的。我们也已经在贝尔实验室的系统V上和Berkeley 4.1BSD中运用了这些例子,只需做一些轻微的改动,而且改动只限于少数例子。不管机器运用何种版本,读者会发现其间的差异是很小的。

其次,虽然本书中有很多参考资料,但它不是一本参考手册。我们知道讲授处理和使用风格要比仅仅讲授细节重要得多。《UNIX程序员手册》是信息的标准源泉。读者需要使用手册去解决那些没有包含在本书中的问题,或者检测读者的系统与本书作者的系统之间的不同之处。

再次,我们相信,实践出真知。本书应在终端旁阅读。这样便于对所讲的内容进行实验、检查或者反驳,探索其极限和变化。边阅读,边实践,回顾小结,然后再更多地阅读。

我们相信,尽管UNIX系统不是完美无缺的,但它是一个相当优秀的计算机环境;我们希望,对本书的阅读能帮助读者也得出这一结论。

衷心感谢提出建设性意见和建议的人们,以及他们对改进代码所做的协助。特别要感谢Jon Bentley、John Linderman、Doug McIlroy和Peter Weinberger,他们以极大的热情阅读了大量的文稿。我们非常感激Al Aho、Ed Bradford、Bob Flandrena、Dave Hanson、Ron Hardin、Marion Harris、Gerard Holzmann、Steve Johnson、Nico Lomuto、Bob Martin、Larry Rosler、Chris Van Wyk和Jim Weythman对初稿所提的意见,同时我们也感谢Mike Bianchi、Elizabeth Bimmler、Joe Carfagno、Don Carter、Tom De Marco、Tom Duff、David Gay、Steve Mahaney、Ron Pinter、Dennis Ritchie、Ed Sitar、Ken Thompson、Mike Tilson、Paul Tukey和Larry Wehr的宝贵建议。

Brian Kernighan

Rob Pike

[1].UNIX是贝尔实验室的商标。“UNIX”不是首字母缩拼词,它是一个同MULTICS稍微有联系的双关语,在UNIX之前,Thompson和Ritchie工作于MULTICS操作系统之上。

第1章 初学UNIX

什么是 UNIX?狭义地看,它是一个分时操作系统内核,即一个控制计算机的资源并将其分配给用户的程序。它让用户运行其程序,并控制与机器连接的外围设备(硬盘、终端、打印机等),提供一个文件系统用以管理诸如程序、数据及文档等长期存储的信息。

广义地看,UNIX 通常不仅包含内核,还包括一些基本程序,如编译器、编辑器、命令语言、用以复制和显示文件的程序等。

从更广的角度来看,UNIX可以包括由用户开发的、运行于用户的UNIX操作系统上的程序,如文档处理工具、统计分析程序以及图像软件包等。

这些有关UNIX的解释究竟正确与否,取决于读者所面对的系统的应用级别。本书其他部分提到UNIX时,会在上下文指示其内在含义。

UNIX系统有时看起来比实际上更复杂——对于新用户而言,很难充分利用可用的资源。所幸它并不难入门——只要了解很少的几个程序,就可以开始工作了。本章会帮助读者尽快地学会使用UNIX系统。本章是概述,不是手册;后续的章节将详细介绍各种内容。本章涉及以下主要内容。

基本操作——登录和退出、简单的命令、纠正输入错误、邮件及终端间通信。

日常使用——文件及文件系统、文件显示、目录以及常用命令。

命令解释器或shell——文件名缩写、输入输出重定向、管道、删字符及消行符设置、命令查询路径定义。

如果你用过UNIX,那么对本章的多数内容应该是熟悉的,可以直接跳至第2章。

即使阅读本章,也需要一份《UNIX程序员手册》。对作者而言,告诉你翻阅手册中的某些内容,比在书中重复这些内容更为方便。本书不是为了替代手册,而是为了教会你充分利用手册中的命令。另外,本书中所叙述的内容可能同你的系统上的内容有所差别。在手册中的开始处有索引,可以协助找出解决某一问题的程序,应学会如何使用它。

最后,有一句忠告:不要害怕实践。如果你是初学者,请放心,不会出现伤害自身或其他用户的事。实践出真知。本章篇幅较长,最好的方法是一次读几页,并在学习过程中不断实践。

1.1 起步

1.1.1 有关终端和输入的一些预备知识

为了避免繁冗解释有关计算机使用的所有事项,作者假定你已经熟悉计算机终端,并知道使用方法。如果下面的叙述使你迷惑不解,请询问身边的专家们。

UNIX 系统是全双工的:你在键盘上敲入的字符送至系统,然后系统回送给终端并在屏幕上显示出来。通常echo进程把字符直接复制到屏幕上,这样就可以看到输入的是什么。但是有的时侯,比如在键入密码时,echo关闭,字符就不会在屏幕上显示出来。

多数键盘字符是普通字符,没有什么特殊的含义。但有些字符是通知计算机怎样解释所键入的内容的。到目前为止,这些键中最重要的键是回车键(RETURN)。回车键说明一行输入的结束,系统做出反应把屏幕上的光标移到下一行的起始处。必须按下回车键后,系统才会对键入的字符做出解释。

回车键是一种控制字符,即不可见的字符,它控制着终端上某些特殊的输入输出。在任一台终端上,回车键都有自己的按键,但是其他大多数控制字符并非如此。相反,它们必须通过按住Ctrl键(有时称为CONTROL键、CNTL键或CTL键)的同时按下另一个键(通常是一个字母)来输入。例如,回车键可以通过按下回车键输入,或者按住CONTROL键同时按下m键。所以回车键又可称为CONTROL+m键,也可以写成Ctrl+m。其他的控制字符有:Ctrl+d,表示程序输入到此结束;Ctrl+g,终端上的振铃鸣响;Ctrl+h,通常称为(退格键)Backspace,它可以用于纠正错误的输入;还有Ctrl+i,通常称为Tab,它使光标往前跳一个Tab间隔,这一点非常类似于普通的打字机。在UNIX系统中,Tab间隔为8个空格字符。在多数终端上Backspace和Tab都有各自的键。

另外两个键也具有特殊的含义:一个是Delete,有时又称为Rubout或某些其他缩写;另一个是Break,有时又称为Interrupt。在多数UNIX系统中,按Delete键可以立即中止程序,而不等待程序完成。在某些系统中,Ctrl+c提供这项功能。而在有些系统中,根据终端的连接方式,Break与Delete或Ctrl+c的功能一样。

1.1.2 与UNIX会话

让我们从与UNIX系统的会话开始。在本书的例子中,你键入的内容用等宽斜体字符表示,计算机回应的内容用等宽正体字符表示,说明性的文字用楷体表示。

建立连接:必要时拨号或打开电源。

系统应该显示:

上述内容就是一段会话的全部,当然,有时人们也做一些其他的工作。本节的下面部分将讨论这段会话,顺便分析一些其他有用的程序段。

1.1.3 登录

登录必须有登录名和口令,这些可以从系统管理员处得到。UNIX系统可以适用于多种类型的终端,但它更倾向于有小写字体的设备,因为 UNIX 区分大小写!如果你的终端只有大写字符(类似某些可视终端和便携式终端),那么使用起来会很困难,应该另外换一台终端。

要确认设备的开关设置为大小写、全双工,以及身边专家们建议的其他设置,诸如传输速率,即波特率。要为终端建立连接,这也许要拨通电话,或者只是拨一下开关。不论哪种情形,系统都会出现:

如果出现乱码,则可能是速率不对,请检查速率和其他设置。如果仍不成功,按几次 Break键或Interrupt键。如果仍不出现登录消息,那就只好请人帮忙了。

出现 login:登录提示后,用小写字母键入登录名,然后按回车键。如果要求密码,系统会有提示,而且屏幕不会显示所输入的密码。

登录一旦成功,系统便会显示一个提示符,通常是一个字符,表示系统已经准备好接收用户的命令。提示符多半是美元符号($)或百分比符号(%),但是你可以将其改为任意的符号,这一点以后再讨论。提示符实际是由一个名为命令解释器或称为shell的程序显示出来的,它是系统对用户的主界面。

在提示符之前可能会有日期消息,或者有关于电子邮件的通知。有时系统会询问你正在使用的终端类型,这有助于系统利用终端所具有的特性。

1.1.4 键入命令

一旦有了提示符,就可以键入命令了,命令就是请系统完成某项工作的要求。我们使用“程序”这一词作为命令的代名词。当看到提示符(假设是$)时,键入date并按下回车键。系统应该回应日期和时间,然后显示下一个提示符,所以整个处理过程在终端上看起来如下所示:

不要忘了回车键,也不要键入$。如果系统没有反应,可以按回车键,应该会有响应。以后不再按回车键,但是在每一行的结尾都需要它。

下一个要尝试的命令是who,它表明当前有哪些人在登录上机:

首列是用户名。第二列是连接的终端的名称(tty即teletype,终端的一个学名)。其余信息是登录的日期和时间。读者还可试验:

如果键入出错,输入了一条不存在的命令,会被告知没有发现该命令:

当然,如果你不恰当地键入了一条实际存在的命令,它会运行,也许会出现奇怪的结果。

1.1.5 异常的终端行为

有时终端会出现一些奇怪的行为,例如,一个字符也许会显示两次,或者回车键可能不把光标置到下一行的首列。通常可以把终端关闭再开启,或者退出登录然后再次登录以消除这些现象。也可以阅读手册第1节中有关stty(set terminal options)命令的叙述。如果终端没有Tab键,要想巧妙地处理该键,可键入命令

接着系统会把tabs转换成正确数量的空格。如果终端的Tab间隔可以设置,命令tabs会正确地为用户设置跳格位置。这时,需要键入

这样即可使系统正确工作——请参阅手册中有关tabs命令的说明。

1.1.6 键入错误

如果出现键入错误,并且在按回车键前发现了它,可有两个方法纠正:一次删去一个字符或者删行后重新键入。

如果键入消行符,其默认值是@,会取消整行,就好像没有键入该行一样,从而可以开始新的一行:

字符#删除所键入的最末一个字符,每个#字符删去一个字符,一直到行的起始处(但不会超越它)。如果键入出错太多,可以进行如下纠正:

专用删字符和消行符是与系统密切相关的。在很多系统中(包括作者使用的系统),删字符都已改为 Backspace 键,这在可视终端上很成功。你可以键入如下内容迅速检查所使用的系统是哪一种情况。

(我们把Backspace键印刷为←,这样就可以看到它。)消行符的另一种常见形式是Ctrl+u。

在本节的其余部分,将使用字符#作为删字符,因为它较为清晰可见,如果系统不一致,请做好思想准备。在1.4.5节将介绍如何设置你所喜欢的删字符和消行符。

假如必须把删字符或者消行符作为文本的一部分输入,该怎么办呢?在#或@之前加入反斜杠(\),它们就失去了特有的含义。所以要输入#或@,只需键入\#或\@。系统会先把终端光标移在@之后的下一行,即使它前面有反斜杠。不过不用担心,该@符号已被记录下来了。

反斜杠有时被称为转义字符,它主要用于指明后续字符是特殊字符。要消除反斜杠,需要键入两个删除字符,即\##。知道这是为什么吗?

键入的字符在到达最终目的地之前,要经过一系列程序的检查和解释,至于如何解释它们,则完全取决于它们如何结束以及如何到达终点。键入的每一字符立即在终端显示,除非回应功能被关闭(而这是极少见的)。在按回车键之前,字符还未被内核保留,所以键入的错误可以用删字符或消行符纠正。当删字符或消行符前端有反斜杠时,内核会去掉反斜杠,并且不加解释地保留后续字符。

按下回车键之后,保留的字符被送到从终端读取字符的程序。程序也许会以特殊的方式解释字符,例如,假若字符的前面有反斜杠,shell就关闭特定的字符解释器。这在第3章会进一步探讨。总之应该认识到,内核处理删字符或消行符,而反斜杠只有在删字符或消行符之前,才会被内核处理;其他剩下的字符都可以被别的程序解释。

练习1-1 说明下面命令的结果:

练习1-2 多数shell(尽管第7版shell并非如此)把#解释为注释,并忽略从#开始至行尾的全部文字。按照这一点,说明下列的文本,假设删字符也是#。

1.1.7 继续键入

在键入的同时,内核读取所键入的内容,即使系统在忙于其他事务时也是如此,所以你可以用最快的速度键入想输入的内容,即使有命令正在显示也没有关系。如果此时系统正在输出,则输入字符会同输出字符混合在一起,但它们会另行存储并以正确的次序解释。你可以一条接一条地输入命令,而无需等待它们完成,甚至不需要等待它们开始。

1.1.8 中止程序

通过按Delete键,可以中止大多数命令。在某些终端上,Break键也起作用。当然这是与系统相关的。在少数程序中,如文本编辑器,Delete 键中止了程序的执行过程但仍停留在该程序中。关闭终端或挂断电话会中止大多数的程序。

如果只打算暂停输出,如在要避免某些关键信息从屏幕上消失时,可以键入Ctrl+s,输出会立即停下来,程序会被挂起直到再次启动。要继续输出,可键入Ctrl+q。

1.1.9 注销

注销的正确方法是按下Ctrl+d,而不用输入命令。这样就通知shell,输入中止了(至于它到底是如何起作用,下一章再作说明)。实际上,可关闭终端或挂断电话来注销,但这样做是否真正注销了,取决于不同的系统。

1.1.10 邮件

系统提供一套邮件系统用于与其他用户通信,所以有时在登录时,在第一个提示符之前会看到下列消息:

要阅读邮件,请键入

邮件消息会显示出来,一次一条消息,最新的消息首先出现。在每一项消息后面,邮件等待用户的行动指示。一般有两种基本响应方式:d,删除该条消息;回车键,不删除该条消息(消息会保留,下次可以阅读消息)。其他处理包括:p,显示消息;s filename(文件名),以你起的名称保存消息;q,从mail中退出。(假如不知道文件这个概念,简单地把它看成一个用选定的名字保存信息的地方,以备今后再使用。文件是 1.2 节的主题,而且本书很多部分都在讨论它。)

同已经介绍的程序相比,mail有各种不同的版本。请阅读手册以了解细节。

给他人发邮件很简单。假设要给登录名为nico的人发邮件。最方便的方法是这样的:

在此键入邮件文本,可键入任意长度的文本,输入完毕后按

Ctrl+d指明信件的结尾,通知mail没有输入了。如果在编辑信件过程中改变了主意,可以按Delete键,而不须使用Ctrl+d。写到一半的信件将存放在名为dead.letter的文件中而不是邮出。

作为练习,给自己发封邮件,然后键入mail读取它(它不像听起来那么奇怪——这是一个简便的提醒机制)。

发邮件还有其他一些方法——发出一封事先准备好的信件,或一次给许多人同时发信,也可以发信给使用其他机器的人们。有关细节请参阅《UNIX程序员手册》第1节中的有关mail的叙述。今后我们将用标记mail(1)来表示手册中第一节中有关mail的叙述。本章讨论的所有命令都可以在第1节中找到。

系统还有日历服务(参见calendar(1)),如果你没有使用过,我们会在第4 章介绍如何设置它。

1.1.11 用户间通信

如果你的UNIX系统是多用户的,某一天,屏幕上可能会出现类似如下的语句:

并伴随着一声蜂鸣声。Mary打算写信给你,但是除非采取明确的行动,否则不能写回信。若要回应,请键入

这样,就建立了一条双向通信途径。现在 Mary 在她的终端上键入的内容,会出现在你的终端上;反之也一样,尽管通信很慢,但毕竟不像是同月球通话。

如果你正在进行某项工作,那么必须进入可以键入命令的状态。通常,正在运行的程序必须中止,但是有些程序,诸如编辑器以及write命令本身,利用“!”命令可暂时切换到shell,请参阅附录A中的表A-2。

write命令没有规则,所以要有一个协议,以免你键入的内容同Mary键入的内容混到一起。通常的做法是,每一方以(o)结束,它表明“完毕”,打算退出的信号是(oo),其含义是“完毕并退出”。

要退出write也可以按下Delete键。注意,键入的错误不会在Mary的终端上出现。

如果要写信给某个未登录的人,或者收件人不想被打扰,系统会通知你。如果目标登录了,但是在一个间隔后没有回答,也许对方太忙或者不在终端旁,只要按 Ctrl+d 或 Delete键就可以退出。如果你不想被打扰,可利用mesg(1)。

1.1.12 新闻

许多 UNIX 系统提供了一种新的服务,可使用户随时了解一些有趣无趣的事件。试试键入

UNIX 系统通过电话拨号连接,形成了一个很大的网络,有关 netnews 和 USENET,请询问身边的专家。

1.1.13 手册

《UNIX程序员手册》描述了大量读者需要了解的关于系统的信息。第1节介绍命令,包括本章讨论的内容。第2节讨论系统调用,这是本书第7章的主题,而第6节是关于游戏的内容。其余章节讨论C程序员使用的函数、文件格式,以及系统维护(这些章节的编号随着系统的不同而变化)。在开始使用手册时,不要忘记使用各种形式的手册索引。可以通过快速地翻阅手册索引,了解相关命令的内容。还有关于UNIX系统运行原理的介绍。

通常手册以在线形式存放,这样可以在终端上阅读。如果你不知道做法,又找不到专家协助,可以在终端上用命令man显示有关页面。下面例子是读取who命令的说明:

而下面例子则说明了man命令。

1.1.14 计算机辅助教学

你的系统中也许有learn命令,这个命令提供了有关文件系统、基本命令、编辑器、文档准备,甚至包括C语言程序设计的计算机辅助教学。请试一下:

它会告诉你从哪儿开始,可以做什么。如果不行,可以再试一下teach。

1.1.15 游戏

游戏往往不被正式承认,但熟悉计算机和终端的最好方式之一,就是玩游戏。UNIX 系统提供了适量的游戏,通常在本地机上就有。问问周围的人,或者查看手册的第6节。

1.2 文件和常用命令

在UNIX系统中信息存储在文件中,它很像日常的办公室文件。每个文件有名字、内容、存放地点以及某些管理信息,诸如所有者以及文件大小等。文件可能是一封信,或者是人名及地址清单,或者是源程序,或者是供某个程序用的数据,甚至是程序的可执行形式以及其他的非文本类型材料。

UNIX 文件组织结构使你可以维护自己的文件而不会影响其他人的文件,并且也防止他人干涉你的文件。UNIX 系统有大量的程序可操作文件,但是现在,我们只介绍最频繁使用的那些。第2章是关于文件系统的具体讨论,其中介绍了许多与文件有关的其他命令。

1.2.1 创建文件

如果想录入一篇文章、一封信或一个程序,那么怎样把这些信息存放在机器中呢?这类任务大多是用文本编辑器完成的,它是一个在计算机中保存和操作信息的程序。在每个UNIX系统中几乎都有一个屏幕编辑器,它利用现代终端的特点,显示你对文件所进行的编辑效果。两个最流行的文本编辑器是vi和emacs。在这里我们不介绍任何屏幕编辑器,其原因部分是由于篇幅所限,部分是由于不存在一个标准的文本编辑器。

不过,有一个名为 ed 的编辑器肯定在你的系统中存在。它不需要特定的终端功能,因此可以在任何终端上工作。它也构成了其他基本程序的基础(包括一些屏幕编辑器),所以这个程序是非常值得学习的。附录A中有关于这个程序的简要说明。

无论我们偏好哪个编辑器,都得熟练掌握以便能够创建文件。这里我们以ed为例进行讨论,这也可保证你能在自己的系统中完成这些例子,当然你可以使用自己最喜好的编辑器。

要用ed建立一个文件junk,可按如下操作进行:

命令 a(append)通知 ed 开始接收文本。“.”指明文本结束,但必须出现在一行的开始处。不要忘掉这个命令,否则任何其他 ed 命令都不会被识别——所键入的一切都会被认为是所添加的文本。

编辑命令w(write)存储所键入的信息。w junk是把内容存放在名为junk的文件中。文件名可以任意起(之所以选择junk这个词,是因为该文件不重要)。

w命令之后,ed显示键入到文件中的字符数。在w命令之前,不会有永久性的存储,所以如果退出系统下班回家,有关的信息不会存储到文件中去(如果在编辑时退出系统,所有的数据会存放在名为ed.hup的文件中,这样下一次可以继续使用)。如果正在编辑时系统崩溃(比如由于软件或硬件的故障而突然停机),该文件只存放最后一次用写命令时所保存的内容。而在 w 之后,信息就永久地保存了。以后可以键入如下命令再访问该文件:

当然,你可以编辑所键入的文本,如纠正拼写错误,修改用词,重新排列段落等。当这些都完成之后,可用q(quit)命令退出编辑器。

1.2.2 列出文件

创建以下两个文件,junk和temp:

字符数从ed开始计数到每一行的结尾,结尾称为换行,系统用它来表示回车键。

ls命令列出文件的名称(并非内容):

这就是刚创建的那两个文件(当然也还会有其他人建立的其他文件)。文件名自动按字母顺序排列。

如同多数命令一样,ls有各种选项用以替代默认功能。在命令行中选项跟随着命令名,通常由“-”和一个代表其含义的字母构成。例如,ls -t 使文件按“时间”顺序排列,最后一次修改的文件,最先列出

-l给出了一个“长”的列表,为每个文件提供了更多信息:

total2 说明文件占据硬盘的数据块数量,一个数据块通常有 512 或 1024 个字符。字串-rw-r--r--说明读写权限。在本例中,所有者(你自己)可以读写,但其他人只能读。后面跟随的1是该文件的链接数量,将在第2章讨论。you是文件的所有者,即创建文件的人。19和22是对应文件的字符数,它们同从ed所得到的数字是一致的。而日期和时间则说明了文件是何时进行最后修改的。

选项可以组合在一起:ls –lt会得到同ls -l一样的数据,但是按照较新文件先列出的原则排序。-u选项说明文件何时被使用:ls –lut按照最新使用的次序,列出一个长(-l)的清单。-r 选项反转输出的顺序,所以 ls -rt 按最早使用的次序排列。也可以只列出感兴趣的文件名字,命令如下:

在命令行中命令后面的字符串,诸如上面例子中的-l和junk,称为程序参数。参数通常是命令所使用的文件名字或选项。

通常的惯例是用负号和单个字母指定选项,诸如-t,或字母组合-lt。一般而言,如果命令接收这类选项,它们应置于文件名参数之前,但可以按任意的次序出现。UNIX 程序在对多选项的处理上是不一致的。例如,标准第7版的ls不会接受

作为ls –lt的同义词,在别的程序中,多重选择项则要求被分开书写。

在学习更多例子之后,你会发现对于选项参数几乎不存在什么规划或系统性。每个命令都有其各自的语法规则,以及关于字母的各种含义(通常同一个功能在不同的命令中有不同的含义)。这种不可预见的行为使人不适应,而且常常被看作系统的缺陷而加以引证。尽管情况正在改善——新的版本通常有较好的一致性——我们仍旧建议,在编写程序时尽可能做得更好些,同时在手边保留一份手册。

1.2.3 显示文件

若现在有了一些文件,那么怎么查看其内容呢?有太多的程序可以完成这项工作。一种方式是使用编辑器:

ed先显示junk中的字符数目。命令1,$p通知印出文件中所有的行。在学会如何使用编辑器后,你可以选择要显示出的部分。

有时候使用编辑器显示并不太合适。举例来说,编辑器ed有处理文件大小的限制——仅限于数千行的文件。另外,它一次只能显示一个文件,而有时可能需要显示若干个文件,一个接一个,中间不间断,所以这里介绍几个其他办法。

首先是 cat,在所有显示命令中,它是最简单的。cat 显示其参数列出的所有文件的内容:

给定名字的文件在终端上被连接(catenate)[1](从而有了名称 cat)在一起,其一个接一个相连,没有间隙。

对短小的文件这么做没有问题,但是对于较长的文件,如果与计算机连接的速率较快,则必须用Ctrl+s迅速停止由cat导出的输出,否则它们就全部从屏幕上流逝了。尽管每个UNIX 系统几乎都有这样的命令,但并不存在一个“标准”的命令,可以在可视终端上一次一屏地显示一份文件。你所用的系统中也许有一个称为 pg 或者 more 的命令。我们所使用的是p,在第6章中将介绍它。

类似于cat,pr命令也可显示出文件名清单中文件的全部内容,但它是以一种在行式打印机上适用的模式显示:每页66行(11英寸),附有最后一次修改文件的日期和时间、页码、页眉上的文件名,以及跨越两页的附加行。这样,可以只显示 junk,然后跳到新的一页开始显示temp:

pr可以输出多列格式:

这表示以3列的形式显示每个文件。你可以使用其他合理的列数,pr会尽其可能完成。Pr -m可以按并行列的形式,显示若干文件。请参见pr(1)。

需要注意的是,pr 并不是可以重新排列行及调整边距的排版程序。真正的排版程序是nroff以及troff,将在第9章讨论。

还有可以在高速打印机上打印文件的命令。请在手册中观看名称为lp及lpr的命令,或者在手册索引中查找“printer”(打印机)。至于使用哪一个命令,取决于连接到系统的设备是什么。pr和lpr经常一起用,用pr对信息做合适的格式处理,再用lpr将其传送至行式打印机上。稍后还会讨论这些内容。

1.2.4 移动、复制和删除文件

现在来看看其他命令。首先是修改文件的名字。改名是通过把文件从一个名字“变动”到另一个名字来完成的,比如:

其作用是把名为junk的文件改名为precious,而文件内容并没有改变。如果再运行ls,会看见不同的一个清单,junk没有了,而precious出现了。

注意,如果把一个文件移往另一个已存在的文件,目标文件会被替换掉。

要复制一个文件副本(也就是,要某文件的两个版本),可以使用cp命令:

对precious制作一个名为precious.save的副本。

最后,如果你已厌倦了文件的创建和移动操作,rm命令可以清除列出的所有文件:

如果要删去的某个文件并不存在,会出现警告。除此以外,rm像多数UNIX命令一样安静地工作,没有提示符,也没有对话,出错信息很简洁,有时甚至没有用。太简单的表达对于初学者而言有些困难,而经验丰富的用户却认为对话多的命令让人厌烦。

1.2.5 文件名

到目前为止,我们并没有说明一个合法文件名应符合哪些要求,现在是说明这些规则的时侯了。首先,文件名长度不应超过14个字符。其次,尽管可以选择几乎所有的字符,但通常应选择那些明显的字符,并避免使用可能引起歧义的字符。例如,我们已看见在ls命令中,ls -t意味着按时间顺序。所以,如果有一个名为-t的文件,那么把它按名称列出来就很困难。(怎样做?)除了把负号作为首字符之外,还有一些字符也有特别的含义。在熟练掌握它们之前,为避免混乱,应该只使用字母、数字、点号和下划线(点号和下划线通常用于把文件名分成若干部分,如前例中的precious.save)。最后,不要忘了区分大小写——junk、Junk和JUNK是三个不同的名称。

1.2.6 有用的命令

我们有了创建文件、列出其名称并显示其内容的基本命令,现在可以学习几个文件处理的命令了。为使讨论更集中,将使用一个名为poem的文件,其内容是Argustus De Morgan的诗歌。先用ed创建该文件:

下面第一个命令用于计算文件的行数、词数和字符数。类似于计数命令 w,我们把这一命令称为wc(word-counting):

也就是说,poem有8行,46个字,263个字符。“字”的定义很简单:任何不含空格、Tab键和新行的字符串。

wc可以为多个文件计数(并显示全部结果),它也可以在需要时,略去计数功能。参见wc(1)。

第二个命令为 grep,它搜寻文件中的符合给定匹配模式的行(该名称来源于 ed 命令g/regular-expression/p,这在附录A中解释)。假如打算在poem中寻找单词“fleas”:

grep在使用-v选项时,会寻找所有不匹配给定模式的行(它来源于编辑器命令v,可以认为它是匹配的逆向使用)。

grep可以用于在多个文件中查询。此时,对每一查询的结果行,会加上文件名做为前缀,这样就知道匹配来源于哪个文件。另外,grep 还有计数、编号等功能。它还能处理比一个字复杂得多的模式匹配,我们将在第4章中讨论该问题。

第 3 个命令是 sort,它把输入内容按字母次序一行行的排序。对于诗句,这样做不是很有意义,但我们也进行一下处理,从而可以看看结果:

排序是逐行的,但默认排列顺序是空白行、大写字母、小写字母,所以它并不是严格地按字母排序的。

sort有很多控制排序方式的选项——逆序、数字序、字典序、略去前导空格序、按行内的字段排序等——不过通常只需要掌握部分选项,以下是最常用的选项。

第4章会进一步讨论sort。

另一个文件检查命令是tail,它印出文件的最后10行。所以这一命令对于上面的8行诗来说太多了,但它对于较大型的文件是有用的。另外,tail有指定行数的选项,这样可以印出poem的最后一行:

tail也可以用来指定从某行开始显示文件:

从第3行开始显示文件(请注意,作为参数的负号通常有逆向含义)。

最后一对命令用于比较文件。假设有一个poem的变种文件new_poem:

这两个文件没有多大的区别,实际上,要仔细观看才能找出差别。这时就要使用比较命令了。cmp找出两个文件第一个有区别的地方:

该结果说明,两文件不同之处在第2行。的确如此,但它没有说明其差别是什么,而且除了第一个差别之外,它就不再作其他的比较了。

另一个文件比较命令是diff,它报告所有被修改、增加或减少的行:

该结果说明,第一个文件(poem)的第2行已被改成了第2个文件(new_poem)的第2行,而第4行也存在不同之处。

一般而言,cmp用于判断两个文件是否确实内容相同。它工作快,适用于任何类型的文件,而不仅仅是文本文件。diff 用于已知道文件之间可能有所差别,进而想准确地知道哪些行有什么不同的情况,它只适用于文本文件。

1.2.7 文件系统命令小结

表1-1列出了是到目前为止已讨论的处理文件的命令。

1.3 目录

系统可以把你的文件(如名为junk的文件)同任何其他人的相同名称的文件区分开。这个区分是依靠把文件分组成目录(文件夹),而不是像图书馆中那样把书放在架上,所以在不同目录中的文件可有相同的名称而不至于出现矛盾。

通常,每个用户都有其个人目录或主目录,也有时称为登录目录,其中只包含了属于用户他或她的文件。当登录时,用户在自己的目录中。用户可以改变工作用的目录——通常称为用户的工作目录或当前目录——但用户的主目录始终不会变化。除非用户采取特别的行动,当建立一个新文件时,它会建在当前目录中。由于当前目录初始装状态下亦即用户自己的主目录,文件不会同其他人的相同名称文件相关。

目录中可以包含其他的目录,当然也可包含一般的文件(“好目录中有较少的目录……”),描述这一段结构组织的自然方式是目录和文件树。可以从树根开始沿着恰当的分支移动,从而找到系统中的任何文件。相反,也可以从任何一处开始,最终到达根部。

我们先从后者开始,所用的基本工具是pwd(print working directory),它显示出用户当前所在的目录的名称:

它说明用户当前在目录you中,you在usr目录中,而usr就在根目录中,根目录通常称为“/”。/字符分隔了名称的每个部分,前面所述的14个字符的限制也适用于名称的每个部分。在很多系统中,/usr是包含系统的普通用户所有目录的一个目录。(即使用户的主目录不是/usr/you,pwd也会印出类似的内容,所以用户能够理解下面的内容是什么。)

如果用户键入

应该得到如同使用ls所得到的完全一样的名单。在没有变量提供时,ls列出当前目录的内容;在给定目录的名称时,它就列出该目录的内容。

其次,试一试:

这应该列出一串名称,其中有读者自己的登录目录。

下一步是试验列出根目录本身。可以得到一个类似于下面的响应:

(不要混淆/的两个含义:它既是根目录的名称,又是文件名的分隔符。)它们多数是目录,但 unix 实际是一个含有 UNIX 内核的可执行形式的文件。有关它的更多内容在第 2章中讨论。

现在试一下:

(如果junk仍在读者的目录中。)名称:

是称为文件的路径名。路径名表征了从根通过目录树到特定文件的全部路径名。在UNIX系统中有一个通用规则,凡是可以使用普通文件名的地方,都可以使用路径名。

文件系统构造得像一个通用逻辑树,用一幅图像可能会更明晰一些:

从图1-1中可以看出,你的junk文件同Parl的或Mary的junk文件没有任何关系。

如果所关心的所有文件都在你自己的目录中,那么路径名并没有多少作用;但是如果你同其他人合作,或者同时有几个项目在进行,那么路径名就实在很有用了。例如,你的朋友可以通过如下操作显示你的junk文件:

类似地,你可以用如下操作找出Mary文件的内容:

或者对她的文件制作一份自己的副本:

或者编辑她的文件:

如果Mary不想让你随便操作她的文件,那么可以做出私用性设置。每个文件和目录都有对应所有者、组以及其他人的读-写-执行的允许权限,用以控制存取。(请回忆ls -l。)在作者的系统中,多数用户在多数时间里发现开放性还是比私用性更为有益,但是在你的系统中可能有不同的策略,有关内容会在第2章中讨论。

作为与路径名有关的最后一个实验,请试一下:

有什么名称看似熟悉吗?当在提示符后键入命令的名称,然后运行命令时,系统寻找该名称的文件。它通常先寻找当前目录(也许多数不会在此目录中),然后在/bin中寻找,最后是在/usr/bin中寻找。目录收集在一起是为了便于查询和管理。要验证这一点,通过使用它们的全路径名试验执行下面这些程序:

练习1-3 尝试:

按所要求的执行,可能会有比通常上班更为有趣的事发生。

改变目录

如果你经常与Mary一起处理她的主目录中的信息,你可以说“我打算在Mary的文件上而不是在自己文件上工作”。这是通过用cd命令改变当前目录而实现的:

现在,当你使用文件名(不带/)作为cat或pr的变量时,它会指向Mary目录中的文件。改变目录并不会影响文件有关的允许权限——如果不能从自己的目录中访问某一文件,改变至另一个目录并不能改变这一事实。

把同某一事物有关的文件都安排在一个目录中而与其他项目分隔开是会带来便利的。例如,如果某人打算写一本书,他或许会尝试把所有的文件都放在一个称为book的目录中。使用命令mkdir建立一个新的目录:

“..”表示当前所在目录的父目录,更接近根一层的目录。“.”是当前目录的同义词。

上述操作把你带回主目录,即登录目录。

一旦书出版了,就可以清除掉有关文件。要删除目录book,先删去其中的所有文件(我们会简单地展示),再用cd指向book的父目录,然后键入

rmdir只删去空目录。

1.4 shell

当系统印出提示符$,你键入命令并得到了执行时,此时并不是内核在与读者对话,而是与一个称为命令解释器或外壳shell的在对话。shell是同date或who一样的普通程序,尽管它可以处理一些不同寻常的事。shell存在于用户和内核机制之间的事实对用户是有帮助的,有些会在这里说明。下面是三个要点。

文件名简写:可以通过指定文件名的模式来选取一套文件名作为程序的变量——shell会找出匹配该模式的文件名。

输入输出重定向:可以把任何程序的输出送到一个文件中而不是终端上,并当作来自文件的而不是终端的输入。输入和输出甚至可以连接到其他程序上。

环境个性化:可以定义自己的命令和简写。

1.4.1 文件名简写

现在讨论文件名模式。假如你正在键入一个像书一样大的文档。逻辑上,这个文档会被分成许多小部分,类似于章、节。从物理上看,也应该被划分开,因为编辑一个大文件是很麻烦的,所以你应该把文本作为一批文件键入。你可以每章分为一个文件,称为 ch1、ch2等。或者每一章拆分成节,可以建成如下的文件:

这也是用于本书的结构。基于系统的命名惯例,你只要看一眼就可以说出某一特定文件在整个系统中的位置。

如果打算印出全本书,那么该怎么办呢?可以进行

但这样很快就令人厌烦,而且容易出现错误。这就是文件名简写的原因所在。如果说

shell认为*的含义是“任何字符串”,所以ch*是一个匹配当前目录中的所有以ch开头的文件的一个模式。shell会按字母顺序[2]建立清单,并把清单传递给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的内容进行排序,而

做类似的工作,但有所差别。因为字符串<temp由shell解释,sort并没有把文件名temp看成一个变量;它对它的标准输入进行了排序,shell进行了重定向,它的输入来自一个文件。然而后一个例子,把名称temp作为变量传递给了sort,由sort读入文件并对其进行排序。可以给sort指定一个文件名清单,如:

但是,如果没有指定文件名,它就对其标准输入进行排序。这是大多数命令的一个基本特性:如果没有指定文件名,就对标准输入进行处理。这里,你可以直接键入命令,观看它们如何工作。例如:

在下一节中,将讨论这一原理如何应用。

练习1-5 解释为什么如下命令:使ls.out被包括在文件名单中。

练习1-6 解释来自如下命令的输出:

如果拼错了命令名,比如:

会发生什么?

1.4.3 管道

上一节的所有例子中都依赖于相同的技巧:把一个程序的输出,通过一个临时文件送到另一个文件的输入,而该临时文件并没有其他的作用。实际上,使用这样一个文件是很笨拙的。这样一种观念导致了UNIX系统的基础性的贡献之一——管道的想法的诞生。管道是不使用临时文件而把一个程序的输出连接到另一个程序的输入的途径,管道线是通过管道对两个或两个以上的程序的连接。

下面通过管道而不是临时文件重新检查前面的一些例子。垂直线字符(¦)通知 shell建立一条管道线:

任何从终端读取的程序都可改由管道读取,任何写到终端上的程序都可以改为写向管道。当没有指定文件名时,任何读取标准输入的程序均可使用管道线。grep、pr、sort 以及wc都以上述方式使用管道线。

在管道线中可以有任意多个程序:

此行命令,在行式打印机上输出三列的文件名清单,而

统计Mary登录的次数。

在管道线上的程序实际是同时在运行,而不是一个接一个运行的。这意味着,在管道线中的程序彼此之间可以相互交互;内核处理有关调度和同步,以使它们全部运行。

当需用管道时,shell作出安排,显然各个程序要进行重定向。当然,在做这些工作时,程序必须反应敏捷。大多数命令遵循一个公共格式,所以它们多半能在任何位置上适用管道的要求。通常,命令按如下方式实施:

如果没有指定文件名,则命令读进标准输入,其默认设备是终端(便于进行实践),但输入可以被重定向为来自文件或者管道。同时,在输出一方,多数命令把输出写到标准设备,其默认设备也是终端,但它也可以重定向到文件或者管道。

命令的出错消息有不同的处理方式,然而,它们也许会在文件或管道中消失。因此每一命令都有一个标准出错输出,它通常被导向终端,如图1-2所示。

到现在为止,所讨论过的命令几乎都符合这一模式;唯一例外的是类似data和who这种命令,它们没有输入,还有一些如cmp及diff,它们有固定数量的文件输入。(请注意它们的“-”选项。)

练习1-7 解释如下两者的不同:

1.4.4 进程

shell除了设置管道之外,它还处理大量的工作。现在花点时间看看同时运行多于一个程序的基本情况,我们已经看到了有管道时的一些情况。例如,可以在一个命令行中用分号分隔两个命令,shell识别分号并把命令分成两个命令:

在shell返回提示符之前,两个命令都(顺序地)执行了。

如果你希望的话,也可以同时运行多于一个的程序。例如,如果需要做一些费时的工作,如计算书本中的字数,但在做其他事之前不想等待wc完成工作,那么可以这样做:

在命令行结尾的&通知shell“开始该命令的运行,然后立即从终端取下一个命令”,这也就是说,不等到其完成,就取下一个命令。这样一来,命令开始工作,当它运行时,用户就可以去做其他的事。把输出导向至wc.out,用户同时做其他的事时就不会受到它的打扰。

一个正在运行的程序的实例称为进程。由&初始化的命令,经由shell显示出的数字称为进程标识符;该进程标识符可以在其他命令中使用,用以引用某一个运行着的程序。

区分程序和进程的概念很重要,wc是一个程序;每次运行程序wc时,就创建了一个新进程。如果同一个程序同时有若干个实例在运行,那么每一个进程都是分隔的,且有一个不同的进程标识符。

如果管道线被&初始化了,如下所示:

其中的全部进程都会立即开始——&适用于整个管道线。然而只有一个进程ID会被显示,它对应于序列中的最后一个进程。

命令

要等到所有进程都被&初始化之后。如果它不立即返回,命令仍然会在运行。可用 Delete 中断wait。

可以用shell印出的进程ID来停止被&初始化的进程:

如果忘了进程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对它做了定义,就可以在每一命令之后得到是否有新邮件到达的通知[3]

(你的系统中,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处理的文件中,从而创建一条新命令。这种方法简单却能产生显著的效果。有关讨论将在第3章中讨论。

1.5 UNIX系统的其余部分

UNIX 系统中的内容远远多于本章中所讨论的内容,也远比本书的内容多。现在,你应可放心地使用系统,特别是要尽量使用手册。当对如何使用命令有疑问时,应查询手册。

经常浏览手册是有必要的,这样可以深入了解已知的命令并认识新命令,包括类似FORTRAN77之类语言编译器,诸如bc(1)中计算器程序,用于机器间通信的cu(1)及uucp(1),图像包,统计软件,以及深奥的unit(1)等。

正如前所述,本书不是要替代手册,只是手册的一种补充。在本章之后,我们会讨论UNIX系统中的内容及程序,首先从手册中的信息开始,然后是系统的各部分及程序。尽管在手册中,程序间关系不明显,但它们构成了UNIX程序设计环境的综合基础。

相关历史和文献

由D.M.Ritchie及K.L.Thompson合著的最早关于UNIX的文章“The UNIX Time-Sharing System”,1974年7月首次发表在Communication of the ACM上,1983年1月CACM再次刊登该文章。(该重印本的第89页印在1983年3月刊上。)这篇为对操作系统感兴趣的人写的综述文献值得所有编程人员阅读。

Bell System Technical Journal(BSTJ)的有关UNIX系统专集(1978年7月),其中有许多讨论后续发展的文章以及一些回顾文章,包括 Ritchie 及 Thompson 对他们发表在 CACM上的文章的一些修改。BSTJ的第2本专集出版于1984年,其中包含新的UNIX论文。

作者为 B.W.Kernighan 及 J.R.Mashey 的“The UNIX Programming Environment”(IEEEComputer Magazine,1981年4月),意图向程序员们介绍该系统的基本特征。

《UNIX程序员手册》不论哪一版都是实用的文献,该文献列出了命令、系统子程序和接口、文件格式以及维护工作。没有手册是不行的。尽管在开始编程之前也许只需要阅读第 1卷中的部分内容就行。手册第7版的第1卷由Holt、Rinehart及Winston出版。

《UNIX程序员手册》第2卷名为“Documents for Use with the UNIX Time-sharing System”,包含有关主要命令的教材及参考手册,特别是它介绍了文档准备程序以及程序开发工具,用户有必要阅读它。

Ann和Nico Lomuto的A UNIX Primer(Prentice-Hall,1983)是为初学者,特别是为非程序员,写的一本入门书。

[1].catenate是“concatenate”的同义词。

[2].顺序也并不是严格按字母序的,其中大写字母先于小写字母。对于在sort中所使用字符的顺序,请参考ascii(7)。

[3].这在shell中的实现是很糟的。在每一个命令之后,可看到有关文件明显地给系统加负荷。而且,如果用户长期在一个编辑器上工作,是不会知道有新邮件到达的,因为用户没有在所登录的shell上运行新的命令。一个较好的设计是每隔数分钟检查一次,而不是在每一个命令之后。在第5章和第7章中介绍了怎样实现这类邮件检查器。第三种可能的方式,是有个自己的邮件通知程序,但这并不对每个人都合适,这个程序当然知晓读者的邮件是何时到达的。

第5章 shell程序设计

尽管大多数用户认为 shell 是一个交互式的命令解释器,但实际上它是一种程序设计语言,它的每一条语句运行一条可执行的命令。shell要同时满足交互执行命令和编程运行命令两种使用方式的要求,它是一种特殊的语言,造成这种局面,既有历史的原因,也有设计方面的影响。它的应用程序范围广泛,而且从语言的观点看目前还有许多争议,但不了解这些争议的细节并不影响用户高效地使用 shell 进行程序设计。本章的目的是通过逐步开发一些有用的 shell 程序,说明 shell 编程的基本原理。本章不是一部 shell的参考手册,阅读本章时应该在手边准备好《UNIX程序员手册》中关于sh(1)的部分,以便随时参考。

和其他很多命令一样,shell程序的使用细节通常可以通过实践很快地掌握。shell手册常常不易于理解,而一个好的例子有时是将问题解释清楚的最好方法。鉴于这一原因,本章围绕着程序实例而不是围绕着shell的特性来组织,是shell编程指南,而不是shell所有功能的罗列。这里不仅讨论shell能做什么,而且结合强调交互式程序测试的思想,讨论shell程序的开发和编写。

当用shell或者其他语言编写程序时,如果恰有其他的人也需要使用这个程序,将有助于程序的进一步改进和完善。因为他人对使用程序的要求往往要比程序编写者本人更苛刻,因此,shell编程的一个重要原则在于提高程序的健壮性,要能处理不正确的输入,即使程序运行错误,也要能给出有用的提示信息。

5.1 定制cal命令

shell程序的一个通常用途是增强或改善应用程序的用户界面。作为一个增强程序界面的例子,考虑cal(1)命令:

这里的缺点是月份必须使用数字输入。而且,当输入 cal 10 时,将打印 10 年的年历,而不是打印当年10月份的月历。因此,要打印一个月的月历,必须同时输入相应的年份。

重要的是,不论cal命令提供的是什么接口,都可以只改变用户接口而不需要改动cal程序本身。可以把命令放在自己的bin目录里,将一个更易于使用的参数语法转化为实际的cal命令要求的参数格式;甚至还可以调用自己的cal版本,这样就更加直接一些。

在编制新的cal命令时,首先要考虑的问题是:cal应该做些什么?我们的基本设想是要求cal能够更合理地工作。cal应该能通过名字识别月份,当有两个参数时,除了将月份名转换为相应数字以外,其他功能和原来的cal命令完全一样。当仅给定一个参数时,cal应打印当年相应月份的月历,而当不提供参数时,它只打印当月的月历。这些正是cal命令的最常用的情况。综上所述,要解决的主要问题是:确定有多少个参数,如何将这些参数转换为标准cal要求的格式。

shell提供的case语句正好适用于进行上述判断:

case语句将单词(word)和模式(pattern)从头至尾进行比较,当遇到第一个匹配的模式时,执行与该模式相应的命令(command),而且仅仅执行这一命令。模式均按照shell模式匹配规则书写,对文件名匹配规则稍微作了一些推广。每项匹配所对应的命令均以双分号(;;)结尾。(最后一个匹配项的命令之后可以不用;;,但为了编辑的方便,通常还是保留它。)

我们的cal版本能确定出现在命令行中参数的个数,处理以字母表示的月份名,然后再调用前述真正的cal。shell变量$#保存了调用shell文件时的参数的个数,其他特殊shell变量列在表5-1中。

第一个 case 条件检查参数个数$#,并选择对应的操作。第一个 case 最后的模式*表示匹配所有的情况,即当参数个数即非0又非1时,执行最后一种情况。(因为各个模式是顺序扫描,所以与所有情形匹配的模式一定是最后扫描的。)m和y变量分别作为月份和年份-当给定两个参数时,此时,我们的这个cal命令与原始的cal命令执行相同的操作。

第一个case语句有两行都包括了这样一条语句:

虽然字面上这一语句的功能不太明显,但是通过下面几条命令很容易看出这一语句的作用。

set是shell的一个内部命令,它能够处理相当多的事情。当没有参数时,set给出环境变量值,正如第3章中所提到的。set还能实现重置基本参数如$1、$2等的功能。set `date`把$1重置为星期几,$2重置为月份,等等。因此,在cal程序的第一个条件语句case中,当没有参数时,按照当前日期设置月份和年份。只有一个参数时,将该参数作为月份,而年份从当前日期中取得。

set 还可以识别多个选项,使用最多的是-v 和-x,设置了这些选项后,每条命令在由shell处理执行时,会返回所运行的命令。这对于调试复杂的shell程序是必不可少的。

剩下的问题是月份的转换,即当月份是文字形式时,要将它转换成数字形式。第二个case语句正是完成这一工作的。无需多说,这段程序的功能是一目了然的。这里唯一不易理解的是case 语句中的¦符号,它与egrep 中的¦符号相同,表示选择,如big¦small 表示或与big 匹配,或与small 匹配。当然这一条件也可以写成[jJ]an*等其他形式,程序能接受全部小写的月份名或者以大写字母开头的月份名,前者是因为UNIX系统主要接收小写,而后者是因为date命令打印的月份的第一个字母就是大写。shell模式匹配的规则列在表5-2内。

在第二个case语句中的最后两种情形用于处理单个的参数,它可能是年份,但第一个case 语句会将它当成月份;如果它是数字,可能用来表示月份,则作为月份处理,否则,作为年份处理。

最后一行使用转换后的参数,调用/usr/bin/cal(真正的cal命令)。新版本的cal程序可以接受以下输入:

若键入cal 1984,将打印出1984年全年日历。

这个改进了的 cal 程序完成与原来的 cal 程序相同的工作,但是实际使用起来却更简单,更容易记忆,在名字选取上也尽量采用简单的方式,即用cal,而不用calendar(它已经是另一个命令)或任何其他不利于记忆的名称,如 ncal。不改变命令的名字还有一个优点,即用户不会因为新名字而受影响。

在结束关于 case语句的讨论之前,还需说明一点:为什么 shell程序的模式匹配规则不同于ed程序及其衍生程序中的匹配规则。毕竟采用两种不同的模式意味着要学习两组规则,要求采用两种代码来处理这两组模式。有些区别源自最初的错误选择,后来也从未改正过——例如,匹配任意符号的模式在ed里使用“.”,在shell里使用“?”,仅仅是为了保持对过去的兼容性,除此之外没有任何其他原因;另一方面,有时不同的模式完成不同的工作。在 ed 中的正则表达式可以搜索行内任何位置出现的字符串,特殊符号^和$的作用是将搜索定位到行首和行尾。然而在默认情况下,对文件名,搜索定位一般为默认,这种情况下,如果用命令

来代替命令

将是非常麻烦的。

练习5-1 如果其他用户希望使用你的 cal 版本,如何使它被所有用户共享?把它放在/usr/bin目录中需要执行哪些操作?

练习5-2 有必要使cal程序在cal 83时打印1983年的年历吗?如果有必要的话,你怎样实现打印1983年的年历。

练习5-3 修改cal使之接受多个月份,例如:

或者一个连续范围的月份

假如现在是12月份,而运行cal Jan,应该得到的是今年的1月份还是明年的1月份?应如何考虑这个问题?

5.2 which

建立自己的命令版本,如cal命令的新版本,会带来一些其他的问题。最明显的例子是,如果Mary一起工作,并且以mary登录,则此时的cal还是标准的版本,除非Mary把新的cal命令连接到她的bin目录里。你可能会非常疑惑——原先的cal命令给出的错误信息不足以使人弄清发生错误的原因。但是这只是这类问题的一个例子。因为shell通过PATH指定的一组目录搜索命令,得到的可能不是所期望的版本。例如,键入一条命令:echo,而实际运行的文件全路径名可能是./echo、/bin/echo、/usr/bin/echo或是别的什么目录,这取决于搜索命令的目录PATH以及文件存在的具体位置。如果存在一个名字相同,但行为完全不同的命令出现在比预期早的路径中,就可能把人弄糊涂。最常见的情况是 test命令,关于这一命令我们在以后还要讨论到,因为大家往往喜欢用test来命名程序的临时版本,但通常会执行错误的test程序[1]。在这种情况下,一个能报告即将执行的程序版本的命令,能为用户提供很有用的服务。

一种实现方法是对 PATH 里的目录进行循环搜索,找出给定名字的每一个执行文件。在第3章里我们用for语句实现对文件名和参数的循环,这里我们给出所要求的循环结构:

因为我们可以在反括号`…`里运行任何命令,所以显而易见的解决方法是在$PATH上运行sed程序,将冒号转换为空格符。我们仍用echo程序做这一试验工作:

显然还存在一个问题。PATH中的空符号串与“.”意义相同。所以,将PATH里的分号转换成空格符不是个好办法-这将丢失空符号串元素。为了生成一个正确的目录列表,我们必须把 PATH 的空符号串元素转换为点字符(即.)。空字符串部分可以位于字符串中间或字符串任意一端,因而要做少许工作以处理所有情况,即:

我们可以把它写成4个分开的sed命令,不过由于sed的4个命令是按顺序执行的,所以实际上调用一次sed即可。

一旦我们有了PATH中的目录元素,前面提到的test(1)命令就可以告诉我们文件是否存在于各个目录里。test命令实际上是一个比较笨拙的UNIX程序。例如,test –r file测试file是否存在并且可读;test -w file命令测试文件是否存在而且可写,但第7版没有提供test –x(尽管System V和其他版本提供了这一功能),否则我们就可以使用它了。我们将使用test –f,这一命令测试文件是否存在,并且不是一个目录,而是一个普通文件。应该查看系统上关于test命令的手册,因为有好几个版本都在使用。

每条命令返回一个“退出状态”,即返回一个值给 shell,指出执行的情况。退出状态是一个小整数,通常0表示“真”(命令运行成功),非0表示“假”(命令运行不成功)。注意,这个值和C语言中的真假正好相反。

由于很多不同的值可以表示“假”,所以不同的失败原因经常用不同的“假”退出状态值来表示,例如,grep程序在存在匹配时返回0,没有匹配时返回1,模式或文件名有错时返回2。每个程序返回一个状态,一般情况下我们对这个值不感兴趣,但对test程序却不一样,它的唯一目的是要返回一个状态值。除此外,test不产生任何输出,也不修改文件。

Shell将上一个程序的退出状态存放在变量$?中:

有几个命令,如cmp和grep,都有选择项-s,使它们仅以适当的状态返回,但不产生任何输出。

shell的if语句按照命令的退出状态执行选择,例如:

其中换行是很重要的,fi、then和else仅在换行符或分号之后才能被识别。else部分是可选的。

if语句至少要运行一条命令——条件中的命令,而case语句直接在shell中做模式匹配。在某些UNIX版本,包括System V中,test是shell的内部函数,所以if和test能和case运行得一样快。如果test不是内部函数,case语句就比if语句更有效,在模式匹配时应该使用case语句来执行,即:

这一语句要比下面的语句运行得快:

这就是为什么有时我们使用shell中的case语句进行条件测试,尽管在大多数编程语言使用if语句就可以了;但另一方面,case语句不能方便地判断文件是否可以读;这时最好使用test命令和if语句。

现在我们开始编写which命令的第一个版本,它将报告与给定命令相匹配的文件:

让我们试验一下这个命令:

开始的case语句进行错误检查。注意echo后的重定向语句1>&2,表示不把错误信息送往管道。shell内部命令exit用于返回状态。当命令不工作时,exit 2用于返回错误状态;当没有找到文件时返回 exit1,当找到文件时返回 exit0。如果程序中这一位置没有给出exit语句,则退出状态就是shell文件最后一个命令执行的状态。

假如在当前目录中有一个test程序,那么会发生什么情况呢?(假定test不是shell内部命令。)

需要做更多的错误检查。可以运行which(假设在当前目录里没有test程序!)去寻找test 文件的全路径,并打印它。但这还是不能令人满意:因为在不同的系统中,test可能位于不同的目录里,而且which要依靠sed和echo,所以还要指定它们的路径名。一个比较简单易行的办法是:在shell文件里固定PATH,使得在执行which命令时,仅搜索/bin和/usr/bin目录。当然,对于which命令,必须保存以前的PATH中的目录搜索顺序。

这样 which 可以工作了,即使在搜索途径中存在一个假的 test(或假的 sed 和假的echo)。

shell提供了另外两个操作符用于组合命令,这就是¦¦ 和&&,它们的使用形式比if语句更加简练方便。例如,¦¦ 能够代替一些if语句:

等同于:

尽管形式上相似,但操作符¦¦ 并不是管道,而是表示“或”的条件操作符。执行¦¦ 左边的命令后,若退出状态为0(成功),则忽略¦¦ 右边的命令;若退出状态为非0(失败),执行右边的命令,并且整个表达式的值是右边命令退出时的状态值。换言之,¦¦ 是“或”条件操作符,左边的命令执行成功时不执行右边的命令。对应的&&条件操作符表示“与”,仅当左边的命令成功才执行右边的命令。

练习5-4 为什么which文件不在退出前把路径PATH恢复成opath?

练习5-5 shell使用esac终止case,使用fi终止if,为什么使用done终止do呢?

练习5-6 把选择项-a加到which命令中,使它打印在PATH里的所有文件,而不是在打印第一个文件之后便退出。提示:match=’exit 0’

练习5-7 修改which,使之可以识别shell的内部函数,如exit。

练习5-8 修改which,使之能检查文件的执行权限。当不能找到文件时,改变which使之打印错误信息。

5.3 while和until循环:观察情况

在第3章中,for循环用于一些简单的迭代程序。一般地,for对一组文件名进行循环,如在for i in *.c中;或对shell程序的所有参数进行循环,如for i in $*。但实际上,shell中的for循环可以有更大的用途,如在which里的for循环。

shell有3种循环:for、while和until。其中for是最常用的一种循环,对于循环变量集中的每一个元素,它执行一次循环体内的一组命令。最常用的循环变量是文件名。while循环和until循环利用命令的退出状态来控制循环体内命令的执行。循环体一直执行,直至作为条件的命令返回状态为非0(对于while循环)或0(对于until循环)。while循环和until循环除了对命令的退出状态进行不同的解释之外,其他完全相同。下面是各种循环的基本格式:

在第二种形式的for循环中,空的循环参数表示$*,这是常用的缩写形式。

控制while或until的条件命令可以是任何命令。例如,下面是一个观察某人(比如Mary)登录情况的循环:

其中sleep命令用于挂起60秒,它总能正确执行(除非发生中断),因此总返回“成功”,这样,这个循环每分钟检查一次,查看Mary是否已登录。

这一版本有个缺点,如果Mary已经登录,也必须等60秒后才能知道Mary登录的信息。还有,一旦Mary已经登录到系统中,你就会每60秒接收到一个报告。可以将这个程序修改一下,使用until循环来编写,此时,如果Mary已登录到系统,你不需延迟就可以立即得到Mary的登录信息:

这是一个更有意思的条件。如果Mary已经登录,who¦grep mary就打印出who列表中mary这一行,并返回“真”值,因为grep返回的状态表示它是否找到了匹配的内容,管道命令的退出状态是最后一个元素的退出状态。

最后,我们可以将这条命令包装起来,给它取一个名字,并安装到系统中:

可以将grep改成egrep,这样可以输入

来同时观察多个人的登录情况。

作为一个更复杂的例子,我们考虑一个功能增强的 who 命令:它不仅能观察所有人的登录情况,并能及时报告用户登录或退出系统的情况。它的基本结构很简单:每分钟运行一次 who 命令,并与一分钟前的输出情况相比较,如果有不同,则报告发生的改变。who命令的输出将保存在一个文件里,我们将这个文件保存在目录/tmp中。为了区分我们的文件和其他进程的文件,可以将shell变量$$(shell命令的进程标识码)放在文件名里,这是一种常用的方法。将命令的名字编码到临时文件名中是系统管理员常用的手段之一。各种命令(包括watchwho命令)常常把文件放在/tmp目录中,最好了解是哪个命令在这么做。

“:”是shell的内部命令,它仅仅评估它的参数,然后返回“真”。在这里,我们也可以用true命令代替它。true命令仅仅返回一个“真”的退出状态。(还有一个false命令。)但是,“:”不需要执行文件系统中的命令,所以它比true命令效率更高一些。

diff命令的输出用<和>区分来自两个文件中的不同数据;awk程序处理diff输出,并以更容易理解的格式显示出来。注意,整个 while 循环的结果通过管道送入 awk,而不是每分钟刷新一次awk。sed不适用于这种处理情况,因为它的输出总是比输入落后一行,即总有一行的输入正在处理,没有打印,这样会导致不必要的延迟。

old生成时是一个空文件,所以watchwho第一次输出的内容是当前所有登录的用户清单。如果用命令who>$old建立old的初值,那么watchwho将只打印用户登录的变化情况,这取决于习惯的不同。

另一个循环程序可以定时地查看信箱,当信箱的内容变化时,程序打印“you have mail”。对shell的内部机制来说,使用MAIL变量是很有用的方法,我们的实现使用了shell变量替代文件,来解释这种差别。

这里再次用到了awk程序,确保只有mailbox增长时才打印消息,而不是变化时,否则,当你删除邮件信息时也会收到消息。(shell的内部版本就受到这一缺点的限制。)

时间间隔仍然设为60秒。但在命令行提供了一个参数,如果键入下面的命令:

就使用新的时间参数。如果提供了时间参数,shell变量t被设为对应的时间间隔,如果提供时间参数,默认的时间间隔为60秒,由下面这一行确定:

这是shell的另一个特点。

${var}等价于$var,它可以用来避免变量名与字母或数字组成的字符串相混杂所引起的问题:

大括号内的某些特殊字符可以指定对变量进行特殊处理。如果该变量没有定义,并且在变量名之后带有一个问号,则打印问号后面的字符串,然后退出 shell(除非是交互情况)。如果没有提供问号后的信息,则打印标准的输出信息:

注意,shell产生的信息中总是包括未定义变量的名字。

另一种形式是变量${var-thing}。当var有定义时,其值为$var;当var没有定义时,其值为thing。${var=thing}的情形与其相仿,但会把$var的值置为thing:

变量取值规则在表5-3中列出。

回到我们原来的例子:

把t设为$1,如果没有提供参数,t设为60。

练习5-9 查看目录/bin或/usr/bin中命令true和false的实现过程。(怎样才能找出它们?)

练习5-10 修改watchfor使之能支持多个参数,查看多个用户的信息,而不需要用户输入’joe¦mary’。

练习5-11 写一个watchwho的版本,用comm代替 awk,比较新旧的数据。你喜欢哪个版本?

练习5-12 写一个watchwho的版本,用shell变量存放who的输出,而不用文件存放,你喜欢哪一个版本?哪个版本运行得更快?watchwho及checkmail自动做&吗?

练习5-13 shell的两种情况:不做任何工作的命令和注解字符#有何不同?两者都需要吗?

5.4 trap:捕获中断

如果在运行watchwho时突然按下Delete键或挂断电话,在目录/tmp中,将保存一个或两个临时文件。Watchwho应该在退出之前清除这些暂存文件。我们需要一定的手段来检测各种中断事件,并进行恢复处理。

按Delete键时,一个中断信号会送给终端上正在运行的所有进程;同样地,当挂断电话时,会传送一个挂断信号。其他信号发生的情形亦同。除非程序有专门处理中断信号措施,否则,中断信号将一律终止程序的运行。如果是中断信号,后台运行的进程(使用&运行)能得到保护,但如果是挂断信号,则得不到保护。

第7章将详细讨论各种中断信号,但你无需知道太多细节就可以在shell中处理中断信号。shell的内部命令trap能生成中断信号发生时所要执行的命令序列:

命令序列是一个单独的参数,一般要用引号括起来。信号值是小整数,用于标识中断信号。例如,2表示Delete键产生的信号,1表示电话挂断信号。shell程序使用的大多数通用信号值列在表5-4中。

为了清除watchwho中的暂存文件,程序要在循环之前加上调用trap的语句,以捕获挂断信号、中断信号和终止信号:

trap的第一个参数是一个命令序列,这个序列类似中断信号发生时立即调用的子程序。命令结束后,程序返回到断点继续执行,除非中断信号终止了它。因此,trap命令序列必须显式地调用exit,否则在中断之后将继续执行 shell程序。实际上 trap中的命令序列将被读取两次,一次是建立 trap 时,一次是 trap 被调用时。因此,命令序列最好用单引号来保护,这样变量仅在 trap 程序执行时才被赋值。在这个例子里没有关系,但在有的情况下,这种区别非常重要,后面我们将看到一个例子。另外,选择项-f表示rm不进行询问。

有时,trap程序在交互上很有用,最常用的情况是保护程序不会由于电话断线引起的挂断信号异常终止:

空的命令序列意味着在进程及其子进程中“不响应中断”。括号的作用是使trap和命令一起在一个后台子 shell 中运行;如果没有括号,trap 将同时作用到登录的 shell 和long-runing-command中的命令上。

命令nohup(1)是一个很短的shell程序,它能提供这种服务。下面是在第7版中nohup的实现:

test -t测试标准输出是否为终端,以决定输出是否需要保存。带nice运行的后台进程的优先级比交互程序的优先级低。(注意:nohup 没有设置 PATH。是否应该设置?)

使用exec主要是为了提高效率,没有exec,程序的运行也不会有问题。exec是一个shell 命令,它用给定程序代替运行 shell 的进程,因此节省了一个 shell 进程,而一般这个shell需要等待程序运行结束。我们还可以在其他一些地方使用exce,例如,在增强的cal程序结尾处调用/usr/bin/cal时可以使用exec。

附带提一下,信号9是一个不能捕获也不能忽略,而且必须执行的信号;它总是要终止程序,在shell环境下,可用如下方法发出信号9:

kill -9的选项不是默认设置,因为按这样的方式来终止进程,进程在结束之前没有任何机会处理善后工作。

练习5-14 上述 nohup 版本把命令的标准错误输出和标准输出结合在一起,这样设计好吗?如果不好,怎样将它们分开呢?

练习5-15 查阅shell内置函数times,在你的.profile文件里增加一行命令;当退出系统时打印出所使用的CPU时间。

练习5-16 写一个程序,使之能在/etc/passwd文件里下寻找一个可用的用户标识符。如果有兴趣(并有访问权力),将这个程序改写成一个命令,在系统里添加新用户。完成上述工作需要什么访问权限?应该如何处理中断?

5.5 overwrite:改写文件

sort排序命令有一个选顶-o,表示覆盖文件:

它等同于下面这个命令:

如果 filel 和 file2 是同一个文件,重定向符号>在排序之前就把输入文件截断。然而,加上-o选项的命令将能正常工作,因为在输出文件建立前,sort先将输入排序并存放在一个临时文件中。

很多其他命令也可用-o选择。例如,sed可以编辑文件如下:

要对所有这样的命令都加上选项-o显然是不现实的。另外,这种做法也不可取:最好是将功能集中起来处理,就像shell使用运算符>那样。我们给出一个程序overwrite完成这项工作。第一种设计如下:

基本的实现思路直截了当——保存全部输入直到文件尾,然后把数据复制到参数文件:

这里使用cp命令,而不使用mv命令,从而当输出文件已存在时,访问权限和所属关系保持不变。

尽管这个版本有个诱人的优点:简单,但是它有一个致命的缺点:如果用户在 cp 过程中按下Delete键,原来的输入文件将被破坏。必须防止对输入文件的覆盖被任何中断终止:

如果到达原文件尾前发生了一个Delete中断,临时文件将被删除而原文件保留。备份完成后信号将被忽略,因此前一个cp不会被中断——一旦cp开始,overwrite将全力进行原文件的修改工作。

还有一个小问题。考虑下面的程序:

如果为 overwrite 提供输入的程序出错,它的输出将会被清零,overwrite 也会尽职尽责地破坏参数文件。

这一问题有几种解决办法。overwrite 可以在重写文件前请求确认,但是增加交互功能会大大削弱 overwrite 的优越性。overwrite 可以检查其输入是否为空(利用 test-z),但这种编程风格很糟糕,而且也不正确:一些输出数据会在探测到错误之前生成。

最好的解决方案是由overwrite运行并控制数据生成程序,以便检查它的退出状态。尽管这与传统和直觉相左——在一条管道中,overwrite通常放在最后,但是为了正确工作,这里它必须最先运行。所幸的是,overwrite不产生标准输出,所以这种处理没有丧失一般性。它的语法也并不陌生:time,nice和nohup都是以其他命令作为参数的命令。

下面是一个安全的版本:

shell的内部命令shift将整个参数表向左移动一个位置:$2成为$1,$3成为$2,依次类推。“$@”提供所有参数(移动后的),与$*类似,但不对参数作解释。在5.7节还要讨论这一问题。

注意,为运行用户命令而对 PATH 进行了重置;否则,overwrite 将不能访问那些不在/bin和/usr/bin目录下的命令。

现在overwrite可以工作了(尽管有些笨拙):

sed命令经常用于词的替换。利用现有的overwrite,很容易写出一个shell程序实现替换功能:

(回忆一下,当for语句循环表为空时,默认等于$*。)使用@代替/表示定界符,因为@与输入字符串冲突的可能性更小。

replace把PATH设为/bin:/usr/bin,而不包括$HOME/bin。这表明overwrite必须在/usr/bin 目录运行下才能使 replace 工作。做这一假定是为了处理简化;如果不能在/usr/bin里安装overwrite,就必须在replace里将$HOME/bin放入PATH,或者明确地写出overwrite的路径名。以后,我们假定所有命令都被有意地放在/usr/bin目录下。

练习5-17 为什么overwrite在trap中不用信号码0,从而在退出时删除文件?提示:试着在运行下列程序时按Delete键:

练习5-18 为replace命令加上选择项-v,用于在/dev/tty上打印所有变动的行。提示:s/$left/$right/g$vflag。

练习5-19 修改replace,使它可以工作于拥有任意字符的替换串。

练习5-20 replace能否把程序中出现的所有变量i改为index?应该怎样修改?

练习5-21 把replace程序放在/us/bin目录是否方便和完全满足使用需要?需要时简单地键入正确的sed命令是否更加可取?为什么?

练习5-22(难) 下一命令不能正常工作。解释原因并修改。

提示:参阅sh(1)中的eval。修改后程序对命令中元字符的解释产生了什么影响?

5.6 zap:使用名字终止进程

kill命令只能通过指定进程号来终止进程。要终止某个后台进程时,一般要运行ps命令以得到进程标识号,然后再把它作为kill的参数输入。通过一个命令程序打印一个参数,再把这个参数手工输入到另一个命令中,这个方法似乎有些笨拙。为什么不写一个程序,如zap,自动完成这些工作呢?

原因之一是终止进程是个危险的操作,执行时必须小心谨慎。一个保险的办法是交互地运行zap,用pick命令选择要终止的进程。

先简要回顾一下 pick 的功能:pick 顺次打印它的每个参数,并请求用户响应,只有用户响应为y时才打印参数。(pick是下一节讨论的主题。)zap使用pick来确认那些通过名字选择的进程确实为用户所要终止的进程。

注意经反斜杠转义的嵌套的反引号。awk程序从pick对ps输出的选择结果中选取进程标识号:

问题是,ps输出的是一些分隔开来的单词,pick将它们视作分离的参数处理,而不是一次处理一行。shell的标准处理是以空格为分割符分裂字符串的,如:

在该程序里,我们要控制shell对字符串的分裂,只用换行符分割两个相邻的“词”。

shell 变量 IFS(内部字段分隔符)是一个字符串,串中是那些用于在参数列表和 for语句中分割词的字符。通常,IFS包括空格符、制表符和换行符,我们也可以把它改成任何有用的形式,如只有一个换行:

把IFS设置为换行符,zap就能很好地工作:

这里我们又使用了两个 shell 编程技巧:首先是通过一个可选参数来指定信号(注意如果不给出参数值,则 SIG无定义,并作为 null字符串处理);其次是用 egrep代替 grep,以便接受更复杂的模式,如’lsleep¦date’。echo 语句用于打印 ps 输出的标题栏。

你也许会想到,为什么这个命令称为zap而不直接称为kill?主要原因是:与cal不同,我们实际上没有提供一个新的kill命令,zap需要交互地操作,此其一;我们还需要保留原来真正的 kill 程序,此其二。同时 zap 的执行速度较慢,所以附加程序的开销可想而知,其中 ps(它是必须运行的)是开销最大的。我们将在下一章提供一个更有效的实现方法。

练习5-23 修改zap,使它由管道打印ps标题,而不必理会ps输出格式的改变。这将在多大程度上使程序复杂化?

5.7 pick命令:空格和参数

我们已经接触了书写shell的pick命令需要的多数命令。我们只需要一种新的机制来接收用户输入。shell内部命令read提供了这一功能,即从标准输入读一行正文,并把读到的文本(不含换行)赋给命名变量:

read 最常用于注册时在.profile 文件里设置环境,主要是建立 shell 环境变量,如TERM。

read 只能读取标准输入,而且不能被重定向。shell 内部命令(与控制流原语不同,如for)都不能使用>或<重定向:

这也许可以说是shell的一个缺陷,但这就是现实。幸运的是,重定向read外围的循环可以达到同样的目的。这是pick命令实现的关键:

echo -n抑制最后一个换行符,因此用户的响应可以打印在提示符的同一行。当然,提示符打印在/dev/tty上,因为这里的标准输出绝大多数情形下并不是终端。

break语句取自C语言,表示终止内部循环。上例中,输入q键时终止for循环。q为quit的缩写,易用,这一退出响应几乎已成为惯例,其他很多程序也采用q。

pick命令行参数里的空格是很有趣的:

要观察pick程序如何读取参数,运行pick并在每次提示符出现后按回车键。程序工作情况很好:for i适当地处理参数。我们还可以用其他方法写循环:

这种方式不能成功,因为循环中的操作数被重复扫描,这样第一个参数中的空格符将它变成两个参数。尝试使用引号括住参数$*:

还是不能正常工作,因为“$*”是由空格符分隔的所有参数连在一起构成的单个词。

可能的解决办法是,shell对字符串“$@”作特殊处理,并将它转换为严格的shell文件参数:

如果不用引号括住$@,它与$*相同;$@只有用双引号括住时才具有特殊性质。我们在overwrite程序中用它来保护用户的命令参数。

上述内容可以归纳为如下规则。

$*和$@扩展为参数,并被重复扫描;参数的空格符将字符串分成多个参数。

“$*”表示shell文件的所有参数及其空格符连在一起作为单个词处理。

“$@”与shell文件接收的参数等价,参数中的空格被忽略,其结果是等同于原来参数的一个单词列表。

如果pick程序没有参数,可以读取标准输入,所以可以使用命令

来代替

这里我们不研究这个版本的 pick:它的复杂度和难以理解的程度远远超过下一章将要介绍的用C语言编写的程序。

下面练习中的前两个题目比较难,属于高级shell程序员教程的内容。

练习5-24 写一个pick程序,当命令行中没有参数支持时它从标准输入读取参数。程序应能正确处理空格符。能否处理q响应?如果不能,试做下一个练习。

练习5-25 虽然shell的内部命令如read和sed不能重定向,但shell本身可以暂时重定向。阅读sh(1)节中对exec命令的描述,考虑如果不调用子shell如何将read定向到/dev/tty。(可以先参阅第7章。)

练习5-26(比较容易) 在.profile文件里使用read初始化TERM和其他相关参数,如Tab停止位。

5.8 news命令:社团服务信息

我们在第1章提到您的系统可以有一个news命令,用以报告用户社团方面的信息。大部分系统都提供新闻服务,尽管命令名和命令细节有所不同。这里给出一个news命令,不是要代替原有的本地命令,而旨在说明在shell里编写这样一个程序是多么地容易。比较两个news命令的实现会是件有趣的事情。

程序利用了一个基本事实,即各个新闻项目是分开存放的,每个文件包含一个新闻项目,存放在特定的目录下,譬如/usr/news。news(我们自己的news程序)将/usr/news目录里的文件和你的注册目录(.news_time)中下一个作为时间戳的文件进行比较。为便于调试,我们用“.”代替新闻文件和.news_time 两者的目录;在使用时,可以方便地修改为/usr/news目录。

touch的功能是把参数文件的最后修改时间改为当前时间,而不改动文件本身。为调试方便,我们只打印新闻文件的名字,而不打印文件内容。循环遇到.news_time便终止,因此所列文件都是比.news_time新的文件。注意,case语句中的符号*可以与/匹配,而/是不会出现在文件名模式中的。

如果.news_time不存在,会出现什么情况?

毫无响应,说明程序有错,这一错误的发生是因为如果 ls 命令找不到文件,它会在打印现有文件有关信息之前,在标准输出上给出错误信息。不可否认这是一个缺陷——诊断信息应该打印在标准错误输出上——不过我们可以识别循环中的问题,通过重定向标准错误输出到标准输出使所有版本同样地工作,从而避开这个问题。(这一问题在更新版本中已经得到适当处理,但是我们仍然将它保留下来,作为一个如何应付小麻烦的示例。)

我们必须将IFS设置为换行符,以免下面的信息被分为3个单独的词:

news 必须打印新闻文件,而不是仅仅打印文件名。发送者和发送时间往往是有用的信息,因此可以使用set命令和ls -1命令在前面打印标题:

这是一个 shell 中程序和数据互交换的例子。set 不接收参数是因为它的参数-rwxrwxrwx以减号开始,与选项相混淆。一个简易可行的修改方法是在参数前加上一个字符作为前缀:

这是一个可行的格式,它在给出文件名的同时给出了作者和日期信息。

下面是news命令的最后版本:

标题中附加的换行符将新闻项逐条分行打印。第一个IFS的值只有一个换行符,因此第一个ls命令产生的信息not found(如果有的话)被作为单个参数处理。下一个赋值将IFS置为空格符,所以第二个ls的输出被分成若干个参数。

练习5-27 在 news 中增加选项 -n(notify),用于报告新闻项而不打印,并且不改变.news_time。可以把这个新的news放在.profile文件中。

练习5-28 把上一练习的news的设计和实现与系统中类似命令作个比较。

5.9 get和put:追踪文件变动

这一节是本章的最后一节,我们将介绍一个更大、更复杂的例子,以此说明shell如何与awk和sed进行合作。

程序在开发过程中不断地改正错误和增加新功能。版本追踪往往是不可少的。特别是有的用户把程序移植到其他的机器上使用——他们常常跑回来问“在我们的版本之后又有哪些改动”,或者“这个那个错误是怎么改的”。保留备份使尝试新思想也变得更加安全稳妥:如果新的程序不成功,利用备份很容易回到原来的程序。

保存所有版本是一种备份方法。然而这样做不易组织,同时占用磁盘空间太多。我们可以利用一个可能性,即相邻两版本间通常大部分是重复不变的,从而只要保存一次即可。用diff -e命令生成一个ed命令列表,可以把老版本转换成新版本:

这样,通过维护一个完整的版本,以及把这个版本转换为任何其他版本的一组编辑命令,就可以把文件的所有版本都保留在一个(不同的)文件中。

这种方法有两种组织方式:一种是保留最新的完整版本和转换到老版本的编辑命令,另一种是保留最老的版本和转换到新版本的编辑命令。虽然后一种方式比较容易实现,但是当有很多版本时,前一种方式使用较快,因为我们一般总是对当前版本感兴趣。

我们选择第一种组织方式。在一个我们称之为“历史文件”的文件中,保存当前版本和多组编辑命令,每组编辑命令把某一版本转换为前一版本。各组编辑命令以下一行信息开始:

个人 日期 摘要

摘要为单行,由个人提供,它描述文件的改动。

有两个维护版本的命令:get和put。get命令从历史文件里获得版本,put命令在得到一个单行的变动摘要后,把一个新的版本放入历史文件中。

在介绍具体实现之前,我们先举例说明 get 和 put 如何工作,以及历史文件是怎样维护的:

其中“编辑命令”是由2d行命令组成的,表示删除文件的第2行,这样就可以将新版本转换为老版本。

编辑命令从上到下编辑历史文件,用来产生所要求的版本:第一组编辑命令把最新的版本转换为次新的版本,下一组命令把次新的版本换成第三新的版本,依此类推。因此,实际上每调用一次ed命令就把文件向老版本推进了一个版本。

如果被修改的文件包含行首为@@@的行,会引起一些麻烦,另外 diff(1)命令的 BUGS段将对只含一个句号的行给出警告。我们选择@@@作为编辑命令的标题头记号,因为标准正文一般不会用到这种序列。

讨论 get 和 put 命令的使用说明和命令的各种不同形式需要很多篇幅,所以我们只给出它们最后的形式。put命令比较简单,其程序为:

读完一行摘要之后,put调用get从历史文件中提取前一个版本。get的-o选项表示给出一个不同的输出文件名。如果 get 找不到历史文件,它返回一个错误状态,而 put 将创建一个新的历史文件。如果历史文件已经存在,then 子句以临时文件的形式创建一个新的历史文件,文件创建依次利用下列内容:最新版本,@@@行,新版本转换为次新版本的编辑命令,以及老的编辑命令和@@@行。最后,用overwrite把临时文件复制到历史文件。

get程序比put程序更复杂,主要是因为有一些选项:

这些选项很普通。-i和-o指定输入和输出文件名。-[0-9]选择特定的版本:0表示最新版本(默认值),-1表示次新版本,依次类推。循环while语句,用test和shift实现对全部参数的遍历。循环不用for语句,因为某些选择项(-i、-o)要使用其他参数,所以要用shift把对应参数移出,而位于for循环语句内部的shift语句不能与for很好的合作。ed的“-”选项关闭了在读写文件时通常使用的符号计数功能。

程序

等同于

(这是我们在put中使用的形式)但是更紧凑,对于熟悉操作符¦¦ 的程序员来说也更清楚。{和}之间的命令表示在当前shell下执行,而不是在子shell下执行;这样就保证了exit是从get退出,而不从子shell退出。字符{和}很像do和done——它们只在分号、换行或命令终止符之后才具有特殊作用。

最后,我们介绍get程序的工作代码。首先,sed把历史文件分成两部分:最新版本和编辑命令集合。然后由 awk 程序处理编辑命令。最后对@@@行进行计数(但不打印),同时传递编辑命令,直到计数值超过所期望的版本数(回忆一下,awk命令的默认动作是打印输入行)。两个ed命令$d和w附加在历史文件中的命令后:$d删除sed留在当前版本里的@@@行,而w命令完成文件的最后写入。这里不需要overwrite,因为get程序只改变文件的版本,而不改变先前的历史文件。

练习5-29 编写一个命令version,完成下述两件工作:

报告摘要,修改日期和修改人,对历史文件中选定版本进行修改。

报告9月20日的版本号。这被典型地用于下面命令中(version可以方便地回送历史文件名):

练习5-30 修改get和put,使之可以操作其他目录下的历史文件,不至于因为.H文件把工作目录弄乱。

练习5-31 一旦工作完成,不再需要保留所有的文件版本。怎样实现从历史文件中删除一些版本呢?

5.10 后记

当需要编写一个新程序时,自然立刻会想到如何用你最喜欢的语言来编写这个程序。对我们来说,最常用的语言是shell。

shell是一种很好的编程语言,虽然它的语法有些特殊。shell属于高级语言,它的操作对象为整个程序。由于shell是交互式语言,所以shell程序能够交互式地开发,可以逐级求精直至它能够令人满意地工作。如果是一个面向更多的用户,可以对shell程序进一步改造,使之更精巧和更实用,以满足广泛使用的需要。不能用shell程序高效地解决问题的情况微乎其微。如果遇到这种例外情况,在手中拥有可靠的设计和和工作实现的情况下可以用C语言编写全部或部分程序。(我们在下一章将遵循这种方法。)

UNIX 环境下编程有其特有原则——将工作建筑在已有成果之上,而不是白手起家;从小做起,逐步发展;使用各种工具验证新的想法。

我们在本章列举了很多例子,用现有的程序和shell很容易实现它们。有时,只需要重新设置参数就可以工作,如cal程序。有时,shell在一组文件名或一个命令执行序列上执行循环,如watchfor程序和checkmail程序。对于比较复杂的例子,用shell编程比用C语言编程要简洁得多,例如,我们完全可以用20行的shell程序news版本代替350行的C语言程序[sic]版本。

有了一个可编程序的命令语言和许多实用的程序还不够。重要的是所有这些组件能够协同工作!它们采用相同的信息表示和通信格式。每一个组件都出色地完成自己的份内工作,shell则根据你的意愿将它们简单高效地连接起来。这种合作能力正是UNIX程序设计环境如此富有成效的关键所在。

相关历史和文献

get和put来源于Source Code Control System(SCCS),这最早是由Marc Rochkind提出来的(“The source code control system”,IEEE Trans.on Software Engineering,1975)。SCCS是一个比我们上面这些简单的程序更强大和灵活得多的系统,它的目标是在工业环境中维护大型程序。然而,SCCS的基础是同一个diff程序。

[1].后面将会介绍如何在经常使用 test 的shell 文件中避免这一问题。

相关图书

Linux常用命令自学手册
Linux常用命令自学手册
庖丁解牛Linux操作系统分析
庖丁解牛Linux操作系统分析
Linux后端开发工程实践
Linux后端开发工程实践
轻松学Linux:从Manjaro到Arch Linux
轻松学Linux:从Manjaro到Arch Linux
Linux高性能网络详解:从DPDK、RDMA到XDP
Linux高性能网络详解:从DPDK、RDMA到XDP
跟老韩学Linux架构(基础篇)
跟老韩学Linux架构(基础篇)

相关文章

相关课程