第25章:启动一个项目

从本章开始,我们开始构建一个程序。该项目的目的是了解各种创建程序用的 shell 特性,更重要的是,创建优秀的(good)程序。

我们要写的程序是一个报表生成器(report generator)。它将会展示各种关于我们系统的统计数据及其状态,并生成 HTML 格式的报表,以便我们能通过如 Firefox 或 Chrome 这样的网页浏览器查看报表。

程序通常是在一系列阶段中构建出来的,每个阶段会加入一些特性和功能。我们程序的第一个阶段是一个最小化的 HTML 文档,其中没有包含什么系统信息。

第一阶段:最小的文档

首先,我们需要知道组织良好的 HTML 文档的格式。看起来像这样:

<html>
    <head>
        <title>Page Title</title>
    </head>
    <body>
        Page body.
    </body>
</html>

如果我们在文本编辑器中键入这些文本并保存为 foo.html 文件,就可以通过下面这个 URL 在 Firefox 中看到文件:

file:///home/username/foo.html

程序的第一阶段可以将这个 HTML 文件输出到标准输出。我们可以写一个程序,轻松完成这个任务。来启动文本编辑器创建一个新的文件 ~/bin/sys_info_page

[me@linuxbox ~]$ vim ~/bin/sys_info_page

键入下列程序:

#!/bin/bash
# Program to output a system information page
echo "<html>"
echo "    <head>"
echo "        <title>Page Title</title>"
echo "    </head>"
echo "    <body>"
echo "        Page body."
echo "    </body>"
echo "</html>"

这是对这个问题的初次尝试,包含了一个 shebang、一条注释(永远是个好主意)和一系列的 echo 命令,一条命令输出一行。在保存文件之后,我们会使其可执行化,并尝试运行它。

[me@linuxbox ~]$ chmod 755 ~/bin/sys_info_page
[me@linuxbox ~]$ sys_info_page

当程序运行,我们应该可以看到 HTML 文档的文本显示在屏幕上,是因为脚本中的 echo 命令将输出结果送到了标准输出。我们将再次运行程序,并将程序的输出重定向到文件 sys_info_page.html 以便可以在网页浏览器中查看结果。

[me@linuxbox ~]$ sys_info_page > sys_info_page.html
[me@linuxbox ~]$ firefox sys_info_page.html

目前为止一切很好。

当写程序时,努力保持简单明了,永远是一个好主意。当一个程序易于阅读和理解,维护起来也就更容易,更不用提减少输入量会使得程序更容易写。我们当前版本的程序运行得很好,但是还可以更简单。我们可以合并所有的 echo 命令为一个,无疑将使在程序输出中添加更多行变得更加容易。所以,来将程序作如下改变:

#!/bin/bash
# Program to output a system information page
echo "<html>
    <head>
        <title>Page Title</title>
    </head>
    <body>
        Page body.
    </body>
</html>"

一个带引号的字符串可以包含新行,所以就可以包含多行文本。shell 会持续读取文本,直到遇到闭合引用标识。在命令行中也一样奏效:

[me@linuxbox ~]$ echo "<html>
>         <head>
>                 <title>Page Title</title>
>         </head>
>         <body>
>                 Page body.
>         </body>
> </html>"

前置的 ">" 字符是包含在 PS2 shell 变量中的 shell 提示符。当在 shell 中键入一个多行文本时,它就会出现。这个特性现在还有点模糊,但是晚些时候,单我们学习到多行编程陈述时,就会显出其相当便利。

第二阶段:加一点数据

既然程序可以生成一个微型文档了,就来加一些数据到报表中吧。我们将作如下变更:

#!/bin/bash

# Program to output a system information page

echo "<html>
        <head>
                <title>System Information Report</title>
        </head>
        <body>
                <h1>System Information Report</h1>
        </body>
</html>"

我们加入了页面标题和报表体中的标题。

变量和常量

然而我们有一个问题。注意字符串 "System Information Report" 是如何重复的吧?对于我们的小型脚本而言这不是问题,但是想象一下要是脚本足够长而且有多个这样的字符串实例。如果我们想要变更标题,将要变更许多地方,工作量不会小。要是我们能整理脚本,使该字符串仅出现一次而非反复出现呢?那就会使脚本更易于维护。下面就是我们所能做的:

#!/bin/bash

# Program to output a system information page

title="System Information Report"

echo "<html>
        <head>
                <title>$title</title>
        </head>
        <body>
                <h1>$title</h1>
        </body>
</html>"

创建一个名为 title 的变量,将其赋值为 System Information Report,我们可以利用参数扩展的优势并将字符串放置在多个位置。

