从本章开始,我们将开始构建一个程序。这个项目的目的是了解如何使用各种shell功能来创建程序,更重要的是,创建好的程序。
我们将编写的程序是一个报告生成器(report generator)。它将展示有关我们的系统及其状态的各种统计数据,并将以HTML格式生成此报告,以便我们可以使用Firefox或Chrome等网络浏览器查看。
程序通常分为一系列阶段构建,每个阶段都添加特性和功能。我们程序的第一阶段将生成一个不包含系统信息的最小HTML文档。稍后会讲到。
我们需要知道的第一件事是格式良好的HTML文档的格式。它看起来像这样:
xxxxxxxxxx<html>    <head>        <title>Page Title</title>    </head>    <body>        Page body.    </body></html>如果我们在文本编辑器中输入此内容并将文件另存为 foo.html ,我们可以在Firefox中使用以下URL查看文件:
xxxxxxxxxxfile:///home/username/foo.html我们程序的第一阶段将能够将此HTML文件输出为标准输出。我们可以很容易地编写一个程序来实现这一点。让我们启动文本编辑器并创建一个名为 ~/bin/sys_info_page 的新文件。
输入以下程序:
xxxxxxxxxx# 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 命令,每行输出一个。保存文件后,我们将使其可执行并尝试运行它。
xxxxxxxxxx[me@linuxbox ~]$ chmod 755 ~/bin/sys_info_page[me@linuxbox ~]$ sys_info_page当程序运行时,我们应该看到屏幕上显示的HTML文档的文本,因为脚本中的 echo 命令将其输出发送到标准输出。我们将再次运行程序,并将程序的输出重定向到文件 sys_info_page.html ,以便我们可以使用web浏览器查看结果。
xxxxxxxxxx[me@linuxbox ~]$ sys_info_page > sys_info_page.html[me@linuxbox ~]$ firefox sys_info_page.html到目前为止,一切顺利。
在编写程序时,力求简单明了总是一个好主意。当程序易于阅读和理解时,维护就更容易了,更不用说它可以通过减少打字量使程序更容易编写。我们当前版本的程序运行良好,但可能更简单。我们实际上可以将所有 echo 命令组合成一个,这肯定会使向程序输出添加更多行变得更加容易。那么,让我们将程序更改为:
xxxxxxxxxx# Program to output a system information page echo "<html>     <head>         <title>Page Title</title>     </head>     <body>         Page body.     </body> </html>"引号字符串可能包含换行符,因此包含多行文本。shell将继续读取文本,直到遇到结束引号。它在命令行上也是这样工作的:
xxxxxxxxxx[me@linuxbox ~]$ echo "<html> >   <head> >       <title>Page Title</title> >   </head> >   <body> >       Page body. >   </body> > </html>"前导 > 字符是PS2 shell变量中包含的shell提示符。每当我们在shell中键入多行语句时,它就会出现。这个特性现在有点模糊,但稍后,当我们介绍多行编程语句时,它将变得非常方便。
现在我们的程序可以生成一个最小的文档,让我们在报告中添加一些数据。为此,我们将进行以下更改:
xxxxxxxxxx# Program to output a system information page echo "<html>     <head>         <title>System Information Report</title>     </head>     <body>         <h1>System Information Report</h1>     </body> </html>"我们在报告正文中添加了页面抬头和标题。
variables —— 变量
constants —— 常量
然而,我们的脚本存在一个问题。请注意字符串“System Information Report”是如何重复的?使用我们的小脚本,这不是问题,但让我们想象一下,我们的脚本真的很长,而且我们有多个这个字符串的实例。如果我们想将标题更改为其他内容,我们必须在多个地方更改它,这可能需要大量的工作。如果我们可以安排脚本,使字符串只出现一次,而不是多次,那会怎么样?这将使脚本的未来维护变得更加容易。我们可以这样做:
xxxxxxxxxx# 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 的变量(variable)并将其赋值为 System Information Report ,我们可以利用参数扩展并将字符串放置在多个位置。
那么,我们如何创建一个变量呢?很简单,我们只是使用它。当shell遇到变量时,它会自动创建它。这与许多编程语言不同,在许多编程语言中,变量在使用前必须显式声明或定义。shell对此非常松懈,这可能会导致一些问题。例如,考虑在命令行上运行的此场景:
xxxxxxxxxx[me@linuxbox ~]$ foo="yes"[me@linuxbox ~]$ echo $fooyes[me@linuxbox ~]$ echo $fool[me@linuxbox ~]$我们首先给变量 foo 赋值 yes ,然后用 echo 显示它的值。接下来,我们显示拼错为 fool 的变量名的值,并得到一个空白结果。这是因为shell在遇到 fool 时愉快地创建了它,并将其默认值设置为 nothing 或空。由此,我们了解到我们必须密切注意拼写!了解这个例子中到底发生了什么也很重要。从我们之前对shell如何执行扩展的了解中,我们知道以下命令:
xxxxxxxxxx[me@linuxbox ~]$ echo $foo经过参数展开,结果如下:
xxxxxxxxxx[me@linuxbox ~]$ echo yes相比之下,以下命令:
xxxxxxxxxx[me@linuxbox ~]$ echo $fool扩展到以下内容:
xxxxxxxxxx[me@linuxbox ~]$ echo空变量扩展为零!这可能会对需要参数的命令造成严重破坏。这里有一个例子:
xxxxxxxxxx[me@linuxbox ~]$ foo=foo.txt[me@linuxbox ~]$ foo1=foo1.txt[me@linuxbox ~]$ cp $foo $foolcp: missing destination file operand after `foo.txt' Try `cp --help' for more information.我们为两个变量foo和foo1赋值。然后我们执行cp,但拼错了第二个参数的名称。展开后,cp命令只发送一个参数,尽管它需要两个。
关于变量名有一些规则:
“变量”一词意味着一个变化的值,在许多应用程序中,变量都是这样使用的。然而,我们应用程序中的变量 title 被用作常量(constant)。常量就像变量一样,因为它有一个名称并包含一个值。不同之处在于常数的值不会改变。在执行几何计算的应用程序中,我们可以将 PI 定义为常数并将其赋值为 3.1415 ,而不是在整个程序中直接使用该数字。shell不区分变量和常量;它们主要是为了程序员的方便。一个常见的约定是使用大写字母表示常量,使用小写字母表示真变量。我们将修改我们的脚本以符合此约定:
xxxxxxxxxx# 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 (只读)选项的 declare 内置命令来强制常量不可变的方法。如果我们这样分配标题:
				declare -r TITLE= "Page Title"
