使用Bash编写Linux Shell脚本-8.调试和版本控制

当我还在布鲁克大学上学的时候,Macquarium实验室中充满了苹果公司的Macintosh Plus电脑。一天,我在为第三年的操作系统课程准备一个程序。我的一个小程序报告没有错误,当我运行它时,黑白色的桌面上出现了竖条,我的软盘被退出来了,计算机然后从新启动。经过更仔细的检查,我意识到我在if语句中使用了不正确的判断符号“=”,应该是“==”。这个小错误导致了不可以预见的结果,从那时起,我将C语言当做一个有精神病的室友,我们可以一起生活、一起工作,但是只要你一不注意它,他就会出来给你捣蛋。

不幸的是外壳脚本和C程序一样难于调试,如同C一样,外壳命令也是假设你已经知晓了你正在做什么,只有在实际运行中有错误,它才会抛出一个错误提示。除非外壳脚本经过完全的测试,否则bug可能存在几个月或几年直到有错的命令执行时,你才可能直到。对于专业的脚本开发人员具有脚本调试工具的扎实的知识是必不可少的。

外壳调试的特点

Bash有几个开关和选项对于跟踪调试脚本是非常有用的。“-n”开关可以使你不用运行脚本就可以检查脚本语法的正确性。通常在开发期间使用这个开关来检查脚本的语法。

$ bash -n bad.sh

bad.sh: line 3: syntaxerror 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” # notcaught

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 iszero”

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 iszero’

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 -rxCVSROOT=/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 disksif nobody is on the computer

#

# Ken O. Burtch

# CVS: $Header$

CVS使用Linux命令cvs进行操作。cvs后面总是跟着一个CVS命令和该命令的参数。

为了增加新的项目目录到CVS库中,使用import命令。import命令将当期目录的文件放置在库中指定的目录。import也需要一个短字符串用来标示是谁增加到这个项目和另一个字符串用来标示项目的状态。这些字符串本来是注释,它可以是任何字符串:你的登录名和init-rel表示初版。

$ cvs import scriptskburtch init-rel

CVS使用环境变量EDITOR或CVSEDITOR作为你的缺省文本编辑器。CVS不识别VISUAL变量,可被编辑的文件显示的内容每行都包含一个前导字符串CVS:

CVS:———————————————————————————————————

CVS: Enter Log. Linesbeginning with ‘CVS: ‘ are removed automatically

CVS:

CVS:———————————————————————————————————

当你编辑完,CVS把你的程序增加到库中,并在更改日志中记录你的添加或修改的内容。并将结果显示在屏幕中。

N scripts/ftp.sh

No conflicts created bythis 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 addprocess_orders.sh

当你修改了你的程序,你可以使用update命令定期提交你的程序。如果其他的程序员也对这个程序做了修改,CVS将更新你的项目目录并将更改反应到脚本中。可以你做的所有更改就不能增加到库中了。

$ cvs update

cvs update: Updating .

有时可能对同一段脚本做出修改,CVS不能自动合并这些修改。CVS称之为confict(冲突)。并在更新时使用C标识。CVS标识出在什么地方有冲突,你必须自己编辑脚本以解决这些冲突。

如果在更新后没有其他问题,你可以继续编辑你的源代码。

为了删除已经存在于库中的脚本,使用rm命令删除它并执行CVS的update命令。CVS会自动删除该文件。

当你正在修改你的源代码,工作团队的其他人并不会得到这些更改,知道你完成了这些脚本,使用commit命令来提交它,提交代码之前,需要删除临时文件以节省库的空间。

$ cvs commit

和import命令一样,CVScommit命令开始一个编辑器并提示你做了哪些更改。

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/1317:27:33; author: kburtch; state: Exp;

branches: 1.1.1;

Initial revision

——————————————

revision 1.1.1.1

date: 1999/01/1317:27:33; author: kburtch; state: Exp; lines: +0 -0

Project started

==================================================================

status命令可以得到相关项目目录的概览和还没有提交到库中脚本的列表。

$ cvs status scripts

cvs status: Examiningscripts

==================================================================

File: ftp.sh Status:Up-to-date

Working revision: 1.1.1.1Wed 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 istypescript

$ bash buggy_script.sh

$ exit

exit

Script done, file istypescript

查看周期性运行的脚本

为了测试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 -xTIMEFORMAT=”%P”

$ time ls > /dev/null

75.34

$ declare -xTIMEFORMAT=”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.95system0:05.84elapsed 69%CPU(0avgtext+0avgdata 0maxresident)k

0inputs+0outputs(4786major+4235minor)pagefaults0swaps

和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 withnon-zero status 1

0.00user 0.00system0:00.02elapsed 0%CPU (0avgtext+0avgdata 0maxresident)k

0inputs+0outputs(142major+19minor)pagefaults 0swaps

$ command time —format“%P” grep ken /etc/aliases

Command exited withnon-zero status 1

0%

$ command time —format“Major faults = %F” grep ken /etc/aliases

Command exited withnon-zero status 1

Major faults = 141

-portablility(-p)开关强迫time遵循POSIX标准,和Bash的time –p一样,关闭了许多扩展特性。

$ command time—portability grep ken /etc/aliases

Command exited withnon-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 -xMANPATH=”$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 CorporationManual”

.SH NAME

supply_ftp.sh /- Bash script to FTP orders tosuppliers

.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 accountinformation

are stored in the text file

.I supplier.txt

.SH RETURN VALUES

The script returns 0 on a successful FTP and 1 ifthe FTP failed.

.SH AUTHOR

Created by Ken O. Burtch.

.SH FILES

/home/data/supplier.txt

.SH VERSION

$Id$

这段内容显示出来的样式如下所示:

SUPPLY_FTP.SH(9)Nightlight Corporation ManualSUPPLY_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 ofthe file to send. supplier is the name of the supplier.

The suppliers and their FTP account information are stored inthetext 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 thecurrent 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 BASHshell/n” >&2

exit 192

fi

if test ! -x “$ls” ; then

printf “$SCRIPT:$LINENO: the command $ls is notavailable — aborting/n “ >&2

exit 192

fi

if test ! -x “$wc” ; then

printf “$SCRIPT: $LINENO: the command $wc is notavailable — 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 tothis 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 lockdirectory

x – extractingorders.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用于在不同的语言中查找消息文件。

见所未见,闻所未闻。

使用Bash编写Linux Shell脚本-8.调试和版本控制

相关文章:

你感兴趣的文章:

标签云: