使用Bash编写Linux Shell脚本-8.调试和版本控制
转载自:http://blog.csdn.net/fox_lht/archive/2010/10/19/5950790.aspx
当我还在布鲁克大学上学的时候, Macquarium 实验室中充满了苹果公司的 Macintosh Plus 电脑。一天,我在为第三年的操作系统课程准备一个程序。我的一个小程序报告没有错误,当我运行它时,黑白色的桌面上出现了竖条,我的软盘被退出来了,计算机然后从新启动。经过更仔细的检查,我意识到我在 if 语句中使用了不正确的判断符号“ = ”,应该是“ == ”。这个小错误导致了不可以预见的结果,从那时起,我将 C 语言当做一个有精神病的室友,我们可以一起生活、一起工作,但是只要你一不注意它,他就会出来给你捣蛋。
不幸的是外壳脚本和 C 程序一样难于调试,如同 C 一样,外壳命令也是假设你已经知晓了你正在做什么,只有在实际运行中有错误,它才会抛出一个错误提示。除非外壳脚本经过完全的测试,否则 bug 可能存在几个月或几年直到有错的命令执行时,你才可能直到。对于专业的脚本开发人员具有脚本调试工具的扎实的知识是必不可少的。
外壳调试的特点
Bash 有几个开关和选项对于跟踪调试脚本是非常有用的。“ -n ”开关可以使你不用运行脚本就可以检查脚本语法的正确性。通常在开发期间使用这个开关来检查脚本的语法。
$ bash -n bad.sh
bad.sh: line 3: syntax error near unexpected token ‘fi’
bad.sh: line 3: ‘fi’
上面的示例说明了在第三行有一个语法错误,术语 token 表示一个关键字或另一段文本在错误的源代码附近。
如果命令返回错误码,使用“ -o errexit ”选项可以中断脚本的执行。但是循环例外,因此如果 if 命令不能返回非零的状态码, if 命令就不能正确的运行。在最简单的脚本中有了这个选项,就不用使用错误处理了。例如一个错误发生在子外壳中,他不会中断脚本。
如果一个变量没有定义,选项“ -o nounset ”会中止脚本并报告一个错误。这个选项报告的信息是变量名拼写错误。 nounset 并不能保证所有的拼写错误都能识别(看列表 8.1 )。
列表 8.1 nounset.bash
#!/bin/bash
#
# A simple script to list files
shopt -o -s nounset
declare -i TOTAL=0
let “TOTAL=TTOAL+1” # not caught
printf “%s\n” “$TOTAL”
if [ $TTOAL -eq 0 ] ; then # caught
printf “TOTAL is %s\n” “$TOTAL”
fi
“-o xtrace” 选项在执行命令前会显示每一个命令,这个命令执行所有的替换和扩展。
declare -i TOTAL=0
if [ $TOTAL -eq 0 ] ; then
printf “%s\n” “$TOTAL is zero”
fi
如果脚本使用了 xtrace 选项,你会看到下面有点类似的结果:
+ alias ‘rm=rm -i’
+ alias ‘cp=cp -i’
+ alias ‘mv=mv -i’
+ ‘[‘ -f /etc/bashrc ‘]’
+ . /etc/bashrc
+++ id -gn
+++ id -un
+++ id -u
++ ‘[‘ root = root -a 0 -gt 99 ‘]’
++ umask 022
++ ‘[‘ ‘’ ‘]’
+ declare -i TOTAL=0
+ ‘[‘ 0 -eq 0 ‘]’
+ printf ‘%s\n’ ‘0 is zero’
0 is zero
前面 11 行命令是在 Linux 发行版的 profile 脚本中的命令。加号表示脚本是如何进行嵌套的。最后四行是 Bash执行所有的替换和扩展之后的脚本段。注意复合命令(例如: if )被省去了(看列表 8.2 )。
列表 8.2 bad.bash
#!/bin/bash
#
# bad.bash: A simple script to list files
shopt -o -s nounset
shopt -o -s xtrace
declare -i RESULT
declare -i TOTAL=3
while [ $TOTAL -ge 0 ] ; do
let “TOTAL—”
let “RESULT=10/TOTAL”
printf “%d\n” “$RESULT”
done
xtrace 显示了脚本每行的处理过程。在这个示例中,脚本在 while 循环中含有一个错误的结果。使用 xtrace 你可以检查变量,看看 -ge 的两边是否变化,最后停止循环时, TOTAL 是否为零。
$ bash bad.bash
+ declare -i RESULT
+ declare -i TOTAL=3
+ ‘[‘ 3 -ge 0 ‘]’
+ let TOTAL—
+ let RESULT=10/TOTAL
+ printf ‘%d\n’ 5
5
+ ‘[‘ 2 -ge 0 ‘]’
+ let TOTAL—
+ let RESULT=10/TOTAL
+ printf ‘%d\n’ 10
10
+ ‘[‘ 1 -ge 0 ‘]’
+ let TOTAL—
+ let RESULT=10/TOTAL
bad.sh: let: RESULT=10/TOTAL: division by 0 (error token is “L”)
+ printf ‘%d\n’ 10
10
+ ‘[‘ 0 -ge 0 ‘]’
+ let TOTAL—
+ let RESULT=10/TOTAL
+ printf ‘%d\n’ -10
-10
+ ‘[‘ -1 -ge 0 ‘]’
你可以使用 PS4 变量来将跟踪的加号提示符更改为别的提示符。设置调试提示符包括变量: LINENO 可以显示当前的行号,第一行开始为 1 。如果使用了外壳的函数, LINENO 会从函数的第一行开始计数。
调试陷阱
内置 trap 命令可以在每一行 Bash 处理之后执行调试命令。通常 trap 和跟踪组合使用,跟踪提供没有在跟踪中列出额外的信息。
当调试陷阱和跟踪组合在一起,调试陷阱本身也在执行跟踪时被显示出来。这相当于使用 printf 命令,但是比较简练,它显示之前将变量的值替换掉变量名。使用一个空命令(“:”)显示变量的值,而不用执行外壳命令。
列表 8.3 dubug_demo.sh
#!/bin/bash
#
# debug_demo.sh : an example of a debug trap
trap ‘: CNT is now $CNT’ DEBUG
declare -i CNT=0
while [ $CNT -lt 3 ] ; do
CNT=CNT+1
done
当进行跟踪时, CNT 的值在每一行后面显示:
$ bash -x debug_demo.sh
+ trap ‘: CNT is now $CNT’ DEBUG
+ declare -i CNT=0
++ : CNT is now 0
+ ‘[‘ 0 -lt 3 ‘]’
++ : CNT is now 0
+ CNT=CNT+1
++ : CNT is now 1
+ ‘[‘ 1 -lt 3 ‘]’
++ : CNT is now 1
+ CNT=CNT+1
++ : CNT is now 2
+ ‘[‘ 2 -lt 3 ‘]’
++ : CNT is now 2
+ CNT=CNT+1
++ : CNT is now 3
+ ‘[‘ 3 -lt 3 ‘]’
++ : CNT is now 3
版本控制系统( CVS )
在商业环境中,金钱和速度往往是一个问题,它通常不足以建立一个完美的程序。总是上一次的修改或最后一次修改导致程序错误或崩溃。如果这样就需要恢复或尽可能快的无损更正错误。
版本控制系统是一个维护数据文件、脚本和源程序的主备份的程序。这个主备份保存在 repository 目录中。每次程序的增加或修改,它会从新提交到 rspository 中一份更改记录,保存了更改的地方、谁改的、什么时间改的。
CVS 是一个版本控制软件,大部分 Linux 发行版都提供了这个软件。旧程序叫 RCS ( Revision control System 修订控制系统), CVS 可以在多个程序员中共享一个脚本并记录任何修改。它可以使用单个文件,整个目录或整个项目。它们可以将文件分组称之为 modules (模块)。 CVS 时间戳文件,维护着版本号,当两个程序员同时提交相同的程序段时,它会提示出错信息。
CVS 在开源程序开发非常流行。它可以通过配置,使一个项目的程序员遍布世界。
为了使用 CVS ,项目或团队的领导者需要建立一个目录作为版本控制库,已经一个字符了称之为 CVSROOT 。接着你可以定义一个环境变量 CVSROOT ,以便 CVS 介意知道在哪里可以找到这个版本控制库。例如:将 /home/repository 作为你的团队的项目库,你可以在 Bash 中这样设置 CVSROOT 。
$ declare -rx CVSROOT=/home/repository
这个库保存了所有和你项目有关的所有文件、更改日志和共享资源的备份。
要加入到这个库中的软件没有特定的要求。可是,当一个程序要被增加或更新, CVS 会读取整个文件寻找特定的字符串。如果存在, CVS 就使用这个程序备份的最新信息替换这些字符串。虽然 Bash 的意义来说它们不是关键字,但是 CVS 将这些字符串称之为关键字。
$Author$— 提交这个文件的用户名。
$Date$— 提交的日期和时间。
$Header$— 一个标准头包含有 RCS 文件完整路径、修订号、时间( UTC )、作者等等。
$Id$— 除了不包含 RCS 文件的完整路径其他合 $Header$ 相同。
$Name$— 如果使用了标签,标签名用于签出这个文件。
$Locker$— 锁定这个文件的用户登录名。 ( 如果没有锁定为空,除非使用 cvs admin –l 否则通常都是空的 ) 。
$Log$— 提交时提供的的日志消息,通常先于头部信息。已存在的日志信息不会被替换掉,通常是插入新的日志信息。
$RCSfile$— 不包含路径信息的 CVS 文件名。
$Revision$— 分配给修订版的修订号。
$Source$—CVS 文件的全路径名。
$State$— 分配给修订版的状态。
CVS 关键字可以加在脚本的任何位置,但是它们应该出现在注释或有引号的字符串中,这避免了关键字被认为是可执行的外壳命令。
# CVS: $Header$
当这个脚本添加到库中, CVS 会填入头部信息。
# CVS: $Header: /home/repository/scripts/ftp.sh,v 1.1 2001/03/26
20:35:27 kburtch Exp $
CVS 关键字 header 应该放置在脚本的头部。
#!/bin/bash
#
# flush.sh: Flush disks if nobody is on the computer
#
# Ken O. Burtch
# CVS: $Header$
CVS 使用 Linux 命令 cvs 进行操作。 cvs 后面总是跟着一个 CVS 命令和该命令的参数。
为了增加新的项目目录到 CVS 库中,使用 import 命令。 import 命令将当期目录的文件放置在库中指定的目录。 import 也需要一个短字符串用来标示是谁增加到这个项目和另一个字符串用来标示项目的状态。这些字符串本来是注释,它可以是任何字符串:你的登录名和 init-rel 表示初版。
$ cvs import scripts kburtch init-rel
CVS 使用环境变量 EDITOR 或 CVSEDITOR 作为你的缺省文本编辑器。 CVS 不识别 VISUAL 变量,可被编辑的文件显示的内容每行都包含一个前导字符串 CVS :
CVS: ———————————————————————————————————
CVS: Enter Log. Lines beginning with ‘CVS: ‘ are removed automatically
CVS:
CVS: ———————————————————————————————————
当你编辑完, CVS 把你的程序增加到库中,并在更改日志中记录你的添加或修改的内容。并将结果显示在屏幕中。
N scripts/ftp.sh
No conflicts created by this import
N scripts/ftp.sh 这一行表示 CVS 建立了一个新的项目称之为 scripts ,增加了一个 Bash 脚本 ftp.sh 。结果ftp.sh 就被保存在 CVS 库中,并且已经可以在开发团队中共享了。从你的目录中删除这个项目目录也没有问题。事实上,在工作在项目中起作用之前,它必须被删除。
使用 CVS 命令 checkout 可以签出项目。这个 CVS 命令在当前目录中保存项目的副本。也可以使用 CVS 建立 CVS 目录来保存私人数据文件。
为了使用 checkout 签出项目,将当前目录移向你的主目录并输入:
$ cvs checkout scripts
cvs checkout: Updating .
U scripts/ftp.sh
就建立了一个 scripts 的子目录,该目录包含该项目文件的副本。 CVS 维护着 ftp.sh 的原始文件。在你编辑你的项目副本时,其他的程序员也可以签出该项目进行编辑。
为了加入新的文件,可以使用 add 命令,例如加入文件 process_orders.sh:
$ cvs add process_orders.sh
当你修改了你的程序,你可以使用 update 命令定期提交你的程序。如果其他的程序员也对这个程序做了修改, CVS 将更新你的项目目录并将更改反应到脚本中。可以你做的所有更改就不能增加到库中了。
$ cvs update
cvs update: Updating .
有时可能对同一段脚本做出修改, CVS 不能自动合并这些修改。 CVS 称之为 confict (冲突)。并在更新时使用 C 标识。 CVS 标识出在什么地方有冲突,你必须自己编辑脚本以解决这些冲突。
如果在更新后没有其他问题,你可以继续编辑你的源代码。
为了删除已经存在于库中的脚本,使用 rm 命令删除它并执行 CVS 的 update 命令。 CVS 会自动删除该文件。
当你正在修改你的源代码,工作团队的其他人并不会得到这些更改,知道你完成了这些脚本,使用 commit 命令来提交它,提交代码之前,需要删除临时文件以节省库的空间。
$ cvs commit
和 import 命令一样, CVS commit 命令开始一个编辑器并提示你做了哪些更改。
CVS commit 命令也会自动修改该脚本的版本号,通常 CVS 项目的开始版本号为 1.1 ,为了使新的开始版本号为 2.1 ,你可以编辑 $Header$ 行的版本号为 2.0 。 CVS 将该脚本的版本号保存为 2.1 。
在任何时候,你都可以获取脚本或整个项目的日志。 CVS 日志命令显示了所有相关日志条目、脚本和版本号。
$ cvs log project
cvs log: Logging project
RCS file: /home/repository/scripts/ftp.sh,v
Working file: scripts/ftp.sh
head: 1.1
branch: 1.1.1
locks: strict
access list:
symbolic names:
p1: 1.1.1.1
keyword substitution: kv
total revisions: 2; selected revisions: 2
description:
——————————————
revision 1.1
date: 1999/01/13 17:27:33; author: kburtch; state: Exp;
branches: 1.1.1;
Initial revision
——————————————
revision 1.1.1.1
date: 1999/01/13 17:27:33; author: kburtch; state: Exp; lines: +0 -0
Project started
==================================================================
status 命令可以得到相关项目目录的概览和还没有提交到库中脚本的列表。
$ cvs status scripts
cvs status: Examining scripts
==================================================================
File: ftp.sh Status: Up-to-date
Working revision: 1.1.1.1 Wed Jan 13 17:27:33 1999
Repository revision: 1.1.1.1 /home/repository/scripts/ftp.sh,v
Sticky Tag: (none)
Sticky Date: (none)
Sticky Options: (none)
CVS 还有其他的一些特性,这儿就不讨论了。如果要详细的信息参考 CVS 手册。
建立副本
使用 tee 命令可以将命令的输出保存在一个文件中。 tee 这个名字意味着把一个管道分为两个,就像一个 T 连接。标准输出的副本被保存在到文件中而不用从新重定向原来的标准输出。为了同时捕捉标准输出和标准错误,需要在将结果流入 tee 之前重定向标准错误到标准输出中。
$ bash buggy_script.sh >& | tee results.txt
tee –append ( -a )开关将输出增加到已存在的文件的结尾。 -ignore-interrupts ( -i )开关保持 tee 运行,即使它被 Linux 信号中断了。
这个技术并不能保证将标准输入的东西也保存在文件中,为了将脚本运行的所有记录都保存在文件中, linux 可以使用 script 命令。当外壳脚本运行于 script 下,一个叫 typescript 的文件被建立于当前的目录中。 typescript 文件是一个文本文件用来记录出现在外壳会话中的所有东西。
你可以使用 exit 命令来停止记录过程。
$ script
Script started, file is typescript
$ bash buggy_script.sh
...
$ exit
exit
Script done, file is typescript
查看周期性运行的脚本
为了测试 cron 脚本而不用把它们安装在 cron 下,可以使用 watch 命令。 watch 周期性的运行一个命令并显示结果。 watch 每 2 秒运行一次。但是你可以使用 -interval= (或 -n )定义不同的秒数。你也可以只显示结果的不同之处( -differences 或 -d )。或者到目前为止的不同之处( -differences=cumulative )。
使用 time 命令统计执行的时间
有两个命令可以对一个程序或脚本进行运行时间的统计。
Bash 内置命令 time 可以告诉你,一个程序运行花了多长时间。你也可以使用 time 来统计包含有管道的命令的运行时间。除了真实的时间用度,该统计还返回脚本用于系统资源的时间而不是脚本运行命令的时间。
显示结果的格式可以使用 TIMEFORMAT 变量进行设置。 TIMEFORMAT 的格式设置类似于 date 命令使用 % 格式码。
n %%— 显示字符“ % ”。
n %[precision][l]R— 消耗的真实时间,以秒为单位。
n %[precision][l]U— 在用户模式下消耗的 CPU 时间,以秒为单位。
n %[precision][l]S— 在系统模式下,消耗的 CPU 时间,以秒为单位。
n %P— 占用 CPU 的百分比,计算公式为 (%U + %S) / %R 。
precision 表示小数显示的位数,缺省值为 3 。字符“ l ”表示显示的值分为分、秒。如果没有 TIMEFORMAT 变量, Bash 使用 \nreal\t%3lR\nuser\t%3lU\nsys%3lS 。
$ unset TIMEFORMAT
$ time ls > /dev/null
real 0m0.018s
user 0m0.010s
sys 0m0.010s
$ declare -x TIMEFORMAT=”%P”
$ time ls > /dev/null
75.34
$ declare -x TIMEFORMAT=”The real time is %lR”
$ time ls > /dev/null
The real time is 0m0.023s
注意:多次运行脚本程序,可能得到的消耗时间不尽相同,可能是计算机上运行的其他程序的影响。为了得到最准确的消耗时间,可以多次运行该脚本,取其最小值。
Linux 也有一个 time 命令,此命令的不同之处在于它不可以包含管道,但是它可以显示一些额外的统计信息。为了使用此命令,必须在前面加上 command 命令来替换掉 Bash 的 time 命令。
$ command time myprog
3.09user 0.95system 0:05.84elapsed 69%CPU(0avgtext+0avgdata 0maxresident)k
0inputs+0outputs(4786major+4235minor)pagefaults 0swaps
和 Bash 的 time 一样, Linux 的 time 也可以将显示结果进行格式化设置。该格式化信息保存在 TIME 变量中,它可以显示的使用 -format ( -f )开关标示。
n %%— 显示字符 %%.
n %E— 程序占用的真正时间,显示格式为小时:分钟:秒数
n %e— 程序使用的真正时间,以秒为单位。
n %S— 系统占用 CPU 的秒数。
n %U— 用户占用 CPU 的秒数。
n %P— 程序使用 CPU 的百分比。
n %M— 程序在内存中的最大尺寸,以千字节为单位。
n %t— 程序的常驻区的平均大小,以千字节为单位。
n %D— 非共享数据区的平均大小。
n %p— 非共享堆栈的平均大小,以千字节为单位。
n %X— 共享文本区的平均大小。
n %Z— 系统页大小,以字节为单位。
n %F— 主页错误号。
n %R— 此页错误号。
n %W— 交换进程的次数。
n %c— 时间片上下文开关的编号。
n %w— 自发的上下文开关的编号。
n %I— 输入的文件系统的编号。
n %O— 输出的文件系统的编号。
n %r— 已接收的 socket 消息的编号。
n %s— 已发送的 socket 消息的编号。
n %k— 接受的信号编号。
n %c —命令行。
n %x— 退出状态。
和你硬件无关的统计显示为零。
$ command time grep ken /etc/aliases
Command exited with non-zero status 1
0.00user 0.00system 0:00.02elapsed 0%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (142major+19minor)pagefaults 0swaps
$ command time —format “%P” grep ken /etc/aliases
Command exited with non-zero status 1
0%
$ command time —format “Major faults = %F” grep ken /etc/aliases
Command exited with non-zero status 1
Major faults = 141
-portablility(-p) 开关强迫 time 遵循 POSIX 标准,和 Bash 的 time –p 一样,关闭了许多扩展特性。
$ command time —portability grep ken /etc/aliases
Command exited with non-zero status 1
real 0.00
user 0.00
sys 0.00
使用 -output ( -o )开关可以将结果重新定向到一个文件中,或者使用 -append ( -a )开关将结果添加到一个文件中。 -verbose ( -v )选项可以得到一份详细的统计报告。
建立手册
Linux 手册页是一个特殊文本文件,使用 groff 程序进行格式化。 groff 基于以前的 Unix 程序 troff (打印机使用的)和 nroff (终端使用的)程序。 Troff 有 1973 年 Joseph E Ossanna 创建,用于给脚本程序建立一个小的手册页,使用户可以在线访问此页。
把你的项目的手册页放置在第 9 段。通常,第 9 段用于对 Linux 内核进行说明,但是现在安装传统 Unix 的说法,第 9 段用于用户自己使用。第九段的手册页保存在 /usr/share/man/man9 的目录中。如果你不想访问这个目录,可以在你的主目录中建立 man9 的目录,并将你的手册页保存在此目录。同时在你的 Bash profile 文件中设置 MANPATH 变量为 $HOME 。手册会寻找保存在主目录下 man9 目录中的内容。
$ mkdir ~/man9
$ declare -x MANPATH=”$HOME:/usr/share/man “
手册页是一个文本文件,它嵌入了一些 groff 标签代码(或宏)这些标签码有点像 Web 页中的 HTML 标签,可以控制空格、布局和页中的图像。你也可以定义自己的 groff 码,这些码总是出现在一行的开始处和一段文本的开始处。
下面是一些 groff 标签码的示例:
.\”$Id$
.TH MAN 9 “25 July 1993” “Linux” “Nightlight Corporation Manual”
.SH NAME ftp.sh \- script to FTP orders to suppliers
.SH SYNOPSIS
.B ftp.sh
.I file
每行开始的 groff 码都有一个点符号( . ),后面有一个或两个字符。例如: .B 表示该行文本为粗体(想 HTML中的 <b> 标签。
groff 预定义类一些宏,它们属于手册页的第 7 段的内容( man 7 man )。一些比较通用 groff 码如下所示:
n .B— 粗体
n .I— 斜体
n .PP— 开始一个新段落
n .RS i— 缩进 i 个字符
n .RE— 结束上一个 RS 缩进
n .UR u— 此文本使用 URL 链接
n .UE— 结束使用 .UR 的链接
n .\”— 表明为注释内容
n .TH— 页的标题
n .SH— 一个子标题
虽然手册页没有严格的规范,但是大部分手册页都包含一个或多个这样的段: SYNOPSIS , DESCRIPTION , RETURN VALUES , EXIT STA-TUS , OPTIONS , USAGE , FILES , ENVIRONMENT , DIAGNOSTICS, SECURITY , CONFORMING TO , NOTE , BUGS , AUTHOR 和 SEE ALSO 。如果使用了 CVS ,你还可以在 VERSION 段中包含 CVS 关键字 $ID$ 。
最容易的建立手册页的方法是拷贝一个已有的手册页然后修改它。
列表 8.4 展示了一个完整的小手册页。
列表 8.4
./”man page supply_ftp.sh.9
.TH “SUPPLY_FTP.SH” 9 “25 May 2001” “Nightlight” “Nightlight Corporation Manual”
.SH NAME
supply_ftp.sh \- Bash script to FTP orders to suppliers
.SH SYNOPSIS
.B supply_ftp.sh
.I file
.I supplier
.SH DESCRIPTION
.B supply_ftp.sh
sends orders via FTP to suppliers.
.I file
is the name of the file to send.
.I supplier
is the name of the supplier. The suppliers and their FTP account information
are stored in the text file
.I supplier.txt
.SH RETURN VALUES
The script returns 0 on a successful FTP and 1 if the FTP failed.
.SH AUTHOR
Created by Ken O. Burtch.
.SH FILES
/home/data/supplier.txt
.SH VERSION
$Id$
这段内容显示出来的样式如下所示:
SUPPLY_FTP.SH(9) Nightlight Corporation Manual SUPPLY_FTP.SH(9)
NAME
supply_ftp.sh - Bash script to FTP orders to suppliers
SYNOPSIS
supply_ftp.sh file supplier
DESCRIPTION
supply_ftp.sh sends orders via FTP to suppliers. file is thename of the file to send. supplier is the name of the supplier.
The suppliers and their FTP account information are stored inthe text file supplier.txt
RETURN VALUES
The script returns 0 on a successful FTP and 1 if the FTP failed.
AUTHOR
Created by Ken O. Burtch.
FILES
/home/data/supplier.txt
VERSION
$Id$
Nightlight 25 May 2001 1
$Id$ 使用 CVS 提交时会被更新。
less 命令知道如何显示手册页。可以使用这个命令来测试你写好的手册页,之后可以提交它。
有些 Linux 发行版提供了一个命令 man2html ,可以将手册转换为 Web 页。例如转换 my_man_page.9 ,可以这样输入这个命令:
$ man2html < my_man_page.9 > my_man_page.html
然后使用网络浏览器检查结果。
源代码的修补
Linux 的 diff 命令可以列出两个或多个文件的不同之处。
使用合适的开关, diff 会建立一个 patch 文件,它包含了一份需要更改一组文件到另一组文件的更新列表。
$ diff -u —recursive —new-file older_directory newer_directory > update.diff
例如:你有一个脚本用于统计当期目录中的文件个数,如列表 8.5 所示:
列表 8.5
#!/bin/bash
#
# file_count: count the number of files in the current directory.
# There are no parameters for this script.
shopt -s -o nounset
declare -rx SCRIPT=${0##*/} # SCRIPT is the name of this script
declare -rx ls=”/bin/ls” # ls command
declare -rx wc=”/usr/bin/wc” # wc command
# Sanity checks
if test -z “$BASH” ; then
printf “Please run this script with the BASH shell\n” >&2
exit 192
fi
if test ! -x “$ls” ; then
printf “$SCRIPT:$LINENO: the command $ls 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
ls -1 | wc -l
exit 0
你后来认为使用 exit $? 比使用 exit 0 更好,你更新了代码。并使用下面的命令:
$ diff -u —recursive —new-file older.sh newer.sh > file_count.diff
建立了 patch 文件,它的内容如下:
@@ -26,5 +26,5 @@
ls -1 | wc -l
-exit 0
+exit $?
“ - ”表示 exit 0 这一行被删除。“ + ”表示 exit $? 这一行被插入。接着使用新脚本更新旧脚本。
Linux 的 patch 命令用于将一个 patch 文件(后缀名是 .diff )更新一个旧文件,并要使用 -pl 和 -s 开关。
$ cd older_directory
$ patch -p1 -s < update.diff
在 file_count 脚本的示例中,因为补丁由一个文件建立而不是一个目录, patch 要求需要有要更新的文件名。
$ patch -p1 -s < file_count.diff
The text leading up to this was:
—————————————
|—- older.sh Tue Feb 26 10:52:55 2002
|+++ newer.sh Tue Feb 26 10:53:56 2002
—————————————
File to patch: older.sh
文件 older.sh 现在和 newer.sh 一样了。
文件归档
shell archive (或 shar )是一个文本文件的集合或将多个脚本压缩为一个单独的文件。在脚本中的数据在这儿表示为文件。二进制文件被 Linux 的 uuencode 命令转换为文本文件。 Shell archive 是一个自解压的归档文件。当外壳脚本执行时,在归档文件中的这些文件被解压缩。
Linux 的 shar 命令是一个新的建立外壳归档文件的工具。
为了将 orders. 提醒他文件保存为外壳归档文件,使用下面的命令:
$ shar orders.txt > orders.shar
shar: Saving orders.txt (text)
解压这个文件,使用下面的命令:
$ bash orders.shar
x - creating lock directory
x - extracting orders.txt (text)
建立当前目录中所有文件的归档,使用下面命令:
$ shar * > myproject.shar
shar 命令闺房当前目录时,当前目录下的所有子目录和文件都被归档。
shar 有大量的开关,详细的使用见本章后面的命令参考。
还有一个 unshar 命令,并不完全是 shar 命令的相反功能。它是从一个电子邮件中读出 shar 归档文件,接着使用 bash 命令进行解压。
Shell archive 用于早期的新闻组压缩文件,它并不是特别的有效率,但是他们提供了一个不常用的外壳脚本的示例,并假设在所有的 Linux 发行版中都有效。
虽然外壳脚本程序也许不会象我的作业那样使你的屏幕布满竖线并退出你的软盘,但是它们会很难调试。了解一些调试中用到的命令,会使你的调试更加容易并能更快的找到和修复你的脚本程序。有了版本控制、打补丁、建立副本,你可以和其他程序员一起工作、处理问题,更新程序,隔离问题等等。有了这些、在下一章中你会发现这些工具需要时即来。
命令参考
tee 命令开关
n — append (or -a)— 将结果增加到要输出的文件的结尾。
n — ignore-interrupts (or -i)— 即使被 Linux 信号中断也保持 tee 命令的运行。
Linux time 命令开关
n — portability (or -p)— 遵循 POSIX 标准。
n — output (or -o)— 直接输出到一个文件中
n — append (or -a)— 将结果添加到一个文件中。
n — verbose (or -v)— 道道详细的统计结果。
Bash Time 命令开关
n %%— 显示字符“ % ”。
n %[precision][l]R— 消耗的真实时间,以秒为单位。
n %[precision][l]U— 在用户模式下,消耗的 CPU 秒数。
n %[precision][l]S— 在系统模式下,消耗的 CPU 秒数。
n %P—CPU 消耗的百分比,计算公式为 (%U + %S) / %R 。
Linux Time 命令格式码
n %%— 显示字符 %%.
n %E— 程序占用的真正时间,显示格式为小时:分钟:秒数
n %e— 程序使用的真正时间,以秒为单位。
n %S— 系统占用 CPU 的秒数。
n %U— 用户占用 CPU 的秒数。
n %P— 程序使用 CPU 的百分比。
n %M— 程序在内存中的最大尺寸,以千字节为单位。
n %t— 程序的常驻区的平均大小,以千字节为单位。
n %D— 非共享数据区的平均大小。
n %p— 非共享堆栈的平均大小,以千字节为单位。
n %X— 共享文本区的平均大小。
n %Z— 系统页大小,以字节为单位。
n %F— 主页错误号。
n %R— 此页错误号。
n %W— 交换进程的次数。
n %c— 时间片上下文开关的编号。
n %w— 自发的上下文开关的编号。
n %I— 输入的文件系统的编号。
n %O— 输出的文件系统的编号。
n %r— 已接收的 socket 消息的编号。
n %s— 已发送的 socket 消息的编号。
n %k— 接受的信号编号。
n %c —命令行。
n %x— 退出状态。
外壳调试( Shell Debugging )选项
n -o errexit— 如果命令返回了错误码则终端外壳脚本的执行。
n -o nounset— 如果使用到的变量没有设置或不存在则终端执行返回错误。
n -o xtrace— 在命令执行之前显示每一个命令。
shar 命令开关
n — quiet (—silent or -q)— 当建立归档文件时隐藏状态消息。
n — intermix-type (-p)— 运行 packing 选项应用到单个文件而不是所有的文件。
n — stdin-file-list (or -S)— 从标准输入中读取文件列表并打包。
n — output-prefix=s (or -o s)— 归档文件按照序号进行命名。 ( 当使用 -whole-size-limit 选项时 )。
n — whole-size-limit=k (or -l k)— 限制归档文件的大小,以千字节为单位,但是不分割文件。
n — split-size-limits=k ( or -L k)— 限制归档文件的大小,如果必要就进行文件的分割。
n — archive-name=name (or -n name)— 增加归档文件名到头部。
n — submitter=email (or -n email)— 增加提交者名字到头部。
n — net-headers— 允许自动生成头部。
n — cut-mark (or -c)— 在归档文件的开始处增加 cut here 行。
n — mixed-uuencode (or -M)—( 缺省 ) 运行文本和编码的二进制文件。
n — text-files (or -T)— 强制将所有的文件以文本形式对待。
n — uuencode (or -B)— 将所有的文件以二进制文件形式对待,使用 uuencode 编码进行打包,这会增加归档文件的大小,在接收方以 uudecode 编码序列进行解码。
n — gzip (or -z)— 使用 gzip 压缩文件,然后再使用 uudecode 进行编码。
n — level-for-gzip=L (or -G L)— 设置压缩级别 (1–9) 。
n — compress (or -Z)— 使用 Linux 压缩命令进行压缩然后使用 uudecode 进行编码。
n — bits-per-code=B (or -b B)— 设置压缩字的尺寸 ( 缺省为 12 bits) 。
n — no-character-count (or -w)— 阻止字符数检查。
n — no-md5-digest (or -D)— 阻止 MD5 效验和检查。
n — force-prefix (or -F)— 每行使用前缀字符。
n — here-delimiter=d (or -d d)— 使用 d 作为文件的分割而不是 SHAR_EOF 。
n — vanilla-operation (or -V)— 建立一个归档文件,可以使用最少的 Linux 命令进行解压。
n — no-piping (or -P)— 在 shar 文件中不使用管道。
n — no-check-existing (or -x)— 覆盖已经存在的 shar 文件。
n — query-user (or -X)— 覆盖文件前提示用户。
n — no-timestamp (or -m)— 解包时不修改时间戳。
n — quiet-unshar (or -Q)— 丢弃抽取的注释。
n — basename (or -f)— 抽取时丢弃路径名,将所有的文件保存在当前目录。
n — no-i18n— 在外壳归档文件中不进行国际化。
— print-text-domain-dir— 显示 shar 查找目录, shar 用于在不同的语言中查找消息文件。