那么,我们要如何创建变量呢?很简单,我们只需要用就好了。当 shell 遇到一个变量时,会自动创建的。这与许多编程语言中必须在使用变量前明确声明(declared)或定义的不同。shell 对此则很松散,所以也可以导致很多问题。例如,考虑发生在命令行中的这类情形:

[me@linuxbox ~]$ foo="yes"
[me@linuxbox ~]$ echo $foo
yes
[me@linuxbox ~]$ echo $fool

[me@linuxbox ~]$

首先,我们将值 yes 分配给变量 foo,随后用 echo 将其显示出来。接下来,我们显示一个拼写错误的 fool 变量的值,而得到了一个空白的结果。这是因为 shell 在遇到 fool 变量时就愉快地创建了这个变量,并给它赋了一个空值。从这里,我们学习到必须注意拼写!同样重要的是,要理解这个例子中到底发生了什么。从刚才看到的 shell 如何执行扩展,我们学习了下列命令:

[me@linuxbox ~]$ echo $foo

承担参数扩展,并导致如下:

[me@linuxbox ~]$ echo yes

对比下面这个命令:

[me@linuxbox ~]$ echo $fool

扩展为:

[me@linuxbox ~]$ echo

空的变量扩展为空!这对需要参数的命令来说是一场浩劫。例如:

[me@linuxbox ~]$ foo=foo.txt
[me@linuxbox ~]$ foo1=foo1.txt
[me@linuxbox ~]$ cp $foo $fool
cp: missing destination file operand after 'foo.txt'
Try 'cp --help' for more information.

我们对两个变量赋值,foofoo1。随后执行 cp,但是拼错了第二个参数名。扩展后,cp 命令仅收到一个参数,但是其需要两个参数。

关于变量名称,有一些规则:

  1. 变量名可以由字母数字字符和下划线字符所组成。

  2. 变量名的第一个字符必须是字母或下划线。

  3. 不能使用空格和标点符号。

「变量」这个词,暗示其值是会变化的,在许多应用中,变量就是这么用的。然而,在我们应用中的这个变量,title,则用作一个常量(constant)。常量如同变量,有一个名称并包含一个值。区别在于常量的值是不变的。在一个执行几何运算的应用程序中,我们可以定义 PI 为常量并将其赋值为 3.1415,而不是在程序中使用字面上的数字。shell 不区分变量和常量,只为程序员方便。一般的惯例是,用大写字母指定常量,用小写字母指定变量。来修改脚本以适应这个惯例:

#!/bin/bash

# Program to output a system information page

TITLE="System Information Report For $HOSTNAME"

echo "<html>
        <head>
                <title>$TITLE</title>
        </head>
        <body>
                <h1>$TITLE</h1>
        </body>
</html>"

我们还优化了标题,加上了一个 shell 变量 HOSTNAME,使其更绚丽些。这个变量是机器在网络上的名称。

注意: shell 实际上提供了一个方法来强制常量的不变性,使用内建的带 -r(read-only 只读)选项的 declare 命令。可以这样给 TITLE 赋值:

declare -r TITLE="Page Title"

shell 会防止任何随后对 TITLE 的赋值。这个功能很少用到,但是会出现在非常正式的脚本中。

为变量和常量赋值

在这里,我们关于扩展的知识才真正开始给我们回报了。我们已经看到,变量是用这种方法赋值的:

variable=value

其中 variable 是变量名,而 value 是一个字符串。和其它一些编程语言不同,shell 不在乎分配到变量的数据类型,都把它们当字符串看待。你可以用带 -i 选项的 declare 命令限制其为整数,但是和设置变量为只读一样,这也是很少用到的。

注意,在赋值过程中,在变量名、等号、变量值之间不能有空格。那么,变量值都可以是什么呢?我可以将任何东西扩展成字符串。

a=z                   # 将字符串 "z" 分配到变量 a。
b="a string"          # 内嵌的空格必须在引号内。
c="a string and $b"   # 其它扩展,如一个变量,也可以扩展到一个赋值中。
d="$(ls -l foo.txt)"  # 一条命令的输出结果。
e=$((5 * 7))          # 算术表达式。
f="\t\ta string\n"    # 转义序列,如制表符和换行符。

多个变量可以在一行中完成赋值。

a=5 b="a string"

在扩展期间,变量名可以用花括号 {} 包含。这在变量名称由于其周围的上下文而变得模棱两可的情况下很有用。这里,我们尝试用一个变量,将 myfile 文件重命名为 myfile1 文件。

