编写Shell脚本——自顶向下设计

自顶向下设计

1、Shell函数

我们的脚本目前生成HTML文件的步骤如下。

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

为了下一阶段的开发,我们将在步骤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函数要想正常工作,只能以超级用户权限执行脚本才行。一种更好的解决方案是让脚本能够根据用户权限调整自己的行为。我们留待后面再讨论。

相关推荐
M78佐菲2 小时前
HTML学习笔记
linux·笔记·学习·tcp/ip·html
XR1234567882 小时前
医院无线整网高密覆盖选型深度解析
运维·网络
新时代牛马2 小时前
嵌入式 Linux 蓝牙框架完整篇:从 HCI、L2CAP 到BlueZ 用户态
linux·运维·服务器
Brilliantwxx3 小时前
【STM32】 初认识USART串口
linux·stm32·单片机·嵌入式硬件·架构
aningx3 小时前
使用自定义bash脚本预防oom卡死
linux
weixin_444579303 小时前
Docker(下):镜像仓库管理
运维·docker·容器
资讯第一线3 小时前
斐讯R1免登录配网》 [APP] [安卓版] 下载
运维
ONLYOFFICE3 小时前
ONLYOFFICE成为Linux操作系统 – Winux的预装办公套件
linux·运维·服务器
其实防守也摸鱼3 小时前
OpenClaw Windows 踩坑实战指南
运维·服务器·数据库·windows·github·copilot