Linux程序设计(第4版)
上QQ阅读APP看书,第一时间看更新

2.8 综合应用

至此,你已学习完作为程序设计语言的shell的主要功能。是时候运用你所学的知识来编写一个实际的示例程序了。

贯穿全书,你将编写一个CD数据库应用程序,从而更好地掌握所学的知识。首先从shell脚本开始,但很快你就会用C语言重写该程序,并给它加上数据库等新功能。

2.8.1 需求

假设你收集了大量的CD唱片,现在为了方便管理,你将设计和实现一个管理CD唱片的程序。在学习Linux程序设计的过程中,实现这样一个电子CD唱片目录看起来是一个比较理想的项目。

开始阶段,你至少应该能够做到把每张CD唱片的基本资料保存起来,如唱片的名称、音乐类型、艺术家或作曲家的名字等。你可能还想再保存一些简单的曲目信息。你希望能够以每张CD唱片为单位进行搜索,而不是以曲目资料为单位。为了让这个小小的应用程序比较完整,你还希望能够在这个应用程序中对唱片资料进行输入、更新和删除。

2.8.2 设计

3项需求(对数据进行更新、检索和显示)应该采用一个简单的菜单就足够了。由于所有需要存储的数据全部都是文本,而且假设你收集的CD唱片不是很多,因此你就没有必要使用一个复杂的数据库,使用一些简单的文本文件即可。将资料保存在文本文件中将使应用程序比较简单,而且如果你的需求发生了变化,操纵文本文件总是要比操纵其他类型的文件更加容易。在万不得已的情况下,你甚至还可以使用一个编辑器来手工输入和删除数据,而不必非要通过编写程序来完成。

你需要为数据存储作出一个重要的设计决策:一个文件够用吗?如果够用,它应该采用什么样的格式?除曲目信息以外,你想要保存的大部分资料在每张CD唱片上只出现一次(我们暂不考虑某些CD唱片包含多位作曲家或艺术家作品的情况),而且几乎所有CD唱片都有多个曲目。

你需要对可以存储在CD唱片上的曲目数量加一个限制吗?这看起来是一个非常随意和没有必要的限制,所以还是立刻放弃这个想法吧!

如果对曲目数量没有限制,你就有以下3种选择。

❑ 只使用一个文件,用一行来保存“标题”信息,再用n行保存该CD唱片上的曲目信息。

❑ 将每张CD唱片的所有信息都放置在一行上,允许该行一直延续直到没有曲目信息需要保存为止。

❑ 把标题信息和曲目信息分开,用不同的文件来分别保存它们。

只有第三种做法能够让你灵活地修改文件的格式,如果今后你想把数据库转换为关系数据库格式的话(将在第7章详细介绍),你就需要修改文件格式,因此我们选择第三种方法。

下一个决策是要在文件里放入哪些信息。

我们决定对每张CD唱片保存以下信息:

❑ CD唱片的目录编号;

❑ 标题;

❑ 曲目类型(古典、摇滚、流行、爵士等);

❑ 作曲家或艺术家。

对曲目,我们只保存两条信息:

❑ 曲目编号;

❑ 曲名。

为了把这两个文件结合起来,你必须把曲目信息和CD唱片上的其他信息关联起来。为此,你需要使用CD唱片的目录编号。因为它对每张CD唱片都是唯一的,所以它在标题文件中只出现一次,在曲目文件中对每首曲目也只出现一次。

让我们来看一个示例标题文件,如表2-24所示。

表2-24

它所对应的曲目文件,如表2-25所示。

表2-25

这两个文件通过目录编号结合在一起。请记住,标题文件中的一个数据项一般都对应曲目文件中的多行数据。

你需要决定的最后一件事情是如何分隔数据项。在关系数据库里,长度固定的数据字段比较常见,但它并非总是最方便的。另一种常见方法是使用逗号,这个例子就选择了这个方法(即用逗号分隔变量,或CSV文件)。

在接下来的“实验”部分,为了不至于让你迷失方向,我们把将要用到的函数列在下面:

实验CD唱片应用程序

(1)和以前一样,这个示例脚本程序的第一行用于确保自己可以作为一个shell脚本程序来执行,接下来是一些版权信息:

(2)首先要做的事情就是,确保设置好脚本程序将要用到的一些全局变量,包括标题文件、曲目文件和一个临时文件。我们还设置Ctrl+C组合键的中断处理,以确保在用户中断脚本程序时删除临时文件:

(3)现在开始定义函数。因为脚本程序是从文件的第一行开始执行,所以这样做可以确保在调用任何一个函数之前都能够找到它的定义。为了避免在几个地方反复编写同样的代码,最开始的两个函数是简单的工具型函数:

(4)接下来是主菜单函数set_menu_choice。菜单的内容是动态变化的,当用户选择了某张CD唱片后,主菜单中会多出几个选项。

注意,echo-e命令可能不能被移植到某些shell中。

(5)接下来是两个很短小的函数insert_title和insert_track,它们用于向数据库文件里添加数据。虽然有的人不喜欢这种长度只有一行的函数,但它们有助于让其他函数的含义更清晰易解。

紧跟着这两个函数的是一个比较大的函数add_record_tracks,它会用到上述两个短小的函数。这个函数使用模式匹配来确保用户未输入逗号(因为我们把逗号用做数据字段之间的分隔符),使用算术操作在用户输入曲目时递增当前曲目的编号:

(6)add_records函数用于输入新CD唱片的标题信息:

(7)find_cd函数的作用是使用grep命令在CD唱片标题文件中查找CD唱片的有关资料。你需要知道查询字符串在标题文件里出现的次数,但grep命令的返回值只能告诉你该字符串是匹配了0次还是多次。为了解决这一问题,我们把grep命令的输出保存到一个临时文件中,文件中的每行对应一次匹配,然后再统计该文件的行数。

单词统计命令wc在其输出中使用空格符分隔被统计文件中的行数、单词数和字符个数。我们使用$(wc -l $temp_file)标记从wc命令的输出结果中提取出第一个参数,并赋值给变量linesfound。如果要用到wc命令输出中的其他参数,你可以利用set命令把shell参数变量设置为wc命令的输出结果。

我们把IFS(内部数据字段分隔符)设置为一个逗号,这样你就可以读取以逗号分隔的数据字段了。另一个可选择的命令是cut。

(8)update_cd函数用于重新输入CD唱片的资料。注意,你想要搜索(使用grep)的行是以$cdcatnum开头(通过标志^)并且其后跟着一个逗号,因此你需要把$cdcatnum变量的扩展放在一对花括号{}里,这样你就可以搜索紧跟在CD目录编号之后的逗号了。这个函数还在get_confirm返回true的情况下,用花括号将要执行的多个语句组成一个语句块。

(9)count_cds函数用于快速统计数据库中CD唱片个数和曲目总数:

(10)remove_records函数用于从数据库文件中删除数据项,它通过grep -v命令删除所有匹配的字符串。注意,你必须使用一个临时文件来完成这一工作。

如果你使用下面这样的命令:

$title_file文件就会在grep命令开始执行之前,被>输出重定向操作设置为空文件,结果导致grep命令将从一个空文件里读取数据。

(11)list_tracks函数还是使用grep命令来找出你想要的行,它通过cut命令来访问你想要的字段,然后通过more命令提供按页输出。如果你对比一下用C语言重新实现这段大约20行左右的代码需要多少条语句的话,你就不得不佩服shell是一个功能多么强大的工具了。

(12)现在所有的函数都已定义好了,你可以进入主程序部分了。开头的几行先确保需要的文件处于一个已知状态,然后调用主菜单函数set_menu_choice,再根据它的输出进行相应的操作。

如果用户选择了退出,程序就先删除临时文件,再显示结束信息,最后成功退出(退出码为0):

2.8.3 应用程序的说明

脚本程序开始处的trap命令用于设置在用户按下Ctrl+C组合键时的中断处理。根据终端设置的不同,Ctrl+C组合键可能引发EXIT或INT信号。

实现菜单选择还有其他的办法,特别值得一提的是bash或ksh提供的select结构(但它未被列在X/Open规范中)。它是一个专门用来处理菜单选择的结构。如果你并不介意脚本程序移植性稍差的话,可以考虑使用它。你还可以利用here文档来实现为用户提供多行信息。

你可能已注意到,当添加一个新的CD唱片记录时,程序并没有检查其主键。新代码只是忽略使用同样主键的后续唱片标题,但把它们的曲目添加到第一个使用该主键的CD唱片的曲目清单中。如下所示:

我们将把这个问题及其他改进留给读者,请充分发挥你们的想象力和创造力,因为你可以在GPL条款之下任意修改这些代码。