从本章开始,我们将开始构建一个程序。这个项目的目的是了解如何使用各种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查看文件:
xxxxxxxxxx
file:///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 $foo
yes
[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 $fool
cp: 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将赋值限制为整数,但是,与将变量设置为只读一样,这种情况很少发生。
请注意,在赋值中,变量名、等号和值之间不得有空格。那么,值可以由什么组成呢?它可以有任何我们可以扩展成字符串的东西。
xxxxxxxxxx
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" # 转义序列,如制表符和换行符。
可以在一行上完成多个变量赋值。
xxxxxxxxxx
a=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文档中的文本经过参数展开、命令替换、算术展开,文字字符 $
和 \
必须用 \
转义。
但是,当我们将起始标记括在引号中时,如下所示:
xxxxxxxxxx
cat << '_EOF_'
引号将被删除,并且不会对文本进行扩展:
xxxxxxxxxx
[me@linuxbox ~]$ foo="some text"
[me@linuxbox ~]$ cat << '_EOF_'
> $foo
> "$foo"
> '$foo'
> \$foo
> _EOF_
$foo
"$foo"
'$foo'
\$foo
here文档可以与任何接受标准输入的命令一起使用。在这个例子中,我们使用一个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"
请注意,此功能可能有点问题,因为许多文本编辑器(和程序员自己)更喜欢使用空格而不是制表符来实现脚本中的缩进。
在本章中,我们启动了一个项目,该项目将带领我们完成构建成功剧本的过程。我们介绍了变量和常量的概念以及如何使用它们。它们是我们将发现的参数扩展的许多应用中的第一个。我们还研究了如何从脚本中生成输出以及嵌入文本块的各种方法。