第二十六章:自顶向下设计

随着程序变得越来越大、越来越复杂,它们的设计、编码和维护变得越来越困难。与任何大型项目一样,将大型复杂任务分解为一系列小型简单任务通常是个好主意。让我们想象一下,我们正试图向一个来自火星的人描述一项常见的日常任务,去市场买食物。我们可以将整个过程描述为以下一系列步骤:

1.上车。 2.开车去市场。 3.停车。 4.进入市场。 5.购买食物。 6.回到车上。 7.开车回家。 8.停车。 9.进屋。

然而,来自火星的人可能需要更多的细节。我们可以进一步将子任务“停车”分解为以下一系列步骤:

1.寻找停车位。 2.将汽车驶入太空。 3.关闭电机。 4.设置驻车制动器。 5.下车。 6.锁车。

“关闭电机”子任务可以进一步细分为“关闭点火开关”、“拔出点火钥匙”等步骤,直到整个去市场过程的每一步都得到充分定义。

识别顶层步骤并开发这些步骤的越来越详细的视图的过程称为自上而下设计(top-down design)。这种技术使我们能够将大型复杂任务分解为许多小型简单任务。自顶向下设计是一种常见的程序设计方法,尤其适用于shell编程。

在本章中,我们将使用自顶向下的设计来进一步开发我们的报告生成器脚本。

第二十六章:自顶向下设计shell功能局部变量Shell函数和重定向保持脚本运行.bashrc 文件中的shell函数总结

shell功能

我们的脚本目前执行以下步骤来生成HTML文档:

1.打开页面。 2.打开页眉。 3.设置页面标题。 4.关闭页眉。 5.打开页面正文。 6.输出页面标题。 7.输出时间戳。 8.关闭页面正文。 9.关闭页面。

对于我们的下一个开发阶段,我们将在步骤7和8之间添加一些任务。这些将包括以下内容:

如果我们为每个任务都有一个命令,我们可以通过命令替换将它们添加到脚本中。

我们可以通过两种方式创建这些附加命令。我们可以编写三个单独的脚本,并将它们放置在PATH中列出的目录中,或者我们可以将脚本作为shell函数(shell functions)嵌入到程序中。正如我们提到的,shell函数是位于其他脚本内部的“迷你脚本”,可以作为自主程序。Shell函数有两种常见的语法形式。首先,这是更正式的形式:

function name { commands return }

以下是一种更简单(通常更受欢迎)的形式:

name () { commands return }

其中 name 是函数的名称, commands 是函数中包含的一系列命令。这两种形式是等效的,可以互换使用。以下是一个演示shell函数使用的脚本:

当shell读取脚本时,它会遍历第1行到第11行,因为这些行由注释和函数定义组成。执行从第12行开始,使用 echo 命令。第13行调用(calls)shell函数step2,shell执行该函数,就像执行任何其他命令一样。然后,程序控制移动到第6行,并执行第二个回声命令。接下来执行第7行。它的 return 命令终止函数,并在函数调用后的行(第14行)将控制返回给程序,并执行最终的 echo 命令。请注意,为了使函数调用被识别为shell函数而不被解释为外部程序的名称,shell函数定义必须在调用之前出现在脚本中。

我们将在脚本中添加最小的shell函数定义,如下所示:

Shell函数名遵循与变量相同的规则。函数必须至少包含一个命令return 命令(可选)满足要求。

局部变量

在我们迄今为止编写的脚本中,所有变量(包括常量)都是全局变量(global variables)。全局变量在整个程序中保持其存在。这对很多事情来说都很好,但有时会使shell函数的使用复杂化。在shell函数内部,通常希望有局部变量(local variables)。局部变量只能在定义它们的shell函数内访问,一旦shell函数终止,局部变量就不复存在。

拥有局部变量允许程序员使用名称可能已经存在的变量,无论是在脚本全局中还是在其他shell函数中,而不必担心潜在的名称冲突。

以下是一个示例脚本,演示了如何定义和使用局部变量:

