Linux 因附带大量的工具和集成这些工具的优秀方法而著名。Peter Seebach 将讨论新工具是如何开发的,以及如何把一个一次性的程序转变为您将在未来几年中使用的实用程序。 Linux 和其他类 UNIX 系统总是附带了大量的工具,它们执行从显而易见的到不可思议的广泛功能。类 UNIX 编程环境的成功很大程度上归功于工具的高品质和选择,以及这些工具之间相互衔接的简易性。 作为开发人员,您可能会发现现有实用程序并不总是能够解决问题。虽然能够通过结合使用现有实用程序来容易地解决许多问题,然而解决其他问题却至少需要一些实际的编程工作。这些后面的任务通常是创建新实用程序的候选任务,结合现有实用程序来创建新实用程序可以通过做最少的工作来解决问题。本文考察优秀实用程序所具有的品质,以及设计这种实用程序所经历的过程。 优秀的实用程序具有哪些品质? 实用程序应该允许您根据手边的材料廉价而容易地构建一次性的应用程序。许多人认为实用程序就像是工具箱中的工具。设计实用程序的目标不是为了让单个工具来做所有事情,而是为了拥有一组工具,其中每个工具都尽可能好地做一件事情。 有些实用程序自身就是相当有用的,而其他实用程序则必须与一系列实用程序配合使用。前者的例子包括
设计实用程序 下面是一些实用程序设计目标;每个目标将在下面单独的小节中介绍。
做好一件事情 设想一下,如果大多数程序都具有排序功能,但是有些仅支持按词法排序,而其他一些仅支持按数字排序,另外一些甚至支持关键字选择而不是对整行排序,那将是一件多么令人沮丧的事情。起码,这也是恼人的。 当您发现某个问题需要解决时,应尝试将问题分解为多个部分,不要重复那些其他实用程序中已经存在的部分。您对允许配合现有工具使用的工具关注得越多,您的实用程序就越有可能保持有用。 也许您需要编写多个程序。完成专门任务的最佳途径通常是编写一两个实用程序,再用一些线索将它们联系起来,而不是编写单个程序来解决整件事情。使用 20 行的 shell 脚本来将新的实用程序与现有工具结合起来是很理想的。如果尝试一次解决整个问题,随之而来的第一个变更就可能要求您全盘重新考虑。 我偶尔需要从数据库生成两列或三列的输出。编写一个程序在单个列中生成输出,然后结合使用一个对输出进行分列的程序,这样通常会更有效率。组合这两个实用程序的 shell 脚本本身是临时性的,单独的实用程序比这个脚本的使用寿命更长。 有些实用程序服务于非常专一的需要。针对一个包含大量内容的目录,如果
清单 1 中的脚本确切地就做一件事情。它不接受任何选项,因为它不需要选项;它仅关心行的长度。归功于 Perl 便利的 成为一个过滤器 记住实用程序需要在命令行和脚本中运行。有时,理想的行为会稍有不同。例如,大多数版本的 实用程序喜欢生活在管道中。管道允许实用程序专注于自己的工作,而不是去关注旁枝末节。为了生活在管道中,实用程序需要从标准输入读取数据,然后向标准输出写出数据。如果您希望处理记录,那么您最好能够使每一行成为一个“记录”。诸如 我偶尔使用这样一个实用程序,它针对一个文件树反复调用其他程序。这充分利用了标准的 UNIX 实用程序过滤器模型,但是该模型仅适用于读取输入然后写出输出的实用程序;不能将它用于就地操作或接受输入输出文件名的实用程序。 可以使用标准输入来运行的大多数程序也完全可以针对单个文件或一组文件运行。注意,可以证明这样违背了反对重复工作的规则;显而易见,这可以通过将 有些程序可能合法地读取一种格式的记录,但是却产生完全不同的输出。这样的一个例子就是将输入材料划分为列的实用程序。这样一个实用程序可能将输入中的行视为记录,但是却在输出中的每行上产生多个记录。 并非每个实用程序都完全符合这个模型。例如, 通用化 对功能进行通用化有时会导致您发现:某个看起来似乎像单个实用程序的程序,实际上却是配合起来使用的两个实用程序。这很好。编写两个设计良好的实用程序可能要比编写一个丑陋的或复杂的实用程序更容易。 做好一件事情并不意味着 仅仅 做一件事情。它意味着处理一致但有用的问题空间。许多人都使用 这条规则,以及做好一件事情的规则,都是一个根本原理的必然结果:无论何时都要尽可能避免代码重复。如果您编写半打程序,其中每个都对行排序,您最终可能必须六次修复六个类似的 bug,而不是去使用一个得到更好维护的 这是编写实用程序的一部分,即把大多数工作添加到完成该实用程序的过程中。您也许没有时间在最初就完全通用化一个实用程序,但是当您一直使用该实用程序就会获得相应的回报。 有时,向某个程序添加相关功能是很有用的,即使这个功能并不是用来完成完全相同的任务。例如,当运行在终端设备上时,对原始二进制数据进行完美打印的程序可能更为有用,因为它使终端进入原始模式。这样使得测试涉及键盘映射、新键盘等的问题变得容易多了。不确定为什么当您按 delete 键时却得到代字号(~)吗? 这是弄清实际发送了什么内容的容易途径。这并不是完全相同的任务,但它足够类似,因而可能成为一个附加特性。 清单 2 中的 健壮 应该尽量确保弄清楚您的实用程序可能要操作哪些数据。不要简单地忽略无法处理的数据的可能性。应该检查这种情况并诊断您的实用程序。错误消息越明确,您对用户就越有帮助。尽量给用户提供足够的信息,以便让他们知道发生了什么情况以及如何解决。当处理数据文件时,尽量准确识别出不良的数据。当尝试解析数字时,不要简单地放弃;应该告诉用户您得到了什么数据,而且如果可能的话,还应该告诉用户该数据位于输入流中的哪一行上。 作为一个很好的例子,请考虑 安全漏洞经常植根于在意料之外的数据面前表现得不够健壮的程序中。务必记住,优秀的实用程序能够设法在 shell 脚本中作为根(root)用户身份运行。诸如 程序对意料之外的数据处理得越好,它就更可能适应变化的环境。通常,设法使程序更健壮会导致您更好地理解该程序的作用,从而更好地使之通用化。 新颖 如果您即将开始编写一个实用程序,请花一点时间浏览一下各种系统,以确定那样的实用程序是否已经存在。不要害怕在 BSD 上借用 Linux 实用程序,或在 Linux 上借用 BSD 实用程序;实用程序代码的乐趣之一在于,几乎所有实用程序都具有很好的可移植性。 不要忘了考察一下组合现有应用程序来形成一个实用程序的可能性。从理论上讲,组合现有程序来形成的实用程序运行得不足够快是可能的,但是编写一个新的实用程序很少会比等待一个稍慢的管道更快。 一个例子实用程序 这个程序仅做一件事情。它以近乎完美的输出格式输出 /usr/include/sys/errno.h 中的 errno 行。例如:
这个程序通用化了吗?是的,非常理想。它同时支持数字和符号名称。另一方面,它不知道关于可能具有相同格式的其他文件的信息,比如 /usr/include/sys/signal.h。可以容易地扩展它来做到这点,但是对于这样一个便利的实用能够程序,简单地创建一个名为“signal”的拷贝来读取 signal.h,同时使用“SIG*”作为模式来匹配名称,这样会更容易。 虽然这仅比对系统头文件使用 另一个例子可能是取消对输入排序的程序(请参阅 参考资料 以获得指向此实用程序的链接)。这相当简单;也就是读入输入文件,以某种方式存储它们,然后生成一个随机顺序来输出那些行。这是一个几乎具有无限应用前景的实用程序。编写这个实用程序也比编写排序程序容易得多;例如,您不需要指定您没有对哪些键排序,或者是您希望按字母顺序、词法顺序还是按数字顺序随机排序。棘手的部分在于读入可能非常长的行。事实上,上面提供的版本在搞欺骗;它假设所读入的行中没有空字节。纠正这个问题要困难多了,我在编写它时懒得去理会它。 结束语 不要在您第一次需要某个实用程序的时候设计它。要等到您具有一些经验之后才着手设计。请随意地编写一两个原型;优秀的实用程序比糟糕的实用程序更能证明所花的时间和研究工作的价值。如果原先设想的出色实用程序最终却在您编写它之后成为无用之物,不要感到遗憾。如果您发现自己对新程序的缺点感到沮丧,您只需再执行另外一个原型化阶段。如果结果证明它是无用的,不奇怪,有时会发生这样的事情。 您要寻求的是这样一个程序,它查找您的最初使用模式之外的通用应用。我编写 优秀的实用程序能够为您在所有不很理想的作品上所花的时间带来回报。要做的下一件事情是使它对其他人可用,以便他们能够试验它。也要使您失败的尝试对其他人可用,也许其他人对某个实用程序具有您所不需要的用途。更重要的是,您的失败的实用程序也许是其他某个人的原型,从而给每个人带来一个美妙的实用程序。
|
