本文共 3138 字,大约阅读时间需要 10 分钟。
本节书摘来自异步社区《UNIX编程环境》一书中的第5章,第5.1节,作者:【美】Brian W. Kernighan , Rob Pike著,更多章节内容可以访问云栖社区“异步社区”公众号查看
尽管大多数用户认为shell是一个交互式的命令解释器,但实际上它是一种程序设计语言,它的每一条语句运行一条可执行的命令。shell要同时满足交互执行命令和编程运行命令两种使用方式的要求,它是一种特殊的语言,造成这种局面,既有历史的原因,也有设计方面的影响。它的应用程序范围广泛,而且从语言的观点看目前还有许多争议,但不了解这些争议的细节并不影响用户高效地使用shell进行程序设计。本章的目的是通过逐步开发一些有用的shell程序,说明shell编程的基本原理。本章不是一部shell的参考手册,阅读本章时应该在手边准备好《UNIX程序员手册》中关于sh(1)的部分,以便随时参考。
和其他很多命令一样,shell程序的使用细节通常可以通过实践很快地掌握。shell手册常常不易于理解,而一个好的例子有时是将问题解释清楚的最好方法。鉴于这一原因,本章围绕着程序实例而不是围绕着shell的特性来组织,是shell编程指南,而不是shell所有功能的罗列。这里不仅讨论shell能做什么,而且结合强调交互式程序测试的思想,讨论shell程序的开发和编写。
当用shell或者其他语言编写程序时,如果恰有其他的人也需要使用这个程序,将有助于程序的进一步改进和完善。因为他人对使用程序的要求往往要比程序编写者本人更苛刻,因此,shell编程的一个重要原则在于提高程序的健壮性,要能处理不正确的输入,即使程序运行错误,也要能给出有用的提示信息。
UNIX编程环境
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中。
表5-1 shell内部变量
第一个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月份?应如何考虑这个问题?
转载地址:http://zltkx.baihongyu.com/