正如我们所看到的,局部变量是通过在变量名前面加上单词 local 来定义的。这将创建一个变量,该变量是定义它的shell函数的局部变量。一旦脱离shell函数,变量就不再存在。当我们运行此脚本时,我们会看到以下结果:

我们看到,在两个shell函数中为局部变量 foo 赋值对函数外定义的 foo 值没有影响。

此功能允许编写shell函数,使其保持彼此独立以及与出现它们的脚本独立。这很有价值,因为它有助于防止程序的一部分干扰另一部分。它还允许编写shell函数,以便它们可以移植。也就是说,它们可以根据需要从一个脚本剪切并粘贴到另一个脚本。

Shell函数和重定向

如果我们仔细看看shell函数是如何编写的,我们可能会注意到我们在【第6章】中提到的一些内容:

花括号内的三个命令组成一个组命令。正如我们在第6章中回忆的那样,在重定向方面,组命令将多个命令组合成一个实体。使用组命令,我们可以同时执行以下两项操作:

和:

shell函数也是如此。让我们考虑以下代码:

很容易看出这个函数的作用,但它的输出去哪里了?它会去我们指定的任何地方。当我们调用此函数时,它会将其组合输出发送到标准输出,如果我们愿意,我们可以将其定向到文件:

或管道:

我们甚至可以使用命令替换将输出存储在变量中:

重定向也适用于标准输入。如果函数包含一个接受标准输入的命令,例如没有参数的cat,我们可以很容易地做到这一点:

保持脚本运行

在开发我们的程序时,保持程序处于可运行状态是有用的。通过这样做,并经常进行测试,我们可以在开发过程的早期发现错误。这将使调试问题变得更加容易。例如,如果我们运行程序,做一个小的更改,然后再次运行程序并发现问题,那么最新的更改很可能是问题的根源。通过添加空函数,程序员称之为存根(stubs),我们可以在早期验证程序的逻辑流程。在构建存根时,最好包含一些向程序员提供反馈的东西,这表明逻辑流程正在执行。如果我们现在查看脚本的输出:

我们看到在时间戳之后的输出中有一些空行,但我们无法确定原因。如果我们更改函数以包含一些反馈:

然后再次运行脚本:

我们现在看到,事实上,我们的三个functions正在执行。

随着我们的函数框架到位并正常工作,是时候充实一些函数代码了。首先,这里是 report_uptime 函数:

这很简单。我们使用here文档来输出节头和 uptime 命令的输出,并用 <pre> 标签包围,以保留命令的格式。 report_disk_space 函数类似。

此函数使用 df -h 命令来确定磁盘空间量。最后,我们将构建 report_home_space 函数。

我们使用 du 命令和 -sh 选项来执行此任务。然而,这并不是问题的完全解决方案。虽然它可以在某些系统上运行(例如Ubuntu),但它在其他系统上无法运行。原因是许多系统设置了主目录的权限,以防止它们具有世界可读性(world-readable),这是一种合理的安全措施。在这些系统上, report_home_space 函数(如所写)只有在我们的脚本以超级用户权限运行时才能工作。更好的解决方案是让脚本根据用户的权限调整其行为。我们将在下一章讨论这个问题。

.bashrc 文件中的shell函数

Shell函数可以很好地替代别名,实际上是创建个人使用的小命令的首选方法。别名在支持的命令类型和shell功能方面受到限制,而shell函数允许编写任何脚本。例如,如果我们喜欢为脚本开发的 report_disk_space shell函数,我们可以为 .bashrc 文件创建一个名为 ds 的类似函数:

总结

在本章中,我们介绍了一种称为自顶向下设计的通用程序设计方法,并了解了如何使用shell函数来构建所需的逐步细化。我们还看到了如何使用局部变量使shell函数彼此独立,并与它们所在的程序独立。这使得shell函数可以以可移植的方式编写,并通过允许将它们放置在多个程序中而可重用(reusable);这大大节省了时间。