4. 脚本初探
我第一次接触计算机还是在高中的学校内,学校购买了一台崭新的TRS-80,Model I,安装在图书馆内,免费提供所有的人使用,我记得我按照一本手册打印出了我的第一个BASIC程序。
10 PRINT “KENWAS HERE”;
20 GOTO 10
运行完程序,我兴奋不已。不但计算机如实地按照我的指令一遍又一遍的执行,屏幕还可以自动滚动。屏幕满了,它自动腾出空间给更多的信息,如果信息的长度超出了64个字符,屏幕自动换行,和上一行对齐。这对我那时的年龄来说简直就是神奇的魔术。
编程的乐趣在于创造一些新东西的乐趣,就像莎士比亚在仲夏夜之梦中所说的即使空无一物也可以有居处和名字。尽管有bug,它们也是以神奇的方法,在一条创新的长路上,一个想象不到的地方竖立了路标,提供你选择要去的地方。一个选择会导致另一个,没有两个程序是完成相同的。创造过程的产物就像油画、歌唱或作诗一样令人乐趣无穷。
现在,我们要在Bash中开始脚本编程的旅行了。本章将介绍脚本编程的基本技术——建立一个优良结构的脚本,重定向标准文件、处理不同的命令。
建立脚本
Bash外壳脚本通常以.sh结尾。下面显示了一个脚本称之为hello.sh,它的作用是在屏幕上显示一条脚本。
# hello.sh
# This is my first shellscript
printf “Hello! Bash is wonderful.”
exit 0
使用井(“#”)号开始的行是注释。它们只是提示阅读脚本的人,而不影响脚本的执行。井号开头的所有行都将被Bash忽略掉。
注释应该总是一些信息,脚本要实现的内容的描述,而不是对每一行脚本命令的说明。我看到太多的脚本只有一行注释“Abandon Hope All Ye Who Enter Here”。在商业软件中,清晰的信息说明有助于解决问题并调试晦涩难懂的程序。
exit命令后面更了一个0,告诉外壳本脚本程序已经成功运行。
你可以使用下面命令来运行这个新建立的脚本程序:
$ bash hello.sh
Hello! Bash is wonderful.!
脚本运行正常,Bash显示了一条消息。
建立正确的脚本
假如你的工作是建立一个脚本程序,目的是系统没有用户时运行命令sync,那么下面的脚本就可以完成上面的工作。
USERS=`who | wc -l`
if [ $USERS -eq 0 ] ;then
sync
fi
上面的脚本适合在Bash命令提示符下操作,但是在脚本中要考虑下面的问题:
n Linux如何知道这是Bash脚本?
n 如果有多个who命令执行哪一个?
n 脚本如何通知外壳程序本脚本是否运行成功?
n 如果sync命令被意外删除或系统管理员变更了权限后会发生什么事?
Bash是非常灵活的语言:需要时它可以用于交互。但是在脚本中,这种灵活性会导致安全漏洞和不可预知的行为。一个好的脚本语言需要做的不仅仅是在提示符下简单的执行相同的命令。
一个好的Bash脚本可以分为下面五部分:
n 头部
n 全局声明
n 完整性检查(sanity check)
n 主脚本
n 清除
上面的每一部分在设计脚本时都扮演着重要的角色。下一步,你再看一眼先前的示例,看看如何根据这五部分来改善它。
头部
头部定义了脚本是那一种,谁写的,它的版本号是多少,做了什么假定或Bash使用的脚本的选项是什么。
脚本的第一行称之为头行。这一行以“#!”开始,这一行字符组合标示了脚本的种类。Linux使用这一行信息来选择执行脚本的外壳程序。这一行是一个绝对路径表明Bash解释器的保存位置。大部分Linux发行版中,第一行都是如下所示:
#!/bin/bash
如果你不知道Bash外壳的位置,使用Linux的whereis命令可以等到:
$ whereis bash
bash: /bin/bash
下面是一个典型的脚本程序:
#!/bin/bash
#
# Flush disks if nobodyis on the computer
#
# Ken O. Burtch
# CVS: $Header$
shopt -s -o nounset
头部之后跟着脚本的目的和作者。CVS行将在第8章,“调试和版本控制”中讨论。shopt –s –o nounset命令探测是否有拼写错误并提交报告说是否有未定义的变量。
全局声明
在脚本顶部的声明适用于整个脚本。在某个地方放置global declarations,阅读者在看脚本时将很容易的明白它们的含义。
# Global Declarations
declare -rxSCRIPT=${0##*/} # SCRIPT isthe name of this script
declare -rxwho=”/usr/bin/who” # the who command- man 1 who
declare -rxsync=”/bin/sync” # the synccommand - man 1 sync
declare -rxwc=”/usr/bin/wc” # the wccommand - man 1 wc
完整性检查
完整性检查保护脚本在计算机上出现意外的更改。通常,一个命令运行在交互方式下,出现了拼写错误,系统找不到该命令会提示你处现了错误。在交互方式下,你可以修改命令并重新提交,可以节省时间。
但是,脚本运行在没有人进行监视的状态下,在脚本执行任何命令之前,需要检查所有的文件是否可以访问,该命令是否可以执行,是否保存在正确的地方。这种检查称之为完整性检查。除非计算机被认为是正确的状态,否则脚本不能开始主脚本的执行。象Linux这样可以高度自定义的操作系统是特别重要的。在一台计算机上能正确运行的脚本不一定在另一台计算机上也可以。
另一种完整性检查是进行实时错误检查。大部分错误在语句执行时被捕获。在脚本执行的早期检查危险情况可以保护脚本任务执行中途出现错误,否则你很难决定是否让脚本继续执行还是中断。
一些系统管理员有时无意识的删除或更改文件的可访问性,可能导致一个脚本无效。有时,环境的变化能更改命令的执行。某些恶意的计算机用户已经知道了如何更改某些人的登陆配置文件,导致你认为正在运行的命令可能不是你实际上使用的那个。
下面的脚本示例中,你需要效验那些你要使用的命令是否在正确的地方,它们是否在脚本中有效。
# Sanity checks
if test -z “$BASH” ;then
printf “$SCRIPT:$LINENO: please run this script with the BASHshell/n” >&2
exit 192
fi
if test ! -x “$who” ;then
printf “$SCRIPT:$LINENO: the command $who is not available —aborting/n “ >&2
exit 192
fi
if test ! -x “$sync” ;then
printf “$SCRIPT:$LINENO: the command $sync is not available —aborting/n “ >&2
exit 192
fi
if test ! -x “$wc” ;then
printf “$SCRIPT:$LINENO: the command $wc is not available — aborting/n“ >&2
exit 192
fi
主脚本
在你效验了系统的完整性之后,你可以执行你的主任务了。
# Flush disks if nobodyis on the computer
USERS=`who | wc -l`
if [ $USERS -eq 0 ] ;then
sync
fi
清除
最后,脚本需要做一些清除工作,例如,删除临时文件,将状态返回给一个人或正在运行的脚本的程序。本例中没有文件需要被清除。在更复杂的脚本中也许使用变量来保存失败返回的状态码。
通过放置清除段在你的程序末尾处,你只需要一行,而不是放置在脚本的各个地方。
exit 0 # all is well
完整的脚本如下所示:
#!/bin/bash
#
# Flush disks if nobodyis on the computer
#
# Ken O. Burtch
# CVS: $Header$
shopt -s -o nounset
# Global Declarations
declare -rxSCRIPT=${0##*/} # SCRIPT isthe name of this script
declare -rxwho=”/usr/bin/who” # the whocommand - man 1 who
declare -rxsync=”/bin/sync” # the synccommand - man 1 sync
declare -rxwc=”/usr/bin/wc” # the wccommand - man 1 wc
# Sanity checks
if test -z “$BASH” ;then
printf “$SCRIPT:$LINENO:please run this script with the BASH shell/n” >&2
exit 192
fi
if test ! -x “$who” ;then
printf “$SCRIPT:$LINENO:the command $who is not available – aborting/n” >&2
exit 192
fi
if test ! -x “$sync” ;then
printf “$SCRIPT:$LINENO:the command $sync is not available — /
aborting/n “ >&2
exit 192
fi
if test ! -x “$wc” ;then
printf “$SCRIPT:$LINENO:the command $wc is not available — aborting/n “ >&2
exit 192
fi
# Flush disks if nobodyis on the computer
USERS=`$who | $wc -l`
if [ $USERS -eq 0 ] ;then
$sync
fi
# Cleanup
exit 0 # all is well
这个脚本程序比只有四行的主程序长多了。通常,主脚本越长,完美风格脚本的头部越少。这个新脚本比只含主程序的脚本更安全和更可靠。
停止一个脚本
logout命令用来结束交互式的登录会话,并不能停止脚本(毕竟,脚本不是一个登录会话)。Bash提供了两个内置的命令来终端脚本的执行。
如前所示,exit命令用于无条件停止一个脚本。exit命令可以包含一个返回状态码给脚本的调用者。状态码0表示没有错误。如果省略了状态码,上一个命令执行的状态由脚本返回。最终,最好是提供一个退出状态码。
exit 0 # all is well
脚本到达了结尾自动停止,仿佛隐含了一个exit命令,但是返回的状态码是上一个命令执行的结果。
suspend命令优点象无条件停止脚本。可是,和exit不同的是,脚本的执行暂停在那,直到有信号唤醒脚本继续执行下去。
suspend # waituntil notified otherwise
这个命令在第十章详细讨论。
还有一个命令sleep。sleep命令暂停一个脚本一段时间,然后它自动被唤醒并继续执行下去。
sleep 5 # wait for 5seconds
sleep命令暂停一个脚本,使用户可以读取显示的内容时是特别有用的。但是,sleep不适合同步事件,因为在计算机运行程序多长时间通常取决于系统的负载、用户数、硬件升级情况和许多其他脚本控制之外的因素。
从键盘读取输入
内置命令read停止脚本的运行并等候用户从键盘输入。输入的文本被分配给和read配对的变量中。
printf “Archive filesfor how many days? “
read ARCHIVE_DAYS
上面的示例中,变量ARCHIVE_DAYS包含了用户输入的天数。
read命令有许多选项,例如:“-p(prompt)”是将printf和read命令合并起来的简写形式。read命令可以利用此选项在屏幕上为等候输入的用户显示一条消息。
read -p “Archive filesfor how many days? “ ARCHIVE_DAYS
“-r(raw input)”选项关闭使用斜杠开头的特殊字符的转义功能。通常,read命令可以将命令中的转义字符给正确的解释出来,例如:“/n”解释为换行,使用“-r”选项,read命令不解释斜杠开头的字符,它认为它们是普通字符。只有你需要自己处理斜杠开头的字符时才需要使用“-r”选项。
read -p “Enter aMicrosoft Windows pathname (backslashes allowed): “ -r MS_PATH
“-e”选项只在交互时工作,在脚本中不起作用。它使你可以使用Bash历史特性来选择返回的行。例如:你能使用上下箭头键来在最近使用的命令中移动。
退出时间可以使用“-t”开关进行设置。假如到了设定的时间没有输入,脚本将继续下一条命令,变量的值不做更改。如果用户在约定的时间后输入,用户输入的内容将会丢失。推出时间以秒为单位。
read -t 5 FILENAME #wait up to 5 seconds to read a filename
如果有一个变量叫TMOUT,即使不用“-t” ,Bash在这个变量的时间到了之后也退出。
如果使用了“-n”开关,输入的字符将受到限制,如果输入的字符达到了最大的字符数,外壳继续下一条命令,等候确认键或回车键的按下。
read -n 10 FILENAME #read no more than 10 characters
如果你没有提供一个变量,read命令将输入的文本保存在变量REPLY中。好的脚本应避免使用缺省行为,避免脚本的阅读者不清楚REPLY变量来自哪里。
read命令还有一些特殊的开关,我们将在第13章“控制台脚本”中讨论。从键盘读取输入之后,read命令返回状态码0。
基本的重定向
你可以将来自命令的消息转移到文件中,就像printf输出到文件之类的命令。Bash将这种行为称之为重定向(redirection)。系统有大量的重定向操作符。
“>”操作符用来将来自命令的消息重定向到文件。重定向操作符之后跟着消息要保存的文件名。例如:将消息“The processing is complete”保存到results.txt文件中。
printf “The processingis complete” > results.txt
“>”操作符总是覆盖要保存的文件。如果几条printf命令将消息重定向到相同的文件中,只有最后一条消息才会出现。
为了不覆盖原始的信息,Bash还有追加操作符“>>”,这个操作符将消息追加到文件的结尾处。
printf “The processingis complete” > results.txt
printf “There were noerrors” >> results.txt
result.txt将包含两条文本:
The processing iscomplete
There were no errors
同样,输入也可以从文件重定向到命令。这个输入重定向符号是“<”。例如:wc(word count)工具对文件进行统计。为了统计文件中的行数使用下面命令:
wc -lines <purchase_orders.txt
wc –lines处理purchase_orders.txt的方式就像它是从键盘中输入的一样。
除了文件,也可以将命令的结果重定向到另一个命令的输入中。这种处理称之为piping(管道),管道使用垂直分隔符“|”。
who | wc —lines # count the number of users
可以使用垂直分隔符连接任意个命令。这样的命令组称之为管线(pipeline)。
如果管道中的命令还没有执行完就被终止了,例如:使用control-c,Bash在屏幕上显示一个消息“BrokenPipe”。
跟在一个命令后面的若干行也可以重定向到命令中,并且被该命令所处理。在脚本中操作符“<<”可以将跟在其后的几行进行处理,仿佛它们是从键盘输入的一样。这个操作符需要跟一个标签来指示行的结束。
wc —lines <<END_OF_LIST
Jones, Allen
Grates, William
Oregano, Percy
END_OF_LIST
上面的示例中,Bash将标签中的三行作为一个整体,就像它们是从键盘中输入的一样或者从嵌入脚本的文件中输入。该命令wc返回的行数为3。
在操作符“<<”后面的列表数据称之为here file(本地文件或本地文档),因为HERE经常使用在Bourne外壳脚本中作为一个标签关键词。
更新的Bash有一个here file的重定向符“<<<”它将一行单独的文本进行重定向。第二章有一个示例。
标准输出、错误和输入
Linux假定所有的输出都到某种文件中。例如:输出到屏幕的文件称之为“/dev/tty”。
printf “Sales are up”> /dev/tty # display on the screen
当消息在程序中没有重定向,消息已不是直接输出到屏幕上。相反,消息直接输出到一个特殊的文件称之为标准文件(standand output)。缺省情况下,标准输出表示屏幕。发送到标准输出重定向到屏幕。Bash使用符号“&1”,表示标准输出,你也可以直接将消息重定向到这个符号上。
printf “Sales are up”> results.txt # sent to a file on disk
printf “Sales are up”> /dev/tty # sendexplicitly to the screen
printf “Sales areup” # sent to screen viastandard output
printf “Sales are up>&1 # same as the lastone
printf “Sales are up>/dev/stdout # sameas the last one
“/dev/stdout”是另一个标准文件名。最后三个例子是相同的。“&1”直接输出到标准文件。
标准输出不一定非要直接输出到屏幕上。例如:看一下下面称之为listorders.sh的脚本程序。
#!/bin/bash
#
# This script shows along listing of the contents of the orders subdirectory.
shopt -s -o nounset
ls -l incoming/orders
exit 0
假设你运行这个脚本并重定向结果到文件中。
$ bash listorders.sh> listing.txt
在脚本内部,标准输出不再指向屏幕。相反,标准输出指向你重定向输出的文件,本例中该文件为listing.txt。
ls -lincoming/orders # listingsaved in listing.txt
ls -l incoming/orders1>&1 # listing saved inlisting.txt
ls -l incoming/orders> /dev/tty # listing displayed onscreen
使用标准输出可以将脚本的输出或脚本的命令结果重新发送到一个新的位置。
脚本通常不需要知道消息要去的地方,但是,有时需要更改它们的去处。例如:有错误发生或提醒用户的警告时,你不想这些消息和其他的消息一起重新指向别的位置。
Liunx还提供了另一个文件供用户查看消息——标准错误(standanderror)。这个文件是所有错误消息的去向。标准错误文件的符号是“&2”。也可以使用“/dev/stderr”文件。错误文件的缺省去向也是屏幕。例如:
printf “$SCRIPT:$LINENO:No files available for processing” >&2
这个命令和不使用“>&2”重定向符的结果是相同的,但是,在内部处理流程它们是有很大区别的。它在屏幕上显示错误消息,而不管先前的标准输出重定向到什么地方了。
标准错误文件也可以重定向。重定向符号和标准输出文件类似,但是不同的是它以2开始。
$ bash listorders.sh2> listorders_errors.txt
上面示例中,所有来自listorders.sh的错误消息保存在listorders_errors.txt中。
如果ls命令将错误消息直接写入“/dev/tty”的屏幕文件中,没有其他方法将消息重定向到一个分开的文件。
Linux认为所有的输入来自某个文件。这个特殊的文件称之为标准输入文件——standardinput。可以使用符号“&0”来表示,也可以使用“/dev/stdin”表示。当命令使用“|”符号链接在一起,第一个命令的标准输出文件成为第二个文件的标准输入。
重定向可以被合并为一个单独的命令。它们的顺序是非常重要的,重定向的处理顺序为从左到右。重定向标准输出和标准错误到一个单独的文件,下列命令将起到作用。
$ bash listorders.sh> listorders_errors.txt >&2
可是,将重定向的顺序翻转并不能正常工作,因为它重定向错误到一个旧的标准输出接着重定向标准输出到文件listorders_errors.txt。
因为将标准输入和标准错误重定向是如此频繁,以至Bash提供了一个短符号“&>”将两者都重定向。
$ bash listorders.sh&> listorders_errors.txt
内置命令对linux命令
原始的Bourne外壳被设计为除非是必要的,否则由外壳外部的程序来实现。即使是算法也必须由外部程序来实现。这种设计使得Bourne外壳程序非常灵活,但是也带来了两个缺点。第一:因为即使是最简单的任务也要将程序加载进来,重新执行,使得系统太慢。第二:不能保证一个命令在一个系统有效也能在另一个系统有效,使得外壳脚本很难移植。
为了处理这些问题,Bash有许多内置的命令。除了和旧的Bourne外壳做基本的兼容,Linux任然有它自己的Bash命令的版本。例如:test是一个Bash内置的命令,但是Linux也有自己的程序——“/usr/bin/test”提供给外壳使用,以便在外壳不提供test命令时使用。
如果你不知道某个命令是否是内置的,Bash的type命令将会告诉你。如果命令是Linux命令,他将显示命令的路径(象whereis命令一样)。
$ type cd
cd is a shell builtin
$ type id
id is /usr/bin/id
“-t”开关显示命令的类型。“-p(路径)”开关显示此命令的哈希表的值,“-a”开关列出所有的命令情况。
$ type -a pwd
pwd is a shell builtin
pwd is /bin/pwd.
有两个开关可以限制查找的范围“-f”开关不检查在Bash内声明的功能。“-P”开关进一步检查Linux命令的路径并忽略Bash命令、功能或别名。
bulitin命令显示的运行一个内置的命令。即使有一个相同名字的别名也照样运行。
builtin pwd
通用command命令显示的运行一个Linux命令,即使有相同名字的内置命令huo别名也运行。
command pwd
使用“-v”或“-V”开关,command显示关于命令的信息,例如它的路径名。使用“-p”开关,command命令查找标准的Linux中bin库;当你更改了PATH变量时,这是非常有用的。
一份完整的内置外壳命令列表附在附录B中。
虽然builtin和command命令在测试和移植老的脚本到Bash时是有用的,但是有良好结构的脚本并不依赖于它们,它们在脚本设计时需要这种模糊性。
内置命令enable可以临时隐藏内置命令并在以后可以再开启。“-n”开关关闭命令。
$ enable test
$ type test
test is a shell builtin
$ enable -n test
$ type test
test is /usr/bin/test
“-d”开关关闭内置命令。你可以将“-p”和“-n”两个开关一起使用,用来显示已关闭的内置命令,或使用“-a”开关显示所有。“-s”开关限制列表为POSIX指定的内置命令。
$ enable -pn
enable -n test
在一个由优良的脚本中,enable应该使用在全局声明段中。在整个脚本中使用enable命令,很难记住某个特定的命令是否为内置命令。
Set和Shopt命令。
Bash有许多选项特性是可以打开或关闭的。这些选项是关于Bash如何和用户进行交互的、它在脚本中的行为是什么、它是Bash如何和其他外壳和标准进行兼容的。
当Bash开始运行时,Bash选项可以通过命令开关进行设置。例如:开始一个Bash会话并不允许使用未定义的变量时使用下面命令:
$ bash -o nounset
在脚本中或Bash的美元提示符下,你可以禁止使用未定义变量使用下面的命令:
$ shopt -s -o nounset
以前,使用set命令打开或关闭选项,随着选项的数量的增加,set命令因为选项使用单个字符表示变得很难使用,最终,Bash提供了shopt(shell options外壳选项)命令来对选项进行控制。你可以只使用字母来设置选项。而其他的由shopt命令提供,因此使的查找和设置某个选项变得让人头昏目眩。
“shopt –s(set)”命令打开一个外壳选项。“shell–u(unset)”命令关闭一个选项。不使用“-s”或“-u”,shopt切换当前的设置。
$ shopt -u -o nounset
shopt本身或加上 “–p”开关将显示选项的列表并显示它们是否已经打开了,“-o”选项不包含在内。为了看到“-o”选项,你需要设置“-o”。一个字母代码的列表保存在外壳变量“$-”中。
这些开关的大部分在特定的环境中是有效的。例如在UUCP网络,感叹号“!”用在电子邮件中,但是Bash使用感叹号用于历史命令的处理。历史处理可以使用下面命令关闭:
$ shopt -u -o histexpand
关闭了历史命令的使用,UUCP电子邮件地址可以用于命令行的交互。完事之后,可以再将历史命令的使用打开。
命令参考
comman命令开关
n -p——在标准的Linux执行文件库中查找命令
n -v——命令的解释
n -V——详细解释命令
enable命令开关
n -a——无论内置命令是否打开,显示所有的内置命令
n -d——关闭并删除内置命令
n -f file——打开并加载内置命令
n -n——关闭内置命令
n -p——显示内置命令列表
n -s——限制结果为指定的POSIX内置命令
read命令开关
n -a array——读取文本到数组中
n -d d——遇到字符d停止读取而不是读取一行
n -e——使用交互式编辑
n -n num——读取num个字符数而不是整行文本
n -p prompt——显示一个提示
n -r——显示源输入
n -s——隐藏输入的字符
n -tsec——在sec秒后退出
suspend命令开关
n -f——即使是登陆外壳也要让脚本挂起暂停运行