[me@linuxbox ~]$ filename="myfile"
[me@linuxbox ~]$ touch "$filename"
[me@linuxbox ~]$ mv "$filename" "$filename1"
mv: missing destination file operand after `myfile'
Try 'mv --help' for more information.

尝试失败,因为 shell 将 mv 命令的第二个参数解释为一个新的(也是空的)变量。这个问题可以这样解决:

[me@linuxbox ~]$ mv "$filename" "${filename}1"

通过加上一对括号,shell 不再将末尾的 1 当作是变量名的一部分了。

注意:良好的操作是将变量和命令替换放在双引号内,以限制 shell 的粉刺作用。当一个变量名包含有一个文件名时,引号更是非常重要。

我们将借此机会向报告中添加一些数据,即报告的创建日期和时间以及创建者的用户名。

#!/bin/bash

# Program to output a system information page

TITLE="System Information Report For $HOSTNAME"
CURRENT_TIME="$(date +"%x %r %Z")"
TIMESTAMP="Generated $CURRENT_TIME, by $USER"

echo "<html>
        <head>
                <title>$TITLE</title>
        </head>
        <body>
                <h1>$TITLE</h1>
                <p>$TIMESTAMP</p>
        </body>
</html>"

here 文档

我们已经看到了输出文本的两种方式,都是使用 echo 命令。还有第三种途径叫作 here 文档(here document)或者 here 脚本(here script)。一份 here 文档是输入输出重定向的另一形式,其中我们将一段文本嵌入到脚本中,并将其输入到命令的标准输入中。工作模式是这样的:

command << token
text
token

其中 command 是接收标准输入的命令名称,token 是用来指示内嵌文本结束的字符串。我们来用 here 文档修改脚本:

#!/bin/bash

# Program to output a system information page

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>
        </body>
</html>
_EOF_

取代 echo,现在的脚本启用了 cat 命令和一份 here 文档。字符串 _EOF_(意即文件结尾 End of File,一个共同约定)用来作为令牌,标记内嵌文本的结束。记住这个令牌必须单独出现在一行内,且不允许有尾随的空格。

那么,使用 here 文档有什么优势呢?它大部分和 echo 一样, 不一样的是,默认情况下,单引号和双引号在 here 文档中都失去了其在 shell 中的特殊意义。这里有一个命令行示例:

[me@linuxbox ~]$ foo="some text"
[me@linuxbox ~]$ cat << _EOF_
> $foo
> "$foo"
> '$foo'
> \$foo
> _EOF_
some text
"some text"
'some text'
$foo

可以看到,shell 不理会引号。把它们当普通字符看待。这就允许我们自如地在 here 文档中内嵌引号。使得更方便地生成报告程序。

here 文档可以用在任何能接收标准输入的命令中。在下面的示例中,我们用一份 here 文档传输一系列命令到 ftp 程序,从远程 FTP 服务器取回一份文件:

#!/bin/bash

# Script to retrieve a file via FTP

FTP_SERVER=ftp.nl.debian.org
FTP_PATH=/debian/dists/stretch/main/installer-amd64/current/images/cdrom REMOTE_FILE=debian-cd_info.tar.gz

ftp -n << _EOF_
open $FTP_SERVER
user anonymous me@linuxbox
cd $FTP_PATH
hash
get $REMOTE_FILE
bye
_EOF_
ls -l "$REMOTE_FILE"

如果我们将重定向符 << 改成 <<-,shell 将忽略 here 文档中的前置制表符(但是不是空格)。这样就允许 here 文档缩进,可以改进可读性。

#!/bin/bash

# Script to retrieve a file via FTP

FTP_SERVER=ftp.nl.debian.org
FTP_PATH=/debian/dists/stretch/main/installer-amd64/current/images/cdrom REMOTE_FILE=debian-cd_info.tar.gz

ftp -n <<- _EOF_
    open $FTP_SERVER
    user anonymous me@linuxbox
    cd $FTP_PATH
    hash
    get $REMOTE_FILE
    bye
    _EOF_

ls -l "$REMOTE_FILE"

这个功能存在一些问题,因为许多文本编辑器(和程序员们自己)更喜欢在脚本中使用空格来代替制表符实现缩进。

总结

本章中,我们启动了一个项目,可以带我们成功地完成创建一个脚本。我们介绍了变量和常量的概念,以及如何使用它们。它们是我们将在参数扩展中找到的许多应用中的第一个。我们还学习了如何从脚本产生输出,以及多种嵌入文本块的方法。

扩展阅读

Last updated

Was this helpful?