自顶向下设计
1、Shell函数
我们的脚本目前生成HTML文件的步骤如下。
- 打开页面。
- 打开正文标题。
- 设置页标题。
- 关闭正文标题。
- 打开页面正文。
- 输出正文标题。
- 输出时间戳。
- 关闭页面正文。
- 关闭页面。
为了下一阶段的开发,我们将在步骤7和8之间添加一些任务。
- 输出系统正常运行时间和负载。这是自上次关机或重启之后系统的运行时长,以及在若干时间间隔内,当前运行在处理器上的平均任务量。
- 输出磁盘空间。这是系统存储设备的整体使用情况。输出主目录空间。这是每个用户使用的存储空间使用量。
如果每个任务都有对应的命令,我们可以直接通过命令替换的方式将其添加到脚本中:
bash
#!/bin/bash
# 程序输出一个系统信息页
TITLE="System Information Report For $HOSTNAME"
CURRENT_TIME="$(date +"%x %r %Z")"
TIMESTAMP="Generated $CURRENT_TIME, by $USER"
cat << _EOF_
<html>
<head>
<title>$TITLE</title>
</head>
<body>
<h1>$TITLE</h1>
<p>$TIMESTAMP</p>
$(report_uptime)
$(report_disk_space)
$(report_home_space)
</body>
</html>
_EOF_
我们可以通过两种方式创建这些额外的命令。要么编写3个独立的脚本,放置在PATH所列出的目录中;要么将脚本作为Shell函数嵌入程序中。我们之前提到过,Shell函数就是位于其他脚本内的"迷你脚本",可用作自主程序(autonomous program)。Shell函数有两种语法形式,一种比较正式:
bash
function name {
commands
return
}
另一种比较简单(通常也是首选):
bash
name () {
commands
return
}
其中,name是函数名称,commands是函数包含的一系列命令。这两种形式都是等价的,可以交换使用。下面的脚本演示了Shell函数的用法:
bash
1 #!/bin/bash
2
3 # Shell函数演示
4
5 function step2 {
6 echo "Step 2"
7 return
8 }
9
10 # 主程序起点
11
12 echo "Step 1"
13 step2
14 echo "Step 3"
当Shell读取脚本的时候,它会跳过第1~11行,这些行包含的是注释和函数定义。从第12行开始执行echo命令。
第13行调用了Shell函数step2,Shell执行该函数和执行其他命令无异。程序控制权转移到第6行,执行第2个echo命令。接着再执行第7行。return命令结束函数执行,将控制权还给函数调用之后的第14行,然后执行最后一个echo命令。注意,为了让函数调用能够被识别为Shell函数,不被解释为外部程序名称,Shell函数的定义在脚本中必须出现在调用之前。
在我们的脚本中添加Shell函数定义:
bash
#!/bin/bash
# 程序输出一个系统信息页
TITLE="System Information Report For $HOSTNAME"
CURRENT_TIME="$(date +"%x %r %Z")"
TIMESTAMP="Generated $CURRENT_TIME, by $USER"
report_uptime () {
return
}
report_disk_space () {
return
}
report_home_space () {
return
}
cat << _EOF_
<html>
<head>
<title>$TITLE</title>
</head>
<body>
<h1>$TITLE</h1>
<p>$TIMESTAMP</p>
$(report_uptime)
$(report_disk_space)
$(report_home_space)
</body>
</html>
_EOF_
2、局部变量
在目前我们所编写的脚本中,所有的变量(包括常量)都是全局变量。在整个程序执行期间,全局变量一直存在。很多时候,这是好事。但是有时候,它会使Shell函数的使用变得复杂起来。在Shell函数内部,往往更需要局部变量。局部变量只能在定义其的Shell函数中有效,一旦Shell函数终止,就不复存在。
局部变量允许程序员使用已经存在的变量名,无论这些变量名表示脚本中的全局变量,还是其他Shell函数中的变量,都不用担心潜在的命名冲突。
下面的例子演示了局部变量的定义和用法:
bash
#!/bin/bash
# local-vars: 演示本地变量的脚本
foo=0 # global variable foo
funct_1 () {
local foo # funct_1局部变量foo
foo=1
echo "funct_1: foo = $foo"
}
funct_2 () {
local foo # funct_2局部变量foo
foo=2
echo "funct_2: foo = $foo"
}
echo "global: foo = $foo"
funct_1
echo "global: foo = $foo"
funct_2
echo "global: foo = $foo"
可以看到,局部变量是通过在变量名之前添加单词local来定义的。所创建出的变量在其定义的Shell函数内部是局部变量。在该Shell函数外部,这个变量不存在。该脚本的执行结果如下:
bash
[me@linuxbox ~]$ local-vars
global: foo = 0
funct_1: foo = 1
global: foo = 0
funct_2: foo = 2
global: foo = 0
我们看到,两个Shell函数中对局部变量foo的赋值并不影响在函数外所定义的变量foo的值。
该特性使所编写的函数之间、函数与其所在的脚本之间彼此独立。这非常重要,因为它有助于阻止程序各部分相互干扰,除此之外,也使写出的函数具备可移植性。也就是说,我们可以根据需要将某个函数从一个脚本挪用到另一个脚本中。
3、保持脚本执行
在开发程序时,让程序保持在可执行的状态非常有用。通过这种方式,加上频繁测试,能够在开发过程的早期检测到错误。这也会让问题的调试变得更容易。例如,如果我们执行程序,做一些小改动,再次执行该程序,此时发现了问题。这就有可能是最近的改动造成的。通过添加空函数(程序员称其为"桩代码"),我们可以较早核实程序的逻辑流程。当构建桩代码时,最好在其中包含一些能够为程序员提供反馈信息的东西,它可以显示出正在执行的逻辑流程。如果现在查看脚本的执行结果:
bash
[me@linuxbox ~]$ sys_info_page
<html>
<head>
<title>System Information Report For twin2</title>
</head>
<body>
<h1>System Information Report For linuxbox</h1>
<p>Generated 03/19/2018 04:02:10 PM EDT, by me</p>
</body>
</html>
我们看到时间戳之后出现了几个空行,但是不确定出现的原因。如果修改函数,在其中加入一些反馈信息:
bash
report_uptime () {
echo "Function report_uptime executed."
return
}
report_disk_space () {
echo "Function report_disk_space executed."
return
}
report_home_space () {
echo "Function report_home_space executed."
return
}
再次执行该脚本:
bash
[me@linuxbox ~]$ sys_info_page
<html>
<head>
<title>System Information Report For linuxbox</title>
</head>
<body>
<h1>System Information Report For linuxbox</h1>
<p>Generated 03/20/2018 05:17:26 AM EDT, by me</p>
Function report_uptime executed.
Function report_disk_space executed.
Function report_home_space executed.
</body>
</html>
现在我们可以看出,这3个函数实际上都执行了。
看来我们的函数设计框架没有问题,可以加入一些函数代码了。先是report_uptime函数:
bash
report_uptime () {
cat <<- _EOF_
<h2>System Uptime</h2>
<pre>$(uptime)</pre>
_EOF_
return
}
看起来相当直观。我们使用here document输出小节标题(section header)和uptime命令的结果,$(uptime)两边的
标签用于保留该命令的格式。reprot_disk_space函数也类似:
bash
report_uptime () {
cat <<- _EOF_
<h2>System Uptime</h2>
<pre>$(uptime)</pre>
_EOF_
return
}
该函数使用df -h命令确定磁盘空间总量。接着,我们再来构建report_home_space函数:
bash
report_home_space () {
cat <<- _EOF_
<h2>Home Space Utilization</h2>
<pre>$(du -sh /home/*)</pre>
_EOF_
return
}
我们使用带有-sh选项的du命令来执行这项任务。但这并非该问题的完整解决方案,尽管这在某些系统(如Ubuntu)中可行,但是在其他系统中就不管用了。原因在于多系统设置了主目录的权限,避免所有用户都能读取其中的内容,这种安全措施是合理的。在这类系统中,report_home_space函数要想正常工作,只能以超级用户权限执行脚本才行。一种更好的解决方案是让脚本能够根据用户权限调整自己的行为。我们留待后面再讨论。