shell将阻止对TITLE的任何后续赋值。此功能很少使用,但它存在于非常正式的脚本中。
这就是我们对扩展的了解真正开始得到回报的地方。正如我们所看到的,变量是这样赋值的:
variable=value
其中 variable 是变量的名称, value 是字符串。与其他一些编程语言不同,shell不关心分配给变量的数据类型;它把它们都当作字符串。您可以通过使用带有 -i 选项的 declare 命令来强制shell将赋值限制为整数,但是,与将变量设置为只读一样,这种情况很少发生。
请注意,在赋值中,变量名、等号和值之间不得有空格。那么,值可以由什么组成呢?它可以有任何我们可以扩展成字符串的东西。
xxxxxxxxxxa=z                     # 将字符串“z”赋值给变量a。 b="a string"            # 嵌入的空格必须在引号内。c="a string and $b"     # 其他扩展,如变量,可以扩展到赋值中。d="$(ls -l foo.txt)"    # 命令的结果。 e=$((5 * 7))            # 算术展开。f="\t\ta string\n"      # 转义序列,如制表符和换行符。可以在一行上完成多个变量赋值。
xxxxxxxxxxa=5 b="a string"在展开过程中,变量名可能会被可选的花括号{}包围。这在变量名因周围上下文而变得模糊的情况下非常有用。在这里,我们尝试使用变量将文件名从 myfile 更改为 myfile1 :
xxxxxxxxxx[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 命令的第二个参数解释为新的(空的)变量。这个问题可以通过以下方式解决:
xxxxxxxxxx[me@linuxbox ~]$ mv "$filename" "${filename}1"通过添加周围的大括号,shell不再将后面的 1 解释为变量名的一部分。
注意:最好将变量和命令替换括在双引号中,以限制shell分词的影响。当变量可能包含文件名时,引用尤为重要。
我们将借此机会在报告中添加一些数据,即报告创建的日期和时间以及创建者的用户名。
xxxxxxxxxx# 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>"我们研究了两种不同的文本输出方法,都使用 echo 命令。还有第三种方法称为 here document 或 here script 。这里的文档是I/O重定向的另一种形式,在这种形式中,我们将一段文本嵌入到脚本中,并将其输入到命令的标准输入中。其工作原理如下:
command << token  text  token
其中 command 是接受标准输入的命令的名称, token 是用于指示嵌入文本结束的字符串。在这里,我们将修改我们的脚本以使用这里的文档:
xxxxxxxxxx# 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_我们的脚本现在使用 cat 和here文档,而不是使用 echo 。字符串 _EOF_(表示文件结束,一种常见的约定)被选为标记,并标记嵌入文本的结束。请注意,标记必须单独出现,并且行上不得有尾随空格。
那么,使用这里的文档有什么好处呢?它与 echo 基本相同,只是默认情况下,这里文档中的单引号和双引号对shell失去了特殊意义。下面是一个命令行示例:
xxxxxxxxxx[me@linuxbox ~]$ foo="some text"[me@linuxbox ~]$ cat << _EOF_ > $foo > "$foo" > '$foo' > \$foo > _EOF_ some text "some text" 'some text' $foo正如我们所看到的,shell不注意引号。它把他们当作普通的字符。这允许我们在此处文档中自由嵌入引用。这可能对我们的报告程序很方便。
here文档中的文本经过参数展开、命令替换、算术展开,文字字符 $ 和 \ 必须用 \ 转义。
但是,当我们将起始标记括在引号中时,如下所示:
xxxxxxxxxxcat << '_EOF_'引号将被删除,并且不会对文本进行扩展:
xxxxxxxxxx[me@linuxbox ~]$ foo="some text"[me@linuxbox ~]$ cat << '_EOF_' > $foo > "$foo" > '$foo' > \$foo > _EOF_ $foo"$foo" '$foo' \$foohere文档可以与任何接受标准输入的命令一起使用。在这个例子中,我们使用一个here文档向 ftp 程序传递一系列命令,以从远程 FTP 服务器检索文件:
xxxxxxxxxx# 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文档进行缩进,从而提高可读性。
xxxxxxxxxx# 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"请注意,此功能可能有点问题,因为许多文本编辑器(和程序员自己)更喜欢使用空格而不是制表符来实现脚本中的缩进。
在本章中,我们启动了一个项目,该项目将带领我们完成构建成功剧本的过程。我们介绍了变量和常量的概念以及如何使用它们。它们是我们将发现的参数扩展的许多应用中的第一个。我们还研究了如何从脚本中生成输出以及嵌入文本块的各种方法。