SHELL十三问
2008125163340
SHELL十三问之五:var=value?export 前后差在哪?
文章整理: 文章来源: 网络
这次让我们暂时丢开command line,先来了解一下bash变量(variable)吧...
所谓的变量,就是就是利用一个特定的"名称"(name)来存取一段可以变化的"值"(value)。
*设定(set)*
在bash中,你可以用"="来设定或重新定义变量的内容:
name=value
在设定变量的时侯,得遵守如下规则:
*等号左右两边不能使用区隔符号(IFS),也应避免使用shell的保留字符(meta charactor)。
*变量名称不能使用$符号。
*变量名称的第一个字母不能是数字(number)。
*变量名称长度不可超过256个字母。
*变量名称及变量值之大小写是有区别的(case sensitive)。
如下是一些变量设定时常见的错误:
A= B :不能有IFS
1A=B :不能以数字开头
$A=B :名称不能有$
a=B :这跟a=b是不同的
如下则是可以接受的设定:
A=" B" :IFS被关闭了(请参考前面的quoting章节)
A1=B :并非以数字开头
A=$B :$可用在变量值内
This_Is_A_Long_Name=b :可用_连接较长的名称或值,且大小写有别。
*变量替换(substitution)*
Shell之所以强大,其中的一个因素是它可以在命令行中对变量作替换(substitution)处理。
在命令行中使用者可以使用$符号加上变量名称(除了在用=号定义变量名称之外),
将变量值给替换出来,然后再重新组建命令行。
比方:
$ A=ls
$ B=la
$ C=/tmp
$ $A -$B $C
(注意:以上命令行的第一个$是shell prompt,并不在命令行之内。)
必需强调的是,我们所提的变量替换,只发生在command line上面。(是的,让我们再回到command line吧�u)
仔细分析最后那行command line,不难发现在被执行之前(在输入CR字符之前),
$符号会对每一个变量作替换处理(将变量值替换出来再重组命令行),最后会得出如下命令行:
ls -la /tmp
还记得第二章我请大家"务必理解"的那两句吗?若你忘了,那我这里再重贴一遍:
若从技术细节来看,shell会依据IFS(Internal Field Seperator)将command line所输入的文字给拆解为"字段"(word)。
然后再针对特殊字符(meta)先作处理,最后再重组整行command line。
这里的$就是command line中最经典的meta之一了,就是作变量替换的�u
在日常的shell操作中,我们常会使用echo命令来查看特定变量的值,例如:
$ echo $A -$B $C
我们已学过,echo命令只单纯将其argument送至"标准输出"(STDOUT,通常是我们的荧幕)。
所以上面的命令会在荧幕上得到如下结果:
http://www.818198.com Page 1
SHELL十三问
ls -la /tmp
这是由于echo命令在执行时,会先将$A(ls)、$B(la)、跟$C(/tmp)给替换出来的结果。
利用shell对变量的替换处理能力,我们在设定变量时就更为灵活了:
A=B
B=$A
这样,B的变量值就可继承A变量"当时"的变量值了。
不过,不要以"数学罗辑"来套用变量的设定,比方说:
A=B
B=C
这样并不会让A的变量值变成C。
上面是单纯定义了两个不同名称的变量:A与 B,它们的值分别是B与 C。再如:
A=B
B=$A
A=C
同样也不会让B的值换成C。
若变量被重复定义的话,则原有旧值将被新值所取代。(这不正是"可变的量"吗? ^_^)
当我们在设定变量的时侯,请记着这点:
*用一个名称储存一个数值
仅此而已。
此外,我们也可利用命令行的变量替换能力来"扩充"(append)变量值:
A=B:C:D
A=$A:E
这样,第一行我们设定A的值为"B:C:D",然后,第二行再将值扩充为"A:B:C:E"。
上面的扩充范例,我们使用区隔符号( : )来达到扩充目的,
要是没有区隔符号的话,如下是有问题的:
A=BCD
A=$AE
因为第二次是将A的值继承$AE的提换结果,而非$A再加E�u
要解决此问题,我们可用更严谨的替换处理:
A=BCD
A=${A}E
上例中,我们使用{}将变量名称的范围给明确定义出来,
如此一来,我们就可以将A的变量值从BCD给扩充为BCDE。
(提示:关于${name}事实上还可做到更多的变量处理能力,这些均属于比较进阶的变量处理,现阶段暂时不介绍了,请大
家自行参考数据。如CU的贴子:
http://www.chinaunix.net/forum/viewtopic.php?t=201843)
* export *
严格来说,我们在当前shell中所定义的变量,均属于"本地变量"(local variable),
只有经过export命令的"输出"处理,才能成为环境变量(environment variable):
$ A=B
$ export A
或:
$ export A=B
经过export输出处理之后,变量A就能成为一个环境变量供其后的命令使用。
在使用export 的时侯,请别忘记shell在命令行对变量的"替换"(substitution)处理,
比方说:
$ A=B
http://www.818198.com Page 2
SHELL十三问
$ B=C
$ export $A
上面的命令并未将A输出为环境变量,而是将B作输出,
这是因为在这个命令行中,$A会首先被提换出B然后再"塞回"作export的参数。
要理解这个export,事实上需要从process的角度来理解才能透彻。
我将于下一章为大家说明process的观念,敬请留意。
*取消变量*
要取消一个变量,在bash中可使用unset命令来处理:
unset A
与export一样,unset命令行也同样会作变量替换(这其实就是shell的功能之一),
因此:
$ A=B
$ B=C
$ unset $A
事实上所取消的变量是B而不是A。
此外,变量一旦经过unset取消之后,其结果是将整个变量拿掉,而不仅是取消其变量值。
如下两行其实是很不一样的:
$ A=
$ unset A
第一行只是将变量A设定为"空值"(null value),但第二行则让变量A不在存在。
虽然用眼睛来看,这两种变量状态在如下命令结果中都是一样的:
$ A=
$ echo $A
$ unset A
$ echo $A
请学员务必能识别null value与unset的本质区别,这在一些进阶的变量处理上是很严格的。
比方说:
$ str= #设为null
$ var=${str=expr} #定义var
$ echo $var
$ echo $str
$ unset str #取消
$ var=${str=expr} #定义var
$ echo $var
expr
$ echo $str
expr
聪明的读者(yes, you!),稍加思考的话,
应该不难发现为何同样的var=${str=expr}在null与unset之下的不同吧?
若你看不出来,那可能是如下原因之一:
a.你太笨了
b.不了解 var=${str=expr} 这个进阶处理
c.对本篇说明还没来得及消化吸收
e.我讲得不好
不知,你选哪个呢?.... ^_^
http://www.818198.com Page 3
SHELL十三问
再来解释一下var=${str=expr}:
首先,var=$str这个大家都可理解吧。
而接下来的思考方向是,究竟$str这个变量是如下哪一种情况呢:
1) unset
2) null
3) not null
1)假如是unset,那么var=${str=expr}的结果将是:
var=expr
str=expr
2)假如是null,那var=${str=expr}的结果是:
var=
str=
3)假如是not null (比方为xyz ),那var=${str=expr}之结果是:
var=xyz
str=xyz
测试如下:
$ showvar() {
> var=${str=expr}
> echo \$var is $var
> echo \$str is $str
> }
$ unset str
$ showvar
$var is expr
$str is expr
$ str=
$ showvar
$var is
$str is
$ str=xyz
$ showvar
$var is xyz
$str is xyz
接下来,再来看看var=${str:=expr}好了:
1) $str为not set:
var=expr
str=expr
2) $str为null:
var=expr
str=expr
3) $str为not null (str=xyz):
var=xyz
str=xyz
测试如下:
$ showvar() {
> var=${str:=expr}
> echo \$var is $var
http://www.818198.com Page 4
SHELL十三问
> echo \$str is $str
> }
$ unset str
$ showvar
$var is expr
$str is expr
$ str=
$ showvar
$var is expr
$str is expr
$ str=xyz
$ showvar
$var is xyz
$str is xyz
最后比教一下${str=expr}与${str:=expr}:
*两者在not set与not null都一致
*但当null值时,${str=expr}会将$var与$str都设为null,但${str:=expr}则设为expr
从这个再延伸出其它模拟,不防请大家"实作"观查一下有何不同?
var=${str-expr} vs var=${str:-expr}
var=${str+expr} vs var=${str:+expr}
var=${str?expr} vs var=${str:?expr}
$ showvar() {
> var=${str-expr}
> echo \$var is $var
> echo \$str is $str
> }
$ unset str
$ showvar
$var is expr
$str is
$ str=
$ showvar
$var is
$str is
$ str=xyz
$ showvar
$var is xyz
$str is xyz
$ showvar() {
> var=${str:-expr}
> echo \$var is $var
> echo \$str is $str
> }
$ unset str
$ showvar
http://www.818198.com Page 5
SHELL十三问
$var is expr
$str is
$ str=
$ showvar
$var is expr
$str is
$ str=xyz
$ showvar
$var is xyz
$str is xyz
可以看出来${str-expr}与${str:-expr}
*两者在$str为not set与not null都一致
*但当null值时,${str-expr}会将$var与$str都设为null,但${str:-expr}则将$var设为expr
$ showvar() {
> var=${str+expr}
> echo \$var is $var
> echo \$str is $str
> }
$ unset str
$ showvar
$var is
$str is
$ str=
$ showvar
$var is expr
$str is
$ str=xyz
$ showvar
$var is expr
$str is xyz
$ showvar() {
> var=${str:+expr}
> echo \$var is $var
> echo \$str is $str
> }
$ unset str
$ showvar
$var is
$str is
$ str=
$ showvar
$var is
$str is
$ str=xyz
$ showvar
$var is expr
http://www.818198.com Page 6
SHELL十三问
$str is xyz
可以看出来${str+expr}与${str:+expr}
*两者在$str为not set与not null都一致
*但当null值时,${str+expr}会将$var与$str都设为expr,但${str:+expr}则将$var设为null
$ showvar() {
> var=${str?expr}
> echo \$var is $var
> echo \$str is $str
> }
$ unset str
$ showvar
expr
$ str=
$ showvar
$var is
$str is
$ str=xyz
$ showvar
$var is xyz
$str is xyz
$ showvar() {
> var=${str:?expr}
> echo \$var is $var
> echo \$str is $str
> }
$ unset str
$ showvar
expr
$ str=
$ showvar
expr
$ str=xyz
$ showvar
$var is xyz
$str is xyz
可以看出来${str?expr}与${str:?expr}
*两者在$str为not set与not null都一致
*但当null值时,${str?expr}会将$var与$str都设为null,但${str:?expr}则将$var设为expr
综上所述:
$var=${str=expr}与$var=${str:=expr}
$var=${str-expr}与$var=${str:-expr}
$var=${str+expr}与$var=${str:+expr}
$var=${str?expr}与$var=${str:?expr}
* 两者在 $str 为not set 与 not null 都一致
* 但当 null 值时不一致
分别描述如下:
$var=${str:-expr}的功能:
http://www.818198.com Page 7
SHELL十三问
当str非空的时候var赋值为str,否则(包括not set和null)将var赋值为expr
$var=${str-expr}的功能:
当str非空的时候var赋值为str,为空的时候var设置为null,not set的时候将var赋值为expr
$var=${str:=expr}的功能:
当str非空的时候var赋值为str,否则(包括not set和null)将var和str均赋值为expr
$var=${str=expr}的功能:
当str非空的时候var赋值为str,为空的时候var设置为null,not set的时候将var和str均赋值为expr
$var=${str:?expr}的功能:
当str非空的时候var赋值为expr,否则将expr写入标准错误后退出
$var=${str?expr}的功能:
当str非空的时候var赋值为expr,为空的时候var设置为null,not set的时候将expr写入标准错误后退出
$var=${str:+expr}的功能:
当str非空的时候var赋值为expr,否则将不做任何操作
$var=${str+expr}的功能:
当str非空的时候var赋值为expr,为空的时候var设置为expr,not set的时候不做任何操作
看来还是带有冒号的简单,只有两种情况:非空和其他,呵呵
[]
SHELL十三问之六:exec 跟 source 差在哪?
文章整理: 文章来源: 网络
这次先让我们从CU Shell版的一个实例贴子来谈起吧:
例中的提问是:
cd /etc/aa/bb/cc可以执行,但是把这条命令写入shell时shell不执行!
这是什么原因呀!
我当时如何回答暂时别去深究,先让我们了解一下进程(process)的观念好了。
首先,我们所执行的任何程序,都是由父进程(parent process)所产生出来的一个子进程(child process),子进程在结束后,
将返回到父进程去。此一现像在Linux系统中被称为 fork。
当子进程被产生的时候,将会从父进程那里获得一定的资源分配、及(更重要的是)继承父进程的环境�u
让我们回到上一章所谈到的"环境变量"吧:
*所谓环境变量其实就是那些会传给子进程的变量。
简单而言,"遗传性"就是区分本地变量与环境变量的决定性指标。
然而,从遗传的角度来看,我们也不难发现环境变量的另一个重要特征:
*环境变量只能从父进程到子进程单向继承。换句话说:在子进程中的环境如何变更,均不会影响父进程的环境。
接下来,再让我们了解一下命令脚本(shell script)的概念。
所谓的shell script讲起来很简单,就是将你平时在shell prompt后所输入的多行command line依序写入一个文件去而已。
其中再加上一些条件判断、互动界面、参数运用、函数调用、等等技巧,得以让script更加"聪明"的执行,但若撇开这些技
巧不谈,我们真的可以简单的看成script只不过依次执行预先写好的命令行而已。
再结合以上两个概念(process + script),那应该就不难理解如下这句话的意思了:
*正常来说,当我们执行一个shell script时,其实是先产生一个sub-shell的子进程,然后sub-shell再去产生命令行的子进程。
然则,那让我们回到本章开始时所提到的例子再从新思考:
cd /etc/aa/bb/cc可以执行,但是把这条命令写入shell时shell不执行!
这是什么原因呀!
我当时的答案是这样的:
因为,一般我们跑的shell script是用subshell去执行的。
http://www.818198.com Page 8
SHELL十三问
从process的观念来看,是parent process产生一个child process去执行,当child结束后,会返回parent,但parent的环境是不会
因child的改变而改变的。
所谓的环境元数很多,凡举effective id, variable, workding dir等等...
其中的workding dir ($PWD)正是楼主的疑问所在:
当用subshell来跑script的话,sub shell的$PWD会因为cd而变更,
但当返回primary shell时,$PWD是不会变更的。
能够了解问题的原因及其原理是很好的,但是?如何解决问题恐怕是我们更感兴趣的�u是吧?^_^
那好,接下来,再让我们了解一下source命令好了。
当你有了fork的概念之后,要理解source就不难:
*所谓source就是让script在当前shell内执行、而不是产生一个sub-shell来执行。
由于所有执行结果均于当前shell内完成,若script的环境有所改变,当然也会改变当前环境了�u
因此,只要我们要将原本单独输入的script命令行变成source命令的参数,就可轻易解决前例提到的问题了。
比方说,原本我们是如此执行 script的:
./my.script
现在改成这样即可:
source ./my.script
或:
. ./my.script
说到这里,我想,各位有兴趣看看/etc底下的众多设定文件,应该不难理解它们被定义后,如何让其它script读取并继承了
吧?
若然,日后你有机会写自己的script,应也不难专门指定一个设定文件以供不同的script一起"共享"了... ^_^
okay,到这里,若你搞得懂fork与source的不同,那接下来再接受一个挑战:
----那exec又与source/fork有何不同呢?
哦...要了解exec或许较为复杂,尤其扯上File Descriptor的话...
不过,简单来说:
* exec也是让script在同一个进程上执行,但是原有进程则被结束了。
也就是简而言之:原有进程会否终止,就是exec与source/fork的最大差异了。
嗯,光是从理论去理解,或许没那么好消化,不如动手"实作+思考"来的印像深刻哦。
下面让我们写两个简单的script,分别命令为1.sh及2.sh:
1.sh
#!/bin/sh
A=B
echo "PID for 1.sh before exec/source/fork:$$"
export A
echo "1.sh: \$A is $A"
case $1 in
exec)
echo "using exec..."
exec ./2.sh ;;
source)
echo "using source..."
. ./2.sh ;;
*)
echo "using fork by default..."
./2.sh ;;
esac
echo "PID for 1.sh after exec/source/fork:$$"
echo "1.sh: \$A is $A"
http://www.818198.com Page 9
SHELL十三问
2.sh
#!/bin/sh
echo "PID for 2.sh: $$"
echo "2.sh get \$A=$A from 1.sh"
A=C
export A
echo "2.sh: \$A is $A"
然后,分别跑如下参数来观察结果:
$ ./1.sh fork
PID for 1.sh before exec/source/fork:531
1.sh: $A is B
using fork by default...
PID for 2.sh:532
2.sh get $A=B from 1.sh
2.sh: $A is C
PID for 1.sh after exec/source/fork:531
1.sh: $A is B
$ ./1.sh source
PID for 1.sh before exec/source/fork:533
1.sh: $A is B
using source...
PID for 2.sh:533
2.sh get $A=B from 1.sh
2.sh: $A is C
PID for 1.sh after exec/source/fork:533
1.sh: $A is C
$ ./1.sh exec
PID for 1.sh before exec/source/fork:537
1.sh: $A is B
using exec...
PID for 2.sh:537
2.sh get $A=B from 1.sh
2.sh: $A is C
############
echo "PID for 1.sh after exec/source/fork:$$"
echo "1.sh: \$A is $A"
已经不会执行了,1.sh的进程已经没了。
##############
[]
SHELL十三问之七:( ) 与 { } 差在哪?
文章整理: 文章来源: 网络
先说一下,为何要用( )或{ }好了。
许多时候,我们在shell操作上,需要在一定条件下一次执行多个命令,也就是说,要么不执行,要么就全执行,而不是每
次依序的判断是否要执行下一个命令。或是,需要从一些命令执行优先次顺中得到豁免,如算术的2*(3+4)那样...
这时候,我们就可引入"命令群组"(command group)的概念:将多个命令集中处理。
http://www.818198.com Page 10
SHELL十三问
在shell command line中,一般人或许不太计较( )与{ }这两对符号的差异,虽然两者都可将多个命令作群组化处理,但若从
技术细节上,却是很不一样的:
( )将command group置于sub-shell去执行,也称nested sub-shell。
{ }则是在同一个shell内完成,也称为non-named command group。
如果你对上一章的fork与source的概念还记得的话,那就不难理解两者的差异了。
要是在command group中扯上变量及其它环境的修改,我们可以根据不同的需求来使用( )或{ }。通常而言,若所作的修改
是临时的,且不想影响原有或以后的设定,那我们就nested sub-shell,反之,则用non-named command group。
是的,光从command line来看,( )与{ }的差别就讲完了,够轻松吧~~~ ^_^
然而,若这两个meta用在其它command meta或领域中(如Regular Expression),还是有很多差别的。只是,我不打算再去说
明了,留给读者自己慢慢发掘好了...
我这里只想补充一个概念,就是function。
所谓的function,就是用一个名字去命名一个 command group,然后再调用这个名字去执行command group。从non-named
command group来推断,大概你也可以猜到我要说的是{ }了吧?
在bash中,function的定义方式有两种:
方式一:
function function_name {
command1
command2
command3
....
}
方式二:
fuction_name () {
command1
command2
command3
....
}
用哪一种方式无所谓,只是若碰到所定义的名称与现有的命令或别名(Alias)冲突的话,方式二或许会失败。
但方式二起码可以少打function这一串英文字母,对懒人来说(如我),又何乐不为呢?... ^_^
function在某一程度来说,也可称为"函式",但请不要与传统编程所使用的函式(library)搞混了,毕竟两者差异很大。
惟一相同的是,我们都可以随时用"已定义的名称"来调用它们...
若我们在shell操作中,需要不断的重复执行某些命令,我们首先想到的,或许是将命令写成命令稿(shell script)。不过,我
们也可以写成function,然后在command line中打上function_name就可当一舨的script来使用了。只是若你在shell中定义的
function,除了可用unset function_name取消外,一旦退出shell,function也跟着取消。
然而,在script中使用function却有许多好处,除了可以提高整体script的执行效能外(因为已被加载),还可以节省许多重复
的代码...
简单而言,若你会将多个命令写成script以供调用的话,那,你可以将function看成是script中的script ... ^_^
而且,透过上一章介绍的source命令,我们可以自行定义许许多多好用的function,再集中写在特定文件中,然后,在其它
的script中用source将它们加载并反复执行。
若你是RedHat Linux的使用者,或许,已经猜得出/etc/rc.d/init.d/functions这个文件是作啥用的了~~~ ^_^
[]
SHELL十三问之八:$(( )) 与 $( ) 还有${ } 差在哪?
文章整理: 文章来源: 网络
我们上一章介绍了( )与{ }的不同,这次让我们扩展一下,看看更多的变化:$( )与${ }又是啥玩意儿呢?
在bash shell中,$( )与` ` (反引号)都是用来做命令替换用(command substitution)的。
所谓的命令替换与我们第五章学过的变量替换差不多,都是用来重组命令行:
http://www.818198.com Page 11
SHELL十三问
*完成引号里的命令行,然后将其结果替换出来,再重组命令行。
例如:
$ echo the last sunday is $(date -d "last sunday" +%Y-%m-%d)
如此便可方便得到上一星期天的日期了...
上例是在linux下,在FreeBSD下应该用下面的:
echo the last Sunday is `date �Cv Sunday +%Y%m%d`
在操作上,用$( )或` `都无所谓,只是我"个人"比较喜欢用$( ),理由是:
1, ` `很容易与' ' (单引号)搞混乱,尤其对初学者来说。
有时在一些奇怪的字形显示中,两种符号是一模一样的(直竖两点)。
当然了,有经验的朋友还是一眼就能分辩两者。只是,若能更好的避免混乱,又何乐不为呢?
2,在多层次的复合替换中,` `须要额外的跳脱( \` )处理,而$( )则比较直观。例如:
这是错的:
command1 `command2 `command3` `
原本的意图是要在command2 `command3`先将command3提换出来给command 2处理,然后再将结果传给command1
`command2 ...`来处理。
然而,真正的结果在命令行中却是分成了`command2 `与``两段。
正确的输入应该如下:
command1 `command2 \`command3\` `
要不然,换成$( )就没问题了:
command1 $(command2 $(command3))
只要你喜欢,做多少层的替换都没问题啦~~~ ^_^
不过,$( )并不是没有弊端的...
首先,` `基本上可用在全部的unix shell中使用,若写成shell script,其移植性比较高。
而$( )并不见的每一种shell都能使用,我只能跟你说,若你用bash2的话,肯定没问题...
接下来,再让我们看${ }吧...它其实就是用来作变量替换用的啦。
一般情况下,$var与${var}并没有啥不一样。
但是用${ }会比较精确的界定变量名称的范围,比方说:
$ A=B
$ echo $AB
原本是打算先将$A的结果替换出来,然后再补一个B字母于其后,但在命令行上,真正的结果却是只会替换变量名称为AB
的值出来...
若使用${ }就没问题了:
$ echo ${A}B
BB
不过,假如你只看到${ }只能用来界定变量名称的话,那你就实在太小看bash了�u
有兴趣的话,你可先参考一下cu本版的精华文章:
http://www.chinaunix.net/forum/viewtopic.php?t=201843
为了完整起见,我这里再用一些例子加以说明${ }的一些特异功能:
假设我们定义了一个变量为:
file=/dir1/dir2/dir3/my.file.txt
我们可以用${ }分别替换获得不同的值:
${file#*/}:拿掉第一条/及其左边的字符串:dir1/dir2/dir3/my.file.txt
${file##*/}:拿掉最后一条/及其左边的字符串:my.file.txt
${file#*.}:拿掉第一个. 及其左边的字符串:file.txt
${file##*.}:拿掉最后一个. 及其左边的字符串:txt
${file%/*}:拿掉最后条/及其右边的字符串:/dir1/dir2/dir3
${file%%/*}:拿掉第一条/及其右边的字符串:(空值)
${file%.*}:拿掉最后一个. 及其右边的字符串:/dir1/dir2/dir3/my.file
http://www.818198.com Page 12
SHELL十三问
${file%%.*}:拿掉第一个. 及其右边的字符串:/dir1/dir2/dir3/my
记忆的方法为:
#是去掉左边(在鉴盘上#在$之左边)
%是去掉右边(在鉴盘上%在$之右边)
单一符号是最小匹配�r两个符号是最大匹配。
${file:0:5}:提取最左边的5个字节:/dir1
${file:5:5}:提取第5个字节右边的连续5个字节:/dir2
我们也可以对变量值里的字符串作替换:
${file/dir/path}:将第一个dir替换为path:/path1/dir2/dir3/my.file.txt
${file//dir/path}:将全部dir替换为path:/path1/path2/path3/my.file.txt
利用${ }还可针对不同的变量状态赋值(没设定、空值、非空值):
${file-my.file.txt}:假如$file没有设定,则使用my.file.txt作传回值。(空值及非空值时不作处理)
${file:-my.file.txt}:假如$file没有设定或为空值,则使用my.file.txt作传回值。(非空值时不作处理)
${file+my.file.txt}:假如$file设为空值或非空值,均使用my.file.txt作传回值。(没设定时不作处理)
${file:+my.file.txt}:若$file为非空值,则使用my.file.txt作传回值。(没设定及空值时不作处理)
${file=my.file.txt}:若$file没设定,则使用my.file.txt作传回值,同时将$file赋值为my.file.txt。(空值及非空值时不作处理)
${file:=my.file.txt}:若$file没设定或为空值,则使用my.file.txt作传回值,同时将$file赋值为my.file.txt。(非空值时不作处理
)
${file?my.file.txt}:若$file没设定,则将my.file.txt输出至STDERR。(空值及非空值时不作处理)
${file:?my.file.txt}:若$file没设定或为空值,则将my.file.txt输出至STDERR。(非空值时不作处理)
tips:
以上的理解在于,你一定要分清楚unset与null及non-null这三种赋值状态.
一般而言, :与null有关,若不带:的话, null不受影响,若带:则连null也受影响.
还有哦,${#var}可计算出变量值的长度:
${#file}可得到27,因为/dir1/dir2/dir3/my.file.txt刚好是27个字节...
接下来,再为大家介稍一下bash的组数(array)处理方法。
一般而言,A="a b c def"这样的变量只是将$A替换为一个单一的字符串,
但是改为A=(a b c def),则是将$A定义为组数...
bash的组数替换方法可参考如下方法:
${A[@]}或${A
· }可得到a b c def (全部组数)
${A[0]}可得到a (第一个组数),${A[1]}则为第二个组数...
${#A[@]}或${#A
· }可得到4 (全部组数数量)
${#A[0]}可得到1 (即第一个组数(a)的长度),${#A[3]}可得到3 (第四个组数(def)的长度)
A[3]=xyz则是将第四个组数重新定义为xyz ...
诸如此类的....
能够善用bash的$( )与${ }可大大提高及简化shell在变量上的处理能力哦
好了,最后为大家介绍$(( ))的用途吧:它是用来作整数运算的。
在bash中,$(( ))的整数运算符号大致有这些:
+ - * /:分别为"加、减、乘、除"。
%:余数运算
& | ^ !:分别为"AND、OR、XOR、NOT"运算。
例:
$ a=5; b=7; c=2
$ echo $(( a+b*c ))
19
$ echo $(( (a+b)/c ))
http://www.818198.com Page 13
SHELL十三问
6
SHELL十三问
groff, tbl, eqn
另一种文本标记和显示格式化语言是 groff. 这是一个对传统 UNIX roff/troff 显示和排
版包的 GNU 增强版本.Man页 使用的就是 groff.
tbl 表处理工具可以认为是 groff 的一部分, 它的功能就是将表标记转化到 groff
命令中.
eqn 等式处理工具也是 groff 的一部分, 它的功能是将等式标记转化到 groff 命令中.
Example 12-26 manview: 查看格式化的man页
################################Start Script#######################################
1 #!/bin/bash
2 # manview.sh: 将man页源文件格式化以方便查看.
3
4 # 当你想阅读man页的时候, 这个脚本就有用了.
5 # 它允许你在运行的时候查看
6 #+ 中间结果.
7
8 E_WRONGARGS=65
9
10 if [ -z "$1" ]
11 then
12 echo "Usage: `basename $0` filename"
13 exit $E_WRONGARGS
14 fi
15
16 # ---------------------------17 groff -Tascii -man $1 | less
18 # 来自于 groff man页.
19 # ---------------------------20
21 # 如果man业中包括表或者等式,
22 #+ 那么上边的代码就够呛了.
23 # 下边的这行代码可以解决上边的这个问题.
24 #
25 # gtbl < "$1" | geqn -Tlatin1 | groff -Tlatin1 -mtty-char -man
26 #
27 # Thanks, S.C.
28
29 exit 0
################################End Script#########################################
lex, yacc
lex 是用于模式匹配的词汇分析产生程序. 在Linux系统上这个命令已经被 flex 取代了.
yacc 工具基于一系列的语法规范生成语法分析程序. 在Linux系统上这个命令已经被
bison 取代了.
注意事项:
[1] 对于 GNU 版本的 tr 命令来说这是唯一一处比那些商业 UNIX 系统上的一般版本合适
的地方.
12.5 文件与归档命令
-------------------归档命令
http://www.818198.com Page 260
SHELL十三问
tar
标准的 UNIX 归档工具. [1] 起初这只是一个 磁带 归档 程序, 而现在这个工具已经被开
发为通用打包程序, 它能够处理所有设备的所有类型的归档文件, 包括磁带设备, 正常文
件, 甚至是 stdout (参见Example 3-4). GNU 的tar工具现在可以接受不同种类的压缩过
滤器, 比如tar czvf archive_name.tar.gz *, 并且可以递归的处理归档文件, 还可以用
gzip 压缩目录下的所有文件, 除了当前目录下($PWD)的 点文件 . [2]
一些有用的 tar 命令选项:
1. -c 创建 (一个新的归档文件)
2. -x 解压文件 (从存在的归档文件中)
3. --delete 删除文件 (从存在的归档文件中)
注意: 这个选项不能用于磁带类型设备.
4. -r 将文件添加到现存的归档文件的尾部
5. -A 将 tar 文件添加到现存的归档文件的尾部
6. -t 列出现存的归档文件中包含的内容
7. -u 更新归档文件
8. -d 使用指定的文件系统 比较归档文件
9. -z 用 gzip 压缩归档文件
(压缩还是解压, 依赖于是否组合了 -c 或 -x)选项
10. -j 用 bzip2 压缩归档文件
注意: 如果想从损坏的用 gzip 压缩过的 tar 文件中取得数据, 那将是很困难的. 所有当
我们归档重要的文件的时候, 一定要保留多个备份.
shar
Shell 归档工具. 存在于 shell 归档文件中的所有文件都是未经压缩的, 并且本质上是一
个shell 脚本,以 #!/bin/sh 开头, 并且包含所有必要的解档命令. Shar 归档文件 至今
还在 Internet 新闻组中使用, 否则的话 shar早就被 tar/gzip 所取代了. unshar 命令
用来解档 shar 归档文件.
ar
创建和操作归档文件的工具, 主要在对2进制目标文件打包成库时才会用到.
rpm
Red Hat 包管理器, 或者说 rpm 工具提供了一种对源文件或2进制文件进行打包的方法.
除此之外, 它还包括安装命令, 并且还检查包的完整性.
一个简单的 rpm -i package_name.rpm 命令对于安装一个包来说就足够了, 虽然这个命
令还有好多其它的选项.
注意: rpm -qf 列出一个文件属于那个包.
bash$ rpm -qf /bin/ls
coreutils-5.2.1-3
注意: rpm -qa 将会列出给定系统上所有安装了的 rpm 包. rpm -qa package_name 命令
将会列出于给定名字匹配的包.
bash$ rpm -qa
redhat-logos-1.1.3-1
glibc-2.2.4-13
cracklib-2.7-12
dosfstools-2.7-1
gdbm-1.8.0-10
ksymoops-2.4.1-1
mktemp-1.5-11
perl-5.6.0-17
reiserfs-utils-3.x.0j-2
http://www.818198.com Page 261
SHELL十三问
...
bash$ rpm -qa docbook-utils
docbook-utils-0.6.9-2
bash$ rpm -qa docbook | grep docbook
docbook-dtd31-sgml-1.0-10
docbook-style-dsssl-1.64-3
docbook-dtd30-sgml-1.0-10
docbook-dtd40-sgml-1.0-11
docbook-utils-pdf-0.6.9-2
docbook-dtd41-sgml-1.0-10
docbook-utils-0.6.9-2
cpio
这个特殊的归档拷贝命令(拷贝输入和输出)现在已经很少能见到了, 因为它已经被 tar/gz
ip 所替代了.现在这个命令只在一些比较特殊的地方还在使用,比如拷贝一个目录树.
Example 12-27 使用 cpio 来拷贝一个目录树
################################Start Script#######################################
1 #!/bin/bash
2
3 # 使用 'cpio' 拷贝目录树.
4
5 # 使用 'cpio' 的优点:
6 # 加速拷贝. 比通过管道使用 'tar' 命令快一些.
7 # 很适合拷贝一些 'cp' 命令
8 #+ 搞不定的的特殊文件(比如名字叫 pipes 的文件, 等等)
9
10 ARGS=2
11 E_BADARGS=65
12
13 if [ $# -ne "$ARGS" ]
14 then
15 echo "Usage: `basename $0` source destination"
16 exit $E_BADARGS
17 fi
18
19 source=$1
20 destination=$2
21
22 find "$source" -depth | cpio -admvp "$destination"
23 # ^^^^^ ^^^^^
24 # 阅读 'find' 和 'cpio' 的man 页来了解这些选项的意义.
25
26
27 # 练习:
28 # -----http://www.818198.com Page 262
SHELL十三问
29
30 # 添加一些代码来检查 'find | cpio' 管道命令的退出码($?)
31 #+ 并且如果出现错误的时候输出合适的错误码.
32
33 exit 0
################################End Script#########################################
rpm2cpio
这个命令可以从 rpm 归档文件中解出一个 cpio 归档文件.
Example 12-28 解包一个 rpm 归档文件
################################Start Script#######################################
1 #!/bin/bash
2 # de-rpm.sh: 解包一个 'rpm' 归档文件
3
4 : ${1?"Usage: `basename $0` target-file"}
5 # 必须指定 'rpm' 归档文件名作为参数.
6
7
8 TEMPFILE=$$.cpio # Tempfile 必须是一个"唯一"的名字.
9 # $$ 是这个脚本的进程 ID.
10
11 rpm2cpio < $1 > $TEMPFILE # 将 rpm 归档文件转换为 cpio 归档文件.
12 cpio --make-directories -F $TEMPFILE -i # 解包 cpio 归档文件.
13 rm -f $TEMPFILE # 删除 cpio 归档文件.
14
15 exit 0
16
17 # 练习:
18 # 添加一些代码来检查 1) "target-file" 是否存在
19 #+ 2) 这个文件是否是一个 rpm 归档文件.
20 # 暗示: 分析 'file' 命令的输出.
################################End Script#########################################
压缩命令
gzip
标准的 GNU/UNIX 压缩工具, 取代了比较差的 compress 命令. 相应的解压命令是gunzip,
gzip -d 是等价的.
zcat 过滤器可以将一个 gzip 文件解压到 stdout, 所以尽可能的使用管道和重定向. 这
个命令事实上就是一个可以工作于压缩文件(包括一些的使用老的 compress 工具压缩的文
件)的 cat 命令. zcat 命令等价于 gzip -dc.
注意: 在某些商业的 UNIX 系统上, zcat 与 uncompress -c 等价, 并且不能工作于
gzip 文件.
参见 Example 7-7.
bzip2
用来压缩的一个可选的工具, 通常比 gzip 命令压缩率更高(所以更慢), 适用于比较大的
文件. 相应的解压命令是 bunzip2.
注意: 新版本的 tar 命令已经直接支持 bzip2 了.
compress, uncompress
这是一个老的, 私有的压缩工具, 一般的商业 UNIX 发行版都会有这个工具. 更有效率的
gzip 工具早就把这个工具替换掉了. Linux 发行版一般也会包含一个兼容的 compress 命
http://www.818198.com Page 263
SHELL十三问
令, 虽然 gunzip 也可以加压用 compress 工具压缩的文件.
注意: znew 命令可以将 compress 压缩的文件转换为 gzip 压缩的文件.
sq
另一种压缩工具, 一个只能工作于排过序的 ASCII 单词列表的过滤器.这个命令使用过滤
器标准的调用语法, sq < input-file > output-file. 速度很快, 但是效率远不及
gzip. 相应的解压命令为 unsq, 调用方法与 sq 相同.
注意: sq 的输出可以通过管道传递给 gzip 以便于进一步的压缩.
zip, unzip
跨平台的文件归档和压缩工具, 与 DOS 下的 pkzip.exe 兼容. zip 归档文件看起来在互
联网上比 tar 包更流行.
unarc, unarj, unrar
这些 Linux 工具可以用来解档那些用 DOS 下的 arc.exe, arj.exe, 和 rar.exe 程序进
行归档的文件.
文件信息
file
确定文件类型的工具. 命令 file file-name 将会用 ascii 文本或数据的形式返回
file-name 文件的详细描述. 这个命令会使用 /usr/share/magic, /etc/magic, 或
/usr/lib/magic 中定义的 魔法数字 来标识包含某种魔法数字的文件, 上边所举出的这
3个文件需要依赖于具体的 Linux/UNIX 发行版.
-f 选项将会让 file 命令运行于批处理模式, 也就是说它会分析 -f 后边所指定的文件,
从中读取需要处理的文件列表, 然后依次执行 file 命令. -z 选项, 当对压缩过的目标文
件使用时, 将会强制分析压缩的文件类型.
bash$ file test.tar.gz
test.tar.gz: gzip compressed data, deflated, last modified: Sun Sep 16 13:34:51 2001, os: Unix
bash file -z test.tar.gz
test.tar.gz: GNU tar archive (gzip compressed data, deflated, last modified: Sun Sep 16 13:34:51 2001, os: Unix)
1 # 在给定的目录中找出sh和Bash脚本文件:
2
3 DIRECTORY=/usr/local/bin
4 KEYWORD=Bourne
5 # Bourne 和 Bourne-Again shell 脚本
6
7 file $DIRECTORY/* | fgrep $KEYWORD
8
9 # 输出:
10
11 # /usr/local/bin/burn-cd: Bourne-Again shell script text executable
12 # /usr/local/bin/burnit: Bourne-Again shell script text executable
13 # /usr/local/bin/cassette.sh: Bourne shell script text executable
14 # /usr/local/bin/copy-cd: Bourne-Again shell script text executable
15 # . . .
Example 12-29 从 C 文件中去掉注释
################################Start Script#######################################
1 #!/bin/bash
2 # strip-comment.sh: 去掉C 程序中的注释 (/* 注释 */)
3
4 E_NOARGS=0
http://www.818198.com Page 264
SHELL十三问
5 E_ARGERROR=66
6 E_WRONG_FILE_TYPE=67
7
8 if [ $# -eq "$E_NOARGS" ]
9 then
10 echo "Usage: `basename $0` C-program-file" >&2 # 将错误消息发到 stderr.
11 exit $E_ARGERROR
12 fi
13
14 # 检查文件类型是否正确.
15 type=`file $1 | awk '{ print $2, $3, $4, $5 }'`
16 # "file $1" echoe 出文件类型 . . .
17 # 然后 awk 会删掉第一个域, 就是文件名 . . .
18 # 然后结果将会传递到变量 "type" 中.
19 correct_type="ASCII C program text"
20
21 if [ "$type" != "$correct_type" ]
22 then
23 echo
24 echo "This script works on C program files only."
25 echo
26 exit $E_WRONG_FILE_TYPE
27 fi
28
29
30 # 相当隐秘的 sed 脚本:
31 #--------32 sed '
33 /^\/\*/d
34 /.*\*\//d
35 ' $1
36 #--------37 # 如果你花上几个小时来学习 sed 语法的话, 上边这个命令还是很好理解的.
38
39
40 # 如果注释和代码在同一行上, 上边的脚本就不行了.
41 #+ 所以需要添加一些代码来处理这种情况.
42 # 这是一个很重要的练习.
43
44 # 当然, 上边的代码也会删除带有 "*/" 的非注释行 --45 #+ 这也不是一个令人满意的结果.
46
47 exit 0
48
49
50 # ----------------------------------------------------------------51 # 下边的代码不会执行, 因为上边已经 'exit 0' 了.
52
http://www.818198.com Page 265
SHELL十三问
53 # Stephane Chazelas 建议使用下边的方法:
54
55 usage() {
56 echo "Usage: `basename $0` C-program-file" >&2
57 exit 1
58 }
59
60 WEIRD=`echo -n -e '\377'` # or WEIRD=$'\377'
61 [[ $# -eq 1 ]] || usage
62 case `file "$1"` in
63 *"C program text"*) sed -e "s%/\*%${WEIRD}%g;s%\*/%${WEIRD}%g" "$1" \
64 | tr '\377\n' '\n\377' \
65 | sed -ne 'p;n' \
66 | tr -d '\n' | tr '\377' '\n';;
67 *) usage;;
68 esac
69
70 # 如果是下列的这些情况, 还是很糟糕:
71 # printf("/*");
72 # or
73 # /* /* buggy embedded comment */
74 #
75 # 为了处理上边所有这些特殊情况(字符串中的注释, 含有 \", \\" ...
76 #+ 的字符串中的注释) 唯一的方法还是写一个 C 分析器
77 #+ (或许可以使用lex 或者 yacc ?).
78
79 exit 0
################################End Script#########################################
which
which command-xxx 将会给出 "command-xxx" 的完整路径. 当你想在系统中准确定位一个
特定的命令或工具的时候, 这个命令就非常有用了.
$bash which rm
/usr/bin/rm
whereis
与上边的 which 很相似, whereis command-xxx 不只会给出 "command-xxx" 的完整路径,
而且还会给出这个命令的 man页 的完整路径.
$bash whereis rm
rm: /bin/rm /usr/share/man/man1/rm.1.bz2
whatis
whatis filexxx 将会在 whatis 数据库中查询 "filexxx". 当你想确认系统命令和重要的
配置文件的时候, 这个命令就非常重要了. 可以把这个命令认为是一个简单的 man 命令.
$bash whatis whatis
whatis (1) - search the whatis database for complete words
Example 12-30 Exploring /usr/X11R6/bin
################################Start Script#######################################
1 #!/bin/bash
2
3 # 在 /usr/X11R6/bin 中的所有神秘的2进制文件都是什么东西?
http://www.818198.com Page 266
SHELL十三问
4
5 DIRECTORY="/usr/X11R6/bin"
6 # 也试试 "/bin", "/usr/bin", "/usr/local/bin", 等等.
7
8 for file in $DIRECTORY/*
9 do
10 whatis `basename $file` # 将会 echo 出这个2进制文件的信息.
11 done
12
13 exit 0
14
15 # 你可能希望将这个脚本的输出重定向, 像这样:
16 # ./what.sh >>whatis.db
17 # 或者一页一页的在 stdout 上查看,
18 # ./what.sh | less
################################End Script#########################################
参见 Example 10-3.
vdir
显示详细的目录列表. 与 ls -l 的效果类似.
这是一个 GNU fileutils.
bash$ vdir
total 10
-rw-r--r-- 1 bozo bozo 4034 Jul 18 22:04 data1.xrolo
-rw-r--r-- 1 bozo bozo 4602 May 25 13:58 data1.xrolo.bak
-rw-r--r-- 1 bozo bozo 877 Dec 17 2000 employment.xrolo
bash ls -l
total 10
-rw-r--r-- 1 bozo bozo 4034 Jul 18 22:04 data1.xrolo
-rw-r--r-- 1 bozo bozo 4602 May 25 13:58 data1.xrolo.bak
-rw-r--r-- 1 bozo bozo 877 Dec 17 2000 employment.xrolo
locate, slocate
locate 命令将会在预先建立好的档案数据库中查询文件. slocate 命令是 locate 的安全
版本( locate 命令可能已经被关联到 slocate 命令上了).
$bash locate hickson
/usr/lib/xephem/catalogs/hickson.edb
readlink
显示符号连接所指向的文件.
bash$ readlink /usr/bin/awk
../../bin/gawk
strings
使用 strings 命令在二进制或数据文件中找出可打印字符. 它将在目标文件中列出所有找
到的可打印字符的序列. 这个命令对于想进行快速查找一个 n 个字符的打印检查来说是很
方便的,也可以用来检查一个未知格式的图片文件 (strings image-file | more 可能会搜
索出像 JFIF 这样的字符串, 那么这就意味着这个文件是一个 jpeg 格式的图片文件).
在脚本中, 你可能会使用 grep 或 sed 命令来分析 strings 命令的输出. 参见
Example 10-7 和 Example 10-9.
Example 12-31 一个"改进过"的 strings 命令
http://www.818198.com Page 267
SHELL十三问
################################Start Script#######################################
1 #!/bin/bash
2 # wstrings.sh: "word-strings" (增强的 "strings" 命令)
3 #
4 # 这个脚本将会过滤 "strings" 命令的输出.
5 #+ 通过排除标准单词列表的形式检查来过滤输出.
6 # 这将有效的过滤掉无意义的字符,
7 #+ 并且指挥输出可以识别的字符.
8
9 # ===========================================================
10 # 脚本参数的标准检查
11 ARGS=1
12 E_BADARGS=65
13 E_NOFILE=66
14
15 if [ $# -ne $ARGS ]
16 then
17 echo "Usage: `basename $0` filename"
18 exit $E_BADARGS
19 fi
20
21 if [ ! -f "$1" ] # 检查文件是否存在.
22 then
23 echo "File \"$1\" does not exist."
24 exit $E_NOFILE
25 fi
26 # ===========================================================
27
28
29 MINSTRLEN=3 # 最小的字符串长度.
30 WORDFILE=/usr/share/dict/linux.words # 字典文件.
31 # 也可以指定一个不同的
32 #+ 单词列表文件,
33 #+ 但这种文件必须是以每个单词一行的方式进行保存.
34
35
36 wlist=`strings "$1" | tr A-Z a-z | tr '[:space:]' Z | \
37 tr -cs '[:alpha:]' Z | tr -s '\173-\377' Z | tr Z ' '`
38
39 # 将'strings' 命令的输出通过管道传递到多个 'tr' 命令中.
40 # "tr A-Z a-z" 全部转换为小写字符.
41 # "tr '[:space:]'" 转换空白字符为多个 Z.
42 # "tr -cs '[:alpha:]' Z" 将非字母表字符转换为多个 Z,
43 #+ 然后去除多个连续的 Z.
44 # "tr -s '\173-\377' Z" 把所有z后边的字符都转换为 Z.
45 #+ 并且去除多余重复的Z.(注意173(123 ascii "{")和377(255 ascii 最后一个字符)都是8进制)
46 #+ 这样处理之后, 我们所有之前需要处理的令我们头痛的字符
47 #+ 就全都转换为字符 Z 了.
http://www.818198.com Page 268
SHELL十三问
48 # 最后"tr Z ' '" 将把所有的 Z 都转换为空格,
49 #+ 这样我们在下边循环中用到的变量 wlist 中的内容就全部以空格分隔了.
50
51 # ****************************************************************
52 # 注意, 我们使用管道来将多个 'tr' 的输出传递到下一个 'tr' 时
53 #+ 每次都使用了不同的参数.
54 # ****************************************************************
55
56
57 for word in $wlist # 重要:
58 # $wlist 这里不能使用双引号.
59 # "$wlist" 不能正常工作.
60 # 为什么不行?
61 do
62
63 strlen=${#word} # 字符串长度.
64 if [ "$strlen" -lt "$MINSTRLEN" ] # 跳过短的字符串.
65 then
66 continue
67 fi
68
69 grep -Fw $word "$WORDFILE" # 只匹配整个单词.
70 # ^^^ # "固定字符串" 和
71 #+ "整个单词" 选项.
72
73 done
74
75
76 exit $?
################################End Script#########################################
比较命令
diff, patch
diff: 一个非常灵活的文件比较工具. 这个工具将会以一行接一行的形式来比较目标文件.
在某些应用中, 比如说比较单词词典, 在通过管道将结果传递给 diff 命令之前, 使用诸
如 sort 和 uniq 命令来对文件进行过滤将是非常有用的.diff file-1 file-2 将会输出
2个文件不同的行,并会通过符号标识出每个不同行所属的文件.
diff 命令的 --side-by-side 选项将会把2个比较中的文件全部输出, 按照左右分隔的形
式, 并会把不同的行标记出来. -c 和 -u 选项也会使得 diff 命令的输出变得容易解释
一些.
还有一些 diff 命令的变种, 比如 sdiff, wdiff, xdiff, 和 mgdiff.
注意: 如果比较的两个文件是完全一样的话, 那么 diff 命令会返回 0 作为退出码, 如果
不同的话就返回 1 作为退出码. 这样 diff 命令就可以用在 shell 脚本的测试结构
中了. (见下边)
diff 命令的一个重要用法就是产生区别文件, 这个文件将用作 patch 命令的 -e 选项的
参数, -e 选项接受 ed 或 ex 脚本.
patch: 灵活的版本工具.给出一个用 diff 命令产生的区别文件, patch 命令可以将一个
老版本的包更新为一个新版本的包. 因为你发布一个小的区别文件远比重新发布一个大的
软件包来的容易得多.对于频繁更新的 Linux 内核来说, 使用补丁包的形式来发布将是一
http://www.818198.com Page 269
SHELL十三问
种很好的方法.
1 patch -p1
3 # 然后把它们应用于其中索引到的文件上.
4 # 那么这个包就被更新为新版本了.
更新 kernel:
1 cd /usr/src
2 gzip -cd patchXX.gz | patch -p0
3 # 使用'patch'来更新内核源文件.
4 # 来自于匿名作者(Alan Cox?)的
5 # Linux 内核文档 "README".
注意: diff 命令也可以递归的比较目录下的所有文件(包含子目录).
bash$ diff -r ~/notes1 ~/notes2
Only in /home/bozo/notes1: file02
Only in /home/bozo/notes1: file03
Only in /home/bozo/notes2: file04
注意: 使用 zdiff 来比较 gzip 文件.
diff3
一个 diff 命令的扩展版本, 可以同时比较3个文件. 如果成功执行那么这个命令就返回0,
但是不幸的是这个命令不给出比较结果的信息.
bash$ diff3 file-1 file-2 file-3
====
1:1c
This is line 1 of "file-1".
2:1c
This is line 1 of "file-2".
3:1c
This is line 1 of "file-3"
sdiff
比较 和/或 编辑2个文件, 将它们合并到一个输出文件中. 因为这个命令的交互特性, 所
以在脚本中很少使用这个命令.
cmp
cmp 命令是上边 diff 命令的一个简单版本. diff 命令会报告两个文件的不同之处, 而
cmp 命令仅仅指出那些位置有不同, 而不会显示不同的具体细节.
注意: 与 diff 一样,如果两个文件相同 cmp 返回0作为退出码, 如果不同返回1. 这样就
可以用在 shell 脚本的测试结构中了.
Example 12-32 在一个脚本中使用 cmp 来比较2个文件.
################################Start Script#######################################
1 #!/bin/bash
2
3 ARGS=2 # 脚本需要2个参数.
4 E_BADARGS=65
5 E_UNREADABLE=66
6
7 if [ $# -ne "$ARGS" ]
8 then
9 echo "Usage: `basename $0` file1 file2"
10 exit $E_BADARGS
http://www.818198.com Page 270
SHELL十三问
11 fi
12
13 if [[ ! -r "$1" || ! -r "$2" ]]
14 then
15 echo "Both files to be compared must exist and be readable."
16 exit $E_UNREADABLE
17 fi
18
19 cmp $1 $2 &> /dev/null # /dev/null 将会禁止 "cmp" 命令的输出.
20 # cmp -s $1 $2 与上边这句结果相同 ("-s" 选项是安静标志)
21 # Thank you Anders Gustavsson for pointing this out.
22 #
23 # 用 'diff' 命令也可以, 比如, diff $1 $2 &> /dev/null
24
25 if [ $? -eq 0 ] # 测试 "cmp" 命令的退出码.
26 then
27 echo "File \"$1\" is identical to file \"$2\"."
28 else
29 echo "File \"$1\" differs from file \"$2\"."
30 fi
31
32 exit 0
################################End Script#########################################
注意: 用 zcmp 处理 gzip 文件.
comm
多功能的文件比较工具. 使用这个命令之前必须先排序.
comm -options first-file second-file
comm file-1 file-2 将会输出3列:
* 第 1 列 = 只在 file-1 中存在的行
* 第 2 列 = 只在 file-2 中存在的行
* 第 2 列 = 两边相同的行.
下列选项可以禁止1列或多列的输出.
* -1 禁止显示第一栏 (译者: 在 File1 中的行)
* -2 禁止显示第二栏 (译者: 在 File2 中的行)
* -3 禁止显示第三栏 (译者: File1 和 File2 公共的行)
* -12 禁止第一列和第二列, (就是说选项可以组合).
一般工具
basename
从文件名中去掉路径信息, 只打印出文件名. 结构 basename $0 可以让脚本知道它自己的
名字, 也就是, 它被调用的名字. 可以用来显示用法信息, 比如如果你调用脚本的时候缺
少参数, 可以使用如下语句:
1 echo "Usage: `basename $0` arg1 arg2 ... argn"
dirname
从带路径的文件名中去掉文件名, 只打印出路径信息.
注意: basename 和 dirname 可以操作任意字符串. 参数可以不是一个真正存在的文件,
甚至可以不是一个文件名.(参见 Example A-7).
Example 12-33 basename 和 dirname
################################Start Script#######################################
http://www.818198.com Page 271
SHELL十三问
1 #!/bin/bash
2
3 a=/home/bozo/daily-journal.txt
4
5 echo "Basename of /home/bozo/daily-journal.txt = `basename $a`"
6 echo "Dirname of /home/bozo/daily-journal.txt = `dirname $a`"
7 echo
8 echo "My own home is `basename ~/`." # `basename ~` also works.
9 echo "The home of my home is `dirname ~/`." # `dirname ~` also works.
10
11 exit 0
################################End Script#########################################
split, csplit
将一个文件分割为几个小段的工具. 这些命令通常用来将大的文件分割, 并备份到软盘上,
或者是为了切成合适的尺寸用 email 上传.
csplit 根据 上下文 来切割文件, 切割的位置将会发生在模式匹配的地方.
sum, cksum, md5sum, sha1sum
这些都是用来产生 checksum 的工具. checksum 的目的是用来检验文件的完整性, 是对文
件的内容进行数学计算而得到的. 出于安全目的一个脚本可能会有一个 checksum 列表,
这样可以确保关键系统文件的内容不会被修改或损坏. 对于需要安全性的应用来说, 应该
使用 md5sum (message digest 5 checksum) 命令, 或者更好的更新的 sha1sum
(安全 Hash 算法).
bash$ cksum /boot/vmlinuz
1670054224 804083 /boot/vmlinuz
bash$ echo -n "Top Secret" | cksum
3391003827 10
bash$ md5sum /boot/vmlinuz
0f43eccea8f09e0a0b2b5cf1dcf333ba /boot/vmlinuz
bash$ echo -n "Top Secret" | md5sum
8babc97a6f62a4649716f4df8d61728f -
注意: cksum 命令将会显示目标的尺寸(字节), 目标可以使文件或 stdout.
md5sum 和 sha1sum 命令在它们收到 stdout 的输入时候, 显示一个 dash .
Example 12-34 检查文件完整性
################################Start Script#######################################
1 #!/bin/bash
2 # file-integrity.sh: 检查一个给定目录下的文件
3 # 是否被改动了.
4
5 E_DIR_NOMATCH=70
6 E_BAD_DBFILE=71
7
8 dbfile=File_record.md5
9 # 存储记录的文件名 (数据库文件).
http://www.818198.com Page 272
SHELL十三问
10
11
12 set_up_database ()
13 {
14 echo ""$directory"" > "$dbfile"
15 # 把目录名写到文件的第一行.
16 md5sum "$directory"/* >> "$dbfile"
17 # 在文件中附上 md5 checksums 和 filenames.
18 }
19
20 check_database ()
21 {
22 local n=0
23 local filename
24 local checksum
25
26 # ------------------------------------------- #
27 # 这个文件检查其实是不必要的,
28 #+ 但是能安全一些.
29
30 if [ ! -r "$dbfile" ]
31 then
32 echo "Unable to read checksum database file!"
33 exit $E_BAD_DBFILE
34 fi
35 # ------------------------------------------- #
36
37 while read record[n]
38 do
39
40 directory_checked="${record[0]}"
41 if [ "$directory_checked" != "$directory" ]
42 then
43 echo "Directories do not match up!"
44 # 换个目录试一下.
45 exit $E_DIR_NOMATCH
46 fi
47
48 if [ "$n" -gt 0 ] # 不是目录名.
49 then
50 filename[n]=$( echo ${record[$n]} | awk '{ print $2 }' )
51 # md5sum 向后写记录,
52 #+ 先写 checksum, 然后写 filename.
53 checksum[n]=$( md5sum "${filename[n]}" )
54
55
56 if [ "${record[n]}" = "${checksum[n]}" ]
57 then
http://www.818198.com Page 273
SHELL十三问
58 echo "${filename[n]} unchanged."
59
60 elif [ "`basename ${filename[n]}`" != "$dbfile" ]
61 # 跳过checksum 数据库文件,
62 #+ 因为在每次调用脚本它都会被修改.
63 # --- 64 # 这不幸的意味着当我们在 $PWD中运行这个脚本
65 #+ 时, 修改这个 checksum 数
66 #+ 据库文件将不会被检测出来.
67 # 练习: 修复这个问题.
68 then
69 echo "${filename[n]} : CHECKSUM ERROR!"
70 # 因为最后的检查, 文件已经被修改.
71 fi
72
73 fi
74
75
76
77 let "n+=1"
78 done <"$dbfile" # 从 checksum 数据库文件中读.
79
80 }
81
82 # =================================================== #
83 # main ()
84
85 if [ -z "$1" ]
86 then
87 directory="$PWD" # 如果没制定参数,
88 else #+ 那么就使用当前的工作目录.
89 directory="$1"
90 fi
91
92 clear # 清屏.
93 echo " Running file integrity check on $directory"
94 echo
95
96 # ------------------------------------------------------------------ #
97 if [ ! -r "$dbfile" ] # 是否需要建立数据库文件?
98 then
99 echo "Setting up database file, \""$directory"/"$dbfile"\"."; echo
100 set_up_database
101 fi
102 # ------------------------------------------------------------------ #
103
104 check_database # 调用主要处理函数.
105
http://www.818198.com Page 274
SHELL十三问
106 echo
107
108 # 你可能想把这个脚本的输出重定向到文件中,
109 #+ 尤其在这个目录中有很多文件的时候.
110
111 exit 0
112
113 # 如果要对数量非常多的文件做完整性检查,
114 #+ 可以考虑一下 "Tripwire" 包,
115 #+ http://sourceforge.net/projects/tripwire/.
116
################################End Script#########################################
参见 Example A-19 和 Example 33-14 , 这两个例子展示了 md5sum 命令的用法.
注意: 已经有 128-bit md5sum 被破解的报告了,所以现在更安全的 160-bit sha1sum 是
非常受欢迎的, 并且已经被加入到 checksum 工具包中.
一些安全顾问认为即使是 sha1sum 也是会被泄漏的. 所以, 下一个工具是什么呢?
-- 512-bit 的 checksum 工具?
bash$ md5sum testfile
e181e2c8720c60522c4c4c981108e367 testfile
bash$ sha1sum testfile
5d7425a9c08a66c3177f1e31286fa40986ffc996 testfile
shred
用随机字符填充文件, 使得文件无法恢复, 这样就可以保证文件安全的被删除. 这个命令
的效果与 Example 12-55 一样, 但是使用这个命令是一种更优雅更彻底的方法.
这是一个 GNU fileutils.
注意: 即使使用了 shred 命令, 高级的(forensic)辩论技术还是能够恢复文件的内容.
编码和解码
uuencode
这个工具用来把二进制文件编码成 ASCII 字符串,这个工具适用于编码e-mail消息体,或者
新闻组消息.
uudecode
这个工具用来把 uuencode 后的 ASCII 字符串恢复为二进制文件.
Example 12-35 Uudecod 编码后的文件
################################Start Script#######################################
1 #!/bin/bash
2 # 在当前目录下 uudecode 所有用 uuencode 编码的文件.
3
4 lines=35 # 允许读头部的 35 行(范围很宽).
5
6 for File in * # Test 所有 $PWD 下的文件.
7 do
8 search1=`head -$lines $File | grep begin | wc -w`
9 search2=`tail -$lines $File | grep end | wc -w`
10 # Uuencode 过的文件在文件开始的地方有个 "begin",
11 #+ 在文件结尾的地方有个 "end".
12 if [ "$search1" -gt 0 ]
http://www.818198.com Page 275
SHELL十三问
13 then
14 if [ "$search2" -gt 0 ]
15 then
16 echo "uudecoding - $File -"
17 uudecode $File
18 fi
19 fi
20 done
21
22 # 小心不要让这个脚本运行自己,
23 #+ 因为它也会把自身也认为是一个 uuencoded 文件,
24 #+ 这都是因为这个脚本自身也包含 "begin" 和 "end".
25
26 # 练习:
27 # -----28 # 修改这个脚本, 让它可以检查一个新闻组的每个文件,
29 #+ 并且如果下一个没找的话就跳过.
30
31 exit 0
################################End Script#########################################
注意: fold -s 命令在处理从 Usenet 新闻组下载下来的长的uudecode 文本消息的时候可
能会有用(可能在管道中).
mimencode, mmencode
mimencode 和 mmencode 命令处理多媒体编码的 email 附件. 虽然 mail 用户代理
(比如 pine 或 kmail) 通常情况下都会自动处理, 但是这些特定的工具允许从命令行或
shell脚本中来手动操作这些附件.
crypt
这个工具曾经是标准的 UNIX 文件加密工具. [3] 政府由于政策上的动机规定禁止加密软
件的输出, 这样导致了 crypt 命令从 UNIX 世界消失, 并且在大多数的 Linux 发行版中
也没有这个命令. 幸运的是, 程序员们想出了一些替代它的方法, 在这些方法中有作者自
己的 cruft (参见 Example A-4).
一些杂项工具
mktemp
使用一个"唯一"的文件名来创建一个 临时文件 [4] . 如果不带参数的在命令行下调用
这个命令时, 将会在 /tmp 目录下产生一个零长度的文件.
bash$ mktemp
/tmp/tmp.zzsvql3154
1 PREFIX=filename
2 tempfile=`mktemp $PREFIX.XXXXXX`
3 # ^^^^^^ 在这个临时的文件名中
4 #+ 至少需要6个占位符.
5 # 如果没有指定临时文件的文件名,
6 #+ 那么默认就是 "tmp.XXXXXXXXXX".
7
8 echo "tempfile name = $tempfile"
9 # tempfile name = filename.QA2ZpY
10 # 或者一些其他的相似的名字...
11
http://www.818198.com Page 276
SHELL十三问
12 # 使用 600 为文件权限
13 #+ 来在当前工作目录下创建一个这样的文件.
14 # 这样就不需要 "umask 177" 了.
15 # 但不管怎么说, 这也是一个好的编程风格.
make
build 和 compile 二进制包的工具. 当源文件被增加或修改时就会触发一些操作, 这个工
具用来控制这些操作.
make 命令将会检查 Makefile, makefile 是文件的依赖和操作列表.
install
特殊目的的文件拷贝命令, 与 cp 命令相似, 但是具有设置拷贝文件的权限和属性的能力.
这个命令看起来是为了安装软件包所定制的, 而且就其本身而言, 这个命令经常出现在
Makefile 中(在 make install : 区中). 在安装脚本中也会看到这个命令的使用.
dos2unix
这个工具是由 Benjamin Lin 和其同事编写的, 目的是将 DOS 格式的文本文件
(以 CR-LF 为行结束符) 转换为 UNIX 格式 (以 LF 为行结束符), 反过来也一样.
ptx
ptx [targetfile] 命令将会输出目标文件的序列改变的索引(交叉引用列表). 如果必要的
话, 这个命令可以在管道中进行更深层次的过滤和格式化.
more, less
分页显示文本文件或 stdout, 一次一屏.可以用来过滤 stdout 的输出 . . . 或一个脚本
的输出.
more 命令的一个有趣的应用就是测试一个命令序列的执行, 来避免可能发生的糟糕的
结果.
1 ls /home/bozo | awk '{print "rm -rf " $1}' | more
2 # ^^^^
3
4 # 检测下边(灾难性的)命令行的效果:
5 # ls /home/bozo | awk '{print "rm -rf " $1}' | sh
6 # 推入 shell 中执行 . . .
注意事项:
[1] 在这里所讨论的一个归档文件, 只不过是存储在一个单一位置上的一些相关文件的
集合.
[2] tar czvf archive_name.tar.gz * 可以 包含当前工作目录下的点文件. 这是一个
未文档化的 GNU tar 的"特征".
[3] 这是一个对称的块密码, 过去曾在单系统或本地网络中用来加密文件, 用来对抗
"public key" 密码类, pgp 就是一个众所周知的例子.
[4] 使用 -d 选项可以创建一个临时的目录.
12.6 通讯命令
------------- 下边命令中的某几个命令你会在 "追踪垃圾邮件" 练习中找到其用法, 用来进行网络数
据的转换和分析.
信息与统计
host
通过名字或 IP 地址来搜索一个互联网主机的信息, 使用 DNS.
bash$ host surfacemail.com
surfacemail.com. has address 202.92.42.236
ipcalc
http://www.818198.com Page 277
SHELL十三问
显示一个主机 IP 信息. 使用 -h 选项, ipcalc 将会做一个 DNS 的反向查询, 通过 IP
地址找到主机(服务器)名.
bash$ ipcalc -h 202.92.42.236
HOSTNAME=surfacemail.com
nslookup
通过 IP 地址在一个主机上做一个互联网的 "名字服务查询". 事实上这与 ipcalc -h 或
dig -x 等价. 这个命令既可以交互运行也可以非交互运行, 换句话说, 就是在脚本中运
行.
nslookup 命令据说已经慢慢被"忽视"了, 但是它还是有它的用处.
bash$ nslookup -sil 66.97.104.180
nslookup kuhleersparnis.ch
Server: 135.116.137.2
Address: 135.116.137.2#53
Non-authoritative answer:
Name: kuhleersparnis.ch
dig
域信息查询. 与 nslookup 很相似, dig 在一个主机上做一个互联网的 "名字服务查询".
这个命令既可以交互运行也可以非交互运行, 换句话说, 就是在脚本中运行.
下边是一些 dig 命令有趣的选项, +time=N 选项用来设置查询超时为 N 秒, +nofail
选项用来持续查询服务器直到收到一个响应, -x 选项会做反向地址查询.
比较下边这3个命令的输出, dig -x , ipcalc -h 和 nslookup.
bash$ dig -x 81.9.6.2
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 11649
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0
;; QUESTION SECTION:
;2.6.9.81.in-addr.arpa. IN PTR
;; AUTHORITY SECTION:
6.9.81.in-addr.arpa. 3600 IN SOA ns.eltel.net. noc.eltel.net.
2002031705 900 600 86400 3600
;; Query time: 537 msec
;; SERVER: 135.116.137.2#53(135.116.137.2)
;; WHEN: Wed Jun 26 08:35:24 2002
;; MSG SIZE rcvd: 91
Example 12-36 查找滥用的连接来报告垃圾邮件发送者
################################Start Script#######################################
1 #!/bin/bash
2 # spam-lookup.sh: 查找滥用的连接来报告垃圾邮件发送者.
3 # 感谢 Michael Zick.
4
5 # 检查命令行参数.
6 ARGCOUNT=1
7 E_WRONGARGS=65
8 if [ $# -ne "$ARGCOUNT" ]
9 then
10 echo "Usage: `basename $0` domain-name"
11 exit $E_WRONGARGS
12 fi
http://www.818198.com Page 278
SHELL十三问
13
14
15 dig +short $1.contacts.abuse.net -c in -t txt
16 # 也试试:
17 # dig +nssearch $1
18 # 尽量找到 "可信赖的名字服务器" 并且显示 SOA 记录.
19
20 # 下边这句也可以:
21 # whois -h whois.abuse.net $1
22 # ^^ ^^^^^^^^^^^^^^^ 指定主机.
23 # 使用这个命令也可以查找多个垃圾邮件发送者, 比如:"
24 # whois -h whois.abuse.net $spamdomain1 $spamdomain2 . . .
25
26
27 # 练习:
28 # -----29 # 扩展这个脚本的功能,
30 #+ 让它可以自动发送 e-mail 来通知
31 #+ 需要对此负责的 ISP 的联系地址.
32 # 暗示: 使用 "mail" 命令.
33
34 exit $?
35
36 # spam-lookup.sh chinatietong.com
37 # 一个已知的垃圾邮件域.(译者: 中国铁通. . .)
38
39 # "[email protected]"
40 # "[email protected]"
41 # "[email protected]"
42
43
44 # 如果想找到这个脚本的一个更详尽的版本,
45 #+ 请访问 SpamViz 的主页, http://www.spamviz.net/index.html.
################################End Script#########################################
Example 12-37 分析一个垃圾邮件域
################################Start Script#######################################
1 #! /bin/bash
2 # is-spammer.sh: 鉴别一个垃圾邮件域
3
4 # $Id: is-spammer, v 1.4 2004/09/01 19:37:52 mszick Exp $
5 # 上边这行是 RCS ID 信息.
6 #
7 # 这是附件中捐献脚本 is_spammer.bash
8 #+ 的一个简单版本.
9
10 # is-spammer
11
12 # 使用外部程序: 'dig'
http://www.818198.com Page 279
SHELL十三问
13 # 测试版本: 9.2.4rc5
14
15 # 使用函数.
16 # 使用 IFS 来分析分配在数组中的字符串.
17 # 检查 e-mail 黑名单.
18
19 # 使用来自文本体中的 domain.name:
20 # http://www.good_stuff.spammer.biz/just_ignore_everything_else
21 # ^^^^^^^^^^^
22 # 或者使用来自任意 e-mail 地址的 domain.name:
23 # [email protected]
24 #
25 # 并将其作为这个脚本的唯一参数.
26 #(另: 你的 Inet 连接应该保证连接)
27 #
28 # 这样, 在上边两个实例中调用这个脚本:
29 # is-spammer.sh spammer.biz
30
31
32 # Whitespace == :Space:Tab:Line Feed:Carriage Return:
33 WSP_IFS=$'\x20'$'\x09'$'\x0A'$'\x0D'
34
35 # No Whitespace == Line Feed:Carriage Return
36 No_WSP=$'\x0A'$'\x0D'
37
38 # 域分隔符为点分10进制 ip 地址
39 ADR_IFS=${No_WSP}'.'
40
41 # 取得 dns 文本资源记录.
42 # get_txt
43 get_txt() {
44
45 # 分析在"."中分配的 $1.
46 local -a dns
47 IFS=$ADR_IFS
48 dns=( $1 )
49 IFS=$WSP_IFS
50 if [ "${dns[0]}" == '127' ]
51 then
52 # 查看此处是否有原因.
53 echo $(dig +short $2 -t txt)
54 fi
55 }
56
57 # 取得 dns 地址资源记录.
58 # chk_adr
59 chk_adr() {
60 local reply
http://www.818198.com Page 280
SHELL十三问
61 local server
62 local reason
63
64 server=${1}${2}
65 reply=$( dig +short ${server} )
66
67 # 假设应答可能是一个错误码 . . .
68 if [ ${#reply} -gt 6 ]
69 then
70 reason=$(get_txt ${reply} ${server} )
71 reason=${reason:-${reply}}
72 fi
73 echo ${reason:-' not blacklisted.'}
74 }
75
76 # 需要从名字中取得 IP 地址.
77 echo 'Get address of: '$1
78 ip_adr=$(dig +short $1)
79 dns_reply=${ip_adr:-' no answer '}
80 echo ' Found address: '${dns_reply}
81
82 # 一个可用的应答至少是4个数字加上3个点.
83 if [ ${#ip_adr} -gt 6 ]
84 then
85 echo
86 declare query
87
88 # 分析点中的分配.
89 declare -a dns
90 IFS=$ADR_IFS
91 dns=( ${ip_adr} )
92 IFS=$WSP_IFS
93
94 # Reorder octets into dns query order.
95 rev_dns="${dns[3]}"'.'"${dns[2]}"'.'"${dns[1]}"'.'"${dns[0]}"'.'
96
97 # 参见: http://www.spamhaus.org (Conservative, well maintained)
98 echo -n 'spamhaus.org says: '
99 echo $(chk_adr ${rev_dns} 'sbl-xbl.spamhaus.org')
100
101 # 参见: http://ordb.org (Open mail relays)
102 echo -n ' ordb.org says: '
103 echo $(chk_adr ${rev_dns} 'relays.ordb.org')
104
105 # 参见: http://www.spamcop.net/ (你可以在这里报告 spammer)
106 echo -n ' spamcop.net says: '
107 echo $(chk_adr ${rev_dns} 'bl.spamcop.net')
108
http://www.818198.com Page 281
SHELL十三问
109 # # # 其他的黑名单操作 # # #
110
111 # 参见: http://cbl.abuseat.org.
112 echo -n ' abuseat.org says: '
113 echo $(chk_adr ${rev_dns} 'cbl.abuseat.org')
114
115 # 参见: http://dsbl.org/usage (Various mail relays)
116 echo
117 echo 'Distributed Server Listings'
118 echo -n ' list.dsbl.org says: '
119 echo $(chk_adr ${rev_dns} 'list.dsbl.org')
120
121 echo -n ' multihop.dsbl.org says: '
122 echo $(chk_adr ${rev_dns} 'multihop.dsbl.org')
123
124 echo -n 'unconfirmed.dsbl.org says: '
125 echo $(chk_adr ${rev_dns} 'unconfirmed.dsbl.org')
126
127 else
128 echo
129 echo 'Could not use that address.'
130 fi
131
132 exit 0
133
134 # 练习:
135 # -----136
137 # 1) 检查脚本的参数,
138 # 并且如果必要的话使用合适的错误消息退出.
139
140 # 2) 检查调用这个脚本的时候是否在线,
141 # 并且如果必要的话使用合适的错误消息退出.
142
143 # 3) Substitute generic variables for "hard-coded" BHL domains.
144
145 # 4) 通过对 'dig' 命令使用 "+time=" 选项
146 来给这个脚本设置一个暂停.
################################End Script#########################################
想获得比上边这个脚本更详细的版本, 参见 Example A-27.
traceroute
跟踪包发送到远端主机过程中的路由信息. 这个命令在 LAN, WAN, 或者在 Internet 上都
可以正常工作. 远端主机可以通过 IP 地址来指定. 这个命令的输出也可以通过管道中的
grep 或 sed 命令来过滤.
bash$ traceroute 81.9.6.2
traceroute to 81.9.6.2 (81.9.6.2), 30 hops max, 38 byte packets
1 tc43.xjbnnbrb.com (136.30.178.8) 191.303 ms 179.400 ms 179.767 ms
2 or0.xjbnnbrb.com (136.30.178.1) 179.536 ms 179.534 ms 169.685 ms
http://www.818198.com Page 282
SHELL十三问
3 192.168.11.101 (192.168.11.101) 189.471 ms 189.556 ms *
...
ping
广播一个 "ICMP ECHO_REQUEST" 包到其他主机上, 既可以是本地网络也可以使远端网络.
这是一个测试网络连接的诊断工具, 应该小心使用.
一个成功的 ping 返回的 退出码 为 0. 可以用在脚本的测试语句中.
bash$ ping localhost
PING localhost.localdomain (127.0.0.1) from 127.0.0.1 : 56(84) bytes of data.
64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=0 ttl=255 time=709 usec
64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=1 ttl=255 time=286 usec
--- localhost.localdomain ping statistics --- 2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/mdev = 0.286/0.497/0.709/0.212 ms
whois
执行DNS (域名系统) 查询lookup. -h 选项允许指定需要查询的特定的 whois 服务器.
参见 Example 4-6 和 Example 12-36.
finger
取得网络上的用户信息. 另外这个命令可以显示一个用户的~/.plan, ~/.project, 和
~/.forward 文件, 如果存在的话.
bash$ finger
Login Name Tty Idle Login Time Office Office Phone
bozo Bozo Bozeman tty1 8 Jun 25 16:59
bozo Bozo Bozeman ttyp0 Jun 25 16:59
bozo Bozo Bozeman ttyp1 Jun 25 17:07
bash$ finger bozo
Login: bozo Name: Bozo Bozeman
Directory: /home/bozo Shell: /bin/bash
Office: 2355 Clown St., 543-1234
On since Fri Aug 31 20:13 (MST) on tty1 1 hour 38 minutes idle
On since Fri Aug 31 20:13 (MST) on pts/0 12 seconds idle
On since Fri Aug 31 20:13 (MST) on pts/1
On since Fri Aug 31 20:31 (MST) on pts/2 1 hour 16 minutes idle
No mail.
No Plan.
处于安全上的考虑, 许多网络都禁用了 finger 以及和它相关的幽灵进程. [1]
chfn
修改 finger 命令所显示出来的用户信息.
vrfy
验证一个互联网的 e-mail 地址.
远端主机接入
sx, rx
sx 和 rx 命令使用 xmodem 协议, 设置服务来向远端主机传输文件和接收文件. 这些都
是通讯安装包的一般部分, 比如 minicom.
sz, rz
sz 和 rz 命令使用 zmodem 协议, 设置服务来向远端主机传输文件和接收文件. zmodem
http://www.818198.com Page 283
SHELL十三问
协议在某些方面比 xmodem强, 比如使用更快的的传输波特率, 并且可以对中断的文件进
行续传.与 sx 一样 rx, 这些都是通讯安装包的一般部分.
ftp
向远端服务器上传或下载的工具和协议. 一个ftp会话可以写到脚本中自动运行. (见
Example 17-6, Example A-4, 和 Example A-13).
uucp, uux, cu
uucp: UNIX 到 UNIX 拷贝. 这是一个通讯安装包, 目的是为了在 UNIX 服务器之间传输
文件. 使用 shell 脚本来处理 uucp 命令序列是一种有效的方法.
因为互联网和电子邮件的出现, uucp 现在看起来已经很落伍了, 但是这个命令在互联网
连接不可用或者不适合使用的地方, 这个命令还是可以完美的运行. uucp 的优点就是它
的容错性, 即使有一个服务将拷贝操作中断了, 那么当连接恢复的时候, 这个命令还是
可以在中断的地方续传.
---
uux: UNIX 到 UNIX 执行. 在远端系统上执行一个命令.这个命令是 uucp 包的一部分.
---
cu: Call Up 一个远端系统并且作为一个简单终端进行连接. 这是一个 telnet 的缩减
版本. 这个命令是 uucp 包的一部分.
telnet
连接远端主机的工具和协议.
注意:telnet 协议本身包含安全漏洞, 因此我们应该适当的避免使用.
wget
wget 工具使用非交互的形式从 web 或 ftp 站点上取得或下载文件. 在脚本中使用正好.
1 wget -p http://www.xyz23.com/file01.html
2 # The -p or --page-requisite 选项将会使得 wget 取得显示指定页时
3 #+ 所需要的所有文件.(译者: 比如内嵌图片和样式表等).
4
5 wget -r ftp://ftp.xyz24.net/~bozo/project_files/ -O $SAVEFILE
6 # -r 选项将会递归的从指定站点
7 #+ 上下载所有连接.
Example 12-38 获得一份股票报价
################################Start Script#######################################
1 #!/bin/bash
2 # quote-fetch.sh: 下载一份股票报价.
3
4
5 E_NOPARAMS=66
6
7 if [ -z "$1" ] # 必须指定需要获取的股票(代号).
8 then echo "Usage: `basename $0` stock-symbol"
9 exit $E_NOPARAMS
10 fi
11
12 stock_symbol=$1
13
14 file_suffix=.html
15 # 获得一个 HTML 文件, 所以要正确命名它.
16 URL='http://finance.yahoo.com/q?s='
17 # Yahoo 金融板块, 后缀是股票查询.
http://www.818198.com Page 284
SHELL十三问
18
19 # -----------------------------------------------------------20 wget -O ${stock_symbol}${file_suffix} "${URL}${stock_symbol}"
21 # -----------------------------------------------------------22
23
24 # 在 http://search.yahoo.com 上查询相关材料:
25 # -----------------------------------------------------------26 # URL="http://search.yahoo.com/search?fr=ush-news&p=${query}"
27 # wget -O "$savefilename" "${URL}"
28 # -----------------------------------------------------------29 # 保存相关 URL 的列表.
30
31 exit $?
32
33 # 练习:
34 # -----35 #
36 # 1) 添加一个测试来验证用户正在线.
37 # (暗示: 对 "ppp" 或 "connect" 来分析 'ps -ax' 的输出.
38 #
39 # 2) 修改这个脚本, 让这个脚本具有获得本地天气预报的能力,
40 #+ 将用户的 zip code 作为参数.
################################End Script#########################################
参见 Example A-29 和 Example A-30.
lynx
lynx 是一个网页浏览器, 也是一个文件浏览器. 它可以(通过使用 -dump 选项)在脚本中
使用. 它的作用是可以从 Web 或 ftp 站点上非交互的获得文件.
1 lynx -dump http://www.xyz23.com/file01.html >$SAVEFILE
使用 -traversal 选项, lynx 将从参数中指定的 HTTP URL 开始, 遍历指定服务器上的
所有链接. 如果与 -crawl 选项一起用的话, 将会把每个输出的页面文本都放到一个 log
文件中.
rlogin
远端登陆, 在远端的主机上开启一个会话. 这个命令存在安全隐患, 所以要使用 ssh 来
代替.
rsh
远端 shell, 在远端的主机上执行命令. 这个命令存在安全隐患, 所以要使用 ssh 来代
替.
rcp
远端拷贝, 在网络上的不同主机间拷贝文件.
rsync
远端同步, 在网络上的不同主机间(同步)更新文件.
bash$ rsync -a ~/sourcedir/*txt /node1/subdirectory/
Example 12-39 更新 Fedora 4
################################Start Script#######################################
1 #!/bin/bash
2 # fc4upd.sh
3
http://www.818198.com Page 285
SHELL十三问
4 # 脚本作者: Frank Wang.
5 # 本书作者作了少量修改.
6 # 授权在本书中使用.
7
8
9 # 使用 rsync 命令从镜像站点上下载 Fedora 4 的更新.
10 # 为了节省空间, 如果有多个版本存在的话,
11 #+ 只下载最新的包.
12
13 URL=rsync://distro.ibiblio.org/fedora-linux-core/updates/
14 # URL=rsync://ftp.kddilabs.jp/fedora/core/updates/
15 # URL=rsync://rsync.planetmirror.com/fedora-linux-core/updates/
16
17 DEST=${1:-/var/www/html/fedora/updates/}
18 LOG=/tmp/repo-update-$(/bin/date +%Y-%m-%d).txt
19 PID_FILE=/var/run/${0##*/}.pid
20
21 E_RETURN=65 # 某些意想不到的错误.
22
23
24 # 一搬 rsync 选项
25 # -r: 递归下载
26 # -t: 保存时间
27 # -v: verbose
28
29 OPTS="-rtv --delete-excluded --delete-after --partial"
30
31 # rsync include 模式
32 # Leading slash causes absolute path name match.
33 INCLUDE=(
34 "/4/i386/kde-i18n-Chinese*"
35 # ^ ^
36 # 双引号是必须的, 用来防止file globbing.
37 )
38
39
40 # rsync exclude 模式
41 # 使用 "#" 临时注释掉一些不需要的包.
42 EXCLUDE=(
43 /1
44 /2
45 /3
46 /testing
47 /4/SRPMS
48 /4/ppc
49 /4/x86_64
50 /4/i386/debug
51 "/4/i386/kde-i18n-*"
http://www.818198.com Page 286
SHELL十三问
52 "/4/i386/openoffice.org-langpack-*"
53 "/4/i386/*i586.rpm"
54 "/4/i386/GFS-*"
55 "/4/i386/cman-*"
56 "/4/i386/dlm-*"
57 "/4/i386/gnbd-*"
58 "/4/i386/kernel-smp*"
59 # "/4/i386/kernel-xen*"
60 # "/4/i386/xen-*"
61 )
62
63
64 init () {
65 # 让管道命令返回可能的 rsync 错误, 比如, 网络延时(stalled network).
66 set -o pipefail
67
68 TMP=${TMPDIR:-/tmp}/${0##*/}.$$ # 保存精炼的下载列表.
69 trap "{
70 rm -f $TMP 2>/dev/null
71 }" EXIT # 删除存在的临时文件.
72 }
73
74
75 check_pid () {
76 # 检查进程是否存在.
77 if [ -s "$PID_FILE" ]; then
78 echo "PID file exists. Checking ..."
79 PID=$(/bin/egrep -o "^[[:digit:]]+" $PID_FILE)
80 if /bin/ps --pid $PID &>/dev/null; then
81 echo "Process $PID found. ${0##*/} seems to be running!"
82 /usr/bin/logger -t ${0##*/} \
83 "Process $PID found. ${0##*/} seems to be running!"
84 exit $E_RETURN
85 fi
86 echo "Process $PID not found. Start new process . . ."
87 fi
88 }
89
90
91 # 根据上边的模式,
92 #+ 设置整个文件的更新范围, 从 root 或 $URL 开始.
93 set_range () {
94 include=
95 exclude=
96 for p in "${INCLUDE[@]}"; do
97 include="$include --include \"$p\""
98 done
99
http://www.818198.com Page 287
SHELL十三问
100 for p in "${EXCLUDE[@]}"; do
101 exclude="$exclude --exclude \"$p\""
102 done
103 }
104
105
106 # 获得并提炼 rsync 更新列表.
107 get_list () {
108 echo $$ > $PID_FILE || {
109 echo "Can't write to pid file $PID_FILE"
110 exit $E_RETURN
111 }
112
113 echo -n "Retrieving and refining update list . . ."
114
115 # 获得列表 -- 为了作为单个命令来运行 rsync 需要 'eval'.
116 # $3 和 $4 是文件创建的日期和时间.
117 # $5 是完整的包名字.
118 previous=
119 pre_file=
120 pre_date=0
121 eval /bin/nice /usr/bin/rsync \
122 -r $include $exclude $URL | \
123 egrep '^dr.x|^-r' | \
124 awk '{print $3, $4, $5}' | \
125 sort -k3 | \
126 { while read line; do
127 # 获得这段运行的秒数, 过滤掉不用的包.
128 cur_date=$(date -d "$(echo $line | awk '{print $1, $2}')" +%s)
129 # echo $cur_date
130
131 # 取得文件名.
132 cur_file=$(echo $line | awk '{print $3}')
133 # echo $cur_file
134
135 # 如果可能的话, 从文件名中取得 rpm 的包名字.
136 if [[ $cur_file == *rpm ]]; then
137 pkg_name=$(echo $cur_file | sed -r -e \
138 's/(^([^_-]+[_-])+)[[:digit:]]+\..*[_-].*$/\1/')
139 else
140 pkg_name=
141 fi
142 # echo $pkg_name
143
144 if [ -z "$pkg_name" ]; then # 如果不是一个 rpm 文件,
145 echo $cur_file >> $TMP #+ 然后添加到下载列表里.
146 elif [ "$pkg_name" != "$previous" ]; then # 发现一个新包.
147 echo $pre_file >> $TMP # 输出最新的文件.
http://www.818198.com Page 288
SHELL十三问
148 previous=$pkg_name # 保存当前状态.
149 pre_date=$cur_date
150 pre_file=$cur_file
151 elif [ "$cur_date" -gt "$pre_date" ]; then # 如果是相同的包, 但是更新一些,
152 pre_date=$cur_date #+ 那么就更新最新的.
153 pre_file=$cur_file
154 fi
155 done
156 echo $pre_file >> $TMP # TMP 现在包含所有
157 #+ 提炼过的列表.
158 # echo "subshell=$BASH_SUBSHELL"
159
160 } # 这里的打括号是为了让最后这句"echo $pre_file >> $TMP"
161 # 也能与整个循环一起放到同一个子 shell ( 1 )中.
162
163 RET=$? # 取得管道命令的返回码.
164
165 [ "$RET" -ne 0 ] && {
166 echo "List retrieving failed with code $RET"
167 exit $E_RETURN
168 }
169
170 echo "done"; echo
171 }
172
173 # 真正的 rsync 的下载部分.
174 get_file () {
175
176 echo "Downloading..."
177 /bin/nice /usr/bin/rsync \
178 $OPTS \
179 --filter "merge,+/ $TMP" \
180 --exclude '*' \
181 $URL $DEST \
182 | /usr/bin/tee $LOG
183
184 RET=$?
185
186 # --filter merge,+/ is crucial for the intention.
187 # + modifier means include and / means absolute path.
188 # Then sorted list in $TMP will contain ascending dir name and
189 #+ prevent the following --exclude '*' from "shortcutting the circuit."
190
191 echo "Done"
192
193 rm -f $PID_FILE 2>/dev/null
194
195 return $RET
http://www.818198.com Page 289
SHELL十三问
196 }
197
198 # -------199 # Main
200 init
201 check_pid
202 set_range
203 get_list
204 get_file
205 RET=$?
206 # -------207
208 if [ "$RET" -eq 0 ]; then
209 /usr/bin/logger -t ${0##*/} "Fedora update mirrored successfully."
210 else
211 /usr/bin/logger -t ${0##*/} "Fedora update mirrored with failure code: $RET"
212 fi
213
214 exit $RET
################################End Script#########################################
使用 rcp, rsync, 和其他一些有安全问题的类似工具, 并将这些工具用在 shell 脚本中
是不明智的. 应该考虑使用 ssh, scp, 或者一个 expect 脚本来代替这些不安全的工具.
ssh
安全 shell, 登陆远端主机并在其上运行命令. 这个工具具有身份认证和加密的功能, 可
以安全的替换 telnet, rlogin, rcp, 和 rsh 等工具. 参见 man页 来获取详细信息.
Example 12-40 使用 ssh
################################Start Script#######################################
1 #!/bin/bash
2 # remote.bash: 使用 ssh.
3
4 # 这个例子是 Michael Zick 编写的.
5 # 授权使用.
6
7
8 # 假设:
9 # -----10 # fd-2(文件描述符2) 并没有被抛弃 ( '2>/dev/null' ).
11 # ssh/sshd 假设 stderr ('2') 将会被显示给用户.
12 #
13 # sshd 正运行在你的机器上.
14 # 对于大多数 '标准' 的发行版, 是应该有的,
15 #+ 并且没有一些稀奇古怪的 ssh-keygen.
16
17 # 在你的机器上从命令行中试一下 ssh:
18 #
19 # $ ssh $HOSTNAME
20 # 不同特殊的准备, 你将被要求输入你的密码.
21 # 输入密码
http://www.818198.com Page 290
SHELL十三问
22 # 完成后, $ exit
23 #
24 # 好使了么? 如果好使了, 你可以做好准备来获取更多的乐趣了.
25
26 # 在你的机器上用 'root'身份来试试 ssh:
27 #
28 # $ ssh -l root $HOSTNAME
29 # 当询问密码时, 输入 root 的密码, 别输入你的密码.
30 # Last login: Tue Aug 10 20:25:49 2004 from localhost.localdomain
31 # 完成后键入 'exit'.
32
33 # 上边的动作将会给你一个交互的shell.
34 # 在 'single command' 模式下建立 sshd 是可能的,
35 #+ 不过这已经超出本例的范围了.
36 # 唯一需要注意的事情是下面都可以工作在
37 #+ 'single command' 模式.
38
39
40 # 一个基本的写输出(本地)命令.
41
42 ls -l
43
44 # 现在在远端机器上使用同样的基本命令.
45 # 使用一套不同的 'USERNAME' 和 'HOSTNAME' :
46 USER=${USERNAME:-$(whoami)}
47 HOST=${HOSTNAME:-$(hostname)}
48
49 # 现在在远端主机上运行上边的命令行命令,
50 #+ 当然, 所有的传输都被加密了.
51
52 ssh -l ${USER} ${HOST} " ls -l "
53
54 # 期望的结果就是在远端主机上列出你的
55 #+ username 主目录的所有文件.
56 # 如果想看点不一样的, 那就
57 #+ 在别的地方运行这个脚本, 别再你的主目录上运行这个脚本.
58
59 # 换句话说, Bash 命令已经作为一个引用行
60 #+ 被传递到远端的shell 中了,这样就可以在远端的机器上运行它了.
61 # 在这种情况下, sshd 代表你运行了 ' bash -c "ls -l" '.
62
63 # 对于每个命令行如果想不输入密码的话,
64 #+ 对于这种类似的议题, 可以参阅
65 #+ man ssh
66 #+ man ssh-keygen
67 #+ man sshd_config.
68
69 exit 0
http://www.818198.com Page 291
SHELL十三问
################################End Script#########################################
注意: 在循环中, ssh 可能会引起意想不到的异常行为. 根据comp.unix 上的shell文档
Usenet post , ssh 继承了循环的标准输入.为了解决这个问题, 使用 ssh 的 -n
或者 -f 选项.
感谢 Jason Bechtel, 指出这点.
scp
安全拷贝, 在功能上与 rcp 很相似, 就是在2个不同的网络主机之间拷贝文件, 但是要通
过鉴权的方式, 并且使用与 ssh 类似的安全层.
Local Network
write
这是一个端到端通讯的工具. 这个工具可以从你的终端上(console 或者 xterm)发送整行
到另一个用户的终端上. mesg 命令当然也可以用来对于一个终端的写权限
因为 write 是需要交互的, 所以这个命令通常不使用在脚本中.
netconfig
用来配置网络适配器(使用 DHCP)的命令行工具. 这个命令对于红帽发行版来说是内置的.
Mail
mail
发送或读取 e-mail 消息.
如果把这个命令行的 mail 客户端当成一个脚本中的命令来使用的话, 效果非常好.
Example 12-41 一个可以mail自己的脚本
################################Start Script#######################################
1 #!/bin/sh
2 # self-mailer.sh: mail自己的脚本
3
4 adr=${1:-`whoami`} # 如果不指定的话, 默认是当前用户.
5 # 键入 'self-mailer.sh [email protected]'
6 #+ 发送这个脚本到这个地址.
7 # 如果只键入 'self-mailer.sh' (不给参数) 的话, 那么这脚本就会被发送给
8 #+ 调用者, 比如 [email protected].
9 #
10 # 如果想了解 ${parameter:-default} 结构的更多细节,
11 #+ 请参见第9章 变量重游中的
12 #+ 第3节 参数替换.
13
14 # ============================================================================
15 cat $0 | mail -s "Script \"`basename $0`\" has mailed itself to you." "$adr"
16 # ============================================================================
17
18 # --------------------------------------------19 # 来自 self-mailing 脚本的一份祝福.
20 # 一个喜欢恶搞的家伙运行了这个脚本,
21 #+ 这导致了他自己收到了这份mail.
22 # 显然的, 有些人确实没什么事好做,
23 #+ 就只能浪费他们自己的时间玩了.
24 # --------------------------------------------25
26 echo "At `date`, script \"`basename $0`\" mailed to "$adr"."
http://www.818198.com Page 292
SHELL十三问
27
28 exit 0
################################End Script#########################################
mailto
与 mail 命令很相似, mailto 命令可以使用命令行或在脚本中发送 e-mail 消息. 然而,
mailto 命令也允许发送 MIME (多媒体) 消息.
vacation
这个工具可以自动回复 e-mail 给发送者, 表示邮件的接受者正在度假暂时无法收到邮件.
这个工具与 sendmail 一起运行于网络上, 并且这个工具不支持拨号的 POPmail 帐号.
注意事项:
[1] 一个幽灵进程指的是并未附加在终端会话中的后台进程. 幽灵进程 在指定的时间执
行指定的服务, 或者由特定的事件出发来执行指定的服务.
12.7 终端控制命令
-----------------影响控制台或终端的命令
tput
初始化终端或者从 terminfo data 中取得终端信息. 不同的选项允许特定的终端操作.
tput clear 与下边的 clear 等价. tput reset 与下边的 reset 等价. tput sgr0 也可
以重置终端, 但是并不清除屏幕.
bash$ tput longname
xterm terminal emulator (XFree86 4.0 Window System)
使用 tput cup X Y 将会把光标移动到当前终端的(X,Y)坐标上. 使用这个命令之前一边
都要先使用一下 clear 命令, 把屏幕清除一下.
注意: stty 提供了一个更强力的命令专门用来设置如何控制终端.
infocmp
这个命令会打印出大量的当前终端的信息. 事实上它是引用了 terminfo 数据库.
bash$ infocmp
# 通过来自于文件的 infocmp 显示出来:
/usr/share/terminfo/r/rxvt
rxvt|rxvt terminal emulator (X Window System),
am, bce, eo, km, mir, msgr, xenl, xon,
colors#8, cols#80, it#8, lines#24, pairs#64,
acsc=``aaffggjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
bel=^G, blink=\E[5m, bold=\E[1m,
civis=\E[?25l,
clear=\E[H\E[2J, cnorm=\E[?25h, cr=^M,
...
reset
重置终端参数并且清除屏幕. 与 clear 命令一样, 光标和提示符将会重新出现在终端的
左上角.
clear
clear 命令只不过是简单的清除控制台或者 xterm 的屏幕. 光标和提示符将会重新出现
在屏幕或者 xterm window 的左上角. 这个命令既可以用在命令行中也可以用在脚本中.
参见 Example 10-25.
script
这个工具将会记录(保存到一个文件中)所有的用户在控制台下的或在 xterm window下的
按键信息. 这其实就是创建了一个会话记录.
12.8 数学计算命令
http://www.818198.com Page 293
SHELL十三问
-----------------"Doing the numbers"
factor
将一个正数分解为多个素数.
bash$ factor 27417
27417: 3 13 19 37
bc
Bash 不能处理浮点运算, 并且缺乏特定的一些操作,这些操作都是一些重要的计算功能.
幸运的是, bc 可以解决这个问题.
bc 不仅仅是个多功能灵活的精确的工具, 而且它还提供许多编程语言才具备的一些方便
的功能.
bc 比较类似于 C 语言的语法.
因为它是一个完整的 UNIX 工具, 所以它可以用在管道中, bc 在脚本中也是很常用的.
这里有一个简单的使用 bc 命令的模版可以用来在计算脚本中的变量. 用在命令替换中.
variable=$(echo "OPTIONS; OPERATIONS" | bc)
Example 12-42 按月偿还贷款
################################Start Script#######################################
1 #!/bin/bash
2 # monthlypmt.sh: 计算按月偿还贷款的数量.
3
4
5 # 这份代码是一份修改版本, 原始版本在 "mcalc" (贷款计算)包中,
6 #+ 这个包的作者是 Jeff Schmidt 和 Mendel Cooper (本书作者).
7 # http://www.ibiblio.org/pub/Linux/apps/financial/mcalc-1.6.tar.gz [15k]
8
9 echo
10 echo "Given the principal, interest rate, and term of a mortgage,"
11 echo "calculate the monthly payment."
12
13 bottom=1.0
14
15 echo
16 echo -n "Enter principal (no commas) "
17 read principal
18 echo -n "Enter interest rate (percent) " # 如果是 12%, 那就键入 "12", 别输入 ".12".
19 read interest_r
20 echo -n "Enter term (months) "
21 read term
22
23
24 interest_r=$(echo "scale=9; $interest_r/100.0" | bc) # 转换成小数.
25 # "scale" 指定了有效数字的个数.
26
27
28 interest_rate=$(echo "scale=9; $interest_r/12 + 1.0" | bc)
29
30
31 top=$(echo "scale=9; $principal*$interest_rate^$term" | bc)
http://www.818198.com Page 294
SHELL十三问
32
33 echo; echo "Please be patient. This may take a while."
34
35 let "months = $term - 1"
36 # ====================================================================
37 for ((x=$months; x > 0; x--))
38 do
39 bot=$(echo "scale=9; $interest_rate^$x" | bc)
40 bottom=$(echo "scale=9; $bottom+$bot" | bc)
41 # bottom = $(($bottom + $bot"))
42 done
43 # ====================================================================
44
45 # --------------------------------------------------------------------
46 # Rick Boivie 给出了一个对上边循环的修改,
47 #+ 这个修改更加有效率, 将会节省大概 2/3 的时间.
48
49 # for ((x=1; x <= $months; x++))
50 # do
51 # bottom=$(echo "scale=9; $bottom * $interest_rate + 1" | bc)
52 # done
53
54
55 # 然后他又想出了一个更加有效率的版本,
56 #+ 将会节省 95% 的时间!
57
58 # bottom=`{
59 # echo "scale=9; bottom=$bottom; interest_rate=$interest_rate"
60 # for ((x=1; x <= $months; x++))
61 # do
62 # echo 'bottom = bottom * interest_rate + 1'
63 # done
64 # echo 'bottom'
65 # } | bc` # 在命令替换中嵌入一个 'for 循环'.
66 # --------------------------------------------------------------------------67 # On the other hand, Frank Wang suggests:
68 # bottom=$(echo "scale=9; ($interest_rate^$term-1)/($interest_rate-1)" | bc)
69
70 # 因为 . . .
71 # 在循环后边的算法
72 #+ 事实上是一个等比数列的求和公式.
73 # 求和公式是 e0(1-q^n)/(1-q),
74 #+ e0 是第一个元素 并且 q=e(n+1)/e(n)
75 #+ 和 n 是元素的数量.
76 # --------------------------------------------------------------------------77
78
79 # let "payment = $top/$bottom"
http://www.818198.com Page 295
SHELL十三问
80 payment=$(echo "scale=2; $top/$bottom" | bc)
81 # 使用2位有效数字来表示美元和美分.
82
83 echo
84 echo "monthly payment = \$$payment" # 在总和的前边显示美元符号.
85 echo
86
87
88 exit 0
89
90
91 # 练习:
92 # 1) 处理输入允许本金总数中的逗号.
93 # 2) 处理输入允许按照百分号和小数点的形式输入利率.
94 # 3) 如果你真正想好好编写这个脚本,
95 # 那么就扩展这个脚本让它能够打印出完整的分期付款表.
################################End Script#########################################
Example 12-43 数制转换
################################Start Script#######################################
1 #!/bin/bash
2 ##########################################################################
3 # 脚本 : base.sh - 用不同的数值来打印数字 (Bourne Shell)
4 # 作者 : Heiner Steven ([email protected])
5 # 日期 : 07-03-95
6 # 类型 : 桌面
7 # $Id: base.sh,v 1.2 2000/02/06 19:55:35 heiner Exp $
8 # ==> 上边这行是 RCS ID 信息.
9 ##########################################################################
10 # 描述
11 #
12 # Changes
13 # 21-03-95 stv fixed error occuring with 0xb as input (0.2)
14 ##########################################################################
15
16 # ==> 在本书中使用这个脚本通过了作者的授权.
17 # ==> 注释是本书作者添加的.
18
19 NOARGS=65
20 PN=`basename "$0"` # 程序名
21 VER=`echo '$Revision: 1.2 $' | cut -d' ' -f2` # ==> VER=1.2
22
23 Usage () {
24 echo "$PN - print number to different bases, $VER (stv '95)
25 usage: $PN [number ...]
26
27 If no number is given, the numbers are read from standard input.
28 A number may be
29 binary (base 2) starting with 0b (i.e. 0b1100)
http://www.818198.com Page 296
SHELL十三问
30 octal (base 8) starting with 0 (i.e. 014)
31 hexadecimal (base 16) starting with 0x (i.e. 0xc)
32 decimal otherwise (i.e. 12)" >&2
33 exit $NOARGS
34 } # ==> 打印出用法信息的函数.
35
36 Msg () {
37 for i # ==> 省略 [list] .
38 do echo "$PN: $i" >&2
39 done
40 }
41
42 Fatal () { Msg "$@"; exit 66; }
43
44 PrintBases () {
45 # 决定数值的数制
46 for i # ==> 省略 [list]...
47 do # ==> 所以是对命令行参数进行操作.
48 case "$i" in
49 0b*) ibase=2;; # 2进制
50 0x*|[a-f]*|[A-F]*) ibase=16;; # 16进制
51 0*) ibase=8;; # 8进制
52 [1-9]*) ibase=10;; # 10进制
53 *)
54 Msg "illegal number $i - ignored"
55 continue;;
56 esac
57
58 # 去掉前缀, 将16进制数字转换为大写(bc需要大写)
59 number=`echo "$i" | sed -e 's:^0[bBxX]::' | tr '[a-f]' '[A-F]'`
60 # ==>使用":" 作为sed分隔符, 而不使用"/".
61
62 # 将数字转换为10进制
63 dec=`echo "ibase=$ibase; $number" | bc` # ==> 'bc' 是个计算工具.
64 case "$dec" in
65 [0-9]*) ;; # 数字没问题
66 *) continue;; # 错误: 忽略
67 esac
68
69 # 在一行上打印所有的转换后的数字.
70 # ==> 'here document' 提供命令列表给'bc'.
71 echo `bc < 72 obase=16; "hex="; $dec
73 obase=10; "dec="; $dec
74 obase=8; "oct="; $dec
75 obase=2; "bin="; $dec
76 !
77 ` | sed -e 's: : :g'
http://www.818198.com Page 297
SHELL十三问
78
79 done
80 }
81
82 while [ $# -gt 0 ]
83 # ==> 这里必须使用一个 "while 循环",
84 # ==>+ 因为所有的 case 都可能退出循环或者
85 # ==>+ 结束脚本.
86 # ==> (感谢, Paulo Marcel Coelho Aragao.)
87 do
88 case "$1" in
89 --) shift; break;;
90 -h) Usage;; # ==> 帮助信息.
91 -*) Usage;;
92 *) break;; # 第一个数字
93 esac # ==> 对于非法输入更严格检查是非常有用的.
94 shift
95 done
96
97 if [ $# -gt 0 ]
98 then
99 PrintBases "$@"
100 else # 从标准输入中读取
101 while read line
102 do
103 PrintBases $line
104 done
105 fi
106
107
108 exit 0
################################End Script#########################################
调用 bc 的另一种可选的方法就是使用 here document ,并把它嵌入到 命令替换 块中.
当一个脚本需要将一个选项列表和多个命令传递到 bc 中时, 这种方法就显得非常合适.
1 variable=`bc << LIMIT_STRING
2 options
3 statements
4 operations
5 LIMIT_STRING
6 `
7
8 ...or...
9
10
11 variable=$(bc << LIMIT_STRING
12 options
13 statements
14 operations
http://www.818198.com Page 298
SHELL十三问
15 LIMIT_STRING
16 )
Example 12-44 使用 "here document" 来调用 bc
################################Start Script#######################################
1 #!/bin/bash
2 # 使用命令替换来调用 'bc'
3 # 并与 'here document' 相结合.
4
5
6 var1=`bc << EOF
7 18.33 * 19.78
8 EOF
9 `
10 echo $var1 # 362.56
11
12
13 # $( ... ) 这种标记法也可以.
14 v1=23.53
15 v2=17.881
16 v3=83.501
17 v4=171.63
18
19 var2=$(bc << EOF
20 scale = 4
21 a = ( $v1 + $v2 )
22 b = ( $v3 * $v4 )
23 a * b + 15.35
24 EOF
25 )
26 echo $var2 # 593487.8452
27
28
29 var3=$(bc -l << EOF
30 scale = 9
31 s ( 1.7 )
32 EOF
33 )
34 # 返回弧度为1.7的正弦.
35 # "-l" 选项将会调用 'bc' 算数库.
36 echo $var3 # .991664810
37
38
39 # 现在, 在函数中试一下...
40 hyp= # 声明全局变量.
41 hypotenuse () # 计算直角三角形的斜边.
42 {
43 hyp=$(bc -l << EOF
44 scale = 9
http://www.818198.com Page 299
SHELL十三问
45 sqrt ( $1 * $1 + $2 * $2 )
46 EOF
47 )
48 # 不幸的是, 不能从bash 函数中返回浮点值.
49 }
50
51 hypotenuse 3.68 7.31
52 echo "hypotenuse = $hyp" # 8.184039344
53
54
55 exit 0
################################End Script#########################################
Example 12-45 计算圆周率
################################Start Script#######################################
1 #!/bin/bash
2 # cannon.sh: 通过开炮来取得近似的圆周率值.
3
4 # 这事实上是一个"Monte Carlo"蒙特卡洛模拟的非常简单的实例:
5 #+ 蒙特卡洛模拟是一种由现实事件抽象出来的数学模型,
6 #+ 由于要使用随机抽样统计来估算数学函数, 所以使用伪随机数来模拟真正的随机.
7
8 # 想象有一个完美的正方形土地, 边长为10000个单位.
9 # 在这块土地的中间有一个完美的圆形湖,
10 #+ 这个湖的直径是10000个单位.
11 # 这块土地的绝大多数面积都是水, 当然只有4个角上有一些土地.
12 # (可以把这个湖想象成为使这个正方形的内接圆.)
13 #
14 # 我们将使用老式的大炮和铁炮弹
15 #+ 向这块正方形的土地上开炮.
16 # 所有的炮弹都会击中这块正方形土地的某个地方.
17 #+ 或者是打到湖上, 或者是打到4个角的土地上.
18 # 因为这个湖占据了这个区域大部分地方,
19 #+ 所以大部分的炮弹都会"扑通"一声落到水里.
20 # 而只有很少的炮弹会"砰"的一声落到4个
21 #+ 角的土地上.
22 #
23 # 如果我们发出的炮弹足够随机的落到这块正方形区域中的话,
24 #+ 那么落到水里的炮弹与打出炮弹的总数的比率,
25 #+ 大概非常接近于 PI/4.
26 #
27 # 原因是所有的炮弹事实上都
28 #+ 打在了这个土地的右上角,
29 #+ 也就是, 笛卡尔坐标系的第一象限.
30 # (之前的解释只是一个简化.)
31 #
32 # 理论上来说, 如果打出的炮弹越多, 就越接近这个数字.
33 # 然而, 对于shell 脚本来说一定会作些让步的,
34 #+ 因为它肯定不能和那些内建就支持浮点运算的编译语言相比.
http://www.818198.com Page 300
SHELL十三问
35 # 当然就会降低精度.
36
37
38 DIMENSION=10000 # 这块土地的边长.
39 # 这也是所产生的随机整数的上限.
40
41 MAXSHOTS=1000 # 开炮次数.
42 # 10000 或更多次的话, 效果应该更好, 但有点太浪费时间了.
43 PMULTIPLIER=4.0 # 接近于 PI 的比例因子.
44
45 get_random ()
46 {
47 SEED=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }')
48 RANDOM=$SEED # 来自于 "seeding-random.sh"
49 #+ 的例子脚本.
50 let "rnum = $RANDOM % $DIMENSION" # 范围小于 10000.
51 echo $rnum
52 }
53
54 distance= # 声明全局变量.
55 hypotenuse () # 从 "alt-bc.sh" 例子来的,
56 { # 计算直角三角形的斜边的函数.
57 distance=$(bc -l << EOF
58 scale = 0
59 sqrt ( $1 * $1 + $2 * $2 )
60 EOF
61 )
62 # 设置 "scale" 为 0 , 好让结果四舍五入为整数值,
63 #+ 这是这个脚本中必须折中的一个地方.
64 # 不幸的是, 这将降低模拟的精度.
65 }
66
67
68 # main() {
69
70 # 初始化变量.
71 shots=0
72 splashes=0
73 thuds=0
74 Pi=0
75
76 while [ "$shots" -lt "$MAXSHOTS" ] # 主循环.
77 do
78
79 xCoord=$(get_random) # 取得随机的 X 与 Y 坐标.
80 yCoord=$(get_random)
81 hypotenuse $xCoord $yCoord # 直角三角形斜边 =
82 #+ distance.
http://www.818198.com Page 301
SHELL十三问
83 ((shots++))
84
85 printf "#%4d " $shots
86 printf "Xc = %4d " $xCoord
87 printf "Yc = %4d " $yCoord
88 printf "Distance = %5d " $distance # 到湖中心的
89 #+ 距离 -- 90 # 起始坐标点 -- 91 #+ (0,0).
92
93 if [ "$distance" -le "$DIMENSION" ]
94 then
95 echo -n "SPLASH! "
96 ((splashes++))
97 else
98 echo -n "THUD! "
99 ((thuds++))
100 fi
101
102 Pi=$(echo "scale=9; $PMULTIPLIER*$splashes/$shots" | bc)
103 # 将比例乘以 4.0.
104 echo -n "PI ~ $Pi"
105 echo
106
107 done
108
109 echo
110 echo "After $shots shots, PI looks like approximately $Pi."
111 # 如果不太准的话, 那么就提高一下运行的次数. . .
112 # 可能是由于运行错误和随机数随机程度不高造成的.
113 echo
114
115 # }
116
117 exit 0
118
119 # 要想知道一个shell脚本到底适不适合作为
120 #+ 一种需要对复杂和精度都有要求的计算应用的模拟的话.
121 #
122 # 一般至少需要两个判断条件.
123 # 1) 作为一种概念的验证: 来显示它可以做到.
124 # 2) 在使用真正的编译语言来实现一个算法之前,
125 #+ 使用脚本来测试和验证这个算法.
################################End Script#########################################
dc
dc (桌面计算器desk calculator) 工具是面向栈的并且使用 RPN (逆波兰表达式
"Reverse Polish Notation" 又叫"后缀表达式"). 与 bc 命令很相像 , 但是这个工具
具备好多只有编程语言才具备的能力.
http://www.818198.com Page 302
SHELL十三问
(译者注: 正常表达式 逆波兰表达式
a+b a,b,+
a+(b-c) a,b,c,-,+
a+(b-c)*d a,d,b,c,-,*,+
)
绝大多数人都避免使用这个工具, 因为它需要非直觉的 RPN 输入. 但是, 它却有特定的
用途.
Example 12-46 将10进制数字转换为16进制数字
################################Start Script#######################################
1 #!/bin/bash
2 # hexconvert.sh: 将10进制数字转换为16进制数字
3
4 E_NOARGS=65 # 缺命令行参数错误.
5 BASE=16 # 16进制.
6
7 if [ -z "$1" ]
8 then
9 echo "Usage: $0 number"
10 exit $E_NOARGS
11 # 需要一个命令行参数.
12 fi
13 # 练习: 添加命令行参数检查.
14
15
16 hexcvt ()
17 {
18 if [ -z "$1" ]
19 then
20 echo 0
21 return # 如果没有参数传递到这个函数中就 "return" 0.
22 fi
23
24 echo ""$1" "$BASE" o p" | dc
25 # "o" 设置输出的基数(数制).
26 # "p" 打印栈顶.
27 # 察看 dc 的 man 页来了解其他的选项.
28 return
29 }
30
31 hexcvt "$1"
32
33 exit 0
################################End Script#########################################
通过仔细学习 dc 命令的 info 页, 可以更深入的理解这个复杂的命令. 但是, 有一些
精通 dc巫术 的小组经常会炫耀他们使用这个强大而又晦涩难懂的工具时的一些技巧,
并以此为乐.
bash$ echo "16i[q]sa[ln0=aln100%Pln100/snlbx]sbA0D68736142snlbxq" | dc"
Bash
http://www.818198.com Page 303
SHELL十三问
Example 12-47 因子分解
################################Start Script#######################################
1 #!/bin/bash
2 # factr.sh: 分解约数
3
4 MIN=2 # 如果比这个数小就不行了.
5 E_NOARGS=65
6 E_TOOSMALL=66
7
8 if [ -z $1 ]
9 then
10 echo "Usage: $0 number"
11 exit $E_NOARGS
12 fi
13
14 if [ "$1" -lt "$MIN" ]
15 then
16 echo "Number to factor must be $MIN or greater."
17 exit $E_TOOSMALL
18 fi
19
20 # 练习: 添加类型检查 (防止非整型的参数).
21
22 echo "Factors of $1:"
23 # ---------------------------------------------------------------------------------24 echo "$1[p]s2[lip/dli%0=1dvsr]s12sid2%0=13sidvsr[dli%0=1lrli2+dsi!>.]ds.xd1<2" | dc
25 # ---------------------------------------------------------------------------------26 # 上边这行代码是 Michel Charpentier 编写的
27 # 在此使用经过授权 (thanks).
28
29 exit 0
################################End Script#########################################
awk
在脚本中使用浮点运算的另一种方法是使用 awk 内建的数学运算函数, 可以用在shell
wrapper中.
Example 12-48 计算直角三角形的斜边
################################Start Script#######################################
1 #!/bin/bash
2 # hypotenuse.sh: 返回直角三角形的斜边.
3 # ( 直角边长的平方和,然后对和取平方根)
4
5 ARGS=2 # 需要将2个直角边作为参数传递进来.
6 E_BADARGS=65 # 错误的参数值.
7
8 if [ $# -ne "$ARGS" ] # 测试传递到脚本中的参数值.
9 then
10 echo "Usage: `basename $0` side_1 side_2"
11 exit $E_BADARGS
http://www.818198.com Page 304
SHELL十三问
12 fi
13
14
15 AWKSCRIPT=' { printf( "%3.7f\n", sqrt($1*$1 + $2*$2) ) } '
16 # 命令 / 传递给awk的参数
17
18
19 # 现在, 将参数通过管道传递给awk.
20 echo -n "Hypotenuse of $1 and $2 = "
21 echo $1 $2 | awk "$AWKSCRIPT"
22
23 exit 0
################################End Script#########################################
12.9 混杂命令
-------------一些不好归类的命令
jot, seq
这些工具通过用户指定的范围和增量来产生一系列的整数.
每个产生出来的整数一般都占一行, 但是可以使用 -s 选项来改变这种设置.
bash$ seq 5
1
2
3
4
5
bash$ seq -s : 5
1:2:3:4:5
jot 和 seq 命令都经常用在 for 循环中.
Example 12-49 使用 seq 来产生循环参数
################################Start Script#######################################
1 #!/bin/bash
2 # 使用 "seq"
3
4 echo
5
6 for a in `seq 80` # 或者 for a in $( seq 80 )
7 # 与 " for a in 1 2 3 4 5 ... 80 "相同 (少敲了好多字!).
8 # 也可以使用 'jot' (如果系统上有的话).
9 do
10 echo -n "$a "
11 done # 1 2 3 4 5 ... 80
12 # 这也是一个通过使用命令的输出
13 # 来产生 "for"循环中 [list] 列表的例子.
14
15 echo; echo
http://www.818198.com Page 305
SHELL十三问
16
17
18 COUNT=80 # 当然, 'seq' 也可以使用一个可替换的参数.
19
20 for a in `seq $COUNT` # 或者 for a in $( seq $COUNT )
21 do
22 echo -n "$a "
23 done # 1 2 3 4 5 ... 80
24
25 echo; echo
26
27 BEGIN=75
28 END=80
29
30 for a in `seq $BEGIN $END`
31 # 传给 "seq" 两个参数, 从第一个参数开始增长,
32 #+ 一直增长到第二个参数为止.
33 do
34 echo -n "$a "
35 done # 75 76 77 78 79 80
36
37 echo; echo
38
39 BEGIN=45
40 INTERVAL=5
41 END=80
42
43 for a in `seq $BEGIN $INTERVAL $END`
44 # 传给 "seq" 三个参数从第一个参数开始增长,
45 #+ 并以第二个参数作为增量,
46 #+ 一直增长到第三个参数为止.
47 do
48 echo -n "$a "
49 done # 45 50 55 60 65 70 75 80
50
51 echo; echo
52
53 exit 0
################################End Script#########################################
一个简单些的例子:
1 # 产生10个连续扩展名的文件,
2 #+ 名字分别是 file.1, file.2 . . . file.10.
3 COUNT=10
4 PREFIX=file
5
6 for filename in `seq $COUNT`
7 do
8 touch $PREFIX.$filename
http://www.818198.com Page 306
SHELL十三问
9 # 或者, 你可以做一些其他的操作,
10 #+ 比如 rm, grep, 等等.
11 done
Example 12-50 字母统计
################################Start Script#######################################
1 #!/bin/bash
2 # letter-count.sh: 统计一个文本文件中字母出现的次数.
3 # 由 Stefano Palmeri 编写.
4 # 经过授权使用在本书中.
5 # 本书作者做了少许修改.
6
7 MINARGS=2 # 本脚本至少需要2个参数.
8 E_BADARGS=65
9 FILE=$1
10
11 let LETTERS=$#-1 # 制定了多少个字母 (作为命令行参数).
12 # (从命令行参数的个数中减1.)
13
14
15 show_help(){
16 echo
17 echo Usage: `basename $0` file letters
18 echo Note: `basename $0` arguments are case sensitive.
19 echo Example: `basename $0` foobar.txt G n U L i N U x.
20 echo
21 }
22
23 # 检查参数个数.
24 if [ $# -lt $MINARGS ]; then
25 echo
26 echo "Not enough arguments."
27 echo
28 show_help
29 exit $E_BADARGS
30 fi
31
32
33 # 检查文件是否存在.
34 if [ ! -f $FILE ]; then
35 echo "File \"$FILE\" does not exist."
36 exit $E_BADARGS
37 fi
38
39
40
41 # 统计字母出现的次数.
42 for n in `seq $LETTERS`; do
43 shift
http://www.818198.com Page 307
SHELL十三问
44 if [[ `echo -n "$1" | wc -c` -eq 1 ]]; then # 检查参数.
45 echo "$1" -\> `cat $FILE | tr -cd "$1" | wc -c` # 统计.
46 else
47 echo "$1 is not a single char."
48 fi
49 done
50
51 exit $?
52
53 # 这个脚本在功能上与 letter-count2.sh 完全相同,
54 #+ 但是运行得更快.
55 # 为什么?
################################End Script#########################################
getopt
getopt 命令将会分析以破折号开头的命令行选项. 这个外部命令与Bash的内建命令
getopts 作用相同. 通过使用 -l 标志, getopt 可以处理长(多字符)选项, 并且也允许参
数重置.
Example 12-51 使用getopt来分析命令行选项
################################Start Script#######################################
1 #!/bin/bash
2 # 使用 getopt.
3
4 # 尝试使用下边的不同的方法来调用这脚本:
5 # sh ex33a.sh -a
6 # sh ex33a.sh -abc
7 # sh ex33a.sh -a -b -c
8 # sh ex33a.sh -d
9 # sh ex33a.sh -dXYZ
10 # sh ex33a.sh -d XYZ
11 # sh ex33a.sh -abcd
12 # sh ex33a.sh -abcdZ
13 # sh ex33a.sh -z
14 # sh ex33a.sh a
15 # 解释上面每一次调用的结果.
16
17 E_OPTERR=65
18
19 if [ "$#" -eq 0 ]
20 then # 脚本需要至少一个命令行参数.
21 echo "Usage $0 -[options a,b,c]"
22 exit $E_OPTERR
23 fi
24
25 set -- `getopt "abcd:" "$@"`
26 # 为命令行参数设置位置参数.
27 # 如果使用 "$*" 来代替 "$@" 的话会发生什么?
28
29 while [ ! -z "$1" ]
http://www.818198.com Page 308
SHELL十三问
30 do
31 case "$1" in
32 -a) echo "Option \"a\"";;
33 -b) echo "Option \"b\"";;
34 -c) echo "Option \"c\"";;
35 -d) echo "Option \"d\" $2";;
36 *) break;;
37 esac
38
39 shift
40 done
41
42 # 通常来说在脚本中使用内建的 'getopts' 命令,
43 #+ 会比使用 'getopt' 好一些.
44 # 参见 "ex33.sh".
45
46 exit 0
################################End Script#########################################
参见 Example 9-12 , 这是对 getopt 命令的一个简单模拟.
run-parts
run-parts 命令 [1] 将会执行目标目录中所有的脚本, 这些将本会以 ASCII 的循序进行
排列. 当然, 这些脚本都需要具有可执行权限.
cron 幽灵进程 会调用 run-parts 来运行 /etc/cron.* 下的所有脚本.
yes
yes 命令的默认行为是向 stdout 中连续不断的输出字符 y,每个y占一行.使用control-c
来结束运行. 如果想换一个输出字符的话, 可以使用 yes 其他的字符串, 这样就会连续
不同的输出你指定的字符串. 那么这样的命令究竟能做什么呢? 在命令行或者脚本中,
yes的输出可以通过重定向或管道来传递给一些需要用户输入进行交互的命令. 事实上,
这个命令可以说是 expect 命令(译者注: 这个命令本书未介绍, 一个自动实现交互的命
令)的一个简化版本.
yes | fsck /dev/hda1 将会以非交互的形式运行fsck(因为需要用户输入的 y 全由yes
命令搞定了)(小心使用!).
yes | rm -r dirname 与 rm -rf dirname 效果相同(小心使用!).
注意: 当用 yes 的管道形式来使用一些可能具有潜在危险的系统命令的时候一定要深思
熟虑, 比如 fsck 或 fdisk. 可能会产生一些意外的副作用.
banner
将会把字符串用一个 ASCII 字符(默认是 '#')来画出来(就是将多个'#'拼出一副字符的
图形).可以作为硬拷贝重定向到打印机上(译者注: 可以使用-w 选项设置宽度).
printenv
对于某个特定的用户, 显示出所有的 环境变量.
bash$ printenv | grep HOME
HOME=/home/bozo
lp
lp 和 lpr 命令将会把文件发送到打印队列中, 并且作为硬拷贝来打印. [2] 这些命令
会纪录它们名字的起始位置并传递到行打印机的另一个位置.
bash$ lp file1.txt 或者 bash lp
bash$ pr -options file1.txt | lp
http://www.818198.com Page 309
SHELL十三问
格式化的包, 比如 groff 和 Ghostscript 就可以将它们的输出直接发送给 lp.
bash$ groff -Tascii file.tr | lp
bash$ gs -options | lp file.ps
还有一些相关的命令, 比如 lpq, 可以查看打印队列, lprm, 可以用来从打印队列中删
除作业.
tee
[UNIX 从管道行业借来的主意.]
这是一个重定向操作, 但是有些不同. 就像管道中的"三通"一样, 这个命令可以将命令或
者管道命令的输出抽出到一个文件中,而且并不影响结果. 当你想将一个正在运行的进程
的输出保存到文件中时, 或者为了debug而保存输出记录的时候, 这个命令就非常有用了.
(重定向)
|----> to file
|
==========================|====================
command ---> command ---> |tee ---> command ---> ---> output of pipe
===============================================
1 cat listfile* | sort | tee check.file | uniq > result.file
(在对排序的结果进行 uniq (去掉重复行) 之前,文件 check.file 中保存了排过序的
"listfiles".)
mkfifo
这个不大引人注意的命令可以创建一个命名管道, 并产生一个临时的先进先出的buffer
用来在两个进程间传输数据. [3] 典型的使用是一个进程向FIFO中写数据, 另一个进程读
出来. 参见 Example A-15.
pathchk
这个命令用来检查文件名的有效性. 如果文件名超过了最大允许长度(255 个字符), 或者
它所在的一个或多个路径搜索不到, 那么就会产生一个错误结果.
不幸的是,并不能够返回一个可识别的错误码, 因此它在脚本中几乎没有什么用. 一般都
使用文件测试操作.
dd
这也是一个不太出名的工具, 但却是一个令人恐惧的 "数据复制" 命令. 最开始, 这个命
令是被用来在UNIX 微机和IBM大型机之间通过磁带来交换数据, 这个命令现在仍然有它的
用途. dd 命令只不过是简单的拷贝一个文件 (或者 stdin/stdout), 但是它会做一些转
换. 下边是一些可能的转换, 比如 ASCII/EBCDIC, [4] 大写/小写, 在输入和输出之间
的字节对的交换, 还有对输入文件做一些截头去尾的工作. dd --help 列出了所有转换,
还有这个强力工具的一些其他选项.
1 # 将一个文件转换为大写:
2
3 dd if=$filename conv=ucase > $filename.uppercase
4 # lcase # 转换为小写
Example 12-52 一个拷贝自身的脚本
################################Start Script#######################################
1 #!/bin/bash
2 # self-copy.sh
3
4 # 这个脚本将会拷贝自身.
5
6 file_subscript=copy
7
http://www.818198.com Page 310
SHELL十三问
8 dd if=$0 of=$0.$file_subscript 2>/dev/null
9 # 阻止dd产生的消息: ^^^^^^^^^^^
10
11 exit $?
################################End Script#########################################
Example 12-53 练习dd
################################Start Script#######################################
1 #!/bin/bash
2 # exercising-dd.sh
3
4 # 由Stephane Chazelas编写.
5 # 本文作者做了少量修改.
6
7 input_file=$0 # 脚本本身.
8 output_file=log.txt
9 n=3
10 p=5
11
12 dd if=$input_file of=$output_file bs=1 skip=$((n-1)) count=$((p-n+1)) 2> /dev/null
13 # 从脚本中把位置n到p的字符提取出来.
14
15 # -------------------------------------------------------16
17 echo -n "hello world" | dd cbs=1 conv=unblock 2> /dev/null
18 # 垂直的 echo "hello world" .
19
20 exit 0
################################End Script#########################################
为了展示dd的多种用途, 让我们使用它来记录按键.
Example 12-54 记录按键
################################Start Script#######################################
1 #!/bin/bash
2 # dd-keypress.sh: 记录按键, 不需要按回车.
3
4
5 keypresses=4 # 记录按键的个数.
6
7
8 old_tty_setting=$(stty -g) # 保存老的终端设置.
9
10 echo "Press $keypresses keys."
11 stty -icanon -echo # 禁用标准模式.
12 # 禁用本地 echo.
13 keys=$(dd bs=1 count=$keypresses 2> /dev/null)
14 # 如果不指定输入文件的话, 'dd' 使用标准输入.
15
16 stty "$old_tty_setting" # 恢复老的终端设置.
17
http://www.818198.com Page 311
SHELL十三问
18 echo "You pressed the \"$keys\" keys."
19
20 # 感谢 Stephane Chazelas, 演示了这种方法.
21 exit 0
################################End Script#########################################
dd 命令可以在数据流上做随即存取.
1 echo -n . | dd bs=1 seek=4 of=file conv=notrunc
2 # "conv=notrunc" 选项意味着输出文件不能被截短.
3
4 # Thanks, S.C.
dd 命令可以将数据或磁盘镜像拷贝到设备中, 也可以从设备中拷贝数据或磁盘镜像, 比
如说磁盘或磁带设备都可以 (Example A-5). 通常用来创建启动盘.
dd if=kernel-image of=/dev/fd0H1440
同样的, dd 可以拷贝软盘的整个内容(甚至是其他操作系统的磁盘格式) 到硬盘驱动器上
(以镜像文件的形式).
dd if=/dev/fd0 of=/home/bozo/projects/floppy.img
dd 命令还有一些其他用途, 包括可以初始化临时交换文件 (Example 28-2) 和 ramdisks
(内存虚拟硬盘) (Example 28-3). 它甚至可以做一些对整个硬盘分区的底层拷贝, 虽然
不建议这么做.
一些(可能是比较无聊的)人总会想一些关于 dd 命令的有趣的应用.
Example 12-55 安全的删除一个文件
################################Start Script#######################################
1 #!/bin/bash
2 # blot-out.sh: 删除一个文件所有的记录.
3
4 # 这个脚本会使用随即字节交替的覆盖
5 #+ 目标文件, 并且在最终删除这个文件之前清零.
6 # 这么做之后, 即使你通过传统手段来检查磁盘扇区
7 #+ 也不能把文件原始数据重新恢复.
8
9 PASSES=7 # 破坏文件的次数.
10 # 提高这个数字会减慢脚本运行的速度,
11 #+ 尤其是对尺寸比较大的目标文件进行操作的时候.
12 BLOCKSIZE=1 # 带有 /dev/urandom 的 I/O 需要单位块尺寸,
13 #+ 否则你可能会获得奇怪的结果.
14 E_BADARGS=70 # 不同的错误退出码.
15 E_NOT_FOUND=71
16 E_CHANGED_MIND=72
17
18 if [ -z "$1" ] # 没指定文件名.
19 then
20 echo "Usage: `basename $0` filename"
21 exit $E_BADARGS
22 fi
23
24 file=$1
25
26 if [ ! -e "$file" ]
http://www.818198.com Page 312
SHELL十三问
27 then
28 echo "File \"$file\" not found."
29 exit $E_NOT_FOUND
30 fi
31
32 echo; echo -n "Are you absolutely sure you want to blot out \"$file\" (y/n)? "
33 read answer
34 case "$answer" in
35 [nN]) echo "Changed your mind, huh?"
36 exit $E_CHANGED_MIND
37 ;;
38 *) echo "Blotting out file \"$file\".";;
39 esac
40
41
42 flength=$(ls -l "$file" | awk '{print $5}') # 5 是文件长度.
43 pass_count=1
44
45 chmod u+w "$file" # Allow overwriting/deleting the file.
46
47 echo
48
49 while [ "$pass_count" -le "$PASSES" ]
50 do
51 echo "Pass #$pass_count"
52 sync # 刷新buffer.
53 dd if=/dev/urandom of=$file bs=$BLOCKSIZE count=$flength
54 # 使用随机字节进行填充.
55 sync # 再刷新buffer.
56 dd if=/dev/zero of=$file bs=$BLOCKSIZE count=$flength
57 # 用0填充.
58 sync # 再刷新buffer.
59 let "pass_count += 1"
60 echo
61 done
62
63
64 rm -f $file # 最后, 删除这个已经被破坏得不成样子的文件.
65 sync # 最后一次刷新buffer.
66
67 echo "File \"$file\" blotted out and deleted."; echo
68
69
70 exit 0
71
72 # 这是一种真正安全的删除文件的办法,
73 #+ 但是效率比较低, 运行比较慢.
74 # GNU 的文件工具包中的 "shred" 命令,
http://www.818198.com Page 313
SHELL十三问
75 #+ 也可以完成相同的工作, 不过更有效率.
76
77 # 使用普通的方法是不可能重新恢复这个文件了.
78 # 然而 . . .
79 #+ 这个简单的例子是不能够抵抗
80 #+ 那些经验丰富并且正规的分析.
81
82 # 这个脚本可能不会很好的运行在日志文件系统上.(译者注: JFS)
83 # 练习 (很难): 像它做的那样修正这个问题.
84
85
86
87 # Tom Vier的文件删除包可以更加彻底
88 #+ 的删除文件, 比这个简单的例子厉害得多.
89 # http://www.ibiblio.org/pub/Linux/utils/file/wipe-2.0.0.tar.bz2
90
91 # 如果想对安全删除文件这一论题进行深度的分析,
92 #+ 可以参见Peter Gutmann的页面,
93 #+ "Secure Deletion of Data From Magnetic and Solid-State Memory".
94 # http://www.cs.auckland.ac.nz/~pgut001/pubs/secure_del.html
################################End Script#########################################
od
od(octal dump)过滤器, 将会把输入(或文件)转换为8进制或者其他进制. 在你需要查看
或处理一些二进制数据文件或者一个不可读的系统设备文件的时候, 这个命令非常有用,
比如/dev/urandom,或者是一个二进制数据过滤器. 参见 Example 9-28 和
Example 12-13.
hexdump
对二进制文件进行 16进制, 8进制, 10进制, 或者 ASCII 码的查阅动作. 这个命令大体
上与上边的od命令作用相同, 但是远不及 od 命令有用.
objdump
显示编译后的2进制文件或2进制可执行文件的信息, 以16进制的形式显示, 或者显示反汇
编列表(使用-d选项).
bash$ objdump -d /bin/ls
/bin/ls: file format elf32-i386
Disassembly of section .init:
080490bc <.init>:
80490bc: 55 push %ebp
80490bd: 89 e5 mov %esp,%ebp
. . .
mcookie
这个命令会产生一个"magic cookie", 这是一个128-bit (32-字符) 的伪随机16进制数字,
这个数字一般都用来作为X server的鉴权"签名". 这个命令还可以用来在脚本中作为一
种生成随机数的手段, 当然这是一种"小吃店"(虽然不太正统, 但是很方便)的风格.
1 random000=$(mcookie)
当然, 完成同样的目的还可以使用 md5 命令.
1 # 产生关于脚本本身的 md5 checksum.
2 random001=`md5sum $0 | awk '{print $1}'`
3 # 使用 'awk' 来去掉文件名.
http://www.818198.com Page 314
SHELL十三问
mcookie 还给出了产生"唯一"文件名的另一种方法.
Example 12-56 文件名产生器
################################Start Script#######################################
1 #!/bin/bash
2 # tempfile-name.sh: 临时文件名产生器
3
4 BASE_STR=`mcookie` # 32-字符的 magic cookie.
5 POS=11 # 字符串中随便的一个位置.
6 LEN=5 # 取得 $LEN 长度连续的字符串.
7
8 prefix=temp # 最终的一个临时文件.
9 # 如果想让这个文件更加唯一,
10 #+ 可以对这个前缀也使用下边的方法来生成.
11
12 suffix=${BASE_STR:POS:LEN}
13 # 提取从第11个字符之后的长度为5的字符串.
14
15 temp_filename=$prefix.$suffix
16 # 构造文件名.
17
18 echo "Temp filename = "$temp_filename""
19
20 # sh tempfile-name.sh
21 # Temp filename = temp.e19ea
22
23 # 与使用 'date' 命令(参考 ex51.sh)来创建唯一文件名
24 #+ 的方法相比较.
25
26 exit 0
################################End Script#########################################
units
这个工具用来在不同的计量单位之间互相转换. 当你在交互模式下正常调用时, 会发现在
脚本中 units 也很有用.
Example 12-57 将米转换为英里
################################Start Script#######################################
1 #!/bin/bash
2 # unit-conversion.sh
3
4
5 convert_units () # 通过参数取得需要转换的单位.
6 {
7 cf=$(units "$1" "$2" | sed --silent -e '1p' | awk '{print $2}')
8 # 除了真正需要转换的部分保留下来外,其他的部分都去掉.
9 echo "$cf"
10 }
11
12 Unit1=miles
13 Unit2=meters
http://www.818198.com Page 315
SHELL十三问
14 cfactor=`convert_units $Unit1 $Unit2`
15 quantity=3.73
16
17 result=$(echo $quantity*$cfactor | bc)
18
19 echo "There are $result $Unit2 in $quantity $Unit1."
20
21 # 如果你传递了两个不匹配的单位会发生什么?
22 #+ 比如分别传入英亩和英里?
23
24 exit 0
################################End Script#########################################
m4
一个隐藏的财宝, m4 是一个强力的宏处理过滤器, [5] 差不多可以说是一种语言了. 虽
然最开始这个工具是用来作为 RatFor 的预处理器而编写的, 但是后来证明 m4 作为独
立的工具也是非常有用的. 事实上, m4 结合了许多工具的功能, 比如 eval, tr, 和 awk,
除此之外, 它还使得宏扩展变得容易.
在 2004年4月的 Linux Journal 的问题列表中有一篇关于 m4 命令用法得非常好的文章.
Example 12-58 使用 m4
################################Start Script#######################################
1 #!/bin/bash
2 # m4.sh: 使用 m4 宏处理器
3
4 # 字符操作
5 string=abcdA01
6 echo "len($string)" | m4 # 7
7 echo "substr($string,4)" | m4 # A01
8 echo "regexp($string,[0-1][0-1],\&Z)" | m4 # 01Z
9
10 # 算术操作
11 echo "incr(22)" | m4 # 23
12 echo "eval(99 / 3)" | m4 # 33
13
14 exit 0
################################End Script#########################################
doexec
doexec 命令允许将一个随便的参数列表传递到一个二进制可执行文件中. 特别的, 甚至
可以传递 arg[0] (相当于脚本中的 $0 ), 这样可以使用不同的名字来调用这个可执行
文件, 并且通过不同的调用的名字, 可以让这个可执行文件执行不同的动作. 这也可以
说是一种将参数传递到可执行文件中的比较绕圈子的做法.
比如, /usr/local/bin 目录可能包含一个 "aaa" 的二进制文件. 使用
doexec /usr/local/bin/aaa list 可以 列出 当前工作目录下所有以 "a" 开头的的文
件, 而使用 doexec /usr/local/bin/aaa delete 将会删除这些文件.
注意: 可执行文件的不同行为必须定义在可执行文件自身的代码中, 可以使用如下的
shell脚本作类比:
1 case `basename $0` in
2 "name1" ) do_something;;
3 "name2" ) do_something_else;;
http://www.818198.com Page 316
SHELL十三问
4 "name3" ) do_yet_another_thing;;
5 * ) bail_out;;
6 esac
dialog
dialog 工具集提供了一种从脚本中调用交互对话框的方法. dialog 的更好的变种版本是
-- gdialog, Xdialog, 和 kdialog -- 事实上是调用的 X-Windows 的界面工具集. 参
见 Example 33-19.
sox
sox 命令, "sound exchange" (声音转换)命令, 可以进行声音文件的转换. 事实上,可执
行文件 /usr/bin/play (现在不建议使用) 只不过是 sox 的一个 shell 包装器而已.
举个例子, sox soundfile.wav soundfile.au 将会把一个 WAV 声音文件转换成一个
(Sun 音频格式) AU 声音文件.
Shell 脚本非常适合于使用 sox 的声音操作来批处理声音文件. 比如, 参见
Linux Radio Timeshift HOWTO 和 MP3do Project. 注意事项:
[1] 这个工具事实上是从 Debian Linux 发行版中的一个脚本借鉴过来的.
[2] 打印队列 就是"在线等待"打印的作业组.
[3] 对于本话题的一个完美的介绍, 请参见 Andy Vaught 的文章, 命名管道的介绍,
(http://www2.linuxjournal.com/lj-issues/issue41/2156.html), 这是
Linux Journal (http://www.linuxjournal.com/)1997年9月的一个问题.
[4] EBCDIC (发音是 "ebb-sid-ick") 是单词 (Extended Binary Coded Decimal
Interchange Code) 的首字母缩写. 这是 IBM 的数据格式, 现在已经不常见了.
dd 命令的 conv=ebcdic 选项的一个比较奇异的使用方法是对一个文件进行快速而
且容易但不太安全的编码.
1 cat $file | dd conv=swab,ebcdic > $file_encrypted
2 # 编码 (看起来好像没什么用).
3 # 应该交换字节(swab), 有点晦涩.
4
5 cat $file_encrypted | dd conv=swab,ascii > $file_plaintext
6 # 解码.
[5] 宏 是一个符号常量, 将会被扩展成一个命令字符串或者一系列的参数操作.
[]
高级Bash脚本编程指南(四)
文章整理: 文章来源: 网络
高级Bash脚本编程指南(四)
第13章 系统与管理命令
======================
在/etc/rc.d目录中的启动和关机脚本中包含了好多有用的(和没用的)这些系统管理命令. 这些
命令通常总是被root用户使用, 用与系统维护或者是紧急文件系统修复.一定要小心使用这些工
具, 因为如果滥用的话, 它们会损坏你的系统.
Users 和 Groups 类命令
users
显示所有的登录的用户. 这个命令与 who -q 基本一致.
groups
列出当前用户和他所属于的组. 这相当于 $GROUPS 内部变量, 但是这个命令将会给出组名
字, 而不是数字.
http://www.818198.com Page 317
SHELL十三问
bash$ groups
bozita cdrom cdwriter audio xgrp
bash$ echo $GROUPS
501
chown, chgrp
chown 命令将会修改一个或多个文件的所有权. 对于root来说这是一种非常好的将文件的
所有权从一个用户换到另一个用户的方法. 一个普通用户不能修改文件的所有权, 即使他
是文件的宿主也不行. [1]
root# chown bozo *.txt
chgrp 将会修改一个或个文件党组所有权. 你必须是这些文件的宿主, 并且是目的组的成
员(或者root), 这样才能使用这个操作.
1 chgrp --recursive dunderheads *.data
2 # "dunderheads"(译者: 晕,蠢才...) 组现在拥有了所有的"*.data"文件.
3 #+ 包括所有$PWD目录下的子目录中的文件(--recursive的作用就是包含子目录).
useradd, userdel
useradd 管理命令将会在系统上添加一个用户帐号, 并且如果指定的话, 还会为特定的用
户创建home目录. 相应的userdel 命令将会从系统上删除一个用户帐号, [2] 并且删除相
应的文件.
注意: adduser命令与useradd是相同的, adduser通常都是一个符号链接.
usermod
修改用户帐号. 可以修改密码, 组身份, 截止日期, 或者给定用户帐号的其他的属性. 使
用这个命令, 用户的密码可能会被锁定, 因为密码会影响到帐号的有效性.
groupmod
修改指定组. 组名字或者ID号都可以使用这个命令来修改.
id
id 将会列出当前进程的真实和有效用户ID, 还有用户的组ID. 这与Bash的内部变量
$UID, $EUID, 和 $GROUPS 很相像.
bash$ id
uid=501(bozo) gid=501(bozo) groups=501(bozo),22(cdrom),80(cdwriter),81(audio)
bash$ echo $UID
501
注意: id 命令只有在有效ID与真实ID不符时才会显示有效id.
参见 Example 9-5.
who
显示系统上所有已经登录的用户.
bash$ who
bozo tty1 Apr 27 17:45
bozo pts/0 Apr 27 17:46
bozo pts/1 Apr 27 17:47
bozo pts/2 Apr 27 17:49
-m 选项将会只给出当前用户的详细信息. 将任意两个参数传递到who中 都等价于who -m,
就像 who am i 或者 who The Man.
bash$ who -m
localhost.localdomain!bozo pts/2 Apr 27 17:49
whoami 与who -m 很相似, 但是只列出用户名.
bash$ whoami
http://www.818198.com Page 318
SHELL十三问
bozo
w
显示所有的登录的用户和属于它们的进程. 这是一个who的扩展版本. w的输出可以通过管
道传递到grep中, 这样就可以查找指定的用户或进程.
bash$ w | grep startx
bozo tty1 - 4:22pm 6:41 4.47s 0.45s startx
logname
显示当前用户的登录名(可以在/var/run/utmp中找到). 这与上边的whoami很相近.
bash$ logname
bozo
bash$ whoami
bozo
然而...
bash$ su
Password: ......
bash# whoami
root
bash# logname
bozo
注意: logname只会打印出登录的用户名, 而whoami 将会给出附着到当前进程的用户名.
就像我们上边看到的那样, 这两个名字有时会不同.
su
使用一个代替的用户来运行一个程序或脚本. su rjones 将会以 rjones 来启动一个
shell. 一个不加参数的su默认就是root. 参见 Example A-15.
sudo
以root(或其他用户)的身份来运行一个命令. 这个命令可以运行在脚本中, 这样就允许以
正规的用户身份来运行脚本.
1 #!/bin/bash
2
3 # 一些命令.
4 sudo cp /root/secretfile /home/bozo/secret
5 # 一些命令.
文件 /etc/sudoers 持有允许调用sudo的用户名.
passwd
设置, 修改, 或者管理用户的密码.
passwd 命令可以用在脚本中, 但可能你不想这么用.
Example 13-1 设置一个新密码
################################Start Script#######################################
1 #!/bin/bash
2 # setnew-password.sh: 只用于说明目的.
3 # 如果真正运行这个脚本并不是一个好主意.
4 # 这个脚本必须以root身份运行.
5
6 ROOT_UID=0 # Root 的 $UID 0.
7 E_WRONG_USER=65 # 不是 root?
8
http://www.818198.com Page 319
SHELL十三问
9 E_NOSUCHUSER=70
10 SUCCESS=0
11
12
13 if [ "$UID" -ne "$ROOT_UID" ]
14 then
15 echo; echo "Only root can run this script."; echo
16 exit $E_WRONG_USER
17 else
18 echo
19 echo "You should know better than to run this script, root."
20 echo "Even root users get the blues... "
21 echo
22 fi
23
24
25 username=bozo
26 NEWPASSWORD=security_violation
27
28 # 检查bozo是否在这里.
29 grep -q "$username" /etc/passwd
30 if [ $? -ne $SUCCESS ]
31 then
32 echo "User $username does not exist."
33 echo "No password changed."
34 exit $E_NOSUCHUSER
35 fi
36
37 echo "$NEWPASSWORD" | passwd --stdin "$username"
38 # 'passwd'命令 '--stdin' 选项允许
39 #+ 从stdin(或者管道)中获得一个新的密码.
40
41 echo; echo "User $username's password changed!"
42
43 # 在脚本中使用'passwd'命令是很危险的.
44
45 exit 0
################################End Script#########################################
passwd 命令的 -l, -u, 和 -d 选项允许锁定, 解锁,和删除一个用户的密码. 只有root
用户可以使用这些选项.
ac
显示用户登录的连接时间, 就像从 /var/log/wtmp 中读取一样. 这是GNU的一个统计工具.
bash$ ac
total 68.08
last
用户最后登录的信息, 就像从/var/log/wtmp中读出来一样. 这个命令也可以用来显示远
端登录.
比如, 显示最后几次系统的重启信息:
http://www.818198.com Page 320
SHELL十三问
bash$ last reboot
reboot system boot 2.6.9-1.667 Fri Feb 4 18:18 (00:02)
reboot system boot 2.6.9-1.667 Fri Feb 4 15:20 (01:27)
reboot system boot 2.6.9-1.667 Fri Feb 4 12:56 (00:49)
reboot system boot 2.6.9-1.667 Thu Feb 3 21:08 (02:17)
. . .
wtmp begins Tue Feb 1 12:50:09 2005
newgrp
不用登出就可以修改用户的组ID. 并且允许存取新组的文件. 因为用户可能同时属于多个
组, 这个命令很少被使用.
终端类命令
tty
显示当前用户终端的名字. 注意每一个单独的xterm窗口都被算作一个不同的终端.
bash$ tty
/dev/pts/1
stty
显示并(或)修改终端设置. 这个复杂命令可以用在脚本中, 并可以用来控制终端的行为和
其显示输出的方法. 参见这个命令的info页, 并仔细学习它.
Example 13-2 设置一个擦除字符
################################Start Script#######################################
1 #!/bin/bash
2 # erase.sh: 在读取输入时使用"stty"来设置一个擦除字符.
3
4 echo -n "What is your name? "
5 read name # 试试退格键
6 #+ 来删除输入的字符.
7 # 有什么问题?
8 echo "Your name is $name."
9
10 stty erase '#' # 将 "hashmark" (#) 设置为退格字符.
11 echo -n "What is your name? "
12 read name # 使用#来删除最后键入的字符.
13 echo "Your name is $name."
14
15 # 警告: 即使在脚本退出后, 新的键值还是保持设置.(译者: 使用stty erase '^?' 恢复)
16
17 exit 0
################################End Script#########################################
Example 13-3 关掉终端对于密码的echo
################################Start Script#######################################
1 #!/bin/bash
2 # secret-pw.sh: 保护密码不被显示
3
4 echo
5 echo -n "Enter password "
6 read passwd
7 echo "password is $passwd"
8 echo -n "If someone had been looking over your shoulder, "
http://www.818198.com Page 321
SHELL十三问
9 echo "your password would have been compromised."
10
11 echo && echo # 在一个"与列表"中产生2个换行.
12
13
14 stty -echo # 关闭屏幕的echo.
15
16 echo -n "Enter password again "
17 read passwd
18 echo
19 echo "password is $passwd"
20 echo
21
22 stty echo # 恢复屏幕的echo.
23
24 exit 0
25
26 # 详细的阅读stty命令的info页, 以便于更好的掌握这个有用并且狡猾的工具.
################################End Script#########################################
一个具有创造性的stty命令的用法, 检测用户所按的键(不用敲回车).
Example 13-4 按键检测
################################Start Script#######################################
1 #!/bin/bash
2 # keypress.sh: 检测用户按键 ("hot keys").
3
4 echo
5
6 old_tty_settings=$(stty -g) # 保存老的设置(为什么?).
7 stty -icanon
8 Keypress=$(head -c1) # 或者 $(dd bs=1 count=1 2> /dev/null)
9 # 在非GNU的系统上
10
11 echo
12 echo "Key pressed was \""$Keypress"\"."
13 echo
14
15 stty "$old_tty_settings" # 恢复老的设置.
16
17 # 感谢, Stephane Chazelas.
18
19 exit 0
################################End Script#########################################
参见 Example 9-3.
注意: 终端与模式terminals and modes
一般情况下, 一个终端都是工作在canonical(标准)模式下. 当用户按键后, 事实上所
产生的字符并没有马上传递到运行在当前终端上的程序. 终端上的一个本地的缓存保
存了这些按键. 当用按下ENTER键的时候, 才会将所有保存的按键信息传递到运行的程
序中. 这就意味着在终端内部存在一个基本的行编辑器.
http://www.818198.com Page 322
SHELL十三问
bash$ stty -a
speed 9600 baud; rows 36; columns 96; line = 0;
intr = ^C; quit = ^\; erase = ^H; kill = ^U; eof = ^D; eol =
start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O;
...
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt
在使用canonical模式的时候, 可以对本地终端行编辑器所定义的特殊按键进行重新定
义.
bash$ cat > filexxx
wha
bash$ cat filexxx
hello world
bash$ wc -c < filexxx
12
控制终端的进程只保存了12个字符(11个字母加上一个换行), 虽然用户敲了26个按键.
在 non-canonical ("raw") 模式, 每次按键(包括特殊定义的按键, 比如 ctl-H)将会
立即发送一个字符到控制进程.
Bash提示符禁用了icanon和echo, 因为它用自己的更好的行编辑器代替了终端的基本
行编辑器. 比如, 当你在Bash提示符下敲ctl-A的时候, 终端将不会显示 ^A, 但是
Bash将会获得\1字符, 然后解释这个字符, 这样光标就移动到行首了.
Stéphane Chazelas
setterm
设置特定的终端属性. 这个命令将向它的终端的stdout写一个字符串, 这个字符串将修改
终端的行为.
bash$ setterm -cursor off
bash$
setterm 命令可以被用在脚本中来修改写到stdout的文本的外观, 虽然如果你仅仅只想完
成这个目的, 还有特定的更好的工具可以用.
1 setterm -bold on
2 echo bold hello
3
4 setterm -bold off
5 echo normal hello
tset
显示或初始化终端设置. 可以说这是stty的功能比较弱的版本.
bash$ tset -r
Terminal type is xterm-xfree86.
Kill is control-U (^U).
Interrupt is control-C (^C).
setserial
设置或者显示串口参数. 这个脚本只能被root用户来运行, 并且通常都在系统安装脚本
中使用.
1 # 来自于 /etc/pcmcia/serial 脚本:
2
3 IRQ=`setserial /dev/$DEVICE | sed -e 's/.*IRQ: //'`
http://www.818198.com Page 323
SHELL十三问
4 setserial /dev/$DEVICE irq 0 ; setserial /dev/$DEVICE irq $IRQ
getty, agetty
一个终端的初始化过程通常都是使用getty或agetty来建立, 这样才能让用户登录. 这些
命令并不用在用户的shell脚本中. 它们的行为与stty很相似.
mesg
使能或禁用当前用户终端的存取权限. 禁用存取权限将会阻止网络上的另一用户向这个终
端写消息.
注意: 当你正在编写文本文件的时候, 在文本中间突然来了一个莫名其妙的消息, 这对你
来说是非常烦人的. 在多用户的网络环境下, 当你不想被打断的时候, 你可能因此希
望禁用对你终端的写权限.
wall
这是一个缩写单词 "write all", 也就是, 向登录到网络上的任何终端的所有用户都发送
一个消息. 最早这是一个管理员的工具, 很有用, 比如, 当系统有问题的时候, 管理可以
警告系统上的所有人暂时离开 (参见 Example 17-1).
bash$ wall System going down for maintenance in 5 minutes!
Broadcast message from bozo (pts/1) Sun Jul 8 13:53:27 2001...
System going down for maintenance in 5 minutes!
注意: 如果某个特定终端使用mesg来禁止了写权限, 那么wall将不会给它发消息.
信息与统计类
uname
输出系统的说明(OS, 内核版本, 等等.)到stdout. 使用 -a 选项, 将会给出详细的信息
(参见 Example 12-5). 使用-s选项只会输出OS类型.
bash$ uname -a
Linux localhost.localdomain 2.2.15-2.5.0 #1 Sat Feb 5 00:13:43 EST 2000 i686 unknown
bash$ uname -s
Linux
arch
显示系统的硬件体系结构. 等价于 uname -m. 参见 Example 10-26.
bash$ arch
i686
bash$ uname -m
i686
lastcomm
给出前一个命令的信息, 存储在/var/account/pacct文件中. 命令名字与用户名字都可以
使用选项来指定. 这是GNU的一个统计工具.
lastlog
列出系统上所有用户最后登录的时间. 存在/var/log/lastlog文件中.
bash$ lastlog
root tty1 Fri Dec 7 18:43:21 -0700 2001
bin **Never logged in**
daemon **Never logged in**
...
bozo tty1 Sat Dec 8 21:14:29 -0700 2001
http://www.818198.com Page 324
SHELL十三问
bash$ lastlog | grep root
root tty1 Fri Dec 7 18:43:21 -0700 2001
注意: 如果用户对于/var/log/lastlog文件没有读权限的话, 那么调用这个命令就会失败.
lsof
列出打开的文件. 这个命令将会把所有当前打开的文件列出一份详细的表格, 包括文件的
所有者信息, 尺寸, 与它们相关的信息等等. 当然, lsof也可以管道输出到 grep 和(或)
awk来分析它的结果.
bash$ lsof
COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME
init 1 root mem REG 3,5 30748 30303 /sbin/init
init 1 root mem REG 3,5 73120 8069 /lib/ld-2.1.3.so
init 1 root mem REG 3,5 931668 8075 /lib/libc-2.1.3.so
cardmgr 213 root mem REG 3,5 36956 30357 /sbin/cardmgr
...
strace
为了跟踪系统和信号的诊断和调试工具. 调用它最简单的方法就是strace COMMAND.
bash$ strace df
execve("/bin/df", ["df"], [/* 45 vars */]) = 0
uname({sys="Linux", node="bozo.localdomain", ...}) = 0
brk(0) = 0x804f5e4
...
这是 Solaris truss命令的Linux的等价工具.
nmap
网络端口扫描器. 这个命令将会扫描一个服务器来定位打开的端口, 并且定位这些端口相
关的服务. 这是一个防止网络被黑客入侵的一个重要的安全工具.
1 #!/bin/bash
2
3 SERVER=$HOST # localhost.localdomain (127.0.0.1).
4 PORT_NUMBER=25 # SMTP 端口.
5
6 nmap $SERVER | grep -w "$PORT_NUMBER" # 这个指定端口打开了么?
7 # grep -w 匹配整个单词,
8 #+ 这样就不会匹配类似于1025这种含有25的端口了.
9
10 exit 0
11
12 # 25/tcp open smtp
nc
nc(netcat)工具是一个完整的工具包, 可以使用它来连接和监听TCP和UDP端口. 它可以用
来作为诊断和测试工具, 也可以用来作为基于脚本的HTTP客户端和服务器的组件.
bash$ nc localhost.localdomain 25
220 localhost.localdomain ESMTP Sendmail 8.13.1/8.13.1; Thu, 31 Mar 2005 15:41:35 -0700
Example 13-5 Checking a remote server for identd
################################Start Script#######################################
1 #! /bin/sh
2 ## Duplicate DaveG's ident-scan thingie using netcat. Oooh, he'll be p*ssed.
3 ## Args: target port [port port port ...]
4 ## Hose stdout _and_ stderr together.
http://www.818198.com Page 325
SHELL十三问
5 ##
6 ## 优点: runs slower than ident-scan, giving remote inetd less cause
7 ##+ for alarm, and only hits the few known daemon ports you specify.
8 ## 缺点: requires numeric-only port args, the output sleazitude,
9 ##+ and won't work for r-services when coming from high source ports.
10 # 脚本作者: Hobbit
11 # 授权使用在本书中.
12
13 # ---------------------------------------------------14 E_BADARGS=65 # 至少需要两个参数.
15 TWO_WINKS=2 # 需要睡多长时间.
16 THREE_WINKS=3
17 IDPORT=113 # Authentication "tap ident" port.
18 RAND1=999
19 RAND2=31337
20 TIMEOUT0=9
21 TIMEOUT1=8
22 TIMEOUT2=4
23 # ---------------------------------------------------24
25 case "${2}" in
26 "" ) echo "Need HOST and at least one PORT." ; exit $E_BADARGS ;;
27 esac
28
29 # Ping 'em once and see if they *are* running identd.
30 nc -z -w $TIMEOUT0 "$1" $IDPORT || { echo "Oops, $1 isn't running identd." ; exit 0 ; }
31 # -z scans for listening daemons.
32 # -w $TIMEOUT = How long to try to connect.
33
34 # Generate a randomish base port.
35 RP=`expr $$ % $RAND1 + $RAND2`
36
37 TRG="$1"
38 shift
39
40 while test "$1" ; do
41 nc -v -w $TIMEOUT1 -p ${RP} "$TRG" ${1} < /dev/null > /dev/null &
42 PROC=$!
43 sleep $THREE_WINKS
44 echo "${1},${RP}" | nc -w $TIMEOUT2 -r "$TRG" $IDPORT 2>&1
45 sleep $TWO_WINKS
46
47 # 这个脚本看起来是不是一个瘸腿脚本, 或者其它更差的什么东西?
48 # ABS Guide 作者注释: "并不是真的那么差,
49 #+ 事实上相当清楚."
50
51 kill -HUP $PROC
52 RP=`expr ${RP} + 1`
http://www.818198.com Page 326
SHELL十三问
53 shift
54 done
55
56 exit $?
57
58 # 注意事项:
59 # ---------60
61 # 尝试注释一下第30行的程序, 并且使用"localhost.localdomain 25"
62 #+ 作为参数来运行这个脚本.
63
64 # For more of Hobbit's 'nc' example scripts,
65 #+ look in the documentation:
66 #+ the /usr/share/doc/nc-X.XX/scripts directory.
################################End Script#########################################
并且, 当然, 这里还有Dr. Andrew Tridgell在BistKeeper事件中臭名卓著的一行脚本:
1 echo clone | nc thunk.org 5000 > e2fsprogs.dat
free
使用表格形式来显示内存和缓存的使用情况. 这个命令的输出非常适合于使用 grep, awk
或者Perl来分析. procinfo命令将会显示free命令所能显示的所有信息, 而且更多.
bash$ free
total used free shared buffers cached
Mem: 30504 28624 1880 15820 1608 16376
-/+ buffers/cache: 10640 19864
Swap: 68540 3128 65412
显示未使用的RAM内存:
bash$ free | grep Mem | awk '{ print $4 }'
1880
procinfo
从/proc pseudo-filesystem中提取和显示所有信息和统计资料. 这个命令将给出更详细
的信息.
bash$ procinfo | grep Bootup
Bootup: Wed Mar 21 15:15:50 2001 Load average: 0.04 0.21 0.34 3/47 6829
lsdev
显示设备, 也就是显示安装的硬件.
bash$ lsdev
Device DMA IRQ I/O Ports
------------------------------------------------ cascade 4 2
dma 0080-008f
dma1 0000-001f
dma2 00c0-00df
fpu 00f0-00ff
ide0 14 01f0-01f7 03f6-03f6
...
du
递归的显示(磁盘)文件的使用状况. 除非指定, 默认是当前工作目录.
bash$ du -ach
http://www.818198.com Page 327
SHELL十三问
1.0k ./wi.sh
1.0k ./tst.sh
1.0k ./random.file
6.0k .
6.0k total
df
使用列表的形式显示文件系统的使用状况.
bash$ df
Filesystem 1k-blocks Used Available Use% Mounted on
/dev/hda5 273262 92607 166547 36% /
/dev/hda8 222525 123951 87085 59% /home
/dev/hda7 1408796 1075744 261488 80% /usr
dmesg
将所有的系统启动消息输出到stdout上. 方便出错,并且可以查出安装了哪些设备驱动和
察看使用了哪些系统中断. dmesg命令的输出当然也可以在脚本中使用 grep, sed, 或
awk 来进行分析.
bash$ dmesg | grep hda
Kernel command line: ro root=/dev/hda2
hda: IBM-DLGA-23080, ATA DISK drive
hda: 6015744 sectors (3080 MB) w/96KiB Cache, CHS=746/128/63
hda: hda1 hda2 hda3 < hda5 hda6 hda7 > hda4
stat
显示一个或多个给定文件(也可以是目录文件或设备文件)的详细的统计信息.
bash$ stat test.cru
File: "test.cru"
Size: 49970 Allocated Blocks: 100 Filetype: Regular File
Mode: (0664/-rw-rw-r--) Uid: ( 501/ bozo) Gid: ( 501/ bozo)
Device: 3,8 Inode: 18185 Links: 1
Access: Sat Jun 2 16:40:24 2001
Modify: Sat Jun 2 16:40:24 2001
Change: Sat Jun 2 16:40:24 2001
如果目标文件不存在, stat 将会返回一个错误信息.
bash$ stat nonexistent-file
nonexistent-file: No such file or directory
vmstat
显示虚拟内存的统计信息.
bash$ vmstat
procs memory swap io system cpu
r b w swpd free buff cache si so bi bo in cs us sy id
0 0 0 0 11040 2636 38952 0 0 33 7 271 88 8 3 89
netstat
显示当前网络的统计和信息, 比如路由表和激活的连接. 这个工具存取/proc/net(第27章)
中的信息. 参见 Example 27-3.
netstat -r 等价于 route 命令.
bash$ netstat
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
Active UNIX domain sockets (w/o servers)
http://www.818198.com Page 328
SHELL十三问
Proto RefCnt Flags Type State I-Node Path
unix 11 [ ] DGRAM 906 /dev/log
unix 3 [ ] STREAM CONNECTED 4514 /tmp/.X11-unix/X0
unix 3 [ ] STREAM CONNECTED 4513
. . .
uptime
显示系统运行的时间, 还有其他一些统计信息.
bash$ uptime
10:28pm up 1:57, 3 users, load average: 0.17, 0.34, 0.27
注意: load average 如果小于或等于1, 那么就意味着系统会马上处理. 如果
load average大于1, 那么就意味着进程需要排队. 如果load average大于3,
那么就意味着, 系统性能已经显著下降了.
hostname
显示系统的主机名字. 这个命令在 /etc/rc.d 安装脚本(/etc/rc.d/rc.sysinit
或类似的)中设置主机名. 等价于uname -n, 并且与$HOSTNAME内部变量很相像.
bash$ hostname
localhost.localdomain
bash$ echo $HOSTNAME
localhost.localdomain
与 hostname 命令很相像的命令还有 domainname, dnsdomainname, nisdomainname, 和
ypdomainname 命令. 使用这些来显示或设置系统DNS 或者 NIS/YP 域名. 对于hostname
命令来说使用不同的选项一样可以达到上边这些命令的目的.
hostid
显示主机的32位的16进制ID.
bash$ hostid
7f0100
注意: 这个命令据说对于特定系统可以获得一个"唯一"的序号. 某些产品的注册过程可能
会需要这个序号来作为用户的许可证. 不幸的是, hostid 只会使用字节转换的方法
来用16进制显示机器的网络地址.
一个没有网络的Linux机器的典型的网络地址设置在/ect/hosts中.
bash$ cat /etc/hosts
127.0.0.1 localhost.localdomain localhost
碰巧, 通过对127.0.0.1进行字节转换, 我们获得了 0.127.1.0, 用16进制表示就是
007f0100, 这就是上边hostid返回的结果. 这样几乎所有的无网络的Linux机器都会
得到这个hostid.
sar
sar (System Activity Reporter系统活动报告) 命令将会给出系统统计的一个非常详细的
概要. Santa Cruz Operation("老" SCO)公司在1999年4月份以开源软件的形式发布了sar.
这个命令并不是基本Linux发行版的一部分, 但是你可以从Sebastien Godard 写的
sysstat utilities 包中获得这个工具.
bash$ sar
Linux 2.4.9 (brooks.seringas.fr) 09/26/03
10:30:00 CPU %user %nice %system %iowait %idle
10:40:00 all 2.21 10.90 65.48 0.00 21.41
10:50:00 all 3.36 0.00 72.36 0.00 24.28
11:00:00 all 1.12 0.00 80.77 0.00 18.11
Average: all 2.23 3.63 72.87 0.00 21.27
http://www.818198.com Page 329
SHELL十三问
14:32:30 LINUX RESTART
15:00:00 CPU %user %nice %system %iowait %idle
15:10:00 all 8.59 2.40 17.47 0.00 71.54
15:20:00 all 4.07 1.00 11.95 0.00 82.98
15:30:00 all 0.79 2.94 7.56 0.00 88.71
Average: all 6.33 1.70 14.71 0.00 77.26
readelf
显示指定的 elf 格式的2进制文件的统计信息. 这个工具是binutils工具包的一部分.
bash$ readelf -h /bin/bash
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
. . .
size
size [/path/to/binary] 命令可以显示2进制可执行文件或归档文件每部分的尺寸. 这个
工具主要是程序员使用.
bash$ size /bin/bash
text data bss dec hex filename
495971 22496 17392 535859 82d33 /bin/bash
系统日志类
logger
附加一个用户产生的消息到系统日之中 (/var/log/messages). 不是root用户也可以调用
logger.
1 logger Experiencing instability in network connection at 23:10, 05/21.
2 # 现在, 运行 'tail /var/log/messages'.
通过在脚本中调用一个logger命令, 就可以将调试信息写到/var/log/messages中.
1 logger -t $0 -i Logging at line "$LINENO".
2 # "-t" 选项可以为长的入口指定标签.
3 # "-i" 选项记录进程ID.
4
5 # tail /var/log/message
6 # ...
7 # Jul 7 20:48:58 localhost ./test.sh[1712]: Logging at line 3.
logrotate
这个工具用来管理系统的log文件, 可以在合适的时候轮换, 压缩, 删除, 和(或)e-mail
它们. 这个工具将从老的log文件中取得一些杂乱的记录保存在/var/log中. 通常使用
cron 来每天运行logrotate.
在/etc/logrotate.conf中添加合适的入口就可以管理自己的log文件了, 就像管理系统
log文件一样.
注意: Stefano Falsetto 创造了rottlog, 他认为这是logrotate的改进版本.
作业控制
ps
进程统计: 通过进程所有者和PID(进程ID)来列出当前执行的进程. 通常都是使用ax选项
http://www.818198.com Page 330
SHELL十三问
来调用这个命令, 并且结果可以通过管道传递到 grep 或 sed 中来搜索特定的进程
(参见 Example 11-12 和 Example 27-2).
bash$ ps ax | grep sendmail
295 ? S 0:00 sendmail: accepting connections on port 25
如果想使用"树"的形式来显示系统进程: ps afjx 或者 ps ax --forest.
pgrep, pkill
ps 命令与grep或kill结合使用.
bash$ ps a | grep mingetty
2212 tty2 Ss+ 0:00 /sbin/mingetty tty2
2213 tty3 Ss+ 0:00 /sbin/mingetty tty3
2214 tty4 Ss+ 0:00 /sbin/mingetty tty4
2215 tty5 Ss+ 0:00 /sbin/mingetty tty5
2216 tty6 Ss+ 0:00 /sbin/mingetty tty6
4849 pts/2 S+ 0:00 grep mingetty
bash$ pgrep mingetty
2212 mingetty
2213 mingetty
2214 mingetty
2215 mingetty
2216 mingetty
pstree
使用"树"形式列出当前执行的进程. -p选项显示PID,和进程名字.
top
连续不断的显示cpu使用率最高的进程. -b 选项将会以文本方式显示, 以便于可以在脚本
中分析或存取.
bash$ top -b
8:30pm up 3 min, 3 users, load average: 0.49, 0.32, 0.13
45 processes: 44 sleeping, 1 running, 0 zombie, 0 stopped
CPU states: 13.6% user, 7.3% system, 0.0% nice, 78.9% idle
Mem: 78396K av, 65468K used, 12928K free, 0K shrd, 2352K buff
Swap: 157208K av, 0K used, 157208K free 37244K cached
PID USER PRI NI SIZE RSS SHARE STAT %CPU %MEM TIME COMMAND
848 bozo 17 0 996 996 800 R 5.6 1.2 0:00 top
1 root 8 0 512 512 444 S 0.0 0.6 0:04 init
2 root 9 0 0 0 0 SW 0.0 0.0 0:00 keventd
...
nice
使用修改后的优先级来运行一个后台作业. 优先级从19(最低)到-20(最高). 只有root用
户可以设置负的(比较高的)优先级. 相关的命令是renice, snice, 和skill.
nohup
保持一个命令的运行, 即使用户登出系统. 这个命令做为前台进程来运行, 除非前边加 &.
如果你在脚本中使用nohup命令, 最好和wait 命令一起使用, 这样可以避免创建一个
孤儿进程或僵尸进程.
pidof
取得一个正在运行的作业的进程ID(PID). 因为一些作业控制命令, 比如kill和renice只
能使用进程的PID(而不是它的名字), 所以有时候必须的取得PID. pidof命令与$PPID内部
变量非常相似.
bash$ pidof xclock
http://www.818198.com Page 331
SHELL十三问
880
Example 13-6 pidof 帮助杀掉一个进程
################################Start Script#######################################
1 #!/bin/bash
2 # kill-process.sh
3
4 NOPROCESS=2
5
6 process=xxxyyyzzz # 使用不存在的进程.
7 # 只不过是为了演示...
8 # ... 并不想在这个脚本中杀掉任何真正的进程.
9 #
10 # 如果, 举个例子, 你想使用这个脚本来断线Internet,
11 # process=pppd
12
13 t=`pidof $process` # 取得$process的pid(进程id).
14 # 'kill'必须使用pid(不能用程序名).
15
16 if [ -z "$t" ] # 如果没这个进程, 'pidof' 返回空.
17 then
18 echo "Process $process was not running."
19 echo "Nothing killed."
20 exit $NOPROCESS
21 fi
22
23 kill $t # 对于顽固的进程可能需要'kill -9'.
24
25 # 这里需要做一个检查, 看看进程是否允许自身被kill.
26 # 或许另一个 " t=`pidof $process` " 或者 ...
27
28
29 # 整个脚本都可以使用下边这句来替换:
30 # kill $(pidof -x process_name)
31 # 但是这就没有教育意义了.
32
33 exit 0
################################End Script#########################################
fuser
取得一个正在存取某个或某些文件(或目录)的进程ID. 使用-k选项将会杀掉这些进程. 对
于系统安全来说, 尤其是在脚本中想阻止未被授权的用户存取系统服务的时候, 这个命令
就显得很有用了.
bash$ fuser -u /usr/bin/vim
/usr/bin/vim: 3207e(bozo)
bash$ fuser -u /dev/null
/dev/null: 3009(bozo) 3010(bozo) 3197(bozo) 3199(bozo)
当正常的插入或删除保存的媒体, 比如CD ROM或者USB闪存设备的时候, fuser的应用也显
得特别重要. 有时候当你想umount一个设备失败的时候(出现设备忙的错误消息), 这意味
http://www.818198.com Page 332
SHELL十三问
着某些用户或进程正在存取这个设备. 使用fuser -um /dev/device_name可以搞定这些,
这样你就可以杀掉所有相关的进程.
bash$ umount /mnt/usbdrive
umount: /mnt/usbdrive: device is busy
bash$ fuser -um /dev/usbdrive
/mnt/usbdrive: 1772c(bozo)
bash$ kill -9 1772
bash$ umount /mnt/usbdrive
fuser 的-n选项可以获得正在存取某一端口的进程. 当和nmap命令组合使用的时候尤其
有用.
root# nmap localhost.localdomain
PORT STATE SERVICE
25/tcp open smtp
root# fuser -un tcp 25
25/tcp: 2095(root)
root# ps ax | grep 2095 | grep -v grep
2095 ? Ss 0:00 sendmail: accepting connections
cron
管理程序调度器, 执行一些日常任务, 比如清除和删除系统log文件, 或者更新slocate命
令的数据库. 这是at命令的超级用户版本(虽然每个用户都可以有自己的crontab文件, 并
且这个文件可以使用crontab命令来修改). 它以幽灵进程T的身份来运行, 并且从
/ect/crontab中获得执行的调度入口.
注意: 一些Linux的风格都使用crond, Matthew Dillon的cron.
进程控制和启动类
init
init 命令是所有进程的父进程. 在系统启动的最后一步调用, init 将会依据
/etc/inittab来决定系统的运行级别. 只能使用root身份来运行它的别名telinit.
telinit
init命令的符号链接, 这是一种修改系统运行级别的一个手段, 通常在系统维护或者紧急
的文件系统修复的时候才用. 只能使用root身份调用. 调用这个命令是非常危险的 - 在
你使用之前确定你已经很好地了解它.
runlevel
显示当前和最后的运行级别, 也就是, 确定你的系统是否终止(runlevel 为0), 还是运行
在单用户模式(1), 多用户模式(2), 或者是运行在X Windows(5), 还是正在重启(6). 这
个命令将会存取/var/run/utmp文件.
halt, shutdown, reboot
设置系统关机的命令, 通常比电源关机的优先级高.
service
开启或停止一个系统服务. 启动脚本在/etc/init.d中, 并且/etc/rc.d在系统启动的时候
使用这个命令来启动服务.
root# /sbin/service iptables stop
Flushing firewall rules: [ OK ]
Setting chains to policy ACCEPT: filter [ OK ]
Unloading iptables modules: [ OK ]
http://www.818198.com Page 333
SHELL十三问
网络类
ifconfig
网络的接口配置和调试工具.
bash$ ifconfig -a
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:16436 Metric:1
RX packets:10 errors:0 dropped:0 overruns:0 frame:0
TX packets:10 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:700 (700.0 b) TX bytes:700 (700.0 b)
ifconfig 命令绝大多数情况都是在启动时候设置接口, 或者在重启的时候关闭它们.
1 # 来自于 /etc/rc.d/init.d/network 的代码片段
2
3 # ...
4
5 # 检查网络是否启动.
6 [ ${NETWORKING} = "no" ] && exit 0
7
8 [ -x /sbin/ifconfig ] || exit 0
9
10 # ...
11
12 for i in $interfaces ; do
13 if ifconfig $i 2>/dev/null | grep -q "UP" >/dev/null 2>&1 ; then
14 action "Shutting down interface $i: " ./ifdown $i boot
15 fi
16 # grep命令的GNU指定的 "-q" 的意思是"安静", 也就是不产生输出.
17 # 这样, 后边重定向到/dev/null的操作就有点重复了.
18
19 # ...
20
21 echo "Currently active devices:"
22 echo `/sbin/ifconfig | grep ^[a-z] | awk '{print $1}'`
23 # ^^^^^ 应该被引用防止globbing.
24 # 下边这段也能工作.
25 # echo $(/sbin/ifconfig | awk '/^[a-z]/ { print $1 })'
26 # echo $(/sbin/ifconfig | sed -e 's/ .*//')
27 # Thanks, S.C.做了额外的注释.
参见 Example 29-6.
iwconfig
这是为了配置无线网络的命令集合. 可以说是上边的ifconfig的无线版本.
route
显示内核路由表信息, 或者查看内核路由表的修改.
bash$ route
Destination Gateway Genmask Flags MSS Window irtt Iface
pm3-67.bozosisp * 255.255.255.255 UH 40 0 0 ppp0
127.0.0.0 * 255.0.0.0 U 40 0 0 lo
http://www.818198.com Page 334
SHELL十三问
default pm3-67.bozosisp 0.0.0.0 UG 40 0 0 ppp0
chkconfig
检查网络配置. 这个命令负责显示和管理在启动过程中所开启的网络服务(这些服务都是
从/etc/rc?.d目录中开启的).
最开始是从IRIX到Red Hat Linux的一个接口, chkconfig在某些Linux发行版中并不是核
心安装的一部分.
bash$ chkconfig --list
atd 0:off 1:off 2:off 3:on 4:on 5:on 6:off
rwhod 0:off 1:off 2:off 3:off 4:off 5:off 6:off
...
tcpdump
网络包的"嗅探器". 这是一个用来分析和调试网络上传输情况的工具, 它所使用的手段是
把匹配指定规则的包头都显示出来.
显示主机bozoville和主机caduceus之间所有传输的ip包.
bash$ tcpdump ip host bozoville and caduceus
当然,tcpdump的输出可以被分析, 可以用我们之前讨论的文本处理工具来分析结果.
文件系统类
mount
加载一个文件系统, 通常都用来安装外部设备, 比如软盘或CDROM. 文件/etc/fstab 将会
提供一个方便的列表, 这个列表列出了所有可用的文件系统, 分区和设备, 另外还包括某
些选项, 比如是否可以自动或者手动的mount. 文件/etc/mtab 显示了当前已经mount的文
件系统和分区(包括虚拟的, 比如/proc).
mount -a 将会mount所有列在/ect/fstab中的文件系统和分区, 除了那些标记有非自动选
项的. 在启动的时候, 在/etc/rc.d中的一个启动脚本(rc.sysinit或者一些相似的脚本)
将会这么调用, mount所有可用的文件系统和分区.
1 mount -t iso9660 /dev/cdrom /mnt/cdrom
2 # 加载 CDROM
3 mount /mnt/cdrom
4 # 方便的方法, 如果 /mnt/cdrom 包含在 /etc/fstab 中
这个多功能的命令甚至可以将一个普通文件mount到块设备中, 并且这个文件就好像一个
文件系统一样. mount可以将文件与一个loopback设备相关联来达到这个目的.
ccomplishes that by associating the file with a loopback device. 这种应用通常
都是用来mount和检查一个ISO9660镜像,在这个镜像被烧录到CDR之前. [3]
Example 13-7 检查一个CD镜像
################################Start Script#######################################
1 # 以root身份...
2
3 mkdir /mnt/cdtest # 如果没有的话,准备一个mount点.
4
5 mount -r -t iso9660 -o loop cd-image.iso /mnt/cdtest # mount这个镜像.
6 # "-o loop" option equivalent to "losetup /dev/loop0"
7 cd /mnt/cdtest # 现在检查这个镜像.
8 ls -alR # 列出目录树中的文件.
9 # 等等.
################################End Script#########################################
umount
卸除一个当前已经mount的文件系统. 在正常删除之前已经mount的软盘和CDROM之前, 这
个设备必须被unmount, 否则文件系统将会损坏.
http://www.818198.com Page 335
SHELL十三问
1 umount /mnt/cdrom
2 # 现在你可以按下退出按钮(指的是cdrom或软盘驱动器上的退出钮), 并安全的退出光盘.
sync
强制写入所有需要更新的buffer上的数据到硬盘上(同步带有buffer的驱动器). 如果不是
严格必要的话,一个sync就可以保证系统管理员或者用户刚刚修改的数据会安全的在突然
的断点中幸存下来. 在比较早以前, 在系统重启前都是使用 sync; sync (两次, 这样保
证绝对可靠), 这是一种很有用的小心的方法.
有时候, 比如当你想安全删除一个文件的时候(参见 Example 12-55), 或者当磁盘灯开始
闪烁的时候, 你可能需要强制马上进行buffer刷新.
losetup
建立和配置loopback设备.
Example 13-8 在一个文件中创建文件系统
################################Start Script#######################################
1 SIZE=1000000 # 1M
2
3 head -c $SIZE < /dev/zero > file # 建立指定尺寸的文件.
4 losetup /dev/loop0 file # 作为loopback设备来建立.
5 mke2fs /dev/loop0 # 创建文件系统.
6 mount -o loop /dev/loop0 /mnt # Mount它.
7
8 # Thanks, S.C.
################################End Script#########################################
mkswap
创建一个交换分区或文件. 交换区域随后必须马上使用swapon来使能.
swapon, swapoff
使能/禁用 交换分区或文件. 这两个命令通常在启动和关机的时候才有效.
mke2fs
创建Linux ext2 文件系统. 这个命令必须以root身份调用.
Example 13-9 添加一个新的硬盘驱动器
################################Start Script#######################################
1 #!/bin/bash
2
3 # 在系统上添加第二块硬盘驱动器.
4 # 软件配置. 假设硬件已经安装了.
5 # 来自于本书作者的一篇文章.
6 # 在"Linux Gazette"的问题#38上, http://www.linuxgazette.com.
7
8 ROOT_UID=0 # 这个脚本必须以root身份运行.
9 E_NOTROOT=67 # 非root用户将会产生这个错误.
10
11 if [ "$UID" -ne "$ROOT_UID" ]
12 then
13 echo "Must be root to run this script."
14 exit $E_NOTROOT
15 fi
16
17 # 要非常谨慎的小心使用!
18 # 如果某步错了, 可能会彻底摧毁你当前的文件系统.
http://www.818198.com Page 336
SHELL十三问
19
20
21 NEWDISK=/dev/hdb # 假设/dev/hdb空白. 检查一下!
22 MOUNTPOINT=/mnt/newdisk # 或者选择另外的mount点.
23
24
25 fdisk $NEWDISK
26 mke2fs -cv $NEWDISK1 # 检查坏块, 详细输出.
27 # 注意: /dev/hdb1, *不是* /dev/hdb!
28 mkdir $MOUNTPOINT
29 chmod 777 $MOUNTPOINT # 让所有用户都具有全部权限.
30
31
32 # 现在, 测试一下...
33 # mount -t ext2 /dev/hdb1 /mnt/newdisk
34 # 尝试创建一个目录.
35 # 如果工作起来了, umount它, 然后继续.
36
37 # 最后一步:
38 # 将下边这行添加到/etc/fstab.
39 # /dev/hdb1 /mnt/newdisk ext2 defaults 1 1
40
41 exit 0
################################End Script#########################################
参见 Example 13-8 和 Example 28-3.
tune2fs
调整ext2文件系统. 可以用来修改文件系统参数, 比如mount的最大数量. 必须以root身
份调用.
注意: 这是一个非常危险的命令. 如果坏了, 你需要自己负责, 因为它可能会破坏你的文
件系统.
dumpe2fs
打印(输出到stdout上)非常详细的文件系统信息. 必须以root身份调用.
root# dumpe2fs /dev/hda7 | grep 'ount count'
dumpe2fs 1.19, 13-Jul-2000 for EXT2 FS 0.5b, 95/08/09
Mount count: 6
Maximum mount count: 20
hdparm
列出或修改硬盘参数. 这个命令必须以root身份调用, 如果滥用的话会有危险.
fdisk
在存储设备上(通常都是硬盘)创建和修改一个分区表. 必须以root身份使用.
注意: 谨慎使用这个命令. 如果出错, 会破坏你现存的文件系统.
fsck, e2fsck, debugfs
文件系统的检查, 修复, 和除错命令集合.
fsck: 检查UNIX文件系统的前端工具(也可以调用其它的工具). 文件系统的类型一般都是
默认的ext2.
e2fsck: ext2文件系统检查器.
debugfs: ext2文件系统除错器. 这个多功能但是危险的工具的用处之一就是(尝试)恢复
删除的文件. 只有高级用户才能用.
http://www.818198.com Page 337
SHELL十三问
上边的这几个命令都必须以root身份调用, 这些命令都很危险, 如果滥用的话会破坏文件
系统.
badblocks
检查存储设备的坏块(物理损坏). 这个命令在格式化新安装的硬盘时或者测试备份的完整
性的时候会被用到. [4] 举个例子, badblocks /dev/fd0 测试一个软盘.
badblocks可能会引起比较糟糕的结果(覆盖所有数据), 在只读模式下就不会发生这种情
况.如果root用户拥有需要测试的设备(通常都是这种情况), 那么root用户必须调用这个
命令.
lsusb, usbmodules
lsusb 命令会列出所有USB(Universal Serial Bus通用串行总线)总线和使用USB的设备.
usbmodules 命令会输出连接USB设备的驱动模块的信息.
root# lsusb
Bus 001 Device 001: ID 0000:0000
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 1.00
bDeviceClass 9 Hub
bDeviceSubClass 0
bDeviceProtocol 0
bMaxPacketSize0 8
idVendor 0x0000
idProduct 0x0000
. . .
mkbootdisk
创建启动软盘, 启动盘可以唤醒系统, 比如当MBR(master boot record主启动记录)坏掉
的时候. mkbootdisk 命令其实是一个Bash脚本, 由Erik Troan所编写, 放在/sbin目录中.
chroot
修改ROOT目录. 一般的命令都是从$PATH中获得的, 相对的默认的根目录是 /. 这个命令
将会把根目录修改为另一个目录(并且也将把工作目录修改到那). 出于安全目的, 这个命
令时非常有用的, 举个例子, 当系统管理员希望限制一些特定的用户, 比如telnet上来的
用户, 将他们限定到文件系统上一个安全的地方(这有时候被称为将一个guest用户限制在
"chroot 监牢"中). 注意, 在使用chroot之后, 系统的二进制可执行文件的目录将不再
可用了.
chroot /opt 将会使得原来的/usr/bin目录变为/opt/usr/bin. 同样,
chroot /aaa/bbb /bin/ls 将会使得ls命令以/aaa/bbb作为根目录, 而不是以前的/.
如果使用alias XX 'chroot /aaa/bbb ls', 并把这句放到用户的~/.bashrc文件中的话,
这将可以有效地限制运行命令"XX"时, 命令"XX"可以使用文件系统的范围.
当从启动盘恢复的时候(chroot 到 /dev/fd0), 或者当系统从死机状态恢复过来并作为进
入lilo的选择手段的时候, chroot命令都是非常方便的. 其它的应用还包括从不同的文件
系统进行安装(一个rpm选项)或者从CDROM上运行一个只读文件系统. 只能以root身份调用,
小心使用.
注意: 由于正常的$PATH将不再被关联了, 所以可能需要将一些特定的系统文件拷贝到
chrooted目录中.
lockfile
这个工具是procmail包的一部分(www.procmail.org). 它可以创建一个锁定文件, 锁定文
件是一种用来控制存取文件, 设备或资源的标记文件. 锁定文件就像一个标记一样被使用,
如果特定的文件, 设备, 或资源正在被一个特定的进程所使用("busy"), 那么对于其它进
http://www.818198.com Page 338
SHELL十三问
程来说, 就只能受限进行存取(或者不能存取).
1 lockfile /home/bozo/lockfiles/$0.lock
2 # 创建一个以脚本名字为前缀的写保护锁定文件.
锁定文件用在一些特定的场合, 比如说保护系统的mail目录以防止多个用户同时修改, 或
者提示一个modem端口正在被存取, 或者显示Netscape的一个实例正在使用它的缓存. 脚本
可以做一些检查工作, 比如说一个特定的进程可以创建一个锁定文件, 那么只要检查这个
特定的进程是否在运行, 就可以判断出锁定文件是否存在了. 注意如果脚本尝试创建一个
已经存在的锁定文件的话, 那么脚本很可能被挂起.
一般情况下, 应用创建或检查锁定文件都放在/var/lock目录中. [5] 脚本可以使用下面
的方法来检测锁定文件是否存在.
1 appname=xyzip
2 # 应用 "xyzip" 创建锁定文件 "/var/lock/xyzip.lock".
3
4 if [ -e "/var/lock/$appname.lock" ]
5 then
6 ...
flock
flock命令比lockfile命令用得少得多.Much less useful than the lockfile command
is flock. It sets an "advisory" lock on a file and then executes a command
while the lock is on. This is to prevent any other process from setting a lock
on that file until completion of the specified command.
1 flock $0 cat $0 > lockfile__$0
2 # Set a lock on the script the above line appears in,
3 #+ while listing the script to stdout.
注意: 与lockfile不同, flock不会自动创建一个锁定文件.
mknod
创建块或者字符设备文件(当在系统上安装新硬盘时可能是必要的). MAKEDEV工具事实上
具有nknod的全部功能, 而且更容易使用.
MAKEDEV
创建设备文件的工具. 必须在/dev目录下, 并且以root身份使用.
root# ./MAKEDEV
这是mknod的高级版本.
tmpwatch
自动删除在指定时间内未被存取过的文件. 通常都是被cron调用, 用来删掉老的log文件.
备份类
dump, restore
dump 命令是一个精巧的文件系统备份工具, 通常都用在比较大的安装和网络上. [6] 它
读取原始的磁盘分区并且以二进制形式来写备份文件. 需要备份的文件可以保存到各种各
样的存储设备上, 包括磁盘和磁带. restore命令用来恢复dump所产生的备份.
fdformat
对软盘进行低级格式化.
系统资源类
ulimit
设置使用系统资源的上限. 通常情况下都是使用-f选项来调用, -f用来设置文件尺寸的限
制(ulimit -f 1000就是将文件大小限制为1M). -c(译者注: 这里应该是作者笔误, 作者
写的是-t)选项来限制coredump(译者注: 核心转储, 程序崩溃时的内存状态写入文件)
尺寸(ulimit -c 0 就是不要coredumps). 一般情况下, ulimit的值应该设置在
/etc/profile 和(或)~/.bash_profile中(参见 Appendix G).
http://www.818198.com Page 339
SHELL十三问
注意: Judicious 使用ulimit 可以保护系统免受可怕的fork炸弹的迫害.
1 #!/bin/bash
2 # 这个脚本只是为了展示用.
3 # 你要自己为运行这个脚本的后果负责 -- 它*将*凝固你的系统.
4
5 while true # 死循环.
6 do
7 $0 & # 这个脚本调用自身 . . .
8 #+ fork无限次 . . .
9 #+ 直道系统完全不动, 因为所有的资源都耗尽了.
10 done # 这就是臭名卓著的 "sorcerer's appentice" 剧情.
11
12 exit 0 # 这里不会真正的推出, 因为这个脚本不会终止.
当这个脚本超过预先设置的限制时, 在/etc/profile中的 ulimit -Hu XX (XX 就是需
要限制的用户进程) 可以终止这个脚本的运行.
quota
显示用户或组的磁盘配额.
setquota
从命令行中设置用户或组的磁盘配额.
umask
设定用户创建文件时权限的缺省mask(掩码). 也可以用来限制特定用户的默认文件属性.
所有用户创建的文件属性都是由umask所指定的. The (octal) 传递给umask的8进制的值定
义了文件的权限. 比如, umask 022将会使得新文件的权限最多为755(777 与非 022) [7]
当然, 用户可以随后使用chmod来修改指定文件的属性. 用户一般都是将umask设置值的地
方放在/etc/profile 和(或) ~/.bash_profile中 (参见 Appendix G).
Example 13-10 使用umask来将输出文件隐藏起来
################################Start Script#######################################
1 #!/bin/bash
2 # rot13a.sh: 与"rot13.sh"脚本相同, 但是会将输出写道"安全"文件中.
3
4 # 用法: ./rot13a.sh filename
5 # 或 ./rot13a.sh
7
8 umask 177 # 文件创建掩码.
9 # 被这个脚本所创建的文件
10 #+ 将具有600权限.
11
12 OUTFILE=decrypted.txt # 结果保存在"decrypted.txt"中
13 #+ 这个文件只能够被
14 # 这个脚本的调用者(or root)所读写.
15
16 cat "$@" | tr 'a-zA-Z' 'n-za-mN-ZA-M' > $OUTFILE
17 # ^^ 从stdin 或文件中输入. ^^^^^^^^^^ 输出重定向到文件中.
18
19 exit 0
################################End Script#########################################
rdev
http://www.818198.com Page 340
SHELL十三问
取得root device, swap space, 或 video mode的相关信息, 或者对它们进行修改. 通常
说来rdev都是被lilo所使用, 但是在建立一个ram disk的时候, 这个命令也很有用. 小心
使用, 这是一个危险的命令.
模块类
lsmod
列出所有安装的内核模块.
bash$ lsmod
Module Size Used by
autofs 9456 2 (autoclean)
opl3 11376 0
serial_cs 5456 0 (unused)
sb 34752 0
uart401 6384 0 [sb]
sound 58368 0 [opl3 sb uart401]
soundlow 464 0 [sound]
soundcore 2800 6 [sb sound]
ds 6448 2 [serial_cs]
i82365 22928 2
pcmcia_core 45984 0 [serial_cs ds i82365]
注意: 使用cat /proc/modules可以得到同样的结果.
insmod
强制一个内核模块的安装(如果可能的话, 使用modprobe来代替) 必须以root身份调用.
rmmod
强制卸载一个内核模块. 必须以root身份调用.
modprobe
模块装载器, 一般情况下都是在启动脚本中自动调用. 必须以root身份调用.
depmod
创建模块依赖文件, 一般都是在启动脚本中调用.
modinfo
输出一个可装载模块的信息.
bash$ modinfo hid
filename: /lib/modules/2.4.20-6/kernel/drivers/usb/hid.o
description: "USB HID support drivers"
author: "Andreas Gal, Vojtech Pavlik
license: "GPL"
杂项类
env
使用设置过的或修改过(并不是修改整个系统环境)的环境变量来运行一个程序或脚本. 使
用 [varname=xxx] 形式可以在脚本中修改环境变量. 如果没有指定参数, 那么这个命令
将会列出所有设置的环境变量.
注意: 在Bash和其它的Bourne shell 衍生物中, 是可以在单一命令行上设置多个变量的.
1 var1=value1 var2=value2 commandXXX
2 # $var1 和 $var2 只设置在'commandXXX'的环境中.
注意: 当不知道shell或解释器的路径的时候, 脚本的第一行(#!行)可以使用env.
1 #! /usr/bin/env perl
2
3 print "This Perl script will run,\n";
4 print "even when I don't know where to find Perl.\n";
http://www.818198.com Page 341
SHELL十三问
5
6 # 便于跨平台移植,
7 # Perl程序可能没在期望的地方.
8 # Thanks, S.C.
ldd
显示一个可执行文件的共享库的依赖关系.
bash$ ldd /bin/ls
libc.so.6 => /lib/libc.so.6 (0x4000c000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x80000000)
watch
以指定的时间间隔来重复运行一个命令.
默认的时间间隔是2秒, 但时刻以使用-n选项来修改.
1 watch -n 5 tail /var/log/messages
2 # 每隔5秒钟显示系统log文件的结尾, /var/log/messages.
strip
从可执行文件中去掉调试符号引用. 这样做可以减小尺寸, 但是就不能调试了.
这个命令一般都用在Makefile中, 但是很少用在shell脚本中.
nm
列出未strip过的编译后的2进制文件的符号.
rdist
远程文件分布客户机程序: 在远端服务器上同步, 克隆, 或者备份一个文件系统.
13.1 分析一个系统脚本
---------------------利用我们所学到的关于管理命令的知识, 让我们一起来练习分析一个系统脚本. 最简单并
且最短的系统脚本之一是killall, 这个脚本被用来在系统关机时挂起运行的脚本.
Example 13-11 killall, 来自于 /etc/rc.d/init.d
################################Start Script#######################################
1 #!/bin/sh
2
3 # --> 本书作者所作的注释全部以"# -->"开头.
4
5 # --> 这是由Miquel van Smoorenburg所编写的
6 # --> 'rc'脚本包的一部分,
7
8 # --> 这个特殊的脚本看起来是是为Red Hat / FC所特定的,
9 # --> (在其它的发行版中可能不会出现).
10
11 # 停止所有正在运行的不必要的服务
12 #+ (there shouldn't be any, so this is just a sanity check)
13
14 for i in /var/lock/subsys/*; do
15 # --> 标准的for/in循环, 但是由于"do"在同一行上,
16 # --> 所以必须添加";".
17 # 检查脚本是否在那.
18 [ ! -f $i ] && continue
19 # --> 这是一种使用"与列表"的聪明的方法, 等价于:
20 # --> if [ ! -f "$i" ]; then continue
21
http://www.818198.com Page 342
SHELL十三问
22 # 取得子系统的名字.
23 subsys=${i#/var/lock/subsys/}
24 # --> 匹配变量名, 在这里就是文件名.
25 # --> 与subsys=`basename $i`完全等价.
26
27 # --> 从锁定文件名中获得
28 # -->+ (如果那里有锁定文件的话,
29 # -->+ 那就证明进程正在运行).
30 # --> 参考一下上边所讲的"锁定文件"的内容.
31
32
33 # 终止子系统.
34 if [ -f /etc/rc.d/init.d/$subsys.init ]; then
35 /etc/rc.d/init.d/$subsys.init stop
36 else
37 /etc/rc.d/init.d/$subsys stop
38 # --> 挂起运行的作业和幽灵进程.
39 # --> 注意"stop"只是一个位置参数,
40 # -->+ 并不是shell内建命令.
41 fi
42 done
################################End Script#########################################
这个没有那么糟. 除了在变量匹配的地方玩了一点花样, 其它也没有别的材料了.
练习 1. 在/etc/rc.d/init.d中, 分析halt脚本. 比脚本killall长一些, 但是概念上很相近.
对这个脚本做一个拷贝, 放到你的home目录下并且用它练习一下(不要以root身份运
行它). 使用-vn标志来模拟运行一下(sh -vn scriptname). 添加详细的注释. 将
"action"命令修改为"echos".
练习 2. 察看/etc/rc.d/init.d下的更多更复杂的脚本. 看看你是不是能够理解其中的一些脚
本. 使用上边的过程来分析这些脚本. 为了更详细的理解, 你可能也需要分析在
usr/share/doc/initscripts-?.??目录下的文件sysvinitfile, 这些都是
"initscript"文件的一部分.
注意事项:
[1] 这是在Linux机器上或者在带有磁盘配额的UNIX系统上的真实情况.
[2] 如果正在被删除的特定的用户已经登录了主机, 那么 userdel 命令将会失败.
[3] 对于烧录CDR的更多的细节, 可以参见Alex Withers的文章, 创建CD, 在
Linux Journal 的1999年的10月文章列表中.
[4] mke2fs的-c选项也会进行坏块检查.
[5] 因为只有root用户才具有对/var/lock目录的写权限, 一般的用户脚本是不能在那里
设置一个锁定文件的.
[6] 单用户的Linux系统的操作更倾向于使用简单的备份工具, 比如tar.
[7] NAND(与非)是一种逻辑操作. 这种操作的效果和减法很相像.
第14章 命令替换
================
命令替换将会重新分配一个命令[1]甚至是多个命令的输出; 它会将命令的输出如实地添加到
另一个上下文中. [2]
使用命令替换的典型形式是使用后置引用(`...`). 后置引用形式的命令(就是被反引号括起来)
将会产生命令行文本.
http://www.818198.com Page 343
SHELL十三问
1 script_name=`basename $0`
2 echo "The name of this script is $script_name."
这样的话, 命令的输出可以被当成传递到另一个命令的参数, 或者保存到变量中, 甚至可以用
来产生for循环的参数列表.
1 rm `cat filename` # "filename" 包含了需要被删除的文件列表.
2 #
3 # S. C. 指出使用这种形式, 可能会产生"参数列表太长"的错误.
4 # 更好的方法是 xargs rm -- < filename
5 # ( -- 同时覆盖了那些以"-"开头的文件所产生的特殊情况 )
6
7 textfile_listing=`ls *.txt`
8 # 变量中包含了当前工作目录下所有的*.txt文件.
9 echo $textfile_listing
10
11 textfile_listing2=$(ls *.txt) # 这是命令替换的另一种形式.
12 echo $textfile_listing2
13 # 同样的结果.
14
15 # 将文件列表放入到一个字符串中的一个可能的问题就是
16 # 可能会混进一个新行.
17 #
18 # 一个安全的将文件列表传递到参数中的方法就是使用数组.
19 # shopt -s nullglob # 如果不匹配, 那就不进行文件名扩展.
20 # textfile_listing=( *.txt )
21 #
22 # Thanks, S.C.
注意: 命令替换将会调用一个subshell.
注意: 命令替换可能会引起word splitting.
1 COMMAND `echo a b` # 2个参数: a and b
2
3 COMMAND "`echo a b`" # 1个参数: "a b"
4
5 COMMAND `echo` # 无参数
6
7 COMMAND "`echo`" # 一个空的参数
8
9
10 # Thanks, S.C.
即使没有引起word splitting, 命令替换也会去掉多余的新行.
1 # cd "`pwd`" # 这句总会正常的工作.
2 # 然而...
3
4 mkdir 'dir with trailing newline
5 '
6
7 cd 'dir with trailing newline
8 '
9
http://www.818198.com Page 344
SHELL十三问
10 cd "`pwd`" # 错误消息:
11 # bash: cd: /tmp/file with trailing newline: No such file or directory
12
13 cd "$PWD" # 运行良好.
14
15
16
17
18
19 old_tty_setting=$(stty -g) # 保存老的终端设置.
20 echo "Hit a key "
21 stty -icanon -echo # 对终端禁用"canonical"模式.
22 # 这样的话, 也会禁用了*本地*的echo.
23 key=$(dd bs=1 count=1 2> /dev/null) # 使用'dd'命令来取得一个按键.
24 stty "$old_tty_setting" # 保存老的设置.
25 echo "You hit ${#key} key." # ${#variable} = number of characters in $variable
26 #
27 # 按键任何键除了回车, 那么输出就是"You hit 1 key."
28 # 按下回车, 那么输出就是"You hit 0 key."
29 # 新行已经被命令替换吃掉了.
30
31 Thanks, S.C.
注意: 当一个变量是使用命令替换的结果做为值的时候, 然后使用echo命令来输出这个变量
(并且不引用这个变量, 就是不用引号括起来), 那么命令替换将会从最终的输出中删掉换
行符. 这可能会引起一些异常情况.
1 dir_listing=`ls -l`
2 echo $dir_listing # 未引用, 就是没用引号括起来
3
4 # 想打出来一个有序的目录列表.Expecting a nicely ordered directory listing.
5
6 # 可惜, 下边将是我们所获得的:
7 # total 3 -rw-rw-r-- 1 bozo bozo 30 May 13 17:15 1.txt -rw-rw-r-- 1 bozo
8 # bozo 51 May 15 20:57 t2.sh -rwxr-xr-x 1 bozo bozo 217 Mar 5 21:13 wi.sh
9
10 # 新行消失了.
11
12
13 echo "$dir_listing" # 用引号括起来
14 # -rw-rw-r-- 1 bozo 30 May 13 17:15 1.txt
15 # -rw-rw-r-- 1 bozo 51 May 15 20:57 t2.sh
16 # -rwxr-xr-x 1 bozo 217 Mar 5 21:13 wi.sh
命令替换甚至允许将整个文件的内容放到变量中, 可以使用重定向或者cat命令.
1 variable1=`
3 # 但是这行将会fork一个新进程, This, however, forks a new process,
4 #+ 所以这行代码将会比第一行代码执行得慢.
5
6 # 注意:
http://www.818198.com Page 345
SHELL十三问
7 # 变量中是可以包含空白的,
8 #+ 甚至是 (厌恶至极的), 控制字符.
1 # 摘录自系统文件, /etc/rc.d/rc.sysinit
2 #+ (这是红帽安装中使用的)
3
4
5 if [ -f /fsckoptions ]; then
6 fsckoptions=`cat /fsckoptions`
7 ...
8 fi
9 #
10 #
11 if [ -e "/proc/ide/${disk[$device]}/media" ] ; then
12 hdmedia=`cat /proc/ide/${disk[$device]}/media`
13 ...
14 fi
15 #
16 #
17 if [ ! -n "`uname -r | grep -- "-"`" ]; then
18 ktag="`cat /proc/version`"
19 ...
20 fi
21 #
22 #
23 if [ $usb = "1" ]; then
24 sleep 5
25 mouseoutput=`cat /proc/bus/usb/devices 2>/dev/null|grep -E "^I.*Cls=03.*Prot=02"`
26 kbdoutput=`cat /proc/bus/usb/devices 2>/dev/null|grep -E "^I.*Cls=03.*Prot=01"`
27 ...
28 fi
注意: 不要将一个非常长的文本文件的内容设置到一个变量中, 除非你有一个非常好的原因非
要这么做不可. 不要将2进制文件的内容保存到变量中.
Example 14-1 愚蠢的脚本策略
################################Start Script#######################################
1 #!/bin/bash
2 # stupid-script-tricks.sh: 朋友, 别在家这么做.
3 # 来自于"Stupid Script Tricks," 卷I.
4
5
6 dangerous_variable=`cat /boot/vmlinuz` # 这是压缩过的Linux内核本身.
7
8 echo "string-length of \$dangerous_variable = ${#dangerous_variable}"
9 # 这个字符串变量的长度是 $dangerous_variable = 794151
10 # (不要使用'wc -c /boot/vmlinuz'来计算长度.)
11
12 # echo "$dangerous_variable"
13 # 千万别尝试这么做! 这样将挂起这个脚本.
14
http://www.818198.com Page 346
SHELL十三问
15
16 # 文档作者已经意识到将二进制文件设置到
17 #+ 变量中是一个没用的应用.
18
19 exit 0
################################End Script#########################################
注意, 在这里是不会发生缓冲区溢出错误. 因为这是一个解释型语言的实例, Bash就是一
种解释型语言, 解释型语言会比编译型语言提供更多的对程序错误的保护措施.
变量替换允许将一个循环的输出放入到一个变量中.这么做的关键就是将循环中echo命令的输
出全部截取.
Example 14-2 从循环的输出中产生一个变量
################################Start Script#######################################
1 #!/bin/bash
2 # csubloop.sh: 从循环的输出中产生一个变量.
3
4 variable1=`for i in 1 2 3 4 5
5 do
6 echo -n "$i" # 对于这里的命令替换来说
7 done` #+ 这个'echo'命令是非常关键的.
8
9 echo "variable1 = $variable1" # variable1 = 12345
10
11
12 i=0
13 variable2=`while [ "$i" -lt 10 ]
14 do
15 echo -n "$i" # 再来一个, 'echo'是必须的.
16 let "i += 1" # 递增.
17 done`
18
19 echo "variable2 = $variable2" # variable2 = 0123456789
20
21 # 这就证明了在一个变量声明中
22 #+ 嵌入一个循环是可行的.
23
24 exit 0
################################End Script#########################################
注意: 命令替换使得扩展有效的Bash工具集变为可能. 这样, 写一段小程序或者一段脚本就可
以达到目的, 因为程序或脚本的输出会传到stdout上(就像一个标准的工具所做的那样),
然后重新将这些输出保存到变量中.(译者: 作者的意思就是在这种情况下写脚本和写程序
作用是一样的.)
1 #include
2
3 /* "Hello, world." C program */
4
5 int main()
6 {
7 printf( "Hello, world." );
http://www.818198.com Page 347
SHELL十三问
8 return (0);
9 }
bash$ gcc -o hello hello.c
1 #!/bin/bash
2 # hello.sh
3
4 greeting=`./hello`
5 echo $greeting
bash$ sh hello.sh
Hello, world.
注意: 对于命令替换来说,$(COMMAND) 形式已经取代了反引号"`".
1 output=$(sed -n /"$1"/p $file) # 来自于 "grp.sh"例子.
2
3 # 将一个文本的内容保存到变量中.
4 File_contents1=$(cat $file1)
5 File_contents2=$(<$file2) # Bash 也允许这么做.
$(...) 形式的命令替换在处理双反斜线(\\)时与`...`形式不同.
bash$ echo `echo \\`
bash$ echo $(echo \\)
$(...) 形式的命令替换是允许嵌套的. [3]
1 word_count=$( wc -w $(ls -l | awk '{print $9}') )
或者, 可以更加灵活. . .
Example 14-3 找anagram(回文构词法, 可以将一个有意义的单词, 变换为1个或多个有意义的单词, 但是还是原来的子母集合)
################################Start Script#######################################
1 #!/bin/bash
2 # agram2.sh
3 # 关于命令替换嵌套的例子.
4
5 # 使用"anagram"工具
6 #+ 这是作者的"yawl"文字表包中的一部分.
7 # http://ibiblio.org/pub/Linux/libs/yawl-0.3.2.tar.gz
8 # http://personal.riverusers.com/~thegrendel/yawl-0.3.2.tar.gz
9
10 E_NOARGS=66
11 E_BADARG=67
12 MINLEN=7
13
14 if [ -z "$1" ]
15 then
16 echo "Usage $0 LETTERSET"
17 exit $E_NOARGS # 脚本需要一个命令行参数.
18 elif [ ${#1} -lt $MINLEN ]
19 then
20 echo "Argument must have at least $MINLEN letters."
21 exit $E_BADARG
22 fi
http://www.818198.com Page 348
SHELL十三问
23
24
25
26 FILTER='.......' # 必须至少有7个字符.
27 # 1234567
28 Anagrams=( $(echo $(anagram $1 | grep $FILTER) ) )
29 # | | 嵌套的命令替换 | |
30 # ( 数组分配 )
31
32 echo
33 echo "${#Anagrams[*]} 7+ letter anagrams found"
34 echo
35 echo ${Anagrams[0]} # 第一个anagram.
36 echo ${Anagrams[1]} # 第二个anagram.
37 # 等等.
38
39 # echo "${Anagrams[*]}" # 在一行上列出所有的anagram . . .
40
41 # 考虑到后边还有"数组"作为单独的一章进行讲解,
42 #+ 这里就不深入了.
43
44 # 可以参阅agram.sh脚本, 这也是一个找出anagram的例子.
45
46 exit $?
################################End Script#########################################
命令替换在脚本中使用的例子:
1. Example 10-7
2. Example 10-26
3. Example 9-28
4. Example 12-3
5. Example 12-19
6. Example 12-15
7. Example 12-49
8. Example 10-13
9. Example 10-10
10. Example 12-29
11. Example 16-8
12. Example A-17
13. Example 27-2
14. Example 12-42
15. Example 12-43
16. Example 12-44
注意事项:
[1] 对于命令替换来说, 这个命令可以是外部的系统命令, 也可以是内部脚本的内建
命令, 甚至是一个脚本函数.
[2] 从技术的角度来讲, 命令替换将会抽取出一个命令的输出, 然后使用=操作赋值到
一个变量中.
[3] 事实上, 对于反引号的嵌套是可行的, 但是只能将内部的反引号转义才行, 就像
http://www.818198.com Page 349
SHELL十三问
John默认指出的那样.
1 word_count=` wc -w \`ls -l | awk '{print $9}'\` `
第15章 算术扩展
================
算术扩展提供了一种强力的工具, 可以在脚本中执行(整型)算法操作. 可以使用backticks,
double parentheses, 或 let来将字符串转换为数字表达式.
一些变化
使用反引号的算术扩展(通常都是和expr一起使用)
1 z=`expr $z + 3` # 'expr'命令将会执行这个扩展.
使用双括号, 和let形式的算术扩展
反引号形式的算术扩展已经被双括号形式所替代了 -- ((...)) 和 $((...)) -- 当然也可以
使用非常方便的let形式.
1 z=$(($z+3))
2 z=$((z+3)) # 也正确.
3 # 使用双括号的形式,
4 #+ 参数解引用
5 #+ 是可选的.
6
7 # $((EXPRESSION)) is arithmetic expansion. # 不要与命令
8 #+ 替换相混淆.
9
10
11
12 # 使用双括号的形式也可以不用给变量赋值.
13
14 n=0
15 echo "n = $n" # n = 0
16
17 (( n += 1 )) # 递增.
18 # (( $n += 1 )) is incorrect!
19 echo "n = $n" # n = 1
20
21
22 let z=z+3
23 let "z += 3" # 使用引用的形式, 允许在变量赋值的时候存在空格.
24 # 'let'操作事实上执行得的是算术赋值,
25 #+ 而不是算术扩展.
下边是一些在脚本中使用算术扩展的例子:
1. Example 12-9
2. Example 10-14
3. Example 26-1
4. Example 26-11
5. Example A-17
第16章 I/O 重定向
==================
默认情况下始终有3个"文件"处于打开状态, stdin (键盘), stdout (屏幕), and stderr
(错误消息输出到屏幕上). 这3个文件和其他打开的文件都可以被重定向. 对于重定向简单的
http://www.818198.com Page 350
SHELL十三问
解释就是捕捉一个文件, 命令, 程序, 脚本, 或者甚至是脚本中的代码块(参见 Example 3-1
和 Example 3-2)的输出, 然后将这些输出作为输入发送到另一个文件, 命令, 程序, 或脚本
中.
每个打开的文件都会被分配一个文件描述符.[1]stdin, stdout, 和stderr的文件描述符分别
是0, 1, 和 2. 对于正在打开的额外文件, 保留了描述符3到9. 在某些时候将这些格外的文件
描述符分配给stdin, stdout, 或者是stderr作为临时的副本链接是非常有用的. [2] 在经过
复杂的重定向和刷新之后需要把它们恢复成正常的样子 (参见 Example 16-1).
1 COMMAND_OUTPUT >
2 # 重定向stdout到一个文件.
3 # 如果没有这个文件就创建, 否则就覆盖.
4
5 ls -lR > dir-tree.list
6 # 创建一个包含目录树列表的文件.
7
8 : > filename
9 # > 会把文件"filename"截断为0长度.
10 # 如果文件不存在, 那么就创建一个0长度的文件(与'touch'的效果相同).
11 # : 是一个占位符, 不产生任何输出.
12
13 > filename
14 # > 会把文件"filename"截断为0长度.
15 # 如果文件不存在, 那么就创建一个0长度的文件(与'touch'的效果相同).
16 # (与上边的": >"效果相同, 但是在某些shell下可能不能工作.)
17
18 COMMAND_OUTPUT >>
19 # 重定向stdout到一个文件.
20 # 如果文件不存在, 那么就创建它, 如果存在, 那么就追加到文件后边.
21
22
23 # 单行重定向命令(只会影响它们所在的行):
24 # -------------------------------------------------------------------- 25
26 1>filename
27 # 重定向stdout到文件"filename".
28 1>>filename
29 # 重定向并追加stdout到文件"filename".
30 2>filename
31 # 重定向stderr到文件"filename".
32 2>>filename
33 # 重定向并追加stderr到文件"filename".
34 &>filename
35 # 将stdout和stderr都重定向到文件"filename".
36
37 #==============================================================================
38 # 重定向stdout, 一次一行.
39 LOGFILE=script.log
40
41 echo "This statement is sent to the log file, \"$LOGFILE\"." 1>$LOGFILE
http://www.818198.com Page 351
SHELL十三问
42 echo "This statement is appended to \"$LOGFILE\"." 1>>$LOGFILE
43 echo "This statement is also appended to \"$LOGFILE\"." 1>>$LOGFILE
44 echo "This statement is echoed to stdout, and will not appear in \"$LOGFILE\"."
45 # 每行过后, 这些重定向命令会自动"reset".
46
47
48
49 # 重定向stderr, 一次一行.
50 ERRORFILE=script.errors
51
52 bad_command1 2>$ERRORFILE # 错误消息发到$ERRORFILE中.
53 bad_command2 2>>$ERRORFILE # 错误消息添加到$ERRORFILE中.
54 bad_command3 # 错误消息echo到stderr,
55 #+ 并且不出现在$ERRORFILE中.
56 # 每行过后, 这些重定向命令也会自动"reset".
57 #==============================================================================
58
59
60
61 2>&1
62 # 重定向stderr到stdout.
63 # 得到的错误消息与stdout一样, 发送到一个地方.
64
65 i>&j
66 # 重定向文件描述符i 到 j.
67 # 指向i文件的所有输出都发送到j中去.
68
69 >&j
70 # 默认的, 重定向文件描述符1(stdout)到 j.
71 # 所有传递到stdout的输出都送到j中去.
72
73 0< FILENAME
74 < FILENAME
75 # 从文件中接受输入.
76 # 与">"是成对命令, 并且通常都是结合使用.
77 #
78 # grep search-word
80
81 [j]<>filename
82 # 为了读写"filename", 把文件"filename"打开, 并且分配文件描述符"j"给它.
83 # 如果文件"filename"不存在, 那么就创建它.
84 # 如果文件描述符"j"没指定, 那默认是fd 0, stdin.
85 #
86 # 这种应用通常是为了写到一个文件中指定的地方.
87 echo 1234567890 > File # 写字符串到"File".
88 exec 3<> File # 打开"File"并且给它分配fd 3.
89 read -n 4 <&3 # 只读4个字符.
http://www.818198.com Page 352
SHELL十三问
90 echo -n . >&3 # 写一个小数点.
91 exec 3>&- # 关闭fd 3.
92 cat File # ==> 1234.67890
93 # 随机存储.
94
95
96
97 |
98 # 管道.
99 # 通用目的的处理和命令链工具.
100 # 与">"很相似, 但是实际上更通用.
101 # 对于想将命令, 脚本, 文件和程序串连起来的时候很有用.
102 cat *.txt | sort | uniq > result-file
103 # 对所有的.txt文件的输出进行排序, 并且删除重复行,
104 # 最后将结果保存到"result-file"中.
可以将输入输出重定向和(或)管道的多个实例结合到一起写在一行上.
1 command < input-file > output-file
2
3 command1 | command2 | command3 > output-file
参见 Example 12-28 和 Example A-15.
可以将多个输出流重定向到一个文件上.
1 ls -yz >> command.log 2>&1
2 # 将错误选项"yz"的结果放到文件"command.log"中.
3 # 因为stderr被重定向到这个文件中,
4 #+ 所有的错误消息也就都指向那里了.
5
6 # 注意, 下边这个例子就不会给出相同的结果.
7 ls -yz 2>&1 >> command.log
8 # 输出一个错误消息, 但是并不写到文件中.
9
10 # 如果将stdout和stderr都重定向,
11 #+ 命令的顺序会有些不同.
关闭文件描述符
n<&- 关闭输入文件描述符n.
0<&-, <&- 关闭stdin.
n>&- 关闭输出文件描述符n.
1>&-, >&- 关闭stdout.
子进程继承了打开的文件描述符. 这就是为什么管道可以工作. 如果想阻止fd被继承, 那么可
以关掉它.
1 # 只重定向stderr到一个管道.
2
3 exec 3>&1 # 保存当前stdout的"值".
4 ls -l 2>&1 >&3 3>&- | grep bad 3>&- # 对'grep'关闭fd 3(但不关闭'ls').
5 # ^^^^ ^^^^
6 exec 3>&- # 现在对于剩余的脚本关闭它.
7
8 # Thanks, S.C.
如果想了解关于I/O重定向更多的细节参见 附录 E.
http://www.818198.com Page 353
SHELL十三问
16.1. 使用exec
--------------exec
可以使用sed 和/或 awk来对每一行进行分析.
Example 16-1 使用exec重定向标准输入
################################Start Script#######################################
1 #!/bin/bash
2 # 使用'exec'重定向标准输入.
3
4
5 exec 6<&0 # 将文件描述符#6与stdin链接起来.
6 # 保存了stdin.
7
8 exec < data-file # stdin被文件"data-file"所代替.
9
10 read a1 # 读取文件"data-file"的第一行.
11 read a2 # 读取文件"data-file"的第二行.
12
13 echo
14 echo "Following lines read from file."
15 echo "-------------------------------"
16 echo $a1
17 echo $a2
18
19 echo; echo; echo
20
21 exec 0<&6 6<&-22 # 现在将stdin从fd #6中恢复, 因为刚才我们把stdin重定向到#6了,
23 #+ 然后关闭fd #6 ( 6<&- ), 好让这个描述符继续被其他进程所使用.
24 #
25 # <&6 6<&- 这么做也可以.
26
27 echo -n "Enter data "
28 read b1 # 现在"read"已经恢复正常了, 就是从stdin中读取.
29 echo "Input read from stdin."
30 echo "----------------------"
31 echo "b1 = $b1"
32
33 echo
34
35 exit 0
################################End Script#########################################
同样的, exec >filename 命令将会把stdout重定向到一个指定的文件中. 这样所有的命令输
出就都会发向那个指定的文件, 而不是stdout.
Example 16-2 使用exec来重定向stdout
################################Start Script#######################################
1 #!/bin/bash
http://www.818198.com Page 354
SHELL十三问
2 # reassign-stdout.sh
3
4 LOGFILE=logfile.txt
5
6 exec 6>&1 # 将fd #6与stdout相连接.
7 # 保存stdout.
8
9 exec > $LOGFILE # stdout就被文件"logfile.txt"所代替了.
10
11 # ----------------------------------------------------------- #
12 # 在这块中所有命令的输出就都发向文件 $LOGFILE.
13
14 echo -n "Logfile: "
15 date
16 echo "-------------------------------------"
17 echo
18
19 echo "Output of \"ls -al\" command"
20 echo
21 ls -al
22 echo; echo
23 echo "Output of \"df\" command"
24 echo
25 df
26
27 # ----------------------------------------------------------- #
28
29 exec 1>&6 6>&- # 恢复stdout, 然后关闭文件描述符#6.
30
31 echo
32 echo "== stdout now restored to default == "
33 echo
34 ls -al
35 echo
36
37 exit 0
################################End Script#########################################
Example 16-3 使用exec在同一脚本中重定向stdin和stdout
################################Start Script#######################################
1 #!/bin/bash
2 # upperconv.sh
3 # 将一个指定的输入文件转换为大写.
4
5 E_FILE_ACCESS=70
6 E_WRONG_ARGS=71
7
8 if [ ! -r "$1" ] # 判断指定的输入文件是否可读?
9 then
http://www.818198.com Page 355
SHELL十三问
10 echo "Can't read from input file!"
11 echo "Usage: $0 input-file output-file"
12 exit $E_FILE_ACCESS
13 fi # 即使输入文件($1)没被指定
14 #+ 也还是会以相同的错误退出(为什么?).
15
16 if [ -z "$2" ]
17 then
18 echo "Need to specify output file."
19 echo "Usage: $0 input-file output-file"
20 exit $E_WRONG_ARGS
21 fi
22
23
24 exec 4<&0
25 exec < $1 # 将会从输入文件中读取.
26
27 exec 7>&1
28 exec > $2 # 将写到输出文件中.
29 # 假设输出文件是可写的(添加检查?).
30
31 # -----------------------------------------------32 cat - | tr a-z A-Z # 转换为大写.
33 # ^^^^^ # 从stdin中读取.Reads from stdin.
34 # ^^^^^^^^^^ # 写到stdout上.
35 # 然而, stdin和stdout都被重定向了.
36 # -----------------------------------------------37
38 exec 1>&7 7>&- # 恢复 stout.
39 exec 0<&4 4<&- # 恢复 stdin.
40
41 # 恢复之后, 下边这行代码将会如期望的一样打印到stdout上.
42 echo "File \"$1\" written to \"$2\" as uppercase conversion."
43
44 exit 0
################################End Script#########################################
I/O重定向是一种避免可怕的子shell中不可存取变量问题的方法.
Example 16-4 避免子shell
################################Start Script#######################################
1 #!/bin/bash
2 # avoid-subshell.sh
3 # Matthew Walker提出的建议.
4
5 Lines=0
6
7 echo
8
9 cat myfile.txt | while read line; # (译者注: 管道会产生子shell)
http://www.818198.com Page 356
SHELL十三问
10 do {
11 echo $line
12 (( Lines++ )); # 增加这个变量的值
13 #+ 但是外部循环却不能存取.
14 # 子shell问题.
15 }
16 done
17
18 echo "Number of lines read = $Lines" # 0
19 # 错误!
20
21 echo "------------------------"
22
23
24 exec 3<> myfile.txt
25 while read line <&3
26 do {
27 echo "$line"
28 (( Lines++ )); # 增加这个变量的值
29 #+ 现在外部循环就可以存取了.
30 # 没有子shell, 现在就没问题了.
31 }
32 done
33 exec 3>&-34
35 echo "Number of lines read = $Lines" # 8
36
37 echo
38
39 exit 0
40
41 # 下边这些行是脚本的结果, 脚本是不会走到这里的.
42
43 $ cat myfile.txt
44
45 Line 1.
46 Line 2.
47 Line 3.
48 Line 4.
49 Line 5.
50 Line 6.
51 Line 7.
52 Line 8.
################################End Script#########################################
注意事项:
[1] 一个文件描述符说白了就是文件系统为了跟踪这个打开的文件而分配给它的一个数字.
也可以的将其理解为文件指针的一个简单版本. 与C中的文件句柄的概念相似.
[2] 使用文件描述符5可能会引起问题. 当Bash使用exec创建一个子进程的时候, 子进程
http://www.818198.com Page 357
SHELL十三问
会继承fd5(参见Chet Ramey的归档e-mail, SUBJECT: RE: File descriptor 5 is
held open). 最好还是不要去招惹这个特定的fd.
第17章 Here Documents
======================
here document 就是一段特殊目的的代码块. 他使用I/O 重定向的形式来将一个命令序列传递
到一个交互程序或者命令中, 比如ftp, cat, 或者ex文本编辑器.
1 COMMAND <
3 InputComesFromHERE
limit string 用来划定命令序列的范围(译者注: 两个相同的limit string之间就是命令序列)
. 特殊符号 << 用来表识limit string. 这个符号具有重定向文件的输出到程序或命令的输入
的作用. 与 interactive-program < command-file 很相象, command-file包含:
1 command #1
2 command #2
3 ...
而here document 的形式看上去是如下的样子:
1 #!/bin/bash
2 interactive-program <
4 command #2
5 ...
6 LimitString
选择一个名字非常诡异的limit string将会避免命令列表和limit string重名的问题.
注意,某些时候here document 用在非交互工具和命令上的时候也会有好的效果, 比如, wall.
Example 17-1 广播: 发送消息给每个登录上的用户
################################Start Script#######################################
1 #!/bin/bash
2
3 wall <
5 (Add an extra dollar for anchovy or mushroom topping.)
6 # 额外的消息文本写在这里.
7 # 注意: 'wall' 会打印注释行.
8 zzz23EndOfMessagezzz23
9
10 # 可以使用更有效率的做法
11 # wall
13 #+ 是一种"小吃店"(快速但是比较脏)的只能使用一次的解决办法.
14
15 exit 0
################################End Script#########################################
即使是某些不大可能的工具, 如vi也可以使用here document.
Example 17-2 仿造文件: 创建一个两行的仿造文件
################################Start Script#######################################
1 #!/bin/bash
2
3 # 用非交互的方式来使用'vi'编辑一个文件.
http://www.818198.com Page 358
SHELL十三问
4 # 模仿'sed'.
5
6 E_BADARGS=65
7
8 if [ -z "$1" ]
9 then
10 echo "Usage: `basename $0` filename"
11 exit $E_BADARGS
12 fi
13
14 TARGETFILE=$1
15
16 # 在文件中插入两行, 然后保存.
17 #--------Begin here document-----------#
18 vi $TARGETFILE <
20 This is line 1 of the example file.
21 This is line 2 of the example file.
22 ^[
23 ZZ
24 x23LimitStringx23
25 #----------End here document-----------#
26
27 # 注意上边^[是一个转义符,键入Ctrl+v
28 #+ 事实上它是
29
30 # Bram Moolenaar指出这种方法不能正常地用在'vim'上, (译者注: Bram Moolenaar是vim作者)
31 #+ 因为可能会有终端的相互影响问题.
32
33 exit 0
################################End Script#########################################
上边的脚本也可以不用vi而用ex来实现. Here document 包含ex命令列表的做法足够形成自己
的类别了, 叫ex scripts.
1 #!/bin/bash
2 # 把所有后缀为".txt"文件
3 #+ 中的"Smith"都替换成"Jones".
4
5 ORIGINAL=Smith
6 REPLACEMENT=Jones
7
8 for word in $(fgrep -l $ORIGINAL *.txt)
9 do
10 # ------------------------------------- 11 ex $word <
13 :wq
14 EOF
15 # :%s 是"ex"的替换命令.
http://www.818198.com Page 359
SHELL十三问
16 # :wq 是保存并退出的意思.
17 # ------------------------------------- 18 done
与"ex scripts"相似的是cat scripts.
Example 17-3 使用cat的多行消息
################################Start Script#######################################
1 #!/bin/bash
2
3 # 'echo' 对于打印单行消息是非常好的,
4 #+ 但是在打印消息块时可能就有点问题了.
5 # 'cat' here document可以解决这个限制.
6
7 cat <
10 This is line 2 of the message.
11 This is line 3 of the message.
12 This is line 4 of the message.
13 This is the last line of the message.
14 -------------------------------------15 End-of-message
16
17 # 用下边这行代替上边的第7行
18 #+ cat > $Newfile <
20 #+ 那么就会把输出写到文件$Newfile中, 而不是stdout.
21
22 exit 0
23
24
25 #--------------------------------------------26 # 下边的代码不会运行, 因为上边的"exit 0".
27
28 # S.C. 指出下边代码也可以运行.
29 echo "-------------------------------------30 This is line 1 of the message.
31 This is line 2 of the message.
32 This is line 3 of the message.
33 This is line 4 of the message.
34 This is the last line of the message.
35 -------------------------------------"
36 # 然而, 文本可能不包含双引号, 除非它们被转义.
################################End Script#########################################
- 选项用来标记here document的limit string (<<-LimitString), 可以抑制输出时前边的tab
(不是空格). 这可以增加一个脚本的可读性.
Example 17-4 带有抑制tab功能的多行消息
################################Start Script#######################################
1 #!/bin/bash
http://www.818198.com Page 360
SHELL十三问
2 # 与之前的例子相同, 但是...
3
4 # - 选项对于here docutment来说,<<- 5 #+ 可以抑制文档体前边的tab,
6 #+ 而*不*是空格 *not* spaces.
7
8 cat <<-ENDOFMESSAGE
9 This is line 1 of the message.
10 This is line 2 of the message.
11 This is line 3 of the message.
12 This is line 4 of the message.
13 This is the last line of the message.
14 ENDOFMESSAGE
15 # 脚本在输出的时候左边将被刷掉.
16 # 就是说每行前边的tab将不会显示.
17
18 # 上边5行"消息"的前边都是tab, 不是空格.
19 # 空格是不受<<-影响的.
20
21 # 注意, 这个选项对于*嵌在*中间的tab没作用.
22
23 exit 0
################################End Script#########################################
here document 支持参数和命令替换. 所以也可以给here document的消息体传递不同的参数,
这样相应的也会修改输出.
Example 17-5 使用参数替换的here document
################################Start Script#######################################
1 #!/bin/bash
2 # 一个使用'cat'命令的here document, 使用了参数替换
3
4 # 不传命令行参数给它, ./scriptname
5 # 传一个命令行参数给它, ./scriptname Mortimer
6 # 传一个2个单词(用引号括起来)的命令行参数给它,
7 # ./scriptname "Mortimer Jones"
8
9 CMDLINEPARAM=1 # 所期望的最少的命令行参数的个数.
10
11 if [ $# -ge $CMDLINEPARAM ]
12 then
13 NAME=$1 # 如果命令行参数超过1个,
14 #+ 那么就只取第一个参数.
15 else
16 NAME="John Doe" # 默认情况下, 如果没有命令行参数的话.
17 fi
18
19 RESPONDENT="the author of this fine script"
20
21
http://www.818198.com Page 361
SHELL十三问
22 cat <
24 Hello, there, $NAME.
25 Greetings to you, $NAME, from $RESPONDENT.
26
27 # This comment shows up in the output (why?).
28
29 Endofmessage
30
31 # 注意上边的空行也打印到输出,
32 # 而上边那行"注释"当然也会打印到输出.
33 # (译者注: 这就是为什么不翻译那行注释的原因, 尽量保持原代码的原样)
34 exit 0
################################End Script#########################################
这是一个包含参数替换的here document的有用的脚本.
Example 17-6 上传一个文件对到"Sunsite"的incoming目录
################################Start Script#######################################
1 #!/bin/bash
2 # upload.sh
3
4 # 上传文件对(Filename.lsm, Filename.tar.gz)
5 #+ 到Sunsite/UNC (ibiblio.org)的incoming目录.
6 # Filename.tar.gz是自身的tar包.
7 # Filename.lsm是描述文件.
8 # Sunsite需要"lsm"文件, 否则就拒绝贡献.
9
10
11 E_ARGERROR=65
12
13 if [ -z "$1" ]
14 then
15 echo "Usage: `basename $0` Filename-to-upload"
16 exit $E_ARGERROR
17 fi
18
19
20 Filename=`basename $1` # 从文件名中去掉目录字符串.
21
22 Server="ibiblio.org"
23 Directory="/incoming/Linux"
24 # 在这里也不一定非得将上边的参数写死在这个脚本中,
25 #+ 可以使用命令行参数的方法来替换.
26
27 Password="your.e-mail.address" # 可以修改成相匹配的密码.
28
29 ftp -n $Server <
31
http://www.818198.com Page 362
SHELL十三问
32 user anonymous "$Password"
33 binary
34 bell # 在每个文件传输后, 响铃.
35 cd $Directory
36 put "$Filename.lsm"
37 put "$Filename.tar.gz"
38 bye
39 End-Of-Session
40
41 exit 0
################################End Script#########################################
在here document的开头引用或转义"limit string"会使得here document的消息体中的参数替
换被禁用.
Example 17-7 关闭参数替换
################################Start Script#######################################
1 #!/bin/bash
2 # 一个使用'cat'的here document, 但是禁用了参数替换.
3
4 NAME="John Doe"
5 RESPONDENT="the author of this fine script"
6
7 cat <<'Endofmessage'
8
9 Hello, there, $NAME.
10 Greetings to you, $NAME, from $RESPONDENT.
11
12 Endofmessage
13
14 # 当"limit string"被引用或转义那么就禁用了参数替换.
15 # 下边的两种方式具有相同的效果.
16 # cat <<"Endofmessage"
17 # cat <<\Endofmessage
18
19 exit 0
################################End Script#########################################
禁用了参数替换后, 将允许输出文本本身(译者注: 就是未转义的原文). 产生脚本甚至是程序
代码就是这种用法的用途之一.
Example 17-8 一个产生另外一个脚本的脚本
################################Start Script#######################################
1 #!/bin/bash
2 # generate-script.sh
3 # 基于Albert Reiner的一个主意.
4
5 OUTFILE=generated.sh # 所产生文件的名字.
6
7
8 # ----------------------------------------------------------- 9 # 'Here document包含了需要产生的脚本的代码.
http://www.818198.com Page 363
SHELL十三问
10 (
11 cat <<'EOF'
12 #!/bin/bash
13
14 echo "This is a generated shell script."
15 # Note that since we are inside a subshell,
16 #+ we can't access variables in the "outside" script.
17
18 echo "Generated file will be named: $OUTFILE"
19 # Above line will not work as normally expected
20 #+ because parameter expansion has been disabled.
21 # Instead, the result is literal output.
22
23 a=7
24 b=3
25
26 let "c = $a * $b"
27 echo "c = $c"
28
29 exit 0
30 EOF
31 ) > $OUTFILE
32 # -----------------------------------------------------------33
34 # 将'limit string'引用起来将会阻止上边
35 #+ here document的消息体中的变量扩展.
36 # 这会使得输出文件中的内容保持here document消息体中的原文.
37
38 if [ -f "$OUTFILE" ]
39 then
40 chmod 755 $OUTFILE
41 # 让所产生的文件具有可执行权限.
42 else
43 echo "Problem in creating file: \"$OUTFILE\""
44 fi
45
46 # 这个方法也用来产生
47 #+ C程序代码, Perl程序代码, Python程序代码, makefile,
48 #+ 和其他的一些类似的代码.
49 # (译者注: 中间一段没译的注释将会被here document打印出来)
50 exit 0
################################End Script#########################################
也可以将here document的输出保存到变量中.
1 variable=$(cat <
3 runs over multiple lines.
4 SETVAR)
5
http://www.818198.com Page 364
SHELL十三问
6 echo "$variable"
同一脚本中的函数也可以接受here document的输出作为自身的参数.
Example 17-9 Here documents与函数
################################Start Script#######################################
1 #!/bin/bash
2 # here-function.sh
3
4 GetPersonalData ()
5 {
6 read firstname
7 read lastname
8 read address
9 read city
10 read state
11 read zipcode
12 } # 这个函数无疑的看起来就一个交互函数, 但是...
13
14
15 # 给上边的函数提供输入.
16 GetPersonalData <
18 Bozeman
19 2726 Nondescript Dr.
20 Baltimore
21 MD
22 21226
23 RECORD001
24
25
26 echo
27 echo "$firstname $lastname"
28 echo "$address"
29 echo "$city, $state $zipcode"
30 echo
31
32 exit 0
################################End Script#########################################
也可以这么使用: 做一个假命令来从一个here document中接收输出. 这么做事实上就是创建了
一个"匿名"的here document.
Example 17-10 "匿名" here Document
################################Start Script#######################################
1 #!/bin/bash
2
3 : <
5 TESTVARIABLES
6
7 exit 0
http://www.818198.com Page 365
SHELL十三问
################################End Script#########################################
注意: 上边所示技术的一种变化可以用来"注释"掉代码块.
Example 17-11 注释掉一段代码块
################################Start Script#######################################
1 #!/bin/bash
2 # commentblock.sh
3
4 : <
6 This is a comment line missing the "#" prefix.
7 This is another comment line missing the "#" prefix.
8
9 &*@!!++=
10 The above line will cause no error message,
11 because the Bash interpreter will ignore it.
12 COMMENTBLOCK
13
14 echo "Exit value of above \"COMMENTBLOCK\" is $?." # 0
15 # 这里将不会显示任何错误.
16
17
18 # 上边的这种技术当然也可以用来注释掉
19 #+ 一段正在使用的代码, 如果你有某些特定调试要求的话.
20 # 这将比对每行都敲入"#"来得方便的多,
21 #+ 而且如果你想恢复的话, 还得将添加上的"#"删除掉.
22
23 : <
25 do
26 cat "$file"
27 done
28 DEBUGXXX
29
30 exit 0
################################End Script#########################################
注意: 关于这种小技巧的另一个应用就是能够产生自文档化(self-documenting)的脚本.
Example 17-12 一个自文档化(self-documenting)的脚本
################################Start Script#######################################
1 #!/bin/bash
2 # self-document.sh: 自文档化(self-documenting)的脚本
3 # Modification of "colm.sh".
4
5 DOC_REQUEST=70
6
7 if [ "$1" = "-h" -o "$1" = "--help" ] # 请求帮助.
8 then
9 echo; echo "Usage: $0 [directory-name]"; echo
10 sed --silent -e '/DOCUMENTATIONXX$/,/^DOCUMENTATIONXX$/p' "$0" |
http://www.818198.com Page 366
SHELL十三问
11 sed -e '/DOCUMENTATIONXX$/d'; exit $DOC_REQUEST; fi
12
13
14 : <
16 ---------------------------------------------------------------17 The command line parameter gives the directory to be listed.
18 If no directory specified or directory specified cannot be read,
19 then list the current working directory.
20
21 DOCUMENTATIONXX
22
23 if [ -z "$1" -o ! -r "$1" ]
24 then
25 directory=.
26 else
27 directory="$1"
28 fi
29
30 echo "Listing of "$directory":"; echo
31 (printf "PERMISSIONS LINKS OWNER GROUP SIZE MONTH DAY HH:MM PROG-NAME\n" \
32 ; ls -l "$directory" | sed 1d) | column -t
33
34 exit 0
################################End Script#########################################
使用cat 脚本 也能够完成相同的目的.
1 DOC_REQUEST=70
2
3 if [ "$1" = "-h" -o "$1" = "--help" ] # 请求帮助.
4 then # 使用"cat 脚本" . . .
5 cat <
7 --------------------------------------------------------------- 8 The command line parameter gives the directory to be listed.
9 If no directory specified or directory specified cannot be read,
10 then list the current working directory.
11
12 DOCUMENTATIONXX
13 exit $DOC_REQUEST
14 fi
参见 Example A-27 可以了解更多关于自文档化脚本的好例子.
注意: Here document创建临时文件, 但是这些文件将在打开后被删除, 并且不能够被任何其
他进程所存取.
bash$ bash -c 'lsof -a -p $$ -d0' << EOF
> EOF
lsof 1213 bozo 0r REG 3,5 0 30386 /tmp/t1213-0-sh (deleted)
注意: 某些工具是不能工作在here document中的.
警告: 结束的limit string, 就是here document最后一行的limit string, 必须开始于第一
http://www.818198.com Page 367
SHELL十三问
个字符位置. 它的前面不能够有任何前置的空白. 而在这个limit string后边的空白也会
引起异常问题. 空白将会阻止limit string的识别.(译者注: 下边这个脚本由于结束
limit string的问题, 造成脚本无法结束, 所有内容全部被打印出来, 所以注释就不译了,
保持例子脚本的原样.)
1 #!/bin/bash
2
3 echo "----------------------------------------------------------------------"
4
5 cat <
7 echo "This is line 2 of the message inside the here document."
8 echo "This is the final line of the message inside the here document."
9 LimitString
10 #^^^^Indented limit string. Error! This script will not behave as expected.
11
12 echo "----------------------------------------------------------------------"
13
14 # These comments are outside the 'here document',
15 #+ and should not echo.
16
17 echo "Outside the here document."
18
19 exit 0
20
21 echo "This line had better not echo." # Follows an 'exit' command.
对于那些使用"here document"得非常复杂的任务, 最好考虑使用expect脚本语言, 这种语言
就是为了达到向交互程序添加输入的目的而量身定做的.
17.1. Here Strings
------------------here string 可以被认为是here document的一种定制形式. 除了COMMAND <<<$WORD 就什么都
没有了, $WORD将被扩展并且被送入COMMAND的stdin中.
Example 17-13 在一个文件的开头添加文本
################################Start Script#######################################
1 #!/bin/bash
2 # prepend.sh: 在文件的开头添加文本.
3 #
4 # Kenny Stauffer所捐助的脚本例子,
5 #+ 被本文作者作了少量的修改.
6
7
8 E_NOSUCHFILE=65
9
10 read -p "File: " file # 'read'命令的 -p 参数显示提示符.
11 if [ ! -e "$file" ]
12 then # 如果没有这个文件那就进来.
13 echo "File $file not found."
14 exit $E_NOSUCHFILE
15 fi
http://www.818198.com Page 368
SHELL十三问
16
17 read -p "Title: " title
18 cat - $file <<<$title > $file.new
19
20 echo "Modified file is $file.new"
21
22 exit 0
23
24 # 下边是'man bash'中的一段:
25 # Here Strings
26 # here document的一种变形,形式如下:
27 #
28 # <<
30 # word被扩展并且提供到command的标准输入中.
################################End Script#########################################
练习: 找出here string的其他用法.
第18章 休息时间
================
这个神奇的暂停可以给读者一个休息的机会, 可能读者到了这里也会会心一笑吧.
Linux同志们, 向你们致敬! 你正在阅读的这些东西, 将会给你们带来好运. 把这份文档发给你
的10个朋友. 在拷贝这份文档之前, 在信的结尾写上一个100行的Bash脚本发送给列表上的第一
个人. 然后在信的底部删除它们的名字并添加你自己的名字.
不要打断这个链条! 并且在48小时之内完成它.
Brooklyn的Wilfred?P.没有成功的发送他的10个拷贝, 当他第2天早上醒来发现他的工作变成了
"COBOL 程序员". Newport?News的Howard?L.在一个月内才发出了他的10个拷贝, 这个时间足够
建立一个100个节点的Beowulf cluster来玩Tuxracer了. Chicago的Amelia?V.对这封信付之一
笑并且打断了这个链条, 不久之后, 她的终端爆炸了, 她现在花了好多天时间为MS Windows写
文档.
千万不要打断这个链条! 今天就把10个拷贝发出去!
[]
高级Bash脚本编程指南(五)(上)
文章整理: 文章来源: 网络
高级Bash脚本编程指南(五)
第四部分 高级
++++++++++++++++
到了这儿,我们将要准备深入脚本编程中一些难的,不寻常的话题.随着话题的展开,我们会
以多种方法和检测边界条件的方式来“打开信封”,看个明白.(当我们涉足未知领域时会发
生什么?).
目录
19. Regular Expressions正则表达式
20. 子shell(Subshells)
21. 受限shell(Restricted Shells)
22. 进程替换
http://www.818198.com Page 369
SHELL十三问
23. 函数
24. 别名(Aliases)
25. 列表结构
26. 数组
27. /dev和/proc
28. 关于Zeros和Nulls
29. 调试
30. 选项
31. 检查遗漏(Gotchas)
32. 脚本编程风格
33. 杂项
34. Bash,版本2和3
第19章 正则表达式
==================
为了充分发挥shell编程的威力, 你需要精通正则表达式. 一些命令和软件包普遍在脚本编程中
使用正则表达式,例如grep, expr, sed和awk.
19.1 一个简要的正则表达式介绍
--------------------------------一个正式表达式是一个字符串.字符串里的字符被称为元字符,它们可能表示了比它们字面上看
起来的意思更丰富的含义.例如,一个引用符号可能表示引用一个人演讲中的话,或者表示下
面将要讲到的引申表示的意思.正则表达式是一个字符或/和元字符组合成的字符集,它们匹配
(或指定)一个模式.
一个正则表达式包含下面一个或多个项:
1. 一个字符集.
这里的字符集里的字符表示的就是它们字面上的意思.正则表达式最简单的情况就是仅
仅由字符集组成,而没有其他的元字符.
2. 锚.
一个锚指明了正则表达式在一行文本中要匹配的位置,例如^和$就是锚.
3. 修饰符
它们用于展开或缩小(即是修改了)正则表达式匹配文本行的范围.修饰符包括了星号.
括号和反斜杠符号.
正则表达是的主要作用是用来文本搜索和字串操作.一个正则表达式匹配一个字符或是一串字
符--完整的一串字符或是另外一个字符串的子串.
星号 -- * -- 匹配前一个字符的任意多次(包括零次).
"1133*"匹配11 + 一个或更多的3 + 可能的其他字符: 113, 1133, 111312, 等等.
点 -- . -- 匹配除了新行符之外的任意一个字符. [1]
"13." 匹配13 + 至少一个任意字符(包括空格): 1133, 11333, 但不匹配 13
(因为少了附加的至少一个任意字符).
脱字符 -- ^ -- 匹配一行的开头,但依赖于上下文环境,可能在正则表达式中表示否定
一个字符集的意思.
美元符 -- $ -- 在正则表达式中匹配行尾.
"^$" 匹配空行.
方括号 -- [...] -- 在正则表达式中表示匹配括号中的一个字符.
"[xyz]" 匹配字符x, y, 或z.
"[c-n]" 匹配从字符c到n之间的任意一个字符.
"[B-Pk-y]" 匹配从B到P 或从k到y的任意一个字符.
"[a-z0-9]" 匹配任意小写字母或数字.
"[^b-d]" 匹配除了从b到d范围内所有的字符. 这是正则表达式中反转意思或取否
http://www.818198.com Page 370
SHELL十三问
的一 个例子.(就好像在别的情形中!字符所扮演的角色).
多个方括号字符集组合使用可以匹配一般的单词和数字模式."[Yy][Ee][Ss]" 匹
配yes, Yes, YES, yEs, 等等.
"[0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][0-9][0-9]"匹配社会安全码
(Social Security number).
反斜杠字符 -- \ -- 转义(escapes) 一个特殊的字符,使这个字符表示原来字面上的意思.
"\$"表示了原来的字面意思"$",而不是在正则表达式中表达的匹配行尾的意思.
同样,"\\"也被解释成了字面上的意思"\".
转义(escape)"尖角号" -- \<...\> -- 用于表示单词的边界.
尖角号必须被转义,因为不这样做的话它们就表示单纯的字面意思
而已.
"\
等等.
bash$ cat textfile
This is line 1, of which there is only one instance.
This is the only instance of line 2.
This is line 3, another line.
This is line 4.
bash$ grep 'the' textfile
This is line 1, of which there is only one instance.
This is the only instance of line 2.
This is line 3, another line.
bash$ grep '\
This is the only instance of line 2.
确定正则表达式能否工作的唯一办法是测试它.
1 TEST FILE: tstfile # 不匹配.
2 # 不匹配.
3 Run grep "1133*" on this file. # 匹配.
4 # 不匹配.
5 # 不匹配.
6 This line contains the number 113. # 匹配.
7 This line contains the number 13. # 不匹配.
8 This line contains the number 133. # 不匹配.
9 This line contains the number 1133. # 匹配.
10 This line contains the number 113312. # 匹配.
11 This line contains the number 1112. # 不匹配.
12 This line contains the number 113312312. # 匹配.
13 This line contains no numbers at all. # 不匹配.
bash$ grep "1133*" tstfile
Run grep "1133*" on this file. # 匹配.
This line contains the number 113. # 匹配.
This line contains the number 1133. # 匹配.
This line contains the number 113312. # 匹配.
This line contains the number 113312312. # 匹配.
http://www.818198.com Page 371
SHELL十三问
扩展的正则表达式. 增加了一些元字符到上面提到的基本的元字符集合里. 它们在egrep,
awk,和Perl中使用.
问号 -- ? -- 匹配零或一个前面的字符. 它一般用于匹配单个字符.
加号 -- + -- 匹配一个或多个前面的字符.它的作用和*很相似,但唯一的区别是它不
匹配零个字符的情况.
1 # GNU 版本的 sed 和 awk 可以使用"+",
2 # 但它应该转义一下.
3
4 echo a111b | sed -ne '/a1\+b/p'
5 echo a111b | grep 'a1\+b'
6 echo a111b | gawk '/a1+b/'
7 # 上面三句都是等价的效果.
8
9 # 多谢, S.C.
转义"大括号" -- \{ \} -- 指示前面正则表达式匹配的次数.
要转义是因为不转义的话大括号只是表示他们字面上的意思.这个用法只是
技巧上的而不是基本正则表达式的内容.
"[0-9]\{5\}" 精确匹配5个数字 (从 0 到 9的数字).
注意: 大括号不能在“经典”(不是POSIX兼容)的正则表达式版本的awk中
使用. 然而, gawk 有一个选项--re-interval来允许使用大括号
(不必转义).
bash$ echo 2222 | gawk --re-interval '/2{3}/'
2222
Perl和一些egrep版本不要求转义大括号.
圆括号 -- ( ) -- 括起一组正则表达式. 它和下面要讲的"|"操作符或在用expr进行子字
符串提取(substring extraction)一起使用很有用.
竖线 -- | -- "或"正则操作符用于匹配一组可选的字符.
bash$ egrep 're(a|e)d' misc.txt
People who read seem to be better informed than those who do not.
The clarinet produces sound by the vibration of its reed.
注意: 一些sed, ed, 和ex的版本像GNU的软件版本一样支持上面描述的扩展正
则表达式的版本.
POSIX字符类. [:class:]
这是另外一个可选的用于指定匹配字符范围的方法.
[:alnum:] 匹配字母和数字.等同于A-Za-z0-9.
[:alpha:] 匹配字母. 等同于A-Za-z.
[:blank:] 匹配一个空格或是一个制表符(tab).
[:cntrl:] 匹配控制字符.
[:digit:] 匹配(十进制)数字. 等同于0-9.
[:graph:] (可打印的图形字符). 匹配 ASCII 码值的33 - 126之间的字符. 这和下面提到的
[:print:]一样,但是不包括空格字符.
[:lower:] 匹配小写字母. 等同于a-z.
[:print:] (可打印字符). 匹配 ASCII码值 32 - 126之间的字符. 这和上面提到的一样
[:graph:],但是增多一个空格字符.
[:space:] 匹配空白字符 (空格符和水平制表符).
[:upper:] 匹配大写字母. 等同于A-Z.
[:xdigit:] 匹配十六进制数字. 等同于0-9A-Fa-f.
注意: POSIX字符类一般都要求用引号或是双方括号double brackets ([[ ]])引起来.
http://www.818198.com Page 372
SHELL十三问
bash$ grep [[:digit:]] test.file
abc=723
这些字符类在一个受限的范围内甚至可能用在能用在通配(globbing)中.
bash$ ls -l ?[[:digit:]][[:digit:]]?
-rw-rw-r-- 1 bozo bozo 0 Aug 21 14:47 a33b
为了理解POSIX字符类在脚本中的使用,请参考例子 12-18 和 例子 12-19.
Sed, awk, 和Perl在脚本中被用作过滤器, "过滤"或转换文件/IO流的时候以正则表达式作为参
数.参考例子 A-12和例子 A-17 来理解这种用法.
在正则表达式这个复杂主题的标准参考是Friedl的Mastering Regular Expressions.由
Dougherty和Robbins写的 Sed & Awk也给出了一个清晰的正则表达式论述. 查看参考书目找
到这个主题更多的信息.
注意事项:
[1] 因为sed, awk, 和 grep 通常处理单行,而不能匹配一个新行符. 在要处理多行的一
个输入时,可以使用点操作符,它可以匹配新行符.
1 #!/bin/bash
2
3 sed -e 'N;s/.*/[&]/' << EOF # Here Document
4 line1
5 line2
6 EOF
7 # 输出:
8 # [line1
9 # line2]
10
11
12
13 echo
14
15 awk '{ $0=$1 "\n" $2; if (/line.1/) {print}}' << EOF
16 line 1
17 line 2
18 EOF
19 # 输出:
20 # line
21 # 1
22
23
24 # 多谢, S.C.
25
26 exit 0
19.1 通配
------------Bash本身没有正则表达式的功能.在脚本里,使用正则表达式的是命令和软件包 -- 例如sed和
awk -- 它们可以解释正则表达式.
Bash所做的是展开文件名扩展 [1] -- 这就是所谓的通配(globbing) -- 但它不是使用标准的
正则表达式. 而是使用通配符. 通配解释标准的通配符:*和?, 方括号括起来的字符,还有其他
的一些特殊的字符(比如说^用来表示取反匹配).然而通配机制的通配符有很大的局限性. 包含
有*号的字符串将不会匹配以点开头的文件,例如.bashrc. [2] 另外,通配机制的? 字符和正则
http://www.818198.com Page 373
SHELL十三问
表达式中表示的意思不一样.
bash$ ls -l
total 2
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 a.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 b.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 c.1
-rw-rw-r-- 1 bozo bozo 466 Aug 6 17:48 t2.sh
-rw-rw-r-- 1 bozo bozo 758 Jul 30 09:02 test1.txt
bash$ ls -l t?.sh
-rw-rw-r-- 1 bozo bozo 466 Aug 6 17:48 t2.sh
bash$ ls -l [ab]*
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 a.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 b.1
bash$ ls -l [a-c]*
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 a.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 b.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 c.1
bash$ ls -l [^ab]*
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 c.1
-rw-rw-r-- 1 bozo bozo 466 Aug 6 17:48 t2.sh
-rw-rw-r-- 1 bozo bozo 758 Jul 30 09:02 test1.txt
bash$ ls -l {b*,c*,*est*}
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 b.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 c.1
-rw-rw-r-- 1 bozo bozo 758 Jul 30 09:02 test1.txt
Bash会对命令行中没有引号引起来的字符尝试文件名扩展. echo 命令可以印证这一点.
bash$ echo *
a.1 b.1 c.1 t2.sh test1.txt
bash$ echo t*
t2.sh test1.txt
注意: 可以改变Bash对通配字符进行解释的行为. set -f 命令可以禁止通配机制, 并且
shopt的选项nocaseglob和nullglob 能改变通配的行为.
参考例子 10-4.
注意事项:
[1] 文件名扩展意思是扩展包含有特殊字符的文件名模式和模板. 例如,example.???可能
扩展成example.001和/或example.txt.
[2] 文件名扩展能匹配点开头的文件,但仅在模式字串明确地包含字面意思的点(.)时才
扩展.
1 ~/[.]bashrc # 不会扩展成 ~/.bashrc
2 ~/?bashrc # 也不会扩展.
3 # 通配机制中的通配符和元字符不会扩展点文件
4 #
http://www.818198.com Page 374
SHELL十三问
5
6 ~/.[b]ashrc # 会扩展成 ~/.bashrc
7 ~/.ba?hrc # 也会.
8 ~/.bashr* # 也会.
9
10 # 可以使用"dotglob"选项把这个特性禁用.
11
12 # 多谢, S.C.
第20章 子shell(Subshells)
==========================
运行一个shell脚本时会启动另一个命令解释器. 就好像你的命令是在命令行提示下被解释的一
样, 类似于批处理文件里的一系列命令.每个shell脚本有效地运行在父shell(parent shell)的
一个子进程里.这个父shell是指在一个控制终端或在一个xterm窗口中给你命令指示符的进程.
shell脚本也能启动他自已的子进程. 这些子shell(即子进程)使脚本因为效率而同时进行多个
子任务执行时能做串行处理.
一般来说,脚本里的一个外部命令(external command)能生成(forks)出一个子进程,然而
Bash内建(builtin)的命令却不这样做,因此,内建命令比起外部的等价命令执行起来更快.
圆括号里的命令列表
( 命令1; 命令2; 命令3; ... )
嵌在圆括号里的一列命令在一个子shell里运行.
注意: 在子shell里的变量不能被这段子shell代码块之外外面的脚本访问.这些变量是不能被
产生这个子shell的父进程(parent process)存取的,实际上它们是局部变量
(local variables).
Example 20-1 子shell中的变量作用域
################################Start Script#######################################
1 #!/bin/bash
2 # subshell.sh
3
4 echo
5
6 echo "Subshell level OUTSIDE subshell = $BASH_SUBSHELL"
7 # Bash, 版本 3, 增加了新的 $BASH_SUBSHELL 变量.
8 echo
9
10 outer_variable=Outer
11
12 (
13 echo "Subshell level INSIDE subshell = $BASH_SUBSHELL"
14 inner_variable=Inner
15
16 echo "From subshell, \"inner_variable\" = $inner_variable"
17 echo "From subshell, \"outer\" = $outer_variable"
18 )
19
20 echo
21 echo "Subshell level OUTSIDE subshell = $BASH_SUBSHELL"
22 echo
23
http://www.818198.com Page 375
SHELL十三问
24 if [ -z "$inner_variable" ]
25 then
26 echo "inner_variable undefined in main body of shell"
27 else
28 echo "inner_variable defined in main body of shell"
29 fi
30
31 echo "From main body of shell, \"inner_variable\" = $inner_variable"
32 # $inner_variable 会以没有初始化的变量来打印
33 #+ 因为变量是在子shell里定义的"局部变量".
34 # 这个有办法补救的吗?
35
36 echo
37
38 exit 0
################################End Script#########################################
参考例子 31-2.
+
在子shell中的目录更改不会影响到父shell.
Example 20-2 列出用户的配置文件
################################Start Script#######################################
1 #!/bin/bash
2 # allprofs.sh: 打印所有用户的配置文件
3
4 # 由 Heiner Steven编写, 并由本书作者修改.
5
6 FILE=.bashrc # 在一般的脚本里,包含用户配置的文件是".profile".
7 #
8
9 for home in `awk -F: '{print $6}' /etc/passwd`
10 do
11 [ -d "$home" ] || continue # 如果没有家目录,跳过此次循环.
12 [ -r "$home" ] || continue # 如果目录没有读权限,跳过此次循环.
13 (cd $home; [ -e $FILE ] && less $FILE)
14 done
15
16 # 当脚本终止时,不必用'cd'命令返回原来的目录,
17 #+ 因为'cd $home'是在子shell中发生的,不影响父shell.
18
19 exit 0
################################End Script#########################################
子shell可用于为一组命令设定临时的环境变量.
1 COMMAND1
2 COMMAND2
3 COMMAND3
4 (
5 IFS=:
6 PATH=/bin
http://www.818198.com Page 376
SHELL十三问
7 unset TERMINFO
8 set -C
9 shift 5
10 COMMAND4
11 COMMAND5
12 exit 3 # 只是从子shell退出.
13 )
14 # 父shell不受影响,变量值没有更改.
15 COMMAND6
16 COMMAND7
它的一个应用是测试是否一个变量被定义了.
1 if (set -u; : $variable) 2> /dev/null
2 then
3 echo "Variable is set."
4 fi # 变量已经在当前脚本中被设置,
5 #+ 或是Bash的一个内部变量,
6 #+ 或是可见环境变量(指已经被导出的环境变量).
7
8 # 也可以写成 [[ ${variable-x} != x || ${variable-y} != y ]]
9 # 或 [[ ${variable-x} != x$variable ]]
10 # 或 [[ ${variable+x} = x ]]
11 # 或 [[ ${variable-x} != x ]]
另一个应用是检查一个加锁的文件:
1 if (set -C; : > lock_file) 2> /dev/null
2 then
3 : # lock_file 不存在,还没有用户运行这个脚本
4 else
5 echo "Another user is already running that script."
6 exit 65
7 fi
8
9 # 由St�phane Chazelas编程
10 #+ 由Paulo Marcel Coelho Aragao修改.
进程在不同的子shell中可以串行地执行.这样就允许把一个复杂的任务分成几个小的子问题来
同时地处理.
Example 20-3 在子shell里进行串行处理
################################Start Script#######################################
1 (cat list1 list2 list3 | sort | uniq > list123) &
2 (cat list4 list5 list6 | sort | uniq > list456) &
3 #列表的合并和排序同时进.
4 #放到后台运行可以确保能够串行执行.
5 #
6 #和下面的有相同的作用:
7 # cat list1 list2 list3 | sort | uniq > list123 &
8 # cat list4 list5 list6 | sort | uniq > list456 &
9
10 wait #在所有的子shell执行完成前不再执行后面的命令.
11
http://www.818198.com Page 377
SHELL十三问
12 diff list123 list456
################################End Script#########################################
用"|"管道操作把I/O流重定向到子shell,例如ls -al | (command).
注意: 在一个花括号内的代码块不会运行一个子shell.
{ command1; command2; command3; ... }
第21章 受限shell(Restricted Shells)
====================================
在受限shell中禁用的命令
在受限shell中运行的脚本或脚本的个代码断会禁用一些正常shell中可以执行的命令.这是
限制脚本用户的权限和最小化运行脚本导致的破坏的安全措施.
使用cd 命令更改工作目录.
更改环境变量$PATH, $SHELL, $BASH_ENV,或$ENV 的值.
读或更改shell环境选项变量$SHELLOPTS的值.
输出重定向.
调用的命令路径中包括有一个或更多个/字符.
调用exec来把当前的受限shell替换成另外一个不同的进程.
脚本中许多其他无意中能破坏或捣乱的命令.
在脚本中企图脱离受限shell模式的操作.
Example 21-1 在受限的情况下运行脚本
################################Start Script#######################################
1 #!/bin/bash
2
3 # 脚本开头以"#!/bin/bash -r"来调用
4 #+ 会使整个脚本在受限模式下运行.
5
6 echo
7
8 echo "Changing directory."
9 cd /usr/local
10 echo "Now in `pwd`"
11 echo "Coming back home."
12 cd
13 echo "Now in `pwd`"
14 echo
15
16 # 不受限的模式下,所有操作都能正常成功.
17
18 set -r
19 # set --restricted 也能起相同的作用.
20 echo "==> Now in restricted mode. <=="
21
22 echo
23 echo
24
25 echo "Attempting directory change in restricted mode."
26 cd ..
27 echo "Still in `pwd`"
28
http://www.818198.com Page 378
SHELL十三问
29 echo
30 echo
31
32 echo "\$SHELL = $SHELL"
33 echo "Attempting to change shell in restricted mode."
34 SHELL="/bin/ash"
35 echo
36 echo "\$SHELL= $SHELL"
37
38 echo
39 echo
40
41 echo "Attempting to redirect output in restricted mode."
42 ls -l /usr/bin > bin.files
43 ls -l bin.files # Try to list attempted file creation effort.
44
45 echo
46
47 exit 0
################################End Script#########################################
第22章 进程替换
================
进程替换与命令替换(command substitution)很相似. 命令替换把一个命令的结果赋给一个
变量,例如 dir_contents=`ls -al`或xref=$( grep word datafile). 进程替换则是把一个进
程的输出回馈给另一个进程 (换句话说,它把一个命令的结果发送给另一个命令).
命令替换的一般形式
由圆括号括起的命令
>(command)
<(command)
启动进程替换. 它是用/dev/fd/
程. [1] (译者注:实际上现代的UNIX类操作系统提供的/dev/fd/n文件是与文件描述相关
的,整数n指的就是在进程运行时对应数字的文件描述符)
注意: 在"<" 或or ">" 与圆括号之间是没有空格的. 如果加了空格将会引起错误信息.
bash$ echo >(true)
/dev/fd/63
bash$ echo <(true)
/dev/fd/63
Bash在两个文件描述符(file descriptors)之间创建了一个管道, --fIn 和 fOut--. true
命令的标准输入被连接到fOut(dup2(fOut, 0)), 然后Bash把/dev/fd/fIn作为参数传给echo.
如果系统的/dev/fd/
进程替换能比较两个不同命令之间的输出,或者甚至相同命令不同选项的输出.
bash$ comm <(ls -l) <(ls -al)
total 12
-rw-rw-r-- 1 bozo bozo 78 Mar 10 12:58 File0
-rw-rw-r-- 1 bozo bozo 42 Mar 10 12:58 File2
-rw-rw-r-- 1 bozo bozo 103 Mar 10 12:58 t2.sh
total 20
http://www.818198.com Page 379
SHELL十三问
drwxrwxrwx 2 bozo bozo 4096 Mar 10 18:10 .
drwx------ 72 bozo bozo 4096 Mar 10 17:58 ..
-rw-rw-r-- 1 bozo bozo 78 Mar 10 12:58 File0
-rw-rw-r-- 1 bozo bozo 42 Mar 10 12:58 File2
-rw-rw-r-- 1 bozo bozo 103 Mar 10 12:58 t2.sh
用进程替换来比较两个不同目录的内容 (考察哪些文件名是相同的,哪些是不同的):
1 diff <(ls $first_directory) <(ls $second_directory)
其他一些进程替换的用法和技巧:
1 cat <(ls -l)
2 # 等同于 ls -l | cat
3
4 sort -k 9 <(ls -l /bin) <(ls -l /usr/bin) <(ls -l /usr/X11R6/bin)
5 # 列出系统中3个主要的'bin'目录的所有文件,并且按文件名排序.
6 # 注意是三个明显不同的命令输出回馈给'sort'.
7
8
9 diff <(command1) <(command2) # 给出两个命令输出的不同之处.
10
11 tar cf >(bzip2 -c > file.tar.bz2) $directory_name
12 # 调用"tar cf /dev/fd/?? $directory_name",和"bzip2 -c > file.tar.bz2".
13 #
14 # 因为/dev/fd/
15 # 所以两个命令之间的管道不必是命名的.
16 #
17 # 这种效果可以模仿出来.
18 #
19 bzip2 -c < pipe > file.tar.bz2&
20 tar cf pipe $directory_name
21 rm pipe
22 # 或者
23 exec 3>&1
24 tar cf /dev/fd/4 $directory_name 4>&1 >&3 3>&- | bzip2 -c > file.tar.bz2 3>&- 25 exec 3>&- 26
27
28 # Thanks, St`phane Chazelas
有个读者给我发来下面关于进程替换的有趣例子A.
1 # 摘自SuSE发行版中的代码片断:
2
3 while read des what mask iface; do
4 # 这里省略了一些命令 ...
5 done < <(route -n)
6
7
8 # 为了测试它,我们来做些动作.
9 while read des what mask iface; do
10 echo $des $what $mask $iface
11 done < <(route -n)
http://www.818198.com Page 380
SHELL十三问
12
13 # 输出:
14 # Kernel IP routing table
15 # Destination Gateway Genmask Flags Metric Ref Use Iface
16 # 127.0.0.0 0.0.0.0 255.0.0.0 U 0 0 0 lo
17
18
19
20 # 由 St�phane Chazelas给出的,一个更容易理解的等价代码是:
21 route -n |
22 while read des what mask iface; do # 管道的输出被赋给了变量.
23 echo $des $what $mask $iface
24 done # 这样就取回了和上面一样的输出.
25 # 但是, Ulrich Gayer指出 . . .
26 #+ 这个简单版本的等价代码在while循环中使用了一个子shell,
27 #+ 因此当管道结束后变量会被毁掉.
28
29
30
31 # 更进一步, Filip Moritz解释了上面两个例子之间有一个细微的不同之处
32 #+ 如下所示.
33
34 (
35 route -n | while read x; do ((y++)); done
36 echo $y # $y 仍然没有被声明或设置
37
38 while read x; do ((y++)); done < <(route -n)
39 echo $y # $y的值为 route -n 输出的行数
40 )
41
42 # 一般来说
43 (
44 : | x=x
45 # 看上去是启动了一个子shell
46 : | ( x=x )
47 # 但
48 x=x < <(:)
49 # 实际上不是
50 )
51
52 # 当解析csv或类似的东西时非常有用.
53 # 事实上,这就是SuSE原本的代码片断所要实现的功能.
注意事项:
[1] 这与命名管道(named pipe)(临时文件)有相同的作用, 事实上命名管道同样在进程
替换中被使用.
第23章 函数
============
和"真正的"编程语言一样, Bash也有函数,虽然在某些实现方面稍有些限制. 一个函数是一个
http://www.818198.com Page 381
SHELL十三问
子程序,用于实现一串操作的代码块(code block),它是完成特定任务的"黑盒子". 当有重复
代码, 当一个任务只需要很少的修改就被重复几次执行时, 这时你应考虑使用函数.
function function_name {
command...
}
或
function_name () {
command...
}
第二种格式的写法更深得C程序员的喜欢(并且也是更可移植的).
因为在C中,函数的左花括号也可以写在下一行中.
function_name ()
{
command...
}
函数被调用或被触发, 只需要简单地用函数名调用.
Example 23-1 简单函数
################################Start Script#######################################
1 #!/bin/bash
2
3 JUST_A_SECOND=1
4
5 funky ()
6 { # 这是一个最简单的函数.
7 echo "This is a funky function."
8 echo "Now exiting funky function."
9 } # 函数必须在调用前声明.
10
11
12 fun ()
13 { # 一个稍复杂的函数.
14 i=0
15 REPEATS=30
16
17 echo
18 echo "And now the fun really begins."
19 echo
20
21 sleep $JUST_A_SECOND # 嘿, 暂停一秒!
22 while [ $i -lt $REPEATS ]
23 do
24 echo "----------FUNCTIONS---------->"
25 echo "<------------ARE-------------"
26 echo "<------------FUN------------>"
27 echo
28 let "i+=1"
29 done
30 }
http://www.818198.com Page 382
SHELL十三问
31
32 # 现在,调用两个函数.
33
34 funky
35 fun
36
37 exit 0
################################End Script#########################################
函数定义必须在第一次调用函数前完成.没有像C中的函数“声明”方法.
1 f1
2 # 因为函数"f1"还没有定义,这会引起错误信息.
3
4 declare -f f1 # 这样也没用.
5 f1 # 仍然会引起错误.
6
7 # 然而...
8
9
10 f1 ()
11 {
12 echo "Calling function \"f2\" from within function \"f1\"."
13 f2
14 }
15
16 f2 ()
17 {
18 echo "Function \"f2\"."
19 }
20
21 f1 # 虽然在它定义前被引用过,
22 #+ 函数"f2"实际到这儿才被调用.
23 # 这样是允许的.
24
25 # Thanks, S.C.
在一个函数内嵌套另一个函数也是可以的,但是不常用.
1 f1 ()
2 {
3
4 f2 () # nested
5 {
6 echo "Function \"f2\", inside \"f1\"."
7 }
8
9 }
10
11 f2 # 引起错误.
12 # 就是你先"declare -f f2"了也没用.
13
http://www.818198.com Page 383
SHELL十三问
14 echo
15
16 f1 # 什么也不做,因为调用"f1"不会自动调用"f2".
17 f2 # 现在,可以正确的调用"f2"了,
18 #+ 因为之前调用"f1"使"f2"在脚本中变得可见了.
19
20 # Thanks, S.C.
函数声明可以出现在看上去不可能出现的地方,那些不可能的地方本该由一个命令出现的地方.
1 ls -l | foo() { echo "foo"; } # 允许,但没什么用.
2
3
4
5 if [ "$USER" = bozo ]
6 then
7 bozo_greet () # 在if/then结构中定义了函数.
8 {
9 echo "Hello, Bozo."
10 }
11 fi
12
13 bozo_greet # 只能由Bozo运行, 其他用户会引起错误.
14
15
16
17 # 在某些上下文,像这样可能会有用.
18 NO_EXIT=1 # 将会打开下面的函数定义.
19
20 [[ $NO_EXIT -eq 1 ]] && exit() { true; } # 在"and-list"(and列表)中定义函数.
21 # 如果 $NO_EXIT 是 1,声明函数"exit ()".
22 # 把"exit"取别名为"true"将会禁用内建的"exit".
23
24 exit # 调用"exit ()"函数, 而不是内建的"exit".
25
26 # Thanks, S.C.
23.1. 复杂函数和函数复杂性
--------------------------函数可以处理传递给它的参数并且能返回它的退出状态码(exit status)给脚本后续使用.
1 function_name $arg1 $arg2
函数以位置来引用传递过来的参数(就好像他们是位置参数(positional parameters)), 例如
$1, $2,以此类推.
Example 23-2 带着参数的函数
################################Start Script#######################################
1 #!/bin/bash
2 # 函数和参数
3
4 DEFAULT=default # 默认的参数值.
5
6 func2 () {
http://www.818198.com Page 384
SHELL十三问
7 if [ -z "$1" ] # 第一个参数是否长度为零?
8 then
9 echo "-Parameter #1 is zero length.-" # 则没有参数传递进来.
10 else
11 echo "-Param #1 is \"$1\".-"
12 fi
13
14 variable=${1-$DEFAULT} #
15 echo "variable = $variable" # 参数替换会表现出什么?
16 # ---------------------------17 # 它用于分辨没有参数和一个只有NULL值的参数.
18 #
19
20 if [ "$2" ]
21 then
22 echo "-Parameter #2 is \"$2\".-"
23 fi
24
25 return 0
26 }
27
28 echo
29
30 echo "Nothing passed."
31 func2 # 没有参数来调用
32 echo
33
34
35 echo "Zero-length parameter passed."
36 func2 "" # 以一个长度为零的参数调用
37 echo
38
39 echo "Null parameter passed."
40 func2 "$uninitialized_param" # 以未初始化的参数来调用
41 echo
42
43 echo "One parameter passed."
44 func2 first # 用一个参数来调用
45 echo
46
47 echo "Two parameters passed."
48 func2 first second # 以二个参数来调用
49 echo
50
51 echo "\"\" \"second\" passed."
52 func2 "" second # 以第一个参数为零长度,而第二个参数是一个ASCII码组成的字符串来调用.
53 echo #
54
http://www.818198.com Page 385
SHELL十三问
55 exit 0
################################End Script#########################################
注意: shift命令可以工作在传递给函数的参数 (参考例子 33-15).
但是,传给脚本的命令行参数怎么办?在函数内部可以看到它们吗?好,让我们来弄清楚.
Example 23-3 函数和被传给脚本的命令行参数
################################Start Script#######################################
1 #!/bin/bash
2 # func-cmdlinearg.sh
3 # 以一个命令行参数来调用这个脚本,
4 #+ 类似 $0 arg1来调用.
5
6
7 func ()
8
9 {
10 echo "$1"
11 }
12
13 echo "First call to function: no arg passed."
14 echo "See if command-line arg is seen."
15 func
16 # 不!命令行参数看不到.
17
18 echo "============================================================"
19 echo
20 echo "Second call to function: command-line arg passed explicitly."
21 func $1
22 # 现在可以看到了!
23
24 exit 0
################################End Script#########################################
与别的编程语言相比,shell脚本一般只传递值给函数,变量名(实现上是指针)如果作为参数传递给函数会被看成是字面上字符
串的意思.函数解释参数是以字面上的意思来解释的.
间接变量引用(Indirect variable references) (参考例子 34-2)提供了传递变量指针给函数的一个笨拙的机制.
Example 23-4 传递间接引用给函数
################################Start Script#######################################
1 #!/bin/bash
2 # ind-func.sh: 传递间接引用给函数.
3
4 echo_var ()
5 {
6 echo "$1"
7 }
8
9 message=Hello
10 Hello=Goodbye
11
12 echo_var "$message" # Hello
http://www.818198.com Page 386
SHELL十三问
13 # 现在,让我们传递一个间接引用给函数.
14 echo_var "${!message}" # Goodbye
15
16 echo "-------------"
17
18 # 如果我们改变"hello"变量的值会发生什么?
19 Hello="Hello, again!"
20 echo_var "$message" # Hello
21 echo_var "${!message}" # Hello, again!
22
23 exit 0
################################End Script#########################################
下一个逻辑问题是:在传递参数给函数之后是否能解除参数的引用.
Example 23-5 解除传递给函数的参数引用
################################Start Script#######################################
1 #!/bin/bash
2 # dereference.sh
3 # 给函数传递不同的参数.
4 # Bruce W. Clare编写.
5
6 dereference ()
7 {
8 y=\$"$1" # 变量名.
9 echo $y # $Junk
10
11 x=`eval "expr \"$y\" "`
12 echo $1=$x
13 eval "$1=\"Some Different Text \"" # 赋新值.
14 }
15
16 Junk="Some Text"
17 echo $Junk "before" # Some Text before
18
19 dereference Junk
20 echo $Junk "after" # Some Different Text after
21
22 exit 0
################################End Script#########################################
Example 23-6 再次尝试解除传递给函数的参数引用
################################Start Script#######################################
1 #!/bin/bash
2 # ref-params.sh: 解除传递给函数的参数引用.
3 # (复杂例子)
4
5 ITERATIONS=3 # 取得输入的次数.
6 icount=1
7
8 my_read () {
http://www.818198.com Page 387
SHELL十三问
9 # 用my_read varname来调用,
10 #+ 输出用括号括起的先前的值作为默认值,
11 #+ 然后要求输入一个新值.
12
13 local local_var
14
15 echo -n "Enter a value "
16 eval 'echo -n "[$'$1'] "' # 先前的值.
17 # eval echo -n "[\$$1] " # 更好理解,
18 #+ 但会丢失用户输入在尾部的空格.
19 read local_var
20 [ -n "$local_var" ] && eval $1=\$local_var
21
22 # "and列表(And-list)": 如果变量"local_var"测试成功则把变量"$1"的值赋给它.
23 }
24
25 echo
26
27 while [ "$icount" -le "$ITERATIONS" ]
28 do
29 my_read var
30 echo "Entry #$icount = $var"
31 let "icount += 1"
32 echo
33 done
34
35
36 # 多谢Stephane Chazelas提供的示范例子.
37
38 exit 0
################################End Script#########################################
退出和返回
退出状态(exit status)
函数返回一个被称为退出状态的值. 退出状态可以由return来指定statement, 否则函数的
退出状态是函数最后一个执行命令的退出状态(0表示成功,非0表示出错代码). 退出状态
(exit status)可以在脚本中由$? 引用. 这个机制使脚本函数也可以像C函数一样有一个"
返回值".
return
终止一个函数.return 命令[1]可选地带一个整数参数,这个整数作为函数的"返回值"返回
给调用此函数的脚本,并且这个值也被赋给变量$?.
Example 23-7 两个数中的最大者
################################Start Script#######################################
1 #!/bin/bash
2 # max.sh: 两个整数中的最大者.
3
4 E_PARAM_ERR=-198 # 如果传给函数的参数少于2个时的返回值.
5 EQUAL=-199 # 如果两个整数值相等的返回值.
6 # 任一个传给函数的参数值溢出
http://www.818198.com Page 388
SHELL十三问
7 #
8
9 max2 () # 返回两个整数的较大值.
10 { # 注意: 参与比较的数必须小于257.
11 if [ -z "$2" ]
12 then
13 return $E_PARAM_ERR
14 fi
15
16 if [ "$1" -eq "$2" ]
17 then
18 return $EQUAL
19 else
20 if [ "$1" -gt "$2" ]
21 then
22 return $1
23 else
24 return $2
25 fi
26 fi
27 }
28
29 max2 33 34
30 return_val=$?
31
32 if [ "$return_val" -eq $E_PARAM_ERR ]
33 then
34 echo "Need to pass two parameters to the function."
35 elif [ "$return_val" -eq $EQUAL ]
36 then
37 echo "The two numbers are equal."
38 else
39 echo "The larger of the two numbers is $return_val."
40 fi
41
42
43 exit 0
44
45 # 练习 (容易):
46 # ---------------47 # 把这个脚本转化成交互式的脚本,
48 #+ 也就是说,让脚本可以要求调用者输入两个整数.
################################End Script#########################################
注意: 为了函数可以返回字符串或是数组,用一个可在函数外可见的变量.
1 count_lines_in_etc_passwd()
2 {
3 [[ -r /etc/passwd ]] && REPLY=$(echo $(wc -l < /etc/passwd))
4 # 如果/etc/passwd可读,则把REPLY设置成文件的行数.
http://www.818198.com Page 389
SHELL十三问
5 # 返回一个参数值和状态信息.
6 # 'echo'好像没有必要,但 . . .
7 #+ 它的作用是删除输出中的多余空白字符.
8 }
9
10 if count_lines_in_etc_passwd
11 then
12 echo "There are $REPLY lines in /etc/passwd."
13 else
14 echo "Cannot count lines in /etc/passwd."
15 fi
16
17 # Thanks, S.C.
Example 23-8 把数字转化成罗马数字
################################Start Script#######################################
1 #!/bin/bash
2
3 # 阿拉伯数字转化为罗马数字
4 # 转化范围: 0 - 200
5 # 这是比较粗糙的,但可以工作.
6
7 # 扩展可接受的范围来作为脚本功能的扩充,这个作为练习完成.
8
9 # 用法: roman number-to-convert
10
11 LIMIT=200
12 E_ARG_ERR=65
13 E_OUT_OF_RANGE=66
14
15 if [ -z "$1" ]
16 then
17 echo "Usage: `basename $0` number-to-convert"
18 exit $E_ARG_ERR
19 fi
20
21 num=$1
22 if [ "$num" -gt $LIMIT ]
23 then
24 echo "Out of range!"
25 exit $E_OUT_OF_RANGE
26 fi
27
28 to_roman () # 在第一次调用函数前必须先定义.
29 {
30 number=$1
31 factor=$2
32 rchar=$3
33 let "remainder = number - factor"
http://www.818198.com Page 390
SHELL十三问
34 while [ "$remainder" -ge 0 ]
35 do
36 echo -n $rchar
37 let "number -= factor"
38 let "remainder = number - factor"
39 done
40
41 return $number
42 # 练习:
43 # --------44 # 解释这个函数是怎么工作的.
45 # 提示: 靠不断地除来分割数字.
46 }
47
48
49 to_roman $num 100 C
50 num=$?
51 to_roman $num 90 LXXXX
52 num=$?
53 to_roman $num 50 L
54 num=$?
55 to_roman $num 40 XL
56 num=$?
57 to_roman $num 10 X
58 num=$?
59 to_roman $num 9 IX
60 num=$?
61 to_roman $num 5 V
62 num=$?
63 to_roman $num 4 IV
64 num=$?
65 to_roman $num 1 I
66
67 echo
68
69 exit 0
################################End Script#########################################
请参考例子 10-28.
注意: 函数最大可返回的正整数为255. return 命令与退出状态(exit status)的概念联
系很紧密,而退出状态的值受此限制.幸运地是有多种(工作区workarounds)来对
付这种要求函数返回大整数的情况.
Example 23-9 测试函数最大的返回值
################################Start Script#######################################
1 #!/bin/bash
2 # return-test.sh
3
4 # 一个函数最大可能返回的值是255.
5
http://www.818198.com Page 391
SHELL十三问
6 return_test () # 无论传给函数什么都返回它.
7 {
8 return $1
9 }
10
11 return_test 27 # o.k.
12 echo $? # 返回 27.
13
14 return_test 255 # 仍然 o.k.
15 echo $? # 返回 255.
16
17 return_test 257 # 错误!
18 echo $? # 返回 1 (返回代码指示错误).
19
20 # ======================================================
21 return_test -151896 # 能够返回这个非常大的负数么?
22 echo $? # 会返回-151896?
23 # 不! 它将返回168.
24 # 2.05b版本之前的Bash是允许
25 #+ 超大负整数作为返回值的.
26 # 但是比它更新一点的版本修正了这个漏洞.
27 # 这将破坏比较老的脚本.
28 # 慎用!
29 # ======================================================
30
31 exit 0
################################End Script#########################################
如果你非常想使用超大整数作为"返回值"的话, 那么只能通过将你想返回的返回值直接的
传递到一个全局变量中的手段来达到目的.
1 Return_Val= # 全局变量, 用来保存函数中需要返回的超大整数.
2
3 alt_return_test ()
4 {
5 fvar=$1
6 Return_Val=$fvar
7 return # Returns 0 (success).
8 }
9
10 alt_return_test 1
11 echo $? # 0
12 echo "return value = $Return_Val" # 1
13
14 alt_return_test 256
15 echo "return value = $Return_Val" # 256
16
17 alt_return_test 257
18 echo "return value = $Return_Val" # 257
19
http://www.818198.com Page 392
SHELL十三问
20 alt_return_test 25701
21 echo "return value = $Return_Val" #25701
一种更优雅的方法是让函数echo出它的返回值, 输出到stdout上, 然后再通过"命令替换"
的手段来捕获它. 参考Section 33.7关于这个问题的讨论.
Example 23-10 比较两个大整数
################################Start Script#######################################
1 #!/bin/bash
2 # max2.sh: 取两个超大整数中最大的.
3
4 # 这个脚本与前面的"max.sh"例子作用相同,
5 #+ 经过修改可以适用于比较超大整数.
6
7 EQUAL=0 # 如果两个参数相同的返回值.
8 E_PARAM_ERR=-99999 # 没有足够的参数传递到函数中.
9 # ^^^^^^ 也可能是传递到函数中的某个参数超出范围了.
10
11 max2 () # 从这两个数中"返回"更大一些的.
12 {
13 if [ -z "$2" ]
14 then
15 echo $E_PARAM_ERR
16 return
17 fi
18
19 if [ "$1" -eq "$2" ]
20 then
21 echo $EQUAL
22 return
23 else
24 if [ "$1" -gt "$2" ]
25 then
26 retval=$1
27 else
28 retval=$2
29 fi
30 fi
31
32 echo $retval # echo(到stdout), 而不是使用返回值.
33 # 为什么?
34 }
35
36
37 return_val=$(max2 33001 33997)
38 # ^^^^ 函数名
39 # ^^^^^ ^^^^^ 这是传递进来的参数
40 # 这事实上是一个命令替换的形式:
41 #+ 会把这个函数当作一个命令来处理,
42 #+ 并且分配这个函数的stdout到变量"return_val"中.
http://www.818198.com Page 393
SHELL十三问
43
44
45 # ========================= OUTPUT ========================
46 if [ "$return_val" -eq "$E_PARAM_ERR" ]
47 then
48 echo "Error in parameters passed to comparison function!"
49 elif [ "$return_val" -eq "$EQUAL" ]
50 then
51 echo "The two numbers are equal."
52 else
53 echo "The larger of the two numbers is $return_val."
54 fi
55 # =========================================================
56
57 exit 0
58
59 # 练习:
60 # -----61 # 1) 找出一种更优雅的方法来测试
62 #+ 传递到函数中的参数.
63 # 2) 在"OUTPUT"的时候简化if/then结构.
64 # 3) 重写这个脚本使其能够从命令行参数中来获取输入.
################################End Script#########################################
下边是获得一个函数的"返回值"的另一个例子. 想要了解这个例子需要一些awk的知识.
1 month_length () # 以月份数作为参数.
2 { # 返回这个月有几天.
3 monthD="31 28 31 30 31 30 31 31 30 31 30 31" # 作为局部变量来声明?
4 echo "$monthD" | awk '{ print $'"${1}"' }' # 有技巧的.
5 # ^^^^^^^^^
6 # 先将参数传递到函数中 ($1 -- 月份号), 然后就到awk了.
7 # Awk将会根据传递进来的月份号来决定打印"print $1 . . . print $12"中的哪个 (依赖于月份号)
8 # 传递参数到内嵌awk脚本的模版:
9 # $'"${script_parameter}"'
10
11 # 需要错误检查来修正参数的范围(1-12)
12 #+ 并且要处理闰年的特殊的2月.
13 }
14
15 # ---------------------------------------------- 16 # 用例:
17 month=4 # 拿4月来举个例子.
18 days_in=$(month_length $month)
19 echo $days_in # 30
20 # ----------------------------------------------
也参考例子 A-7.
练习: 用我们已经学到的扩展先前罗马数字那个例子脚本能接受任意大的输入.
重定向
重定向函数的标准输入
http://www.818198.com Page 394
SHELL十三问
函数本质上是一个代码块(code block), 这样意思着它的标准输入可以被重定向
(就像在例子 3-1中显示的).
Example 23-11 用户名的真实名
################################Start Script#######################################
1 #!/bin/bash
2 # realname.sh
3 #
4 # 由用户名而从/etc/passwd取得"真实名".
5
6
7 ARGCOUNT=1 # 需要一个参数.
8 E_WRONGARGS=65
9
10 file=/etc/passwd
11 pattern=$1
12
13 if [ $# -ne "$ARGCOUNT" ]
14 then
15 echo "Usage: `basename $0` USERNAME"
16 exit $E_WRONGARGS
17 fi
18
19 file_excerpt () # 以要求的模式来扫描文件,然后打印文件相关的部分.
20 {
21 while read line # "while" does not necessarily need "[ condition ]"
22 do
23 echo "$line" | grep $1 | awk -F":" '{ print $5 }' # awk指定使用":"为界定符.
24 done
25 } <$file # 重定向函数的标准输入.
26
27 file_excerpt $pattern
28
29 # Yes, this entire script could be reduced to
30 # grep PATTERN /etc/passwd | awk -F":" '{ print $5 }'
31 # or
32 # awk -F: '/PATTERN/ {print $5}'
33 # or
34 # awk -F: '($1 == "username") { print $5 }' # real name from username
35 # 但是,这些可能起不到示例的作用.
36
37 exit 0
################################End Script#########################################
还有一个办法,可能是更好理解的重定向函数标准输入方法.它为函数内的一个括号内的
代码块调用标准输入重定向.
1 # 用下面的代替:
2 Function ()
3 {
4 ...
http://www.818198.com Page 395
SHELL十三问
5 } < file
6
7 # 也试一下这个:
8 Function ()
9 {
10 {
11 ...
12 } < file
13 }
14
15 # 同样,
16
17 Function () # 可以工作.
18 {
19 {
20 echo $*
21 } | tr a b
22 }
23
24 Function () # 这个不会工作
25 {
26 echo $*
27 } | tr a b # 这儿的内嵌代码块是强制的.
28
29
30 # Thanks, S.C.
注意事项:
[1] return命令是Bash内建(builtin)的.
23.2. 局部变量
--------------怎么样使一个变量变成局部的?
局部变量
如果变量用local来声明,那么它只能在该变量声明的代码块(block of code)中可见.
这个代码块就是局部"范围". 在一个函数内,局部变量意味着只能在函数代码块内它才
有意义.
Example 23-12 局部变量的可见范围
################################Start Script#######################################
1 #!/bin/bash
2 # 在函数内部的全局和局部变量.
3
4 func ()
5 {
6 local loc_var=23 # 声明为局部变量.
7 echo # 使用内建的'local'关键字.
8 echo "\"loc_var\" in function = $loc_var"
9 global_var=999 # 没有声明为局部变量.
10 # 默认为全局变量.
11 echo "\"global_var\" in function = $global_var"
http://www.818198.com Page 396
SHELL十三问
12 }
13
14 func
15
16 # 现在,来看看是否局部变量"loc_var"能否在函数外面可见.
17
18 echo
19 echo "\"loc_var\" outside function = $loc_var"
20 # $loc_var outside function =
21 # 不, $loc_var不是全局可访问的.
22 echo "\"global_var\" outside function = $global_var"
23 # $global_var outside function = 999
24 # $global_var 是全局可访问的.
25 echo
26
27 exit 0
28 # 与In contrast to C相比, 在函数内声明的Bash变量只有在
29 #+ 它被明确声明成局部的变量时才是局部的.
################################End Script#########################################
注意: 在函数调用之前,所有在函数内声明且没有明确声明为local的变量都可在函数体
外可见.
1 #!/bin/bash
2
3 func ()
4 {
5 global_var=37 # 在函数还没有被调用前
6 #+ 变量只在函数内可见.
7 } # 函数结束
8
9 echo "global_var = $global_var" # global_var =
10 # 函数"func"还没有被调用,
11 #+ 所以变量$global_var还不能被访问.
12
13 func
14 echo "global_var = $global_var" # global_var = 37
15 # 已经在函数调用时设置了值.
23.2.1. 局部变量使递归变得可能.
-------------------------------局部变量可以递归, [1] 但这个办法会产生大量的计算,因此它在shell脚本中是被明确表明
不推荐的. [2]
Example 23-13 用局部变量来递归
################################Start Script#######################################
1 #!/bin/bash
2
3 # 阶乘
4 # --------- 5
6
http://www.818198.com Page 397
SHELL十三问
7 # bash允许递归吗?
8 # 嗯, 允许, 但是...
9 # 它太慢以致你难以忍受.
10
11
12 MAX_ARG=5
13 E_WRONG_ARGS=65
14 E_RANGE_ERR=66
15
16
17 if [ -z "$1" ]
18 then
19 echo "Usage: `basename $0` number"
20 exit $E_WRONG_ARGS
21 fi
22
23 if [ "$1" -gt $MAX_ARG ]
24 then
25 echo "Out of range (5 is maximum)."
26 # 现在让我们来了解实际情况.
27 # 如果你想求比这个更大的范围的阶乘,
28 #+ 应该重新用一个真正的编程语言来写.
29 exit $E_RANGE_ERR
30 fi
31
32 fact ()
33 {
34 local number=$1
35 # 变量"number"必须声明为局部,
36 #+ 否则它不会工作.
37 if [ "$number" -eq 0 ]
38 then
39 factorial=1 # 0的阶乘为1.
40 else
41 let "decrnum = number - 1"
42 fact $decrnum # 递归调用(函数内部调用自己本身).
43 let "factorial = $number * $?"
44 fi
45
46 return $factorial
47 }
48
49 fact $1
50 echo "Factorial of $1 is $?."
51
52 exit 0
################################End Script#########################################
也请参考例子 A-16的脚本递归的例子. 必须意识到递归也意味着巨大的资源消耗和缓慢的运
http://www.818198.com Page 398
SHELL十三问
行,因此它不适合在脚本中使用.
注意事项:
[1] Herbert Mayer 给递归下的定义是". . . expressing an algorithm by using a
simpler version of that same algorithm(用一个相同算法的版本来表示一个算法)
. . ." 递归函数是调用它自己本身的函数.
[2] 太多层的递归可能会引起脚本段错误而崩溃.
1 #!/bin/bash
2
3 # 警告: 运行这个脚本可能使你的系统失去响应!
4 # 如果你运气不错,在它使用完所有可用内存之前会段错误而退出.
5
6 recursive_function ()
7 {
8 echo "$1" # 使函数做些事情以加速产生段错误.
9 (( $1 < $2 )) && recursive_function $(( $1 + 1 )) $2;
10 # 当第一个参数比第二个参数少时,
11 #+ 把第1个参数增1再次递归.
12 }
13
14 recursive_function 1 50000 # 递归 50,000 次!
15 # 非常可能段错误 (依赖于栈的大小,它由ulimit -m设置).
16
17 # 这种深度的递归甚至可能由于耗尽栈的内存大小而引起C程序的段错误.
18 #
19
20
21 echo "This will probably not print."
22 exit 0 # 这个脚本将不会从这儿正常退出.
23
24 # 多谢, St`phane Chazelas.
23.3. 不使用局部变量的递归
--------------------------函数甚至可以不使用局部变量来调用自己.
Example 23-14 汉诺塔
################################Start Script#######################################
1 #! /bin/bash
2 #
3 # 汉诺塔(The Towers Of Hanoi)
4 # Bash script
5 # Copyright (C) 2000 Amit Singh. All Rights Reserved.
6 # http://hanoi.kernelthread.com
7 #
8 # 在bash version 2.05b.0(13)-release下测试通过
9 #
10 # 经过作者同意后在"Advanced Bash Scripting Guide"书中使用
11 #
12 # 由ABS的作者做了少许修改.
13
http://www.818198.com Page 399
SHELL十三问
14 #=================================================================#
15 # 汉诺塔是由Edouard Lucas提出的数学谜题 ,
16 #+ 他是19世纪的法国数学家.
17 #
18 # 有三个直立的柱子竖在地面上.
19 # 第一个柱子有一组的盘子套在上面.
20 # 这些盘子是平整的,中间带着孔,
21 #+ 因此它们才能套在柱子上面.
22 # 这组盘子有不同的直径,它们是依照直径从小到大来从高到低放置.
23 #
24 # 最小的盘在最高,最大的盘在最底部.
25 #
26 # 现在的任务是要把这一组的盘子从一个柱子全部地搬到另一个柱子上.
27 #
28 # 你只能一次从一个柱子上移动一个盘子到另一个柱子.
29 # 允许把盘子重新移回到它原来的最初位置.
30 # 你可以把一个小的盘子放在大的盘子上面,
31 #+ 但不能把大的盘子放在小的盘子上面.
32 # 请注意这一点.
33 #
34 # 对于这一组盘子,数量少时,只需要移动很少的次数就能达到要求.
35 #+ 但随着这组盘子的数量的增加,
36 #+ 移动的次数几乎成倍增长的,
37 #+ 而移动的策略变得愈加复杂.
38 #
39 # 想了解更多的信息, 请访问 http://hanoi.kernelthread.com.
40 #
41 #
42 # ... ... ...
43 # | | | | | |
44 # _|_|_ | | | |
45 # |_____| | | | |
46 # |_______| | | | |
47 # |_________| | | | |
48 # |___________| | | | |
49 # | | | | | |
50 # .--------------------------------------------------------------.
51 # |**************************************************************|
52 # #1 #2 #3
53 #
54 #=================================================================#
55
56
57 E_NOPARAM=66 # 没有参数传给脚本.
58 E_BADPARAM=67 # 传给脚本的盘子数不合法.
59 Moves= # 保存移动次数的全局变量.
60 # 这儿修改了原脚本.
61
http://www.818198.com Page 400
SHELL十三问
62 dohanoi() { # 递归函数.
63 case $1 in
64 0)
65 ;;
66 *)
67 dohanoi "$(($1-1))" $2 $4 $3
68 echo move $2 "-->" $3
69 let "Moves += 1" # 这儿修改了原脚本.
70 dohanoi "$(($1-1))" $4 $3 $2
71 ;;
72 esac
73 }
74
75 case $# in
76 1)
77 case $(($1>0)) in # 至少要有一个盘子.
78 1)
79 dohanoi $1 1 3 2
80 echo "Total moves = $Moves"
81 exit 0;
82 ;;
83 *)
84 echo "$0: illegal value for number of disks";
85 exit $E_BADPARAM;
86 ;;
87 esac
88 ;;
89 *)
90 echo "usage: $0 N"
91 echo " Where \"N\" is the number of disks."
92 exit $E_NOPARAM;
93 ;;
94 esac
95
96 # 练习:
97 # --------- 98 # 1) 从现在这个位置以下的命令会不会总是被执行?
99 # 为什么? (容易)
100 # 2) 解释这个可运行的"dohanoi"函数的原理.
101 # (难)
################################End Script#########################################
第24章 别名(Aliases)
=====================
Bash别名本质上是一个简称, 缩写, 这可避免键入过长的命令序列. 例如,如果我们添加
alias lm="ls -l | more" 这一行到文件~/.bashrc file里, 然后每次在命令行键入lm 将会
自动被替换成ls -l | more. 这使用户在命令行不必键冗长的命令序列也避免了记忆复杂的命
令及众多选项. 设置alias rm="rm -i" (交互式删除)可以使你犯下错误时不必过度悲伤,它
能避免你不小心删除重要文件.
http://www.818198.com Page 401
SHELL十三问
在脚本里,别名机制不是非常的有用. 如果把别名机制想像成C预处理器的某些功能将会非常
好,比如宏扩展,但是,不幸的是Bash不能在别名中扩展参数. [1] 而且,别名不能在“混合
型的结构”中使用,比如if/then语句, 循环, 和函数. 还有一个限制是别名不能递归地扩展.
大多数情况Almost invariably, 我们想让别名完成的工作都能被函数更高效地完成.
Example 24-1 脚本中的别名
################################Start Script#######################################
1 #!/bin/bash
2 # alias.sh
3
4 shopt -s expand_aliases
5 # 必须设置这个选项,否则脚本不会扩展别名功能.
6
7
8 # 首先, 来点有趣的.
9 alias Jesse_James='echo "\"Alias Jesse James\" was a 1959 comedy starring Bob Hope."'
10 Jesse_James
11
12 echo; echo; echo;
13
14 alias ll="ls -l"
15 # 可以使用单引号(')或双引号(")来定义一个别名.
16
17 echo "Trying aliased \"ll\":"
18 ll /usr/X11R6/bin/mk* #* 别名工作了.
19
20 echo
21
22 directory=/usr/X11R6/bin/
23 prefix=mk* # 看通配符会不会引起麻烦.
24 echo "Variables \"directory\" + \"prefix\" = $directory$prefix"
25 echo
26
27 alias lll="ls -l $directory$prefix"
28
29 echo "Trying aliased \"lll\":"
30 lll # 详细列出在/usr/X11R6/bin目录下所有以mk开头的文件.
31 # 别名能处理连接变量 -- 包括通配符 -- o.k.
32
33
34
35
36 TRUE=1
37
38 echo
39
40 if [ TRUE ]
41 then
42 alias rr="ls -l"
http://www.818198.com Page 402
SHELL十三问
43 echo "Trying aliased \"rr\" within if/then statement:"
44 rr /usr/X11R6/bin/mk* #* 引起错误信息!
45 # 别名不能在混合结构中使用.
46 echo "However, previously expanded alias still recognized:"
47 ll /usr/X11R6/bin/mk*
48 fi
49
50 echo
51
52 count=0
53 while [ $count -lt 3 ]
54 do
55 alias rrr="ls -l"
56 echo "Trying aliased \"rrr\" within \"while\" loop:"
57 rrr /usr/X11R6/bin/mk* #* 在这儿,别名也不会扩展.
58 # alias.sh: line 57: rrr: command not found
59 let count+=1
60 done
61
62 echo; echo
63
64 alias xyz='cat $0' # 脚本打印自身内容.
65 # 注意是单引号(强引用).
66 xyz
67 # 虽然Bash的文档它是不会工作的,但好像它是可以工作的.
68 #
69 #
70 # 然而,就像 Steve Jacobson指出,
71 #+ 参数"$0"立即扩展成了这个别名的声明.
72
73 exit 0
################################End Script#########################################
unalias 命令删除先前设置的别名.
Example 24-2 unalias: 设置和删除别名
################################Start Script#######################################
1 #!/bin/bash
2 # unalias.sh
3
4 shopt -s expand_aliases # 打开别名功能扩展.
5
6 alias llm='ls -al | more'
7 llm
8
9 echo
10
11 unalias llm # 删除别名.
12 llm
13 # 引起错误信息,因为'llm'已经不再有效了.
http://www.818198.com Page 403
SHELL十三问
14
15 exit 0
################################End Script#########################################
bash$ ./unalias.sh
total 6
drwxrwxr-x 2 bozo bozo 3072 Feb 6 14:04 .
drwxr-xr-x 40 bozo bozo 2048 Feb 6 14:04 ..
-rwxr-xr-x 1 bozo bozo 199 Feb 6 14:04 unalias.sh
./unalias.sh: llm: command not found
注意事项:
[1] 但是, 别名好像能扩展位置参数.
第25章 列表结构
================
"与列表(and list)"和"或列表(or list)" 结构提供一种处理一串连续命令的方法. 它们能有
效地替代复杂的嵌套if/then语句甚至可以代替case语句.
连接命令
与列表(and list)
1 command-1 && command-2 && command-3 && ... command-n
如果每个命令都返回真值(0)将会依次执行下去. 当某个命令返回假值(非零值), 整个命
令链就会结束执行(第一个返回假的命令将会是最后一个执行的命令,后面的都不再执行).
Example 25-1 使用"与列表(and list)"来测试命令行参数
################################Start Script#######################################
1 #!/bin/bash
2 # "and list"
3
4 if [ ! -z "$1" ] && echo "Argument #1 = $1" && [ ! -z "$2" ] && echo "Argument #2 = $2"
5 then
6 echo "At least 2 arguments passed to script."
7 # 所有连接起来的命令都返回真.
8 else
9 echo "Less than 2 arguments passed to script."
10 # 整个命令列表中至少有一个命令返回假值.
11 fi
12 # 注意"if [ ! -z $1 ]" 可以工作,但它是有所假定的等价物,
13 # if [ -n $1 ] 不会工作.
14 # 但是, 加引用可以让它工作.
15 # if [ -n "$1" ] 就可以了.
16 # 小心!
17 # 最好总是引起要测试的变量.
18
19
20 # 这是使用"纯粹"的 if/then 语句完成的同等功能.
21 if [ ! -z "$1" ]
22 then
23 echo "Argument #1 = $1"
24 fi
25 if [ ! -z "$2" ]
26 then
http://www.818198.com Page 404
SHELL十三问
27 echo "Argument #2 = $2"
28 echo "At least 2 arguments passed to script."
29 else
30 echo "Less than 2 arguments passed to script."
31 fi
32 # 这会更长且不如"与列表"精致.
33
34
35 exit 0
################################End Script#########################################
Example 25-2 用"与列表"的另一个命令行参数测试
################################Start Script#######################################
1 #!/bin/bash
2
3 ARGS=1 # 期望的参数个数.
4 E_BADARGS=65 # 如果用户给出不正确的参数个数的退出码.
5
6 test $# -ne $ARGS && echo "Usage: `basename $0` $ARGS argument(s)" && exit $E_BADARGS
7 # 如果 条件1 测试为真(表示传给脚本的参数不对),
8 #+ 则余下的命令会被执行,并且脚本结束运行.
9
10 # 下面的代码只有当上面的测试失败时才会执行.
11 echo "Correct number of arguments passed to this script."
12
13 exit 0
14
15 # 为了检查退出码,脚本结束后用"echo $?"来查看退出码.
################################End Script#########################################
当然,一个与列表也能给变量设置默认值.
1 arg1=$@ # 不管怎样,设置变量$arg1为命令行参数.
2
3 [ -z "$arg1" ] && arg1=DEFAULT
4 # 如果没有在命令行上指定参数则把$arg1设置为DEFAULT.
或列表(or list)
1 command-1 || command-2 || command-3 || ... command-n
只要前一个命令返回假命令链就会依次执行下去. 一旦有一个命令返回真, 命令链就会结
束(第一个返回真的命令将会是最后一个执行的命令). 这显然和"与列表"正好相反.
Example 25-3 "或列表"和"与列表"的结合使用
################################Start Script#######################################
1 #!/bin/bash
2
3 # delete.sh, 不是很聪明的文件删除功能.
4 # 用法: delete filename
5
6 E_BADARGS=65
7
8 if [ -z "$1" ]
9 then
http://www.818198.com Page 405
SHELL十三问
10 echo "Usage: `basename $0` filename"
11 exit $E_BADARGS # 没有参数? 跳出脚本.
12 else
13 file=$1 # 设置文件名.
14 fi
15
16
17 [ ! -f "$file" ] && echo "File \"$file\" not found. \
18 Cowardly refusing to delete a nonexistent file."
19 # 与列表, 用于文件不存在时给出一个错误信息.
20 # 注意 echo 命令的参数用了一个转义符继续使第二行也是这个命令的参数.
21
22 [ ! -f "$file" ] || (rm -f $file; echo "File \"$file\" deleted.")
23 # 或列表, 用于存在文件时删除此文件.
24
25 # 注意上面两个相反的逻辑.
26 # 与列表为真时才执行, 或列表为假时执行.
27
28 exit 0
################################End Script#########################################
注意: 如果在与列表的第一个命令返回真时,它会执行.
1 # ==> 下面的片断摘自Miquel van Smoorenburg写的 /etc/rc.d/init.d/single 脚本
2 #+==> 示例与和或列表的使用.
3 # ==> "箭头"的注释由本书作者添加.
4
5 [ -x /usr/bin/clear ] && /usr/bin/clear
6 # ==> 如果 /usr/bin/clear 存在, 则调用它.
7 # ==> 在调用一个命令前检查它是否存在,
8 #+==> 以避免产生错误信息和其他难读懂的结果.
9
10 # ==> . . .
11
12 # 如果他们想在单用户模式下运行某些程序, 可能也会运行这个...
13 for i in /etc/rc1.d/S[0-9][0-9]* ; do
14 # 检查脚本是否可执行.
15 [ -x "$i" ] || continue
16 # ==> 如果在目录$PWD中相应的文件没有发现,
17 #+==> 则会跳过此次循环.
18
19 # 不接受备份文件和由rpm产生的文件.
20 case "$1" in
21 *.rpmsave|*.rpmorig|*.rpmnew|*~|*.orig)
22 continue;;
23 esac
24 [ "$i" = "/etc/rc1.d/S00single" ] && continue
25 # ==> 设置脚本名,但还不执行它.
26 $i start
27 done
http://www.818198.com Page 406
SHELL十三问
28
29 # ==> . . .
注意: 与列表或是或列表的退出状态是最后一个执行命令的退出状态.
灵活地组合"与"和"或"列表是允许的,但这样逻辑会很容易变得费解并且需要较多的测试.
1 false && true || echo false # false
2
3 # 结果等同
4 ( false && true ) || echo false # false
5 # 但不同与
6 false && ( true || echo false ) # (没有输出)
7
8 # 注意是从左到右来分组并求值的,
9 #+ 因为逻辑操作符"&&"和"||"有相同的优先处理权.
10
11 # 最好避免这种复杂,除非你确实知道你在做什么.
12
13 # Thanks, S.C.
参考例子 A-7和例子 7-4 演示的使用与/或列表测试变量的例子.
第26章 数组
============
较新的Bash版本支持一维数组. 数组元素可以用符号variable[xx]来初始化. 另外,脚本可以
用declare -a variable语句来清楚地指定一个数组. 要访问一个数组元素,可以使用花括号
来访问,即${variable[xx]}.
Example 26-1 简单的数组用法
################################Start Script#######################################
1 #!/bin/bash
2
3
4 area[11]=23
5 area[13]=37
6 area[51]=UFOs
7
8 # 数组成员不必一定要连贯或连续的.
9
10 # 数组的一部分成员允许不被初始化.
11 # 数组中空缺元素是允许的.
12 # 实际上,保存着稀疏数据的数组(“稀疏数组”)在电子表格处理软件中非常有用.
13 #
14
15
16 echo -n "area[11] = "
17 echo ${area[11]} # {大括号}是需要的.
18
19 echo -n "area[13] = "
20 echo ${area[13]}
21
22 echo "Contents of area[51] are ${area[51]}."
23
http://www.818198.com Page 407
SHELL十三问
24 # 没有初始化内容的数组元素打印空值(NULL值).
25 echo -n "area[43] = "
26 echo ${area[43]}
27 echo "(area[43] unassigned)"
28
29 echo
30
31 # 两个数组元素的和被赋值给另一个数组元素
32 area[5]=`expr ${area[11]} + ${area[13]}`
33 echo "area[5] = area[11] + area[13]"
34 echo -n "area[5] = "
35 echo ${area[5]}
36
37 area[6]=`expr ${area[11]} + ${area[51]}`
38 echo "area[6] = area[11] + area[51]"
39 echo -n "area[6] = "
40 echo ${area[6]}
41 # 这里会失败是因为整数和字符串相加是不允许的.
42
43 echo; echo; echo
44
45 # -----------------------------------------------------------------46 # 另一个数组, "area2".
47 # 另一种指定数组元素的值的办法...
48 # array_name=( XXX YYY ZZZ ... )
49
50 area2=( zero one two three four )
51
52 echo -n "area2[0] = "
53 echo ${area2[0]}
54 # 啊哈, 从0开始计数(即数组的第一个元素是[0], 而不是 [1]).
55
56 echo -n "area2[1] = "
57 echo ${area2[1]} # [1] 是数组的第二个元素.
58 # -----------------------------------------------------------------59
60 echo; echo; echo
61
62 # -----------------------------------------------63 # 第三种数组, "area3".
64 # 第三种指定数组元素值的办法...
65 # array_name=([xx]=XXX [yy]=YYY ...)
66
67 area3=([17]=seventeen [24]=twenty-four)
68
69 echo -n "area3[17] = "
70 echo ${area3[17]}
71
http://www.818198.com Page 408
SHELL十三问
72 echo -n "area3[24] = "
73 echo ${area3[24]}
74 # -----------------------------------------------75
76 exit 0
################################End Script#########################################
注意: Bash 允许把变量当成数组来操作,即使这个变量没有明确地被声明为数组.
1 string=abcABC123ABCabc
2 echo ${string[@]} # abcABC123ABCabc
3 echo ${string[*]} # abcABC123ABCabc
4 echo ${string[0]} # abcABC123ABCabc
5 echo ${string[1]} # 没有输出!
6 # 为什么?
7 echo ${#string[@]} # 1
8 # 数组中只有一个元素.
9 # 且是这个字符串本身.
10
11 # Thank you, Michael Zick, for pointing this out.
类似的示范请参考Bash variables are untyped.
Example 26-2 格式化一首诗
################################Start Script#######################################
1 #!/bin/bash
2 # poem.sh: 排印出作者喜欢的一首诗.
3
4 # 诗的行数 (一小节诗).
5 Line[1]="I do not know which to prefer,"
6 Line[2]="The beauty of inflections"
7 Line[3]="Or the beauty of innuendoes,"
8 Line[4]="The blackbird whistling"
9 Line[5]="Or just after."
10
11 # 出处.
12 Attrib[1]=" Wallace Stevens"
13 Attrib[2]="\"Thirteen Ways of Looking at a Blackbird\""
14 # 此诗是公众的 (版权期已经到期了).
15
16 echo
17
18 for index in 1 2 3 4 5 # 5行.
19 do
20 printf " %s\n" "${Line[index]}"
21 done
22
23 for index in 1 2 # 打印两行出处行.
24 do
25 printf " %s\n" "${Attrib[index]}"
26 done
27
http://www.818198.com Page 409
SHELL十三问
28 echo
29
30 exit 0
31
32 # 练习:
33 # --------34 # 修改这个脚本使其从一个文本文件中提取内容打印一首行.
################################End Script#########################################
数组元素有它们独有的语法, 并且甚至Bash命令和操作符有特殊的选项可以支持数组使用.
Example 26-3 多种数组操作
################################Start Script#######################################
1 #!/bin/bash
2 # array-ops.sh: 数组更多有趣的用法.
3
4
5 array=( zero one two three four five )
6 # 元素 0 1 2 3 4 5
7
8 echo ${array[0]} # zero
9 echo ${array:0} # zero
10 # 第一个元素的参数扩展,
11 #+ 从位置0开始 (即第一个字符).
12 echo ${array:1} # ero
13 # 第一个元素的参数扩展,
14 #+ 从位置1开始 (即第二个字符).
15
16 echo "--------------"
17
18 echo ${#array[0]} # 4
19 # 数组第一个元素的长度.
20 echo ${#array} # 4
21 # 数组第一个元素的长度.
22 # (另一种写法)
23
24 echo ${#array[1]} # 3
25 # 数组第二个元素的长度.
26 # Bash的数组是0开始索引的.
27
28 echo ${#array[*]} # 6
29 # 数组中元素的个数.
30 echo ${#array[@]} # 6
31 # 数组中元素的个数.
32
33 echo "--------------"
34
35 array2=( [0]="first element" [1]="second element" [3]="fourth element" )
36
37 echo ${array2[0]} # 第一个元素
http://www.818198.com Page 410
SHELL十三问
38 echo ${array2[1]} # 第二个元素
39 echo ${array2[2]} #
40 # 因为初始化时没有指定,因此值为空(null).
41 echo ${array2[3]} # 第四个元素
42
43
44 exit 0
################################End Script#########################################
大部分标准的字符串操作符 可以用于数组操作.
Example 26-4 用于数组的字符串操作符
################################Start Script#######################################
1 #!/bin/bash
2 # array-strops.sh: 用于数组的字符串操作符.
3 # 由Michael Zick编码.
4 # 已征得作者的同意.
5
6 # 一般来说,任何类似 ${name ... } 写法的字符串操作符
7 #+ 都能在一个数组的所有字符串元素中使用
8 #+ 像${name[@] ... } 或 ${name[*] ...} 的写法.
9
10
11 arrayZ=( one two three four five five )
12
13 echo
14
15 # 提取尾部的子串
16 echo ${arrayZ[@]:0} # one two three four five five
17 # 所有的元素.
18
19 echo ${arrayZ[@]:1} # two three four five five
20 # 在第一个元素 element[0]后面的所有元素.
21
22 echo ${arrayZ[@]:1:2} # two three
23 # 只提取在元素 element[0]后面的两个元素.
24
25 echo "-----------------------"
26
27 # 子串删除
28 # 从字符串的前部删除最短的匹配,
29 #+ 匹配字串是一个正则表达式.
30
31 echo ${arrayZ[@]#f*r} # one two three five five
32 # 匹配表达式作用于数组所有元素.
33 # 匹配了"four"并把它删除.
34
35 # 字符串前部最长的匹配
36 echo ${arrayZ[@]##t*e} # one two four five five
37 # 匹配表达式作用于数组所有元素.
http://www.818198.com Page 411
SHELL十三问
38 # 匹配"three"并把它删除.
39
40 # 字符串尾部的最短匹配
41 echo ${arrayZ[@]%h*e} # one two t four five five
42 # 匹配表达式作用于数组所有元素.
43 # 匹配"hree"并把它删除.
44
45 # 字符串尾部的最长匹配
46 echo ${arrayZ[@]%%t*e} # one two four five five
47 # 匹配表达式作用于数组所有元素.
48 # 匹配"three"并把它删除.
49
50 echo "-----------------------"
51
52 # 子串替换
53
54 # 第一个匹配的子串会被替换
55 echo ${arrayZ[@]/fiv/XYZ} # one two three four XYZe XYZe
56 # 匹配表达式作用于数组所有元素.
57
58 # 所有匹配的子串会被替换
59 echo ${arrayZ[@]//iv/YY} # one two three four fYYe fYYe
60 # 匹配表达式作用于数组所有元素.
61
62 # 删除所有的匹配子串
63 # 没有指定代替字串意味着删除
64 echo ${arrayZ[@]//fi/} # one two three four ve ve
65 # 匹配表达式作用于数组所有元素.
66
67 # 替换最前部出现的字串
68 echo ${arrayZ[@]/#fi/XY} # one two three four XYve XYve
69 # 匹配表达式作用于数组所有元素.
70
71 # 替换最后部出现的字串
72 echo ${arrayZ[@]/%ve/ZZ} # one two three four fiZZ fiZZ
73 # 匹配表达式作用于数组所有元素.
74
75 echo ${arrayZ[@]/%o/XX} # one twXX three four five five
76 # 为什么?
77
78 echo "-----------------------"
79
80
81 # 在从awk(或其他的工具)取得数据之前 -- 82 # 记得:
83 # $( ... ) 是命令替换.
84 # 函数以子进程运行.
85 # 函数将输出打印到标准输出.
http://www.818198.com Page 412
SHELL十三问
86 # 用read来读取函数的标准输出.
87 # name[@]的写法指定了一个"for-each"的操作.
88
89 newstr() {
90 echo -n "!!!"
91 }
92
93 echo ${arrayZ[@]/%e/$(newstr)}
94 # on!!! two thre!!! four fiv!!! fiv!!!
95 # Q.E.D: 替换部分的动作实际上是一个'赋值'.
96
97 # 使用"For-Each"型的
98 echo ${arrayZ[@]//*/$(newstr optional_arguments)}
99 # 现在Now, 如果if Bash只传递匹配$0的字符串给要调用的函数. . .
100 #
101
102 echo
103
104 exit 0
################################End Script#########################################
命令替换能创建数组的新的单个元素.
Example 26-5 将脚本的内容传给数组
################################Start Script#######################################
1 #!/bin/bash
2 # script-array.sh: 把此脚本的内容传进数组.
3 # 从Chris Martin的e-mail中得到灵感 (多谢!).
4
5 script_contents=( $(cat "$0") ) # 把这个脚本($0)的内容存进数组.
6 #
7
8 for element in $(seq 0 $((${#script_contents[@]} - 1)))
9 do # ${#script_contents[@]}
10 #+ 表示数组中元素的个数.
11 #
12 # 问题:
13 # 为什么需要 seq 0 ?
14 # 试试更改成 seq 1.
15 echo -n "${script_contents[$element]}"
16 # 将脚本的每行列成一个域.
17 echo -n " -- " # 使用" -- "作为域分隔符.
18 done
19
20 echo
21
22 exit 0
23
24 # 练习:
25 # --------http://www.818198.com Page 413
SHELL十三问
26 # 修改这个脚本使它能按照它原本的格式输出,
27 #+ 连同空白符,换行,等等.
28 #
################################End Script#########################################
在数组的环境里, 一些 Bash 内建的命令 含义有一些轻微的改变. 例如, unset 会删除数组
元素, 或甚至删除整个数组.
Example 26-6 一些数组专用的工具
################################Start Script#######################################
1 #!/bin/bash
2
3 declare -a colors
4 # 所有脚本后面的命令都会把
5 #+ 变量"colors"作为数组对待.
6
7 echo "Enter your favorite colors (separated from each other by a space)."
8
9 read -a colors # 键入至少3种颜色以用于下面的示例.
10 # 指定'read'命令的选项,
11 #+ 允许指定数组元素.
12
13 echo
14
15 element_count=${#colors[@]}
16 # 专用语法来提取数组元素的个数.
17 # element_count=${#colors[*]} 也可以.
18 #
19 # "@"变量允许分割引号内的单词
20 #+ (依靠空白字符来分隔变量).
21 #
22 # 这就像"$@" 和"$*"在位置参数中表现出来的一样.
23 #
24
25 index=0
26
27 while [ "$index" -lt "$element_count" ]
28 do # List all the elements in the array.
29 echo ${colors[$index]}
30 let "index = $index + 1"
31 done
32 # 每个数组元素被列为单独的一行.
33 # 如果这个没有要求, 可以用 echo -n "${colors[$index]} "
34 #
35 # 可以用一个"for"循环来做:
36 # for i in "${colors[@]}"
37 # do
38 # echo "$i"
39 # done
40 # (Thanks, S.C.)
http://www.818198.com Page 414
SHELL十三问
41
42 echo
43
44 # 再次列出数组中所有的元素, 但使用更优雅的做法.
45 echo ${colors[@]} # echo ${colors[*]} 也可以.
46
47 echo
48
49 # "unset"命令删除一个数组元素或是整个数组.
50 unset colors[1] # 删除数组的第二个元素.
51 # 作用等同于 colors[1]=
52 echo ${colors[@]} # 再列出数组,第二个元素没有了.
53
54 unset colors # 删除整个数组.
55 # unset colors[*] 或
56 #+ unset colors[@] 都可以.
57 echo; echo -n "Colors gone."
58 echo ${colors[@]} # 再列出数组, 则为空了.
59
60 exit 0
################################End Script#########################################
正如在前面的例子中看到的, ${array_name[@]}和${array_name[*]} 都与数组的所有元素相
关. 同样地, 为了计算数组的元素个数, 可以用${#array_name[@]} 或${#array_name[*]}.
${#array_name} 是数组第一个元素${array_name[0]}的长度(字符数) .
Example 26-7 关于空数组和空数组元素
################################Start Script#######################################
1 #!/bin/bash
2 # empty-array.sh
3
4 # 多谢 Stephane Chazelas 制作这个例子最初的版本,
5 #+ 并由 Michael Zick 扩展了.
6
7
8 # 空数组不同与含有空值元素的数组.
9
10 array0=( first second third )
11 array1=( '' ) # "array1" 由一个空元素组成.
12 array2=( ) # 没有元素 . . . "array2" 是空的.
13
14 echo
15 ListArray()
16 {
17 echo
18 echo "Elements in array0: ${array0[@]}"
19 echo "Elements in array1: ${array1[@]}"
20 echo "Elements in array2: ${array2[@]}"
21 echo
22 echo "Length of first element in array0 = ${#array0}"
http://www.818198.com Page 415
SHELL十三问
23 echo "Length of first element in array1 = ${#array1}"
24 echo "Length of first element in array2 = ${#array2}"
25 echo
26 echo "Number of elements in array0 = ${#array0[*]}" # 3
27 echo "Number of elements in array1 = ${#array1[*]}" # 1 (惊奇!)
28 echo "Number of elements in array2 = ${#array2[*]}" # 0
29 }
30
31 # ===================================================================
32
33 ListArray
34
35 # 尝试扩展这些数组.
36
37 # 增加一个元素到数组.
38 array0=( "${array0[@]}" "new1" )
39 array1=( "${array1[@]}" "new1" )
40 array2=( "${array2[@]}" "new1" )
41
42 ListArray
43
44 # 或
45 array0[${#array0[*]}]="new2"
46 array1[${#array1[*]}]="new2"
47 array2[${#array2[*]}]="new2"
48
49 ListArray
50
51 # 当像上面的做法增加数组时,数组像 '栈'
52 # 上面的做法是 'push(压栈)'
53 # 栈高是:
54 height=${#array2[@]}
55 echo
56 echo "Stack height for array2 = $height"
57
58 # 'pop(出栈)' 是:
59 unset array2[${#array2[@]}-1] # 数组是以0开始索引的,
60 height=${#array2[@]} #+ 这就意味着第一个元素下标是 0.
61 echo
62 echo "POP"
63 echo "New stack height for array2 = $height"
64
65 ListArray
66
67 # 只列出数组array0的第二和第三个元素.
68 from=1 #是以0开始的数字
69 to=2 #
70 array3=( ${array0[@]:1:2} )
http://www.818198.com Page 416
SHELL十三问
71 echo
72 echo "Elements in array3: ${array3[@]}"
73
74 # 像一个字符串一样处理(字符的数组).
75 # 试试其他的字符串格式.
76
77 # 替换:
78 array4=( ${array0[@]/second/2nd} )
79 echo
80 echo "Elements in array4: ${array4[@]}"
81
82 # 替换所有匹配通配符的字符串.
83 array5=( ${array0[@]//new?/old} )
84 echo
85 echo "Elements in array5: ${array5[@]}"
86
87 # 当你开始觉得对此有把握的时候 . . .
88 array6=( ${array0[@]#*new} )
89 echo # 这个可能会使你感到惊奇.
90 echo "Elements in array6: ${array6[@]}"
91
92 array7=( ${array0[@]#new1} )
93 echo # 数组array6之后就没有惊奇了.
94 echo "Elements in array7: ${array7[@]}"
95
96 # 这看起来非常像 . . .
97 array8=( ${array0[@]/new1/} )
98 echo
99 echo "Elements in array8: ${array8[@]}"
100
101 # 那么我们怎么总结它呢So what can one say about this?
102
103 # 字符串操作在数组var[@]的每一个元素中执行.
104 #
105 # 因此Therefore : 如果结果是一个零长度的字符串,
106 #+ Bash支持字符串向量操作,
107 #+ 元素会在结果赋值中消失不见.
108
109 # 提问, 这些字符串是强还是弱引用?
110
111 zap='new*'
112 array9=( ${array0[@]/$zap/} )
113 echo
114 echo "Elements in array9: ${array9[@]}"
115
116 # 当你还在想你在Kansas州的何处时 . . .
117 array10=( ${array0[@]#$zap} )
118 echo
http://www.818198.com Page 417
SHELL十三问
119 echo "Elements in array10: ${array10[@]}"
120
121 # 把 array7 和 array10比较.
122 # 把 array8 和 array9比较.
123
124 # 答案: 必须用弱引用.
125
126 exit 0
################################End Script#########################################
${array_name[@]}和${array_name[*]} 的关系类似于$@ and $*. 这种数组用法非常有用.
1 # 复制一个数组.
2 array2=( "${array1[@]}" )
3 # 或
4 array2="${array1[@]}"
5
6 # 给数组增加一个元素.
7 array=( "${array[@]}" "new element" )
8 # 或
9 array[${#array[*]}]="new element"
10
11 # Thanks, S.C.
注意: array=( element1 element2 ... elementN ) 初始化操作, 依赖于命令替换
(command substitution)使将一个文本内容加载进数组成为可能.
1 #!/bin/bash
2
3 filename=sample_file
4
5 # cat sample_file
6 #
7 # 1 a b c
8 # 2 d e fg
9
10
11 declare -a array1
12
13 array1=( `cat "$filename"`) # 加载$filename文件的内容进数组array1.
14 # 打印文件到标准输出 #
15 #
16 # array1=( `cat "$filename" | tr '\n' ' '`)
17 # 把文件里的换行变为空格.
18 # 这是没必要的,因为Bash做单词分割时会把换行变为空格.
19 #
20
21 echo ${array1[@]} # 打印数组.
22 # 1 a b c 2 d e fg
23 #
24 # 文件中每个由空白符分隔开的“词”都被存在数组的一个元素里
25 #
http://www.818198.com Page 418
SHELL十三问
26
27 element_count=${#array1[*]}
28 echo $element_count # 8
出色的技巧使数组的操作技术又多了一种.
Example 26-8 初始化数组
################################Start Script#######################################
1 #! /bin/bash
2 # array-assign.bash
3
4 # 数组操作是Bash特有的,
5 #+ 因此脚本名用".bash"结尾.
6
7 # Copyright (c) Michael S. Zick, 2003, All rights reserved.
8 # 许可证: 没有任何限制,可以用于任何目的的反复使用.
9 # Version: $ID$
10 #
11 # 由William Park添加注释.
12
13 # 基于Stephane Chazelas提供在本书中的一个例子
14 #
15
16 # 'times' 命令的输出格式:
17 # User CPU <空格> System CPU
18 # User CPU of dead children <空格> System CPU of dead children
19
20 # Bash赋一个数组的所有元素给新的数组变量有两种办法.
21 #
22 # 在Bash版本2.04, 2.05a 和 2.05b,
23 #+ 这两种办法都对NULL的值的元素全部丢弃.
24 # 另一种数组赋值办法是维护[下标]=值之间的关系将会在新版本的Bash支持.
25 #
26
27 # 可以用外部命令来构造一个大数组,
28 #+ 但几千个元素的数组如下就可以构造了.
29 #
30
31 declare -a bigOne=( /dev/* )
32 echo
33 echo 'Conditions: Unquoted, default IFS, All-Elements-Of'
34 echo "Number of elements in array is ${#bigOne[@]}"
35
36 # set -vx
37
38
39
40 echo
41 echo '- - testing: =( ${array[@]} ) - -'
42 times
http://www.818198.com Page 419
SHELL十三问
43 declare -a bigTwo=( ${bigOne[@]} )
44 # ^ ^
45 times
46
47 echo
48 echo '- - testing: =${array[@]} - -'
49 times
50 declare -a bigThree=${bigOne[@]}
51 # 这次没有用括号.
52 times
53
54 # 正如Stephane Chazelas指出的那样比较输出的数组可以了解第二种格式的赋值比第三和第四的times的更快
55 #
56 #
57 # William Park 解释explains:
58 #+ bigTwo 数组是被赋值了一个单字符串,
59 #+ bigThree 则赋值时一个一个元素的赋值.
60 # 所以, 实际上的情况是:
61 # bigTwo=( [0]="... ... ..." )
62 # bigThree=( [0]="..." [1]="..." [2]="..." ... )
63
64
65 # 我在本书的例子中仍然会继续用第一种格式,
66 #+ 因为我认为这会对说明清楚更有帮助.
67
68 # 我的例子中的可复用的部分实际上还是会使用第二种格式,
69 #+ 因为这种格式更快一些.
70
71 # MSZ: 很抱歉早先的失误(应是指本书的先前版本).
72
73
74 # 注:
75 # ----76 # 在31和43行的"declare -a"语句不是必须的,
77 #+ 因为会在使用Array=( ... )赋值格式时暗示它是数组.
78 #
79 # 但是, 省略这些声明会导致后面脚本的相关操作更慢一些.
80 #
81 # 试一下, 看有什么变化.
82
83 exit 0
################################End Script#########################################
注意: 对变量增加 declare -a 语句声明可以加速后面的数组操作速度.
Example 26-9 复制和连接数组
################################Start Script#######################################
1 #! /bin/bash
2 # CopyArray.sh
3 #
http://www.818198.com Page 420
SHELL十三问
4 # 由 Michael Zick编写.
5 # 在本书中使用已得到许可.
6
7 # 怎么传递变量名和值处理,返回就用使用该变量,
8 #+ 或说"创建你自己的赋值语句".
9
10
11 CpArray_Mac() {
12
13 # 创建赋值命令语句
14
15 echo -n 'eval '
16 echo -n "$2" # 目的变量名
17 echo -n '=( ${'
18 echo -n "$1" # 源名字
19 echo -n '[@]} )'
20
21 # 上面的全部会合成单个命令.
22 # 这就是函数所有的功能.
23 }
24
25 declare -f CopyArray # 函数"指针"
26 CopyArray=CpArray_Mac # 建立命令
27
28 Hype()
29 {
30
31 # 要复制的数组名为 $1.
32 # (接合数组,并包含尾部的字符串"Really Rocks".)
33 # 返回结果的数组名为 $2.
34
35 local -a TMP
36 local -a hype=( Really Rocks )
37
38 $($CopyArray $1 TMP)
39 TMP=( ${TMP[@]} ${hype[@]} )
40 $($CopyArray TMP $2)
41 }
42
43 declare -a before=( Advanced Bash Scripting )
44 declare -a after
45
46 echo "Array Before = ${before[@]}"
47
48 Hype before after
49
50 echo "Array After = ${after[@]}"
51
http://www.818198.com Page 421
SHELL十三问
52 # 有多余的字符串?
53
54 echo "What ${after[@]:3:2}?"
55
56 declare -a modest=( ${after[@]:2:1} ${after[@]:3:2} )
57 # ---- 子串提取 ----58
59 echo "Array Modest = ${modest[@]}"
60
61 # 'before'变量变成什么了 ?
62
63 echo "Array Before = ${before[@]}"
64
65 exit 0
################################End Script#########################################
Example 26-10 关于连接数组的更多信息
################################Start Script#######################################
1 #! /bin/bash
2 # array-append.bash
3
4 # Copyright (c) Michael S. Zick, 2003, All rights reserved.
5 # 许可: 可以无限制的以任何目的任何格式重复使用.
6 # 版本: $ID$
7 #
8 # 格式上由M.C做了轻微的修改.
9
10
11 # 数组操作是Bash特有的属性.
12 # 原来的 UNIX /bin/sh 没有类似的功能.
13
14
15 # 把此脚本的输出管道输送给 'more'
16 #+ 以便输出不会滚过终端屏幕.
17
18
19 # 下标依次使用.
20 declare -a array1=( zero1 one1 two1 )
21 # 下标有未使用的 ([1] 没有被定义).
22 declare -a array2=( [0]=zero2 [2]=two2 [3]=three2 )
23
24 echo
25 echo '- Confirm that the array is really subscript sparse. -'
26 echo "Number of elements: 4" # 这儿是举例子就用硬编码.
27 for (( i = 0 ; i < 4 ; i++ ))
28 do
29 echo "Element [$i]: ${array2[$i]}"
30 done
31 # 也可以参考basics-reviewed.bash更多的常见代码.
http://www.818198.com Page 422
SHELL十三问
32
33
34 declare -a dest
35
36 # 组合 (添加) 两个数组到第三个数组.
37 echo
38 echo 'Conditions: Unquoted, default IFS, All-Elements-Of operator'
39 echo '- Undefined elements not present, subscripts not maintained. -'
40 # # 那些未定义的元素不存在; 组合时会丢弃这些元素.
41
42 dest=( ${array1[@]} ${array2[@]} )
43 # dest=${array1[@]}${array2[@]} # 奇怪的结果, 或者叫臭虫.
44
45 # 现在, 打印出结果.
46 echo
47 echo '- - Testing Array Append - -'
48 cnt=${#dest[@]}
49
50 echo "Number of elements: $cnt"
51 for (( i = 0 ; i < cnt ; i++ ))
52 do
53 echo "Element [$i]: ${dest[$i]}"
54 done
55
56 # 把一个数组赋值给另一个数组的单个元素 (两次).
57 dest[0]=${array1[@]}
58 dest[1]=${array2[@]}
59
60 # 列出结果.
61 echo
62 echo '- - Testing modified array - -'
63 cnt=${#dest[@]}
64
65 echo "Number of elements: $cnt"
66 for (( i = 0 ; i < cnt ; i++ ))
67 do
68 echo "Element [$i]: ${dest[$i]}"
69 done
70
71 # 检测第二个元素的改变.
72 echo
73 echo '- - Reassign and list second element - -'
74
75 declare -a subArray=${dest[1]}
76 cnt=${#subArray[@]}
77
78 echo "Number of elements: $cnt"
79 for (( i = 0 ; i < cnt ; i++ ))
http://www.818198.com Page 423
SHELL十三问
80 do
81 echo "Element [$i]: ${subArray[$i]}"
82 done
83
84 # 用 '=${ ... }' 把整个数组的值赋给另一个数组的单个元素
85 #+ 使数组所有元素值被转换成了一个字符串,各元素的值由一个空格分开(其实是IFS的第一个字符).
86 #
87 #
88
89 # 如果原先的元素没有包含空白符 . . .
90 # 如果原先的数组下标都是连续的 . . .
91 # 我们就能取回最初的数组结构.
92
93 # 恢复第二个元素的修改回元素.
94 echo
95 echo '- - Listing restored element - -'
96
97 declare -a subArray=( ${dest[1]} )
98 cnt=${#subArray[@]}
99
100 echo "Number of elements: $cnt"
101 for (( i = 0 ; i < cnt ; i++ ))
102 do
103 echo "Element [$i]: ${subArray[$i]}"
104 done
105 echo '- - Do not depend on this behavior. - -'
106 echo '- - This behavior is subject to change - -'
107 echo '- - in versions of Bash newer than version 2.05b - -'
108
109 # MSZ: 很抱歉早先时混淆的几个要点(译者注:应该是指本书早先的版本).
110
111 exit 0
################################End Script#########################################
--数组允许在脚本中实现一些常见的熟悉算法.这是否是必要的好想法在此不讨论,留给读者自
行判断.
Example 26-11 一位老朋友: 冒泡排序
################################Start Script#######################################
1 #!/bin/bash
2 # bubble.sh: 排序法之冒泡排序.
3
4 # 回忆冒泡排序法. 在这个版本中要实现它...
5
6 # 靠连续地多次比较数组元素来排序,
7 #+ 比较两个相邻的元素,如果排序顺序不对,则交换两者的顺序.
8 # 当第一轮比较结束后,最"重"的元素就被排到了最底部.
9 # 当第二轮比较结束后,第二"重"的元素就被排到了次底部的位置.
10 # 以此类推.
http://www.818198.com Page 424
SHELL十三问
11 # 这意味着每轮的比较不需要比较先前已"沉淀"好的数据.
12 # 因此你会注意到后面数据的打印会比较快一些.
13
14
15 exchange()
16 {
17 # 交换数组的两个元素.
18 local temp=${Countries[$1]} # 临时保存要交换的一个元素.
19 #
20 Countries[$1]=${Countries[$2]}
21 Countries[$2]=$temp
22
23 return
24 }
25
26 declare -a Countries # 声明数组,
27 #+ 在此是可选的,因为下面它会被按数组来初始化.
28
29 # 是否允许用转义符(\)将数组的各变量值放到几行上?
30 #
31 # 是的.
32
33 Countries=(Netherlands Ukraine Zaire Turkey Russia Yemen Syria \
34 Brazil Argentina Nicaragua Japan Mexico Venezuela Greece England \
35 Israel Peru Canada Oman Denmark Wales France Kenya \
36 Xanadu Qatar Liechtenstein Hungary)
37
38 # "Xanadu" 是个虚拟的充满美好的神话之地.
39 #
40
41
42 clear # 开始之前清除屏幕.
43
44 echo "0: ${Countries[*]}" # 从0索引的元素开始列出整个数组.
45
46 number_of_elements=${#Countries[@]}
47 let "comparisons = $number_of_elements - 1"
48
49 count=1 # 传递数字.
50
51 while [ "$comparisons" -gt 0 ] # 开始外部的循环
52 do
53
54 index=0 # 每轮开始前重设索引值为0.
55
56 while [ "$index" -lt "$comparisons" ] # 开始内部循环
57 do
58 if [ ${Countries[$index]} \> ${Countries[`expr $index + 1`]} ]
http://www.818198.com Page 425
SHELL十三问
59 # 如果原来的排序次序不对...
60 # 回想一下 \> 在单方括号里是is ASCII 码的比较操作符.
61 #
62
63 # if [[ ${Countries[$index]} > ${Countries[`expr $index + 1`]} ]]
64 #+ 也可以.
65 then
66 exchange $index `expr $index + 1` # 交换.
67 fi
68 let "index += 1"
69 done # 内部循环结束
70
71 # ----------------------------------------------------------------------72 # Paulo Marcel Coelho Aragao 建议使用更简单的for-loops.
73 #
74 # for (( last = $number_of_elements - 1 ; last > 1 ; last-- ))
75 # do
76 # for (( i = 0 ; i < last ; i++ ))
77 # do
78 # [[ "${Countries[$i]}" > "${Countries[$((i+1))]}" ]] \
79 # && exchange $i $((i+1))
80 # done
81 # done
82 # ----------------------------------------------------------------------83
84
85 let "comparisons -= 1" # 因为最"重"的元素冒到了最底部,
86 #+ 我们可以每轮少做一些比较.
87
88 echo
89 echo "$count: ${Countries[@]}" # 每轮结束后,打印一次数组.
90 echo
91 let "count += 1" # 增加传递计数.
92
93 done # 外部循环结束
94 # 完成.
95
96 exit 0
################################End Script#########################################
--在数组内嵌一个数组有可能做到吗?
1 #!/bin/bash
2 # "内嵌" 数组.
3
4 # Michael Zick 提供这个例子,
5 #+ 由William Park作了些纠正和解释.
6
7 AnArray=( $(ls --inode --ignore-backups --almost-all \
http://www.818198.com Page 426
SHELL十三问
8 --directory --full-time --color=none --time=status \
9 --sort=time -l ${PWD} ) ) # 命令及选项.
10
11 # 空格是有意义的 . . . 不要在上面引号引用任何东西.
12
13 SubArray=( ${AnArray[@]:11:1} ${AnArray[@]:6:5} )
14 # 这个数组有6个元素:
15 #+ SubArray=( [0]=${AnArray[11]} [1]=${AnArray[6]} [2]=${AnArray[7]}
16 # [3]=${AnArray[8]} [4]=${AnArray[9]} [5]=${AnArray[10]} )
17 #
18 # Bash中的数组像是字符串(char *)型的(循环)链表.
19 #
20 # 因此, 这实际上不是内嵌的数组,
21 #+ 但它的功能是相似的.
22
23 echo "Current directory and date of last status change:"
24 echo "${SubArray[@]}"
25
26 exit 0
--内嵌数组和间接引用(indirect references) 的组合使用产生了一些有趣的用法.
Example 26-12 内嵌数组和间接引用
################################Start Script#######################################
1 #!/bin/bash
2 # embedded-arrays.sh
3 # 内嵌数组和间接引用.
4
5 # 由Dennis Leeuw编写.
6 # 已获使用许可.
7 # 由本文作者修改.
8
9
10 ARRAY1=(
11 VAR1_1=value11
12 VAR1_2=value12
13 VAR1_3=value13
14 )
15
16 ARRAY2=(
17 VARIABLE="test"
18 STRING="VAR1=value1 VAR2=value2 VAR3=value3"
19 ARRAY21=${ARRAY1[*]}
20 ) # 把ARRAY1数组嵌到这个数组里.
21
22 function print () {
23 OLD_IFS="$IFS"
24 IFS=$'\n' # 这是为了在每个行打印一个数组元素.
25 #
http://www.818198.com Page 427
SHELL十三问
26 TEST1="ARRAY2[*]"
27 local ${!TEST1} # 试下删除这行会发生什么.
28 # 间接引用.
29 # 这使 $TEST1只在函数内存取.
30 #
31
32
33 # 我们看看还能干点什么.
34 echo
35 echo "\$TEST1 = $TEST1" # 变量的名称.
36 echo; echo
37 echo "{\$TEST1} = ${!TEST1}" # 变量的内容.
38 # 这就是间接引用的作用.
39 #
40 echo
41 echo "-------------------------------------------"; echo
42 echo
43
44
45 # 打印变量
46 echo "Variable VARIABLE: $VARIABLE"
47
48 # 打印一个字符串元素
49 IFS="$OLD_IFS"
50 TEST2="STRING[*]"
51 local ${!TEST2} # 间接引用 (像上面一样).
52 echo "String element VAR2: $VAR2 from STRING"
53
54 # 打印一个字符串元素
55 TEST2="ARRAY21[*]"
56 local ${!TEST2} # 间接引用 (像上面一样).
57 echo "Array element VAR1_1: $VAR1_1 from ARRAY21"
58 }
59
60 print
61 echo
62
63 exit 0
64
65 # 脚本作者注,
66 #+ "你可以很容易地将其扩展成Bash的一个能创建hash的脚本."
67 # (难) 留给读者的练习: 实现它.
################################End Script#########################################
--数组使埃拉托色尼素数筛子有了shell脚本的实现. 当然, 如果是追求效率的应用自然应该用
一种编译型的语言,例如用C. 这种脚本运行实在是太慢.
Example 26-13 复杂数组应用: 埃拉托色尼素数筛子
################################Start Script#######################################
http://www.818198.com Page 428
SHELL十三问
1 #!/bin/bash
2 # sieve.sh (ex68.sh)
3
4 # 埃拉托色尼素数筛子
5 # 找素数的经典算法.
6
7 # 在同等数量的数值内这个脚本比用C写的版本慢很多.
8 #
9
10 LOWER_LIMIT=1 # 从1开始.
11 UPPER_LIMIT=1000 # 到 1000.
12 # (如果你很有时间的话,你可以把它设得更高 . . . )
13
14 PRIME=1
15 NON_PRIME=0
16
17 let SPLIT=UPPER_LIMIT/2
18 # 优化:
19 # 只需要测试中间到最大之间的值 (为什么?).
20
21
22 declare -a Primes
23 # Primes[] 是一个数组.
24
25
26 initialize ()
27 {
28 # 初始化数组.
29
30 i=$LOWER_LIMIT
31 until [ "$i" -gt "$UPPER_LIMIT" ]
32 do
33 Primes[i]=$PRIME
34 let "i += 1"
35 done
36 # 假定所有的数组成员都是需要检查的 (素数)
37 #+ 一直到检查完成前.
38 }
39
40 print_primes ()
41 {
42 # 打印出所有Primes[]数组中被标记为素数的元素.
43
44 i=$LOWER_LIMIT
45
46 until [ "$i" -gt "$UPPER_LIMIT" ]
47 do
48
http://www.818198.com Page 429
SHELL十三问
49 if [ "${Primes[i]}" -eq "$PRIME" ]
50 then
51 printf "%8d" $i
52 # 每个数字打印前先打印8个空格, 数字是在偶数列打印的.
53 fi
54
55 let "i += 1"
56
57 done
58
59 }
60
61 sift () # 查出非素数.
62 {
63
64 let i=$LOWER_LIMIT+1
65 # 我们都知道1是素数, 所以我们从2开始.
66
67 until [ "$i" -gt "$UPPER_LIMIT" ]
68 do
69
70 if [ "${Primes[i]}" -eq "$PRIME" ]
71 # 不要处理已经过滤过的数字 (被标识为非素数).
72 then
73
74 t=$i
75
76 while [ "$t" -le "$UPPER_LIMIT" ]
77 do
78 let "t += $i "
79 Primes[t]=$NON_PRIME
80 # 标识为非素数.
81 done
82
83 fi
84
85 let "i += 1"
86 done
87
88
89 }
90
91
92 # ==============================================
93 # main ()
94 # 继续调用函数.
95 initialize
96 sift
http://www.818198.com Page 430
SHELL十三问
97 print_primes
98 # 这就是被称为结构化编程的东西了.
99 # ==============================================
100
101 echo
102
103 exit 0
104
105
106
107 # -------------------------------------------------------- #
108 # 因为前面的一个'exit',所以下面的代码不会被执行.
109
110 # 下面是Stephane Chazelas写的一个埃拉托色尼素数筛子的改进版本,
111 #+ 运行会稍微快一点.
112
113 # 必须在命令行上指定参数(寻找素数的限制范围).
114
115 UPPER_LIMIT=$1 # 值来自命令行.
116 let SPLIT=UPPER_LIMIT/2 # 从中间值到最大值.
117
118 Primes=( '' $(seq $UPPER_LIMIT) )
119
120 i=1
121 until (( ( i += 1 ) > SPLIT )) # 仅需要从中间值检查.
122 do
123 if [[ -n $Primes[i] ]]
124 then
125 t=$i
126 until (( ( t += i ) > UPPER_LIMIT ))
127 do
128 Primes[t]=
129 done
130 fi
131 done
132 echo ${Primes[*]}
133
134 exit 0
################################End Script#########################################
比较这个用数组的素数产生器和另一种不用数组的例子 A-16.
--
数组可以做一定程度的扩展,以模拟支持Bash原本不支持的数据结构.
Example 26-14 模拟下推的堆栈
################################Start Script#######################################
1 #!/bin/bash
2 # stack.sh: 下推的堆栈模拟
3
4 # 类似于CPU栈, 下推的堆栈依次保存数据项,
http://www.818198.com Page 431
SHELL十三问
5 #+ 但取出时则反序进行, 后进先出.
6
7 BP=100 # 栈数组的基点指针.
8 # 从元素100开始.
9
10 SP=$BP # 栈指针.
11 # 初始化栈底.
12
13 Data= # 当前栈的内容.
14 # 必须定义成全局变量,
15 #+ 因为函数的返回整数有范围限制.
16
17 declare -a stack
18
19
20 push() # 把一个数据项压入栈.
21 {
22 if [ -z "$1" ] # 没有可压入的?
23 then
24 return
25 fi
26
27 let "SP -= 1" # 更新堆栈指针.
28 stack[$SP]=$1
29
30 return
31 }
32
33 pop() # 从栈中弹出一个数据项.
34 {
35 Data= # 清空保存数据项中间变量.
36
37 if [ "$SP" -eq "$BP" ] # 已经没有数据可弹出?
38 then
39 return
40 fi # 这使SP不会超过100,
41 #+ 例如, 这可保护一个失控的堆栈.
42
43 Data=${stack[$SP]}
44 let "SP += 1" # 更新堆栈指针.
45 return
46 }
47
48 status_report() # 打印堆栈的当前状态.
49 {
50 echo "-------------------------------------"
51 echo "REPORT"
52 echo "Stack Pointer = $SP"
http://www.818198.com Page 432
SHELL十三问
53 echo "Just popped \""$Data"\" off the stack."
54 echo "-------------------------------------"
55 echo
56 }
57
58
59 # =======================================================
60 # 现在,来点乐子.
61
62 echo
63
64 # 看你是否能从空栈里弹出数据项来.
65 pop
66 status_report
67
68 echo
69
70 push garbage
71 pop
72 status_report # 压入garbage, 弹出garbage.
73
74 value1=23; push $value1
75 value2=skidoo; push $value2
76 value3=FINAL; push $value3
77
78 pop # FINAL
79 status_report
80 pop # skidoo
81 status_report
82 pop # 23
83 status_report # 后进, 先出!
84
85 # 注意堆栈指针每次压栈时减,
86 #+ 每次弹出时加一.
87
88 echo
89
90 exit 0
91
92 # =======================================================
93
94
95 # 练习:
96 # --------- 97
98 # 1) 修改"push()"函数,使其调用一次就能够压入多个数据项.
99 #
100
http://www.818198.com Page 433
SHELL十三问
101 # 2) 修改"pop()"函数,使其调用一次就能弹出多个数据项.
102 #
103
104 # 3) 给那些有临界操作的函数增加出错检查.
105 # 即是指是否一次完成操作或没有完成操作返回相应的代码,
106 # + 没有完成要启动合适的处理动作.
107 #
108
109 # 4) 这个脚本为基础,
110 # + 写一个栈实现的四则运算计算器.
################################End Script#########################################
--
要想操作数组的下标需要中间变量. 如果确实要这么做, 可以考虑使用一种更强功能的编程语
言, 例如 Perl 或 C.
Example 26-15 复杂的数组应用: 列出一种怪异的数学序列
################################Start Script#######################################
1 #!/bin/bash
2
3 # Douglas Hofstadter的有名的"Q-series":
4
5 # Q(1) = Q(2) = 1
6 # Q(n) = Q(n - Q(n-1)) + Q(n - Q(n-2)), 当 n>2 时
7
8 # 这是令人感到陌生的也是没有规律的"乱序"整数序列.
9 # 序列的头20个如下所示:
10 # 1 1 2 3 3 4 5 5 6 6 6 8 8 8 10 9 10 11 11 12
11
12 # 参考Hofstadter的书, "Goedel, Escher, Bach: An Eternal Golden Braid",
13 #+ 页码 137.
14
15
16 LIMIT=100 # 计算数的个数.
17 LINEWIDTH=20 # 很行要打印的数的个数.
18
19 Q[1]=1 # 序列的头2个是 1.
20 Q[2]=1
21
22 echo
23 echo "Q-series [$LIMIT terms]:"
24 echo -n "${Q[1]} " # 打印头2个数.
25 echo -n "${Q[2]} "
26
27 for ((n=3; n <= $LIMIT; n++)) # C风格的循环条件.
28 do # Q[n] = Q[n - Q[n-1]] + Q[n - Q[n-2]] 当 n>2 时
29 # 需要将表达式分步计算,
30 #+ 因为Bash不擅长处理此类复杂计算.
31
32 let "n1 = $n - 1" # n-1
http://www.818198.com Page 434
SHELL十三问
33 let "n2 = $n - 2" # n-2
34
35 t0=`expr $n - ${Q[n1]}` # n - Q[n-1]
36 t1=`expr $n - ${Q[n2]}` # n - Q[n-2]
37
38 T0=${Q[t0]} # Q[n - Q[n-1]]
39 T1=${Q[t1]} # Q[n - Q[n-2]]
40
41 Q[n]=`expr $T0 + $T1` # Q[n - Q[n-1]] + Q[n - Q[n-2]]
42 echo -n "${Q[n]} "
43
44 if [ `expr $n % $LINEWIDTH` -eq 0 ] # 格式化输出.
45 then # ^ 取模操作
46 echo # 把行分成内部的块.
47 fi
48
49 done
50
51 echo
52
53 exit 0
54
55 # 这是Q-series问题的迭代实现.
56 # 更直接明了的递归实现留给读者完成.
57 # 警告: 递归地计算这个序列会花很长的时间.
################################End Script#########################################
--
Bash 只支持一维数组,但有一些技巧可用来模拟多维数组.
Example 26-16 模拟二维数组,并使它倾斜
################################Start Script#######################################
1 #!/bin/bash
2 # twodim.sh: 模拟二维数组.
3
4 # 一维数组由单行组成.
5 # 二维数组由连续的行组成.
6
7 Rows=5
8 Columns=5
9 # 5 X 5 的数组Array.
10
11 declare -a alpha # char alpha [Rows] [Columns];
12 # 不必要的声明. 为什么?
13
14 load_alpha ()
15 {
16 local rc=0
17 local index
18
http://www.818198.com Page 435
SHELL十三问
19 for i in A B C D E F G H I J K L M N O P Q R S T U V W X Y
20 do # 如果你高兴,可以使用不同的符号.
21 local row=`expr $rc / $Columns`
22 local column=`expr $rc % $Rows`
23 let "index = $row * $Rows + $column"
24 alpha[$index]=$i
25 # alpha[$row][$column]
26 let "rc += 1"
27 done
28
29 # 更简单的办法
30 #+ declare -a alpha=( A B C D E F G H I J K L M N O P Q R S T U V W X Y )
31 #+ 但这就缺少了二维数组的感觉了.
32 }
33
34 print_alpha ()
35 {
36 local row=0
37 local index
38
39 echo
40
41 while [ "$row" -lt "$Rows" ] # 以行顺序为索引打印行的各元素:
42 do #+ 即数组列值变化快,
43 #+ 行值变化慢.
44 local column=0
45
46 echo -n " " # 依行倾斜打印正方形的数组.
47
48 while [ "$column" -lt "$Columns" ]
49 do
50 let "index = $row * $Rows + $column"
51 echo -n "${alpha[index]} " # alpha[$row][$column]
52 let "column += 1"
53 done
54
55 let "row += 1"
56 echo
57
58 done
59
60 # 等同于
61 # echo ${alpha[*]} | xargs -n $Columns
62
63 echo
64 }
65
66 filter () # 过滤出负数的数组索引.
http://www.818198.com Page 436
SHELL十三问
67 {
68
69 echo -n " " # 产生倾斜角度.
70 # 解释怎么办到的.
71
72 if [[ "$1" -ge 0 && "$1" -lt "$Rows" && "$2" -ge 0 && "$2" -lt "$Columns" ]]
73 then
74 let "index = $1 * $Rows + $2"
75 # Now, print it rotated现在,打印旋转角度.
76 echo -n " ${alpha[index]}"
77 # alpha[$row][$column]
78 fi
79
80 }
81
82
83
84
85 rotate () # 旋转数组 45 度 -- 86 { #+ 在左下角"平衡"图形.
87 local row
88 local column
89
90 for (( row = Rows; row > -Rows; row-- ))
91 do # 从后面步进数组. 为什么?
92
93 for (( column = 0; column < Columns; column++ ))
94 do
95
96 if [ "$row" -ge 0 ]
97 then
98 let "t1 = $column - $row"
99 let "t2 = $column"
100 else
101 let "t1 = $column"
102 let "t2 = $column + $row"
103 fi
104
105 filter $t1 $t2 # 过滤出负数数组索引.
106 # 如果你不这样做会怎么样?
107 done
108
109 echo; echo
110
111 done
112
113 # 数组旋转灵感源于Herbert Mayer写的
114 #+ "Advanced C Programming on the IBM PC," 的例子 (页码. 143-146)
http://www.818198.com Page 437
SHELL十三问
115 #+ (看参考书目附录).
116 # 这也能看出C能做的事情有多少能用shell脚本做到.
117 #
118
119 }
120
121
122 #--------------- 现在, 可以开始了. ------------#
123 load_alpha # 加载数组.
124 print_alpha # 打印数组.
125 rotate # 反时钟旋转数组45度.
126 #-----------------------------------------------------#
127
128 exit 0
129
130 # 这是有点做作,不太优雅.
131
132 # 练习:
133 # ---------134 # 1) 重写数组加载和打印函数,
135 # 使其更直观和容易了解.
136 #
137 # 2) 指出数组旋转函数是什么原理.
138 # Hint索引: 思考数组从尾向前索引的实现.
139 #
140 # 3) 重写脚本使其可以处理非方形数组Rewrite this script to handle a non-square array,
141 # 例如 6 X 4 的数组.
142 # 尝试旋转数组时做到最小"失真".
################################End Script#########################################
二维数组本质上等同于一维数组, 而只增加了使用行和列的位置来引用和操作元素的寻址模式.
关于二维数组更好的例子, 请参考例子 A-10.
--
另一个有趣的使用数组的脚本:
* 例子 14-3
[]
高级Bash脚本编程指南(五)(下)
文章整理: 文章来源: 网络
第27章 /dev 和 /proc
=====================
Linux 或 UNIX 机器都带有/dev和/proc目录用于特殊目的.
27.1. /dev
----------在 /dev 目录内包含以或不以硬件形式出现的物理设备条目. [1] 包含被挂载的文件系统的硬
设备分区在/dev目录下都有对应的条目, 就像df命令所展示的.
bash$ df
Filesystem 1k-blocks Used Available Use%
Mounted on
http://www.818198.com Page 438
SHELL十三问
/dev/hda6 495876 222748 247527 48% /
/dev/hda1 50755 3887 44248 9% /boot
/dev/hda8 367013 13262 334803 4% /home
/dev/hda5 1714416 1123624 503704 70% /usr
在其他方面, /dev 目录也包含环回设备(loopback devices) , 例如/dev/loop0. 环回设备是
一个使普通文件能被像对待块设备一样来进行存取的机制. [2] 这使我们可以将一个大文件内
的整个文件系统挂载到系统目录下. 参考例子 13-8和例子 13-7.
/dev还有少量的伪设备用于特殊的用途, 例如/dev/null, /dev/zero, /dev/urandom,
/dev/sda1, /dev/udp, 和/dev/tcp.
例如:
为了挂载(mount) 一个USB闪盘设备, 将下面一行添加到/etc/fstab. [3]
1 /dev/sda1 /mnt/flashdrive auto noauto,user,noatime 0 0
(也请参考例子 A-23.)
当对/dev/tcp/$host/$port 伪设备文件执行一个命令时, Bash会打开一个相关的TCP的socket.
[4]
从nist.gov得到时间:
bash$ cat 53082 04-03-18 04:26:54 68 0 0 502.3 UTC(NIST) *
[Mark贡献了上面的例子.]
下载一个 URL:
bash$ exec 5<>/dev/tcp/www.net.cn/80
bash$ echo -e "GET / HTTP/1.0\n" >&5
bash$ cat <&5
[Thanks, Mark 和 Mihai Maties.]
Example 27-1 利用/dev/tcp 来检修故障
################################Start Script#######################################
1 #!/bin/bash
2 # dev-tcp.sh: 用/dev/tcp 重定向来检查Internet连接.
3
4 # Troy Engel编写.
5 # 已得到作者允许.
6
7 TCP_HOST=www.dns-diy.com # 一个已知的 ISP.
8 TCP_PORT=80 # http的端口是80 .
9
10 # 尝试连接. (有些像 'ping' . . .)
11 echo "HEAD / HTTP/1.0" >/dev/tcp/${TCP_HOST}/${TCP_PORT}
12 MYEXIT=$?
13
14 : <
16 using a special character device for both TCP and UDP redirections. These
17 redirections are used identically as STDIN/STDOUT/STDERR. The device entries
18 are 30,36 for /dev/tcp:
19
20 mknod /dev/tcp c 30 36
http://www.818198.com Page 439
SHELL十三问
21
22 >From the bash reference:
23 /dev/tcp/host/port
24 If host is a valid hostname or Internet address, and port is an integer
25 port number or service name, Bash attempts to open a TCP connection to the
26 corresponding socket.
27 EXPLANATION
28
29
30 if [ "X$MYEXIT" = "X0" ]; then
31 echo "Connection successful. Exit code: $MYEXIT"
32 else
33 echo "Connection unsuccessful. Exit code: $MYEXIT"
34 fi
35
36 exit $MYEXIT
################################End Script#########################################
译者补充上面这个例子输出的解释(EXPLANATION)译文:
如果bash以--enable-net- redirections选项来编译,它就拥有了使用一个特殊字符设备来完
成TCP和UDP重定向功能的能力.这种重定向能力就像 STDIN/STDOUT/STDERR一样被标识.该字
符设备/dev/tcp的主次设备号是30,36:
mknod /dev/tcp c 30 36
>摘自bash参考手册:
/dev/tcp/host/port
如果host是一个有效的主机名或因特网有效地址,并且port是一个整数的端口号或是服务名称
,Bash会尝试打开一个相对应的TCP连接socket.
注意事项:
[1] /dev目录中的条目是为各种物理设备和虚拟设备提供的挂载点. 这些条目使用非常少
的设备空间.
一些像/dev/null, /dev/zero, 和 /dev/urandom的设备是虚拟的. 它们不是真正的
物理设备,而只是存在于软件的虚拟设备.
[2] 块设备读或写(或两者兼之)数据都是以块为单位的进行的, 与之相对应的字符设备
则使用字符为单位来进行存取.块设备典型的有硬盘和CD-ROM设备,字符设备典型的
例子如键盘.
[3] 当然,挂载点/mnt/flashdrive必须存在,如果不存在,以root用户来执行
mkdir /mnt/flashdrive.
为了最终能挂载设备,用下面的命令: mount /mnt/flashdrive
较新的Linux发行版自动把闪盘设备挂载到/media目录.
[4] socket是一种特殊的用于通信的I/O端口. 它允许同一台主机内不同硬件设备间的数
据传输,允许在相同网络中的主机间的数据传输,也允许穿越不同网络的主机间的数
据传输,当然,也允许在Internet上不同位置主机间的数据传输.
27.2. /proc
-----------/proc目录实际上是一个伪文件系统 . 在 /proc 目录里的文件是当前运行系统和内核进程及
它们的相关信息和统计.
bash$ cat /proc/devices
Character devices:
1 mem
http://www.818198.com Page 440
SHELL十三问
2 pty
3 ttyp
4 ttyS
5 cua
7 vcs
10 misc
14 sound
29 fb
36 netlink
128 ptm
136 pts
162 raw
254 pcmcia
Block devices:
1 ramdisk
2 fd
3 ide0
9 md
bash$ cat /proc/interrupts
CPU0
0: 84505 XT-PIC timer
1: 3375 XT-PIC keyboard
2: 0 XT-PIC cascade
5: 1 XT-PIC soundblaster
8: 1 XT-PIC rtc
12: 4231 XT-PIC PS/2 Mouse
14: 109373 XT-PIC ide0
NMI: 0
ERR: 0
bash$ cat /proc/partitions
major minor #blocks name rio rmerge rsect ruse wio wmerge wsect wuse running use aveq
3 0 3007872 hda 4472 22260 114520 94240 3551 18703 50384 549710 0 111550 644030
3 1 52416 hda1 27 395 844 960 4 2 14 180 0 800 1140
3 2 1 hda2 0 0 0 0 0 0 0 0 0 0 0
3 4 165280 hda4 10 0 20 210 0 0 0 0 0 210 210
...
bash$ cat /proc/loadavg
0.13 0.42 0.27 2/44 1119
bash$ cat /proc/apm
1.16 1.2 0x03 0x01 0xff 0x80 -1% -1 ?
http://www.818198.com Page 441
SHELL十三问
Shell 脚本可以从/proc目录中的一些文件里提取数据. [1]
1 FS=iso # ISO 文件系统是否被内核支持?
2
3 grep $FS /proc/filesystems # iso9660
1 kernel_version=$( awk '{ print $3 }' /proc/version )
1 CPU=$( awk '/model name/ {print $4}' < /proc/cpuinfo )
2
3 if [ $CPU = Pentium ]
4 then
5 run_some_commands
6 ...
7 else
8 run_different_commands
9 ...
10 fi
1 devfile="/proc/bus/usb/devices"
2 USB1="Spd=12"
3 USB2="Spd=480"
4
5
6 bus_speed=$(grep Spd $devfile | awk '{print $9}')
7
8 if [ "$bus_speed" = "$USB1" ]
9 then
10 echo "USB 1.1 port found."
11 # 这儿开始操作USB 1.1相关的动作.
12 fi
/proc目录下有许多不相同的数字命名的子目录. 这些子目录的数字名字都映射对应的当前正
在运行的进程的进程号(process ID) . 这些子目录里面有许多文件用于保存对应进程的信息.
文件 stat 和 status 保存着进程运行时的各项统计, the cmdline文件保存该进程的被调用
时的命令行参数, 而and the exe 文件是该运行进程完整路径名的符号链接. 还有其他一些文
件,但从脚本的观点来看它们都非常的有意思.
Example 27-2 搜索与一个PID相关的进程
################################Start Script#######################################
1 #!/bin/bash
2 # pid-identifier.sh: 给出指定PID的进程的程序全路径.
3
4 ARGNO=1 # 此脚本期望的参数个数.
5 E_WRONGARGS=65
6 E_BADPID=66
7 E_NOSUCHPROCESS=67
8 E_NOPERMISSION=68
9 PROCFILE=exe
10
11 if [ $# -ne $ARGNO ]
12 then
13 echo "Usage: `basename $0` PID-number" >&2 # 帮助信息重定向到标准出错.
14 exit $E_WRONGARGS
http://www.818198.com Page 442
SHELL十三问
15 fi
16
17 pidno=$( ps ax | grep $1 | awk '{ print $1 }' | grep $1 )
18 # 搜索命令"ps"输出的第一列.
19 # 然后再次确认是真正我们要寻找的进程,而不是这个脚本调用而产生的进程.
20 # 后一个"grep $1"会滤掉这个可能产生的进程.
21 #
22 # pidno=$( ps ax | awk '{ print $1 }' | grep $1 )
23 # 也可以, 由 Teemu Huovila指出.
24
25 if [ -z "$pidno" ] # 如果过滤完后结果是一个空字符串,
26 then # 没有对应的PID进程在运行.
27 echo "No such process running."
28 exit $E_NOSUCHPROCESS
29 fi
30
31 # 也可以用:
32 # if ! ps $1 > /dev/null 2>&1
33 # then # 没有对应的PID进程在运行.
34 # echo "No such process running."
35 # exit $E_NOSUCHPROCESS
36 # fi
37
38 # 为了简化整个进程,使用"pidof".
39
40
41 if [ ! -r "/proc/$1/$PROCFILE" ] # 检查读权限.
42 then
43 echo "Process $1 running, but..."
44 echo "Can't get read permission on /proc/$1/$PROCFILE."
45 exit $E_NOPERMISSION # 普通用户不能存取/proc目录的某些文件.
46 fi
47
48 # 最后两个测试可以用下面的代替:
49 # if ! kill -0 $1 > /dev/null 2>&1 # '0'不是一个信号,
50 # 但这样可以测试是否可以
51 # 向该进程发送信号.
52 # then echo "PID doesn't exist or you're not its owner" >&2
53 # exit $E_BADPID
54 # fi
55
56
57
58 exe_file=$( ls -l /proc/$1 | grep "exe" | awk '{ print $11 }' )
59 # 或 exe_file=$( ls -l /proc/$1/exe | awk '{print $11}' )
60 #
61 # /proc/pid-number/exe 是进程程序全路径的符号链接.
62 #
http://www.818198.com Page 443
SHELL十三问
63
64 if [ -e "$exe_file" ] # 如果 /proc/pid-number/exe 存在 ...
65 then # 则相应的进程存在.
66 echo "Process #$1 invoked by $exe_file."
67 else
68 echo "No such process running."
69 fi
70
71
72 # 这个被详细讲解的脚本几乎可以用下面的命令代替:
73 # ps ax | grep $1 | awk '{ print $5 }'
74 # 然而, 这样并不会工作...
75 # 因为'ps'输出的第5列是进程的argv[0](即命令行第一个参数,调用时程序用的程序路径本身),
76 # 但不是可执行文件.
77 #
78 # 然而, 下面的两个都可以工作.
79 # find /proc/$1/exe -printf '%l\n'
80 # lsof -aFn -p $1 -d txt | sed -ne 's/^n//p'
81
82 # 由Stephane Chazelas附加注释.
83
84 exit 0
################################End Script#########################################
Example 27-3 网络连接状态
################################Start Script#######################################
1 #!/bin/bash
2
3 PROCNAME=pppd # ppp 守护进程
4 PROCFILENAME=status # 在这儿寻找信息.
5 NOTCONNECTED=65
6 INTERVAL=2 # 两秒刷新一次.
7
8 pidno=$( ps ax | grep -v "ps ax" | grep -v grep | grep $PROCNAME | awk '{ print $1 }' )
9 # 搜索ppp守护进程'pppd'的进程号.
10 # 一定要过滤掉由搜索进程产生的该行进程.
11 #
12 # 正如Oleg Philon指出的那样,
13 #+ 使用"pidof"命令会相当的简单.
14 # pidno=$( pidof $PROCNAME )
15 #
16 # 颇有良心的建议:
17 #+ 当命令序列变得复杂的时候,去寻找更简洁的办法. .
18
19
20 if [ -z "$pidno" ] # 如果没有找到此进程号,则进程没有运行.
21 then
22 echo "Not connected."
23 exit $NOTCONNECTED
http://www.818198.com Page 444
SHELL十三问
24 else
25 echo "Connected."; echo
26 fi
27
28 while [ true ] # 死循环,这儿可以有所改进.
29 do
30
31 if [ ! -e "/proc/$pidno/$PROCFILENAME" ]
32 # 进程运行时,对应的"status"文件会存在.
33 then
34 echo "Disconnected."
35 exit $NOTCONNECTED
36 fi
37
38 netstat -s | grep "packets received" # 取得一些连接统计.
39 netstat -s | grep "packets delivered"
40
41
42 sleep $INTERVAL
43 echo; echo
44
45 done
46
47 exit 0
48
49 # 当要停止它时,可以用Control-C终止.
50
51 # 练习:
52 # ---------53 # 改进这个脚本,使它能按"q"键退出.
54 # 给脚本更友好的界面.
################################End Script#########################################
注意: 一般来说, 写/proc目录里的文件是危险 ,因为这样会破坏这个文件系统或摧毁机器.
注意事项:
[1] 一些系统命令, 例如 procinfo, free, vmstat, lsdev, 和uptime也能做类似的事情.
第28章 关于Zeros和Nulls
========================
/dev/zero和/dev/null
使用/dev/null
把/dev/null看作"黑洞". 它非常等价于一个只写文件. 所有写入它的内容都会永远丢失.
而尝试从它那儿读取内容则什么也读不到. 然而, /dev/null对命令行和脚本都非常的有用.
禁止标准输出.
1 cat $filename >/dev/null
2 # 文件内容丢失,而不会输出到标准输出.
禁止标准错误 (来自例子 12-3).
1 rm $badname 2>/dev/null
2 # 这样错误信息[标准错误]就被丢到太平洋去了.
禁止标准输出和标准错误的输出.
http://www.818198.com Page 445
SHELL十三问
1 cat $filename 2>/dev/null >/dev/null
2 # 如果"$filename"不存在,将不会有任何错误信息提示.
3 # 如果"$filename"存在, 文件的内容不会打印到标准输出.
4 # 因此Therefore, 上面的代码根本不会输出任何信息.
5 #
6 # 当只想测试命令的退出码而不想有任何输出时非常有用.
7 #
8 #
9 # cat $filename &>/dev/null
10 # 也可以, 由 Baris Cicek 指出.
删除一个文件的内容, 但是保留文件本身, 和所有的文件权限(来自于Example 2-1和
Example 2-3):
1 cat /dev/null > /var/log/messages
2 # : > /var/log/messages 有同样的效果, 但不会产生新的进程.(因为:是内建的)
3
4 cat /dev/null > /var/log/wtmp
自动清空日志文件的内容 (特别适合处理这些由商业Web站点发送的讨厌的"cookies"):
Example 28-1 隐藏cookie而不再使用
################################Start Script#######################################
1 if [ -f ~/.netscape/cookies ] # 如果存在则删除.
2 then
3 rm -f ~/.netscape/cookies
4 fi
5
6 ln -s /dev/null ~/.netscape/cookies
7 # 现在所有的cookies都会丢入黑洞而不会保存在磁盘上了.
################################End Script#########################################
使用/dev/zero
像/dev/null一样, /dev/zero也是一个伪文件, 但它实际上产生连续不断的null的流
(二进制的零流,而不是ASCII型的). 写入它的输出会丢失不见, 而从/dev/zero读出一
连串的null也比较困难, 虽然这也能通过od或一个十六进制编辑器来做到. /dev/zero主
要的用处是用来创建一个指定长度用于初始化的空文件,就像临时交换文件.
Example 28-2 用/dev/zero创建一个交换临时文件
################################Start Script#######################################
1 #!/bin/bash
2 # 创建一个交换文件.
3
4 ROOT_UID=0 # Root 用户的 $UID 是 0.
5 E_WRONG_USER=65 # 不是 root?
6
7 FILE=/swap
8 BLOCKSIZE=1024
9 MINBLOCKS=40
10 SUCCESS=0
11
12
13 # 这个脚本必须用root来运行.
14 if [ "$UID" -ne "$ROOT_UID" ]
http://www.818198.com Page 446
SHELL十三问
15 then
16 echo; echo "You must be root to run this script."; echo
17 exit $E_WRONG_USER
18 fi
19
20
21 blocks=${1:-$MINBLOCKS} # 如果命令行没有指定,
22 #+ 则设置为默认的40块.
23 # 上面这句等同如:
24 # --------------------------------------------------25 # if [ -n "$1" ]
26 # then
27 # blocks=$1
28 # else
29 # blocks=$MINBLOCKS
30 # fi
31 # --------------------------------------------------32
33
34 if [ "$blocks" -lt $MINBLOCKS ]
35 then
36 blocks=$MINBLOCKS # 最少要有 40 个块长.
37 fi
38
39
40 echo "Creating swap file of size $blocks blocks (KB)."
41 dd if=/dev/zero of=$FILE bs=$BLOCKSIZE count=$blocks # 把零写入文件.
42
43 mkswap $FILE $blocks # 将此文件建为交换文件(或称交换分区).
44 swapon $FILE # 激活交换文件.
45
46 echo "Swap file created and activated."
47
48 exit $SUCCESS
################################End Script#########################################
关于 /dev/zero 的另一个应用是为特定的目的而用零去填充一个指定大小的文件, 如挂载一个
文件系统到环回设备 (loopback device) (参考例子 13-8) 或"安全地" 删除一个文件
(参考例子 12-55).
Example 28-3 创建ramdisk
################################Start Script#######################################
1 #!/bin/bash
2 # ramdisk.sh
3
4 # "ramdisk"是系统RAM内存的一段,
5 #+ 它可以被当成是一个文件系统来操作.
6 # 它的优点是存取速度非常快 (包括读和写).
7 # 缺点: 易失性, 当计算机重启或关机时会丢失数据.
8 #+ 会减少系统可用的RAM.
http://www.818198.com Page 447
SHELL十三问
9 #
10 # 那么ramdisk有什么作用呢?
11 # 保存一个较大的数据集在ramdisk, 比如一张表或字典,
12 #+ 这样可以加速数据查询, 因为在内存里查找比在磁盘里查找快得多.
13
14
15 E_NON_ROOT_USER=70 # 必须用root来运行.
16 ROOTUSER_NAME=root
17
18 MOUNTPT=/mnt/ramdisk
19 SIZE=2000 # 2K 个块 (可以合适的做修改)
20 BLOCKSIZE=1024 # 每块有1K (1024 byte) 的大小
21 DEVICE=/dev/ram0 # 第一个 ram 设备
22
23 username=`id -nu`
24 if [ "$username" != "$ROOTUSER_NAME" ]
25 then
26 echo "Must be root to run \"`basename $0`\"."
27 exit $E_NON_ROOT_USER
28 fi
29
30 if [ ! -d "$MOUNTPT" ] # 测试挂载点是否已经存在了,
31 then #+ 如果这个脚本已经运行了好几次了就不会再建这个目录了
32 mkdir $MOUNTPT #+ 因为前面已经建立了.
33 fi
34
35 dd if=/dev/zero of=$DEVICE count=$SIZE bs=$BLOCKSIZE # 把RAM设备的内容用零填充.
36 # 为何需要这么做?
37 mke2fs $DEVICE # 在RAM设备上创建一个ext2文件系统.
38 mount $DEVICE $MOUNTPT # 挂载设备.
39 chmod 777 $MOUNTPT # 使普通用户也可以存取这个ramdisk.
40 # 但是, 只能由root来缷载它.
41
42 echo "\"$MOUNTPT\" now available for use."
43 # 现在 ramdisk 即使普通用户也可以用来存取文件了.
44
45 # 注意, ramdisk是易失的, 所以当计算机系统重启或关机时ramdisk里的内容会消失.
46 #
47 # 拷贝所有你想保存文件到一个常规的磁盘目录下.
48
49 # 重启之后, 运行这个脚本再次建立起一个 ramdisk.
50 # 仅重新加载 /mnt/ramdisk 而没有其他的步骤将不会正确工作.
51
52 # 如果加以改进, 这个脚本可以放在 /etc/rc.d/rc.local,
53 #+ 以使系统启动时能自动设立一个ramdisk.
54 # 这样很合适速度要求高的数据库服务器.
55
56 exit 0
http://www.818198.com Page 448
SHELL十三问
################################End Script#########################################
最后值得一提的是, ELF二进制文件利用了/dev/zero.
第29章 调试
============
Debugging is twice as hard as writing the code in the first place. Therefore,
if you write the code as cleverly as possible, you are, by definition, not smart
enough to debug it.
Brian Kernighan
Bash shell 没有自带调试器, 甚至没有任何调试类型的命令或结构. [1] 脚本里的语法错误
或拼写错误会产生含糊的错误信息,通常这些在调试非功能性的脚本时没什么帮助.
Example 29-1 一个错误的脚本
################################Start Script#######################################
1 #!/bin/bash
2 # ex74.sh
3
4 # 这是一个错误的脚本.
5 # 哪里有错?
6
7 a=37
8
9 if [$a -gt 27 ]
10 then
11 echo $a
12 fi
13
14 exit 0
################################End Script#########################################
脚本的输出:
./ex74.sh: [37: command not found
上面的脚本有什么错误(线索: 注意if的后面)?
Example 29-2 丢失关键字(keyword)
################################Start Script#######################################
1 #!/bin/bash
2 # missing-keyword.sh: 会产生什么样的错误信息?
3
4 for a in 1 2 3
5 do
6 echo "$a"
7 # done # 第7行的必需的关键字 'done' 被注释掉了.
8
9 exit 0
################################End Script#########################################
脚本的输出:
missing-keyword.sh: line 10: syntax error: unexpected end of file
注意错误信息中说明的错误行不必一定要参考, 但那行是Bash解释器最终认识到是个错误的
地方.
出错信息可能在报告语法错误的行号时会忽略脚本的注释行.
如果脚本可以执行,但不是你所期望的那样工作怎么办? 这大多是由于常见的逻辑错误产生的.
http://www.818198.com Page 449
SHELL十三问
Example 29-3 另一个错误脚本
################################Start Script#######################################
1 #!/bin/bash
2
3 # 这个脚本目的是为了删除当前目录下的所有文件,包括文件名含有空格的文件.
4 #
5 # 但不能工作.
6 # 为什么?
7
8
9 badname=`ls | grep ' '`
10
11 # 试试这个:
12 # echo "$badname"
13
14 rm "$badname"
15
16 exit 0
################################End Script#########################################
为了找出 例子 29-3 的错误可以把echo "$badname" 行的注释去掉. echo 出来的信息对你
判断是否脚本以你希望的方式运行时很有帮助.
在这个实际的例子里, rm "$badname" 不会达到想要的结果,因为$badname 没有引用起来.
加上引号以保证rm 命令只有一个参数(这就只能匹配一个文件名). 一个不完善的解决办法是
删除A partial fix is to remove to quotes from $badname and to reset $IFS to contain only a newline, IFS=$'\n'. 不过, 存在更简单的
办法.
1 # 修正删除包含空格文件名时出错的办法.
2 rm *\ *
3 rm *" "*
4 rm *' '*
5 # Thank you. S.C.
总结该脚本的症状,
1. 终止于一个"syntax error"(语法错误)的信息, 或
2. 它能运行, 但不是按期望的那样运行(逻辑错误).
3. 它能运行,运行的和期望的一样, 但有讨厌的副作用 (逻辑炸弹).
用来调试不能工作的脚本的工具包括
1. echo 语句可用在脚本中的有疑问的点上以跟踪了解变量的值, 并且也可以了解后续脚
本的动作.
注意: 最好只在调试时才使用echo语句.
1 ### debecho (debug-echo), by Stefano Falsetto ###
2 ### 只有变量 DEBUG 设置了值时才会打印传递进来的变量值. ###
3 debecho () {
4 if [ ! -z "$DEBUG" ]; then
5 echo "$1" >&2
6 # ^^^ 打印到标准出错
7 fi
8 }
9
10 DEBUG=on
http://www.818198.com Page 450
SHELL十三问
11 Whatever=whatnot
12 debecho $Whatever # whatnot
13
14 DEBUG=
15 Whatever=notwhat
16 debecho $Whatever # (这儿就不会打印了.)
2. 使用 tee 过滤器来检查临界点的进程或数据流.
3. 设置选项 -n -v -x
sh -n scriptname 不会实际运行脚本,而只是检查脚本的语法错误. 这等同于把
set -n 或 set -o noexec 插入脚本中. 注意还是有一些语法错误不能被这种检查找
出来.
sh -v scriptname 在实际执行一个命令前打印出这个命令. 这也等同于在脚本里设置
set -v 或 set -o verbose.
选项 -n 和 -v 可以一块使用. sh -nv scriptname 会打印详细的语法检查.
sh -x scriptname 打印每个命令的执行结果, 但只用在某些小的方面. 它等同于脚本
中插入 set -x 或 set -o xtrace.
把 set -u 或 set -o nounset 插入到脚本里并运行它, 就会在每个试图使用没有申明
过的变量的地方打印出一个错误信息.
4. 使用一个"assert"(断言) 函数在脚本的临界点上测试变量或条件.
(这是从C语言中借用来的.)
Example 29-4 用"assert"测试条件
################################Start Script#######################################
1 #!/bin/bash
2 # assert.sh
3
4 assert () # 如果条件测试失败,
5 { #+ 则打印错误信息并退出脚本.
6 E_PARAM_ERR=98
7 E_ASSERT_FAILED=99
8
9
10 if [ -z "$2" ] # 没有传递足够的参数.
11 then
12 return $E_PARAM_ERR # 什么也不做就返回.
13 fi
14
15 lineno=$2
16
17 if [ ! $1 ]
18 then
19 echo "Assertion failed: \"$1\""
20 echo "File \"$0\", line $lineno"
21 exit $E_ASSERT_FAILED
22 # else
23 # return
24 # 返回并继续执行脚本后面的代码.
25 fi
26 }
http://www.818198.com Page 451
SHELL十三问
27
28
29 a=5
30 b=4
31 condition="$a -lt $b" # 会错误信息并从脚本退出.
32 # 把这个“条件”放在某个地方,
33 #+ 然后看看有什么现象.
34
35 assert "$condition" $LINENO
36 # 脚本以下的代码只有当"assert"成功时才会继续执行.
37
38
39 # 其他的命令.
40 # ...
41 echo "This statement echoes only if the \"assert\" does not fail."
42 # ...
43 # 余下的其他命令.
44
45 exit 0
################################End Script#########################################
5. 用变量$LINENO和内建的caller.
6. 捕捉exit.
脚本中的The exit 命令会触发信号0,终结进程,即脚本本身. [2] 这常用来捕捉
exit命令做某事, 如强制打印变量值. trap 命令必须是脚本中第一个命令.
捕捉信号
trap
当收到一个信号时指定一个处理动作; 这在调试时也很有用.
注意: 信号是发往一个进程的非常简单的信息, 要么是由内核发出要么是由另一个进程,
以告诉接收进程采取一些指定的动作 (一般是中止). 例如, 按Control-C, 发送
一个用户中断( 即 INT 信号)到运行中的进程.
1 trap '' 2
2 # 忽略信号 2 (Control-C), 没有指定处理动作.
3
4 trap 'echo "Control-C disabled."' 2
5 # 当按 Control-C 时显示一行信息.
Example 29-5 捕捉 exit
################################Start Script#######################################
1 #!/bin/bash
2 # 用trap捕捉变量值.
3
4 trap 'echo Variable Listing --- a = $a b = $b' EXIT
5 # EXIT 是脚本中exit命令产生的信号的信号名.
6 #
7 # 由"trap"指定的命令不会被马上执行,只有当发送了一个适应的信号时才会执行.
8 #
9
10 echo "This prints before the \"trap\" --"
11 echo "even though the script sees the \"trap\" first."
http://www.818198.com Page 452
SHELL十三问
12 echo
13
14 a=39
15
16 b=36
17
18 exit 0
19 # 注意到注释掉上面一行的'exit'命令也没有什么不同,
20 #+ 这是因为执行完所有的命令脚本都会退出.
################################End Script#########################################
Example 29-6 在Control-C后清除垃圾
################################Start Script#######################################
1 #!/bin/bash
2 # logon.sh: 简陋的检查你是否还处于连线的脚本.
3
4 umask 177 # 确定临时文件不是全部用户都可读的.
5
6
7 TRUE=1
8 LOGFILE=/var/log/messages
9 # 注意 $LOGFILE 必须是可读的
10 #+ (用 root来做:chmod 644 /var/log/messages).
11 TEMPFILE=temp.$$
12 # 创建一个"唯一的"临时文件名, 使用脚本的进程ID.
13 # 用 'mktemp' 是另一个可行的办法.
14 # 举例:
15 # TEMPFILE=`mktemp temp.XXXXXX`
16 KEYWORD=address
17 # 上网时, 把"remote IP address xxx.xxx.xxx.xxx"这行
18 # 加到 /var/log/messages.
19 ONLINE=22
20 USER_INTERRUPT=13
21 CHECK_LINES=100
22 # 日志文件中有多少行要检查.
23
24 trap 'rm -f $TEMPFILE; exit $USER_INTERRUPT' TERM INT
25 # 如果脚本被control-c中断了,则清除临时文件.
26
27 echo
28
29 while [ $TRUE ] #死循环.
30 do
31 tail -$CHECK_LINES $LOGFILE> $TEMPFILE
32 # 保存系统日志文件的最后100行到临时文件.
33 # 这是需要的, 因为新版本的内核在登录网络时产生许多日志文件信息.
34 search=`grep $KEYWORD $TEMPFILE`
35 # 检查"IP address" 短语是不是存在,
36 #+ 它指示了一次成功的网络登录.
http://www.818198.com Page 453
SHELL十三问
37
38 if [ ! -z "$search" ] # 引号是必须的,因为变量可能会有一些空白符.
39 then
40 echo "On-line"
41 rm -f $TEMPFILE # 清除临时文件.
42 exit $ONLINE
43 else
44 echo -n "." # -n 选项使echo不会产生新行符,
45 #+ 这样你可以从该行的继续打印.
46 fi
47
48 sleep 1
49 done
50
51
52 # 注: 如果你更改KEYWORD变量的值为"Exit",
53 #+ 这个脚本就能用来在网络登录后检查掉线
54 #
55
56 # 练习: 修改脚本,像上面所说的那样,并修正得更好
57 #
58
59 exit 0
60
61
62 # Nick Drage 建议用另一种方法:
63
64 while true
65 do ifconfig ppp0 | grep UP 1> /dev/null && echo "connected" && exit 0
66 echo -n "." # 在连接上之前打印点 (.....).
67 sleep 2
68 done
69
70 # 问题: 用 Control-C来终止这个进程可能是不够的.
71 #+ (点可能会继续被打印.)
72 # 练习: 修复这个问题.
73
74
75
76 # Stephane Chazelas 也提出了另一个办法:
77
78 CHECK_INTERVAL=1
79
80 while ! tail -1 "$LOGFILE" | grep -q "$KEYWORD"
81 do echo -n .
82 sleep $CHECK_INTERVAL
83 done
84 echo "On-line"
http://www.818198.com Page 454
SHELL十三问
85
86 # 练习: 讨论这几个方法的优缺点.
87 #
################################End Script#########################################
注意: trap 的DEBUG参数在每个命令执行完后都会引起一个指定的执行动作,例如,这可用来
跟踪变量.
Example 29-7 跟踪变量
################################Start Script#######################################
1 #!/bin/bash
2
3 trap 'echo "VARIABLE-TRACE> \$variable = \"$variable\""' DEBUG
4 # 在每个命令行显示变量$variable 的值.
5
6 variable=29
7
8 echo "Just initialized \"\$variable\" to $variable."
9
10 let "variable *= 3"
11 echo "Just multiplied \"\$variable\" by 3."
12
13 exit $?
14
15 # "trap 'command1 . . . command2 . . .' DEBUG" 的结构适合复杂脚本的环境
16 #+ 在这种情况下多次"echo $variable"比较没有技巧并且也耗时.
17 #
18 #
19
20 # Thanks, Stephane Chazelas 指出这一点.
21
22
23 脚本的输出:
24
25 VARIABLE-TRACE> $variable = ""
26 VARIABLE-TRACE> $variable = "29"
27 Just initialized "$variable" to 29.
28 VARIABLE-TRACE> $variable = "29"
29 VARIABLE-TRACE> $variable = "87"
30 Just multiplied "$variable" by 3.
31 VARIABLE-TRACE> $variable = "87"
################################End Script#########################################
当然, trap 命令除了调试还有其他的用处.
Example 29-8 运行多进程 (在多处理器的机器里)
################################Start Script#######################################
1 #!/bin/bash
2 # parent.sh
3 # 在多处理器的机器里运行多进程.
4 # 作者: Tedman Eng
5
http://www.818198.com Page 455
SHELL十三问
6 # 这是要介绍的两个脚本的第一个,
7 #+ 这两个脚本都在要在相同的工作目录下.
8
9
10
11
12 LIMIT=$1 # 要启动的进程总数
13 NUMPROC=4 # 当前进程数 (forks?)
14 PROCID=1 # 启动的进程ID
15 echo "My PID is $$"
16
17 function start_thread() {
18 if [ $PROCID -le $LIMIT ] ; then
19 ./child.sh $PROCID&
20 let "PROCID++"
21 else
22 echo "Limit reached."
23 wait
24 exit
25 fi
26 }
27
28 while [ "$NUMPROC" -gt 0 ]; do
29 start_thread;
30 let "NUMPROC--"
31 done
32
33
34 while true
35 do
36
37 trap "start_thread" SIGRTMIN
38
39 done
40
41 exit 0
42
43
44
45 # ======== 下面是第二个脚本 ========
46
47
48 #!/bin/bash
49 # child.sh
50 # 在多处理器的机器里运行多进程.
51 # 这个脚本由parent.sh脚本调用(即上面的脚本).
52 # 作者: Tedman Eng
53
http://www.818198.com Page 456
SHELL十三问
54 temp=$RANDOM
55 index=$1
56 shift
57 let "temp %= 5"
58 let "temp += 4"
59 echo "Starting $index Time:$temp" "$@"
60 sleep ${temp}
61 echo "Ending $index"
62 kill -s SIGRTMIN $PPID
63
64 exit 0
65
66
67 # ======================= 脚本作者注 ======================= #
68 # 这不是完全没有bug的脚本.
69 # 我运行LIMIT = 500 ,在过了开头的一二百个循环后,
70 #+ 这些进程有一个消失了!
71 # 不能确定是不是因为捕捉信号产生碰撞还是其他的原因.
72 # 一但信号捕捉到,在下一个信号设置之前,
73 #+ 会有一个短暂的时间来执行信号处理程序,
74 #+ 这段时间内很可能会丢失一个信号捕捉,因此失去生成一个子进程的机会.
75
76 # 毫无疑问会有人能找出这个bug的原因,并且修复它
77 #+ . . . 在将来的某个时候.
78
79
80
81 # ===================================================================== #
82
83
84
85 # ----------------------------------------------------------------------#
86
87
88
89 #################################################################
90 # 下面的脚本由Vernia Damiano原创.
91 # 不幸地是, 它不能正确工作.
92 #################################################################
93
94 #!/bin/bash
95
96 # 必须以最少一个整数参数来调用这个脚本
97 #+ (这个整数是协作进程的数目).
98 # 所有的其他参数被传给要启动的进程.
99
100
101 INDICE=8 # 要启动的进程数目
http://www.818198.com Page 457
SHELL十三问
102 TEMPO=5 # 每个进程最大的睡眼时间
103 E_BADARGS=65 # 没有参数传给脚本的错误值.
104
105 if [ $# -eq 0 ] # 检查是否至少传了一个参数给脚本.
106 then
107 echo "Usage: `basename $0` number_of_processes [passed params]"
108 exit $E_BADARGS
109 fi
110
111 NUMPROC=$1 # 协作进程的数目
112 shift
113 PARAMETRI=( "$@" ) # 每个进程的参数
114
115 function avvia() {
116 local temp
117 local index
118 temp=$RANDOM
119 index=$1
120 shift
121 let "temp %= $TEMPO"
122 let "temp += 1"
123 echo "Starting $index Time:$temp" "$@"
124 sleep ${temp}
125 echo "Ending $index"
126 kill -s SIGRTMIN $$
127 }
128
129 function parti() {
130 if [ $INDICE -gt 0 ] ; then
131 avvia $INDICE "${PARAMETRI[@]}" &
132 let "INDICE--"
133 else
134 trap : SIGRTMIN
135 fi
136 }
137
138 trap parti SIGRTMIN
139
140 while [ "$NUMPROC" -gt 0 ]; do
141 parti;
142 let "NUMPROC--"
143 done
144
145 wait
146 trap - SIGRTMIN
147
148 exit $?
149
http://www.818198.com Page 458
SHELL十三问
150 : <
152 能接受许多不同的文件,并在一个多处理器的机器上运行
153 所以我想(我也将会)使指定数目的进程运行,并且每个进程终止后都能启动一个新的
154
155
156 "wait"命令没什么帮助, 因为它是等候一个指定的或所有的后台进程.
157 所以我写了这个使用了trap指令的bash脚本来做这个任务.
158
159 --Vernia Damiano
160 SCRIPT_AUTHOR_COMMENTS
################################End Script#########################################
注意: trap '' SIGNAL (两个引号引空) 在脚本中禁用了 SIGNAL 信号的动作(即忽略了).
trap SIGNAL 则恢复了 SIGNAL 信号前次的处理动作. 这在保护脚本的某些临界点的
位置不受意外的中断影响时很有用.
1 trap '' 2 # 信号 2是 Control-C, 现在被忽略了.
2 command
3 command
4 command
5 trap 2 # 再启用Control-C
6
Bash的版本 3 增加了下面的特殊变量用于调试.
1. $BASH_ARGC
2. $BASH_ARGV
3. $BASH_COMMAND
4. $BASH_EXECUTION_STRING
5. $BASH_LINENO
6. $BASH_SOURCE
7. $BASH_SUBSHELL
注意事项:
[1] Rocky Bernstein的 Bash debugger 实际上填补了这个空白.
[2] 依据惯例,信号0 被指定为退出(exit).
第30章 选项
============
选项用来更改shell或/和脚本行为的机制.
set 命令用来在脚本里激活各种选项. 在脚本中任何你想让选项生效的地方,插入
set -o option-name 或, 用更简短的格式, set -option-abbrev. 这两种格式都是等价的.
1 #!/bin/bash
2
3 set -o verbose
4 # 执行前打印命令.
1 #!/bin/bash
2
3 set -v
4 # 和上面的有完全相同的效果.
注意: 为了在脚本里停用一个选项, 插入 set +o option-name 或 set +option-abbrev.
1 #!/bin/bash
2
http://www.818198.com Page 459
SHELL十三问
3 set -o verbose
4 # 激活命令回显.
5 command
6 ...
7 command
8
9 set +o verbose
10 # 停用命令回显.
11 command
12 # 没有回显命令了.
13
14
15 set -v
16 # 激活命令回显.
17 command
18 ...
19 command
20
21 set +v
22 # 停用命令回显.
23 command
24
25 exit 0
26
另一个在脚本里启用选项的方法是在脚本头部的#!后面指定选项.
1 #!/bin/bash -x
2 #
3 # 下面是脚本的主要内容.
4
从命令行来激活脚本的选项也是可以办到的. 一些不能和set一起用的选项可以用在命令行指
定. -i是其中之一, 可以使脚本以交互方式运行.
bash -v script-name
bash -o verbose script-name
下面的表格列举了一些有用的选项. 它们都可以用简短格式来指定(以一个短横线开头)也可
以用完整的名字来指定(用双短横线开头或用-o来指定).
table 30-1 Bash 选项
==================================================================================
| 缩写 | 名称 | 作用
==================================================================================
| -C | noclobber | 防止重定向时覆盖文件 (此作用会被>|覆盖)
==================================================================================
| -D | (none) | 列出双引号引起的含有$前缀的字符串,但不执行脚本
| | | 中的命令
==================================================================================
| -a | allexport | 导出所有定义的变量到环境变量中
==================================================================================
| -b | notify | 当后台任务终止时给出通知 (在脚本中用的不多)
==================================================================================
http://www.818198.com Page 460
SHELL十三问
| -c... | (none) | 从...读命令
==================================================================================
| -e | errexit | 脚本发生第一个错误时就中止脚本运行,即当一个命令
| | | 返回非零值时退出脚本 (除了until 或 while loops,
| | | if-tests, list constructs)
==================================================================================
| -f | noglob | 文件名替换停用(指像*这样的符号不能替换为文件名了)
==================================================================================
| -i | interactive | 使脚本以交互式方式运行
==================================================================================
| -n | noexec | 从脚本里读命令但不执行它们(语法检查)
==================================================================================
| -o Option-Name | (none) | 调用Option-Name 选项
==================================================================================
| -o posix | POSIX | 更改Bash或脚本的行为,使之符合POSIX标准.
==================================================================================
| -p | privileged | 脚本作为"suid"程序来运行 (小心!)
==================================================================================
| -r | restricted | 脚本在受限模式中运行 (参考第21章).
==================================================================================
| -s | stdin | 从标准输入读命令
==================================================================================
| -t | (none) | 第一个命令后就退出
==================================================================================
| -u | nounset | 当使用一个未定义的变量时产生一个错误信息,并强制
| | | 退出脚本.
==================================================================================
| -v | verbose | 执行命令之前打印命令到标准输出
==================================================================================
| -x | xtrace | 与-v相似, 但打印完整的命令
==================================================================================
| - | (none) | 选项列表结束的标志. 后面的参数是位置参数.
==================================================================================
| -- | (none) | 释放位置参数. 如果参数列表被指定了(-- arg1 arg2),
| | | 则位置参数被依次设置为参数列表中的值.
==================================================================================
第31章 Gotchas
===============
Turandot: Gli enigmi sono tre, la morte una!
Caleph: No, no! Gli enigmi sono tre, una la vita!
Puccini
将保留字和字符声明为变量名.
1 case=value0 # 引发错误.
2 23skidoo=value1 # 也会有错误.
3 # 以数字开头的变量名是由shell保留使用的.
4 # 试试 _23skidoo=value1. 用下划线开头的变量名是允许的.
5
6 # 但是 . . . 仅使用下划线来用做变量名也是不行的.
http://www.818198.com Page 461
SHELL十三问
7 _=25
8 echo $_ # $_ 是一个特殊的变量,被设置为最后命令的最后一个参数.
9
10 xyz((!*=value2 # 引起严重的错误.
11 # 在第三版的Bash, 标点不能在变量名中出现.
用连字符或其他保留字符当做变量名(或函数名).
1 var-1=23
2 # 用 'var_1' 代替.
3
4 function-whatever () # 错误
5 # 用 'function_whatever ()' 代替.
6
7
8 # 在第三版的 Bash, 标点不能在函数名中使用.
9 function.whatever () # 错误
10 # 用 'functionWhatever ()' 代替.
给变量和函数使用相同的名字. 这会使脚本不能分辨两者.
1 do_something ()
2 {
3 echo "This function does something with \"$1\"."
4 }
5
6 do_something=do_something
7
8 do_something do_something
9
10 # 这些都是合法的,但让人混淆.
不适当地使用宽白符(whitespace). 和其它的编程语言相比,Bash非常讲究空白字符的使用.
1 var1 = 23 # 'var1=23' 正确.
2 # 上面一行,Bash试图执行命令"var1"
3 # 并且它的参数是"="和"23".
4
5 let c = $a - $b # 'let c=$a-$b' 或 'let "c = $a - $b"'是正确的.
6
7 if [ $a -le 5] # if [ $a -le 5 ] 是正确的.
8 # if [ "$a" -le 5 ] 会更好.
9 # [[ $a -le 5 ]] 也可以.
未初始化的变量(指赋值前的变量)被认为是NULL值的,而不是有零值.
1 #!/bin/bash
2
3 echo "uninitialized_var = $uninitialized_var"
4 # uninitialized_var =
混淆测试里的= 和 -eq 操作符. 请记住, = 是比较字符变量而 -eq 比较整数.
1 if [ "$a" = 273 ] # $a 是一个整数还是一个字符串?
2 if [ "$a" -eq 273 ] # 如果$a 是一个整数,用这个表达式.
3
4 # 有时你能混用 -eq 和 = 而没有不利的结果.
5 # 然而 . . .
http://www.818198.com Page 462
SHELL十三问
6
7
8 a=273.0 # 不是一个整数.
9
10 if [ "$a" = 273 ]
11 then
12 echo "Comparison works."
13 else
14 echo "Comparison does not work."
15 fi # Comparison does not work.
16
17 # 与 a=" 273" 和 a="0273" 一样.
18
19
20 # 同样, 问题仍然是试图对非整数值使用 "-eq" 测试.
21
22 if [ "$a" -eq 273.0 ]
23 then
24 echo "a = $a"
25 fi # 因错误信息而中断.
26 # test.sh: [: 273.0: integer expression expected
误用字符串比较操作符.
Example 31-1 数字和字符串比较是不相等同的
################################Start Script#######################################
1 #!/bin/bash
2 # bad-op.sh: 在整数比较中使用字符串比较.
3
4 echo
5 number=1
6
7 # 下面的 "while" 循环有两个错误:
8 #+ 一个很明显,另一个比较隐蔽.
9
10 while [ "$number" < 5 ] # 错误! 应该是: while [ "$number" -lt 5 ]
11 do
12 echo -n "$number "
13 let "number += 1"
14 done
15 # 尝试运行时会收到错误信息而退出:
16 #+ bad-op.sh: line 10: 5: No such file or directory
17 # 在单括号里, "<" 需要转义,
18 #+ 而即使是如此, 对此整数比较它仍然是错的.
19
20
21 echo "---------------------"
22
23
24 while [ "$number" \< 5 ] # 1 2 3 4
http://www.818198.com Page 463
SHELL十三问
25 do #
26 echo -n "$number " # 看起来好像是能工作的, 但 . . .
27 let "number += 1" #+ 它其实是在对 ASCII 码的比较,
28 done #+ 而非是对数值的比较.
29
30 echo; echo "---------------------"
31
32 # 下面这样便会引起问题了. 例如:
33
34 lesser=5
35 greater=105
36
37 if [ "$greater" \< "$lesser" ]
38 then
39 echo "$greater is less than $lesser"
40 fi # 105 is less than 5
41 # 事实上, "105" 小于 "5"
42 #+ 是因为使用了字符串比较 (以ASCII码的排序顺序比较).
43
44 echo
45
46 exit 0
################################End Script#########################################
有时在测试时的方括号([ ])里的变量需要引用起来(双引号). 如果没有这么做可能会引起不
可预料的结果. 参考例子 7-6, 例子 16-5, 和 例子 9-6.
在脚本里的命令可能会因为脚本没有运行权限而导致运行失败. 如果用户不能在命令行里调用
一个命令,即使把这个命令加到一个脚本中也一样会失败. 这时可以尝试更改访命令的属性,
甚至可能给它设置suid位(当然是以root来设置).
试图用 - 来做重定向操作(事实上它不是操作符)会导致令人讨厌的意外.
1 command1 2> - | command2 # 试图把command1的错误重定向到一个管道里...
2 # ...不会工作.
3
4 command1 2>& - | command2 # 也没有效果.
5
6 Thanks, S.C.
用 Bash 版本 2+ 的功能可以当有错误信息时引发修复动作. 老一些的 Linux机器可能默认的
安装是 1.XX 版本的Bash.
1 #!/bin/bash
2
3 minimum_version=2
4 # 因为 Chet Ramey 经常给Bash增加新的特性,
5 # 你把 $minimum_version 设为 2.XX比较合适,或者是其他合适的值.
6 E_BAD_VERSION=80
7
8 if [ "$BASH_VERSION" \< "$minimum_version" ]
9 then
10 echo "This script works only with Bash, version $minimum or greater."
11 echo "Upgrade strongly recommended."
http://www.818198.com Page 464
SHELL十三问
12 exit $E_BAD_VERSION
13 fi
14
15 ...
在非Linux的机器上使用Bourne shell脚本(#!/bin/sh)的Bash专有功能可能会引起不可预料的
行为. Linux系统通常都把sh 取别名为 bash, 但在其他的常见的UNIX系统却不一定是这样.
使用Bash中没有文档化的属性是危险的尝试. 在这本书的前几版中有几个脚本依赖于exit或
return的值没有限制不能用负整数(虽然限制了exit或return 的最大值是255). 不幸地是,
在版本 2.05b 以上这种情况就消失了. 参考See 例子 23-9.
一个带有DOS风格新行符 (\r\n) 的脚本会执行失败, 因为#!/bin/bash\r\n 不是合法的,不同
于合法的#!/bin/bash\n. 解决办法就是把脚本转换成UNIX风格的新行符.
1 #!/bin/bash
2
3 echo "Here"
4
5 unix2dos $0 # 脚本先把自己改成DOS格式.
6 chmod 755 $0 # 更改回执行权限.
7 # 'unix2dos'命令会删除执行权限.
8
9 ./$0 # 脚本尝试再次运行自己本身.
10 # 但它是一个DOS文件而不会正常工作了.
11
12 echo "There"
13
14 exit 0
shell脚本以 #!/bin/sh 行开头将不会在Bash兼容的模式下运行. 一些Bash专有的功能可能会
被禁用掉. 那些需要完全使用Bash专有扩展特性的脚本应该用#!/bin/bash开头.
脚本里在 here document 的终结输入的字符串前加入空白字符会引起不可预料的结果.
脚本不能export(导出)变量到它的父进程(parent process),或父进程的环境里. 就像我
们学的生物一样,一个子进程可以从父进程里继承但不能去影响父进程.
1 WHATEVER=/home/bozo
2 export WHATEVER
3 exit 0
bash$ echo $WHATEVER
bash$
可以确定, 回到命令提示符, $WHATEVER 变量仍然没有设置.
在子SHELL(subshell)设置和操作变量 , 然后尝试在子SHELL的作用范围外使用相同名的变
量将会导致非期望的结果.
Example 31-2 子SHELL缺陷
################################Start Script#######################################
1 #!/bin/bash
2 # 在子SHELL中的变量缺陷.
3
4 outer_variable=outer
5 echo
6 echo "outer_variable = $outer_variable"
7 echo
http://www.818198.com Page 465
SHELL十三问
8
9 (
10 # 子SHELL开始
11
12 echo "outer_variable inside subshell = $outer_variable"
13 inner_variable=inner # Set
14 echo "inner_variable inside subshell = $inner_variable"
15 outer_variable=inner # Will value change globally?
16 echo "outer_variable inside subshell = $outer_variable"
17
18 # 导出变量会有什么不同吗?
19 # export inner_variable
20 # export outer_variable
21 # 试试看.
22
23 # 子SHELL结束
24 )
25
26 echo
27 echo "inner_variable outside subshell = $inner_variable" # Unset.
28 echo "outer_variable outside subshell = $outer_variable" # Unchanged.
29 echo
30
31 exit 0
32
33 # 如果你没有注释第 19 和 20行会怎么样?
34 # 会有什么不同吗?
################################End Script#########################################
把 echo 的输出用管道(Piping)输送给read命令可能会产生不可预料的结果. 在这个情况下,
read 表现地好像它是在一个子SHELL里一样. 可用set 命令代替 (就像在例子 11-16里的一
样).
Example 31-3 把echo的输出用管道输送给read命令
################################Start Script#######################################
1 #!/bin/bash
2 # badread.sh:
3 # 尝试用 'echo 和 'read'
4 #+ 来达到不用交互地给变量赋值的目的.
5
6 a=aaa
7 b=bbb
8 c=ccc
9
10 echo "one two three" | read a b c
11 # 试图重新给 a, b, 和 c赋值.
12
13 echo
14 echo "a = $a" # a = aaa
15 echo "b = $b" # b = bbb
http://www.818198.com Page 466
SHELL十三问
16 echo "c = $c" # c = ccc
17 # 重新赋值失败.
18
19 # ------------------------------20
21 # 用下面的另一种方法.
22
23 var=`echo "one two three"`
24 set -- $var
25 a=$1; b=$2; c=$3
26
27 echo "-------"
28 echo "a = $a" # a = one
29 echo "b = $b" # b = two
30 echo "c = $c" # c = three
31 # 重新赋值成功.
32
33 # ------------------------------34
35 # 也请注意echo值到'read'命令里是在一个子SHELL里起作用的.
36 # 所以,变量的值只在子SHELL里被改变了.
37
38 a=aaa # 从头开始.
39 b=bbb
40 c=ccc
41
42 echo; echo
43 echo "one two three" | ( read a b c;
44 echo "Inside subshell: "; echo "a = $a"; echo "b = $b"; echo "c = $c" )
45 # a = one
46 # b = two
47 # c = three
48 echo "-----------------"
49 echo "Outside subshell: "
50 echo "a = $a" # a = aaa
51 echo "b = $b" # b = bbb
52 echo "c = $c" # c = ccc
53 echo
54
55 exit 0
################################End Script#########################################
事实上, 也正如 Anthony Richardson 指出的那样, 管道任何的数据到循环里都会引起相似的
问题.
1 # 循环管道问题.
2 # Anthony Richardson编写此例,
3 #+ Wilbert Berendsen补遗此例.
4
5
http://www.818198.com Page 467
SHELL十三问
6 foundone=false
7 find $HOME -type f -atime +30 -size 100k |
8 while true
9 do
10 read f
11 echo "$f is over 100KB and has not been accessed in over 30 days"
12 echo "Consider moving the file to archives."
13 foundone=true
14 # ------------------------------------ 15 echo "Subshell level = $BASH_SUBSHELL"
16 # Subshell level = 1
17 # 没错, 现在是在子shell里头运行.
18 # ------------------------------------ 19 done
20
21 # foundone 变量在此总是有false值
22 #+ 因此它是在子SHELL里被设为true值的
23 if [ $foundone = false ]
24 then
25 echo "No files need archiving."
26 fi
27
28 # =====================现在, 使用正确的方法:=================
29
30 foundone=false
31 for f in $(find $HOME -type f -atime +30 -size 100k) # 没有使用管道.
32 do
33 echo "$f is over 100KB and has not been accessed in over 30 days"
34 echo "Consider moving the file to archives."
35 foundone=true
36 done
37
38 if [ $foundone = false ]
39 then
40 echo "No files need archiving."
41 fi
42
43 # ==================另一种方法==================
44
45 # 脚本中读变量值的相应部分替换在代码块里头读变量,
46 #+ 这使变量能在相同的子SHELL里共享了.
47 # Thank you, W.B.
48
49 find $HOME -type f -atime +30 -size 100k | {
50 foundone=false
51 while read f
52 do
53 echo "$f is over 100KB and has not been accessed in over 30 days"
http://www.818198.com Page 468
SHELL十三问
54 echo "Consider moving the file to archives."
55 foundone=true
56 done
57
58 if ! $foundone
59 then
60 echo "No files need archiving."
61 fi
62 }
相关的问题是:当尝试写 tail -f 的输出给管道并传递给grep时会发生问题.
1 tail -f /var/log/messages | grep "$ERROR_MSG" >> error.log
2 # "error.log"文件里将不会写入任何东西.
--
在脚本中使用"suid" 的命令是危险的, 因为这会危及系统安全. [1]
用shell编写CGI程序是值得商榷的. Shell脚本的变量不是"类型安全的", 这样它用于CGI连接
使用时会引发不希望的结果. 其次, 它很难防范骇客的攻击.
Bash 不能正确处理双斜线 (//) 字符串.
在Linux 或 BSD上写的Bash脚本可能需要修正以使它们也能在商业的UNIX (或 Apple OSX)上运
行. 这些脚本常使用比一般的UNIX系统上的同类工具更强大功能的GNU 命令和过滤工具. 这方
面一个明显的例子是文本处理工具tr.
Danger is near thee --
Beware, beware, beware, beware.
Many brave hearts are asleep in the deep.
So beware --
Beware.
A.J. Lamb and H.W. Petrie
注意事项:
[1] 给脚本设置suid 权限是没有用的.
第32章 脚本编程风格
====================
写脚本时养成结构化和系统方法的习惯. 即使你在信封背后随便做一下草稿也是有益的,要养
成在写代码前花几分钟来规划和组织你的想法.
这儿是一些风格的指南. 注意这节文档不是想成为一个官方Shell编程风格.
32.1. 非官方的Shell脚本风格
---------------------------* 注释你的代码.这会使你的代码更容易让别人理解和赏识,同时也便于你维护.
1 PASS="$PASS${MATRIX:$(($RANDOM%${#MATRIX})):1}"
2 # 当你去年写下这句代码时非常的了解它在干什么事,但现在它完全是一个谜.
3 # (摘自 Antek Sawicki的"pw.sh" 脚本.)
给脚本和函数加上描述性的头部信息.
1 #!/bin/bash
2
3 #************************************************#
4 # xyz.sh #
5 # written by Bozo Bozeman #
6 # July 05, 2001 #
7 # #
8 # 清除项目文件. #
http://www.818198.com Page 469
SHELL十三问
9 #************************************************#
10
11 E_BADDIR=65 # 没有那样的目录.
12 projectdir=/home/bozo/projects # 要清除的目录.
13
14 # --------------------------------------------------------- #
15 # cleanup_pfiles () #
16 # 删除指定目录里的所有文件. #
17 # 参数: $target_directory #
18 # 返回: 成功返回0 , 失败返回$E_BADDIR值. #
19 # --------------------------------------------------------- #
20 cleanup_pfiles ()
21 {
22 if [ ! -d "$1" ] # 测试目标目录是否存在.
23 then
24 echo "$1 is not a directory."
25 return $E_BADDIR
26 fi
27
28 rm -f "$1"/*
29 return 0 # 成功.
30 }
31
32 cleanup_pfiles $projectdir
33
34 exit 0
确认 #!/bin/bash 在脚本的第一行,在任何头部注释行之前.
* 避免使用 "魔数," [1] 它是硬编码的字符常量. 用有意义的变量名来代替. 这使脚本
更容易理解并允许在不破坏应用的情况下做改变和更新.
1 if [ -f /var/log/messages ]
2 then
3 ...
4 fi
5 # 一年以后,你决定让脚本改为检查 /var/log/syslog.
6 # 那么现在就需要你手动修改脚本里每一处的要改动的代码,
7 # 希望不要有你疏漏的地方.
8
9 # 更好的办法是:
10 LOGFILE=/var/log/messages # 只需要改动一行.
11 if [ -f "$LOGFILE" ]
12 then
13 ...
14 fi
* 为变量和函数选择描述性的名字.
1 fl=`ls -al $dirname` # 含义含糊.
2 file_listing=`ls -al $dirname` # 更好的名字.
3
4
http://www.818198.com Page 470
SHELL十三问
5 MAXVAL=10 # 同一个脚本所有程序代码使用脚本常量.
6 while [ "$index" -le "$MAXVAL" ]
7 ...
8
9
10 E_NOTFOUND=75 # 把错误代码的代表的变量名大写U,
11 # +并以"E_"开头.
12 if [ ! -e "$filename" ]
13 then
14 echo "File $filename not found."
15 exit $E_NOTFOUND
16 fi
17
18
19 MAIL_DIRECTORY=/var/spool/mail/bozo # 环境变量名用大写.
20 export MAIL_DIRECTORY
21
22
23 GetAnswer () # 函数名用适当的大小写混合组成.
24 {
25 prompt=$1
26 echo -n $prompt
27 read answer
28 return $answer
29 }
30
31 GetAnswer "What is your favorite number? "
32 favorite_number=$?
33 echo $favorite_number
34
35
36 _uservariable=23 # 语法允许, 但不推荐.
37 # 用户定义的变量最好不要用下划线开头.
38 # 把这个留给系统变量使用更好.
* 用有含义和系统的方法来使用退出代码(exit codes).
1 E_WRONG_ARGS=65
2 ...
3 ...
4 exit $E_WRONG_ARGS
也参考附录 D.
最后 建议在脚本中使用/usr/include/sysexits.h的退出码, 虽然它们主要由 C 和 C++
语言编程时使用.
* 使用标准的参数选项. 最后 建议使用下面一组参数标志.
1 -a All: Return all information (including hidden file info).
2 -b Brief: Short version, usually for other scripts.
3 -c Copy, concatenate, etc.
4 -d Daily: Use information from the whole day, and not merely
5 information for a specific instance/user.
http://www.818198.com Page 471
SHELL十三问
6 -e Extended/Elaborate: (often does not include hidden file info).
7 -h Help: Verbose usage w/descs, aux info, discussion, help.
8 See also -V.
9 -l Log output of script.
10 -m Manual: Launch man-page for base command.
11 -n Numbers: Numerical data only.
12 -r Recursive: All files in a directory (and/or all sub-dirs).
13 -s Setup & File Maintenance: Config files for this script.
14 -u Usage: List of invocation flags for the script.
15 -v Verbose: Human readable output, more or less formatted.
16 -V Version / License / Copy(right|left) / Contribs (email too).
也参考附录 F.
* 把复杂的脚本分割成简单一些的模块. 用合适的函数来实现各个功能. 参考例子 34-4.
* 如果有简单的结构可以使用,不要使用复杂的结构.
1 COMMAND
2 if [ $? -eq 0 ]
3 ...
4 # 多余的并且也不直接明了.
5
6 if COMMAND
7 ...
8 # 更简练 (或者可能会损失一些可读性).
... reading the UNIX source code to the Bourne shell
(/bin/sh). I was shocked at how much simple algorithms could
be made cryptic, and therefore useless, by a poor choice of
code style. I asked myself, "Could someone be proud of this
code?"
Landon Noll
注意事项:
[1] 在上下文, "魔数" 和用来指明文件类型的 魔数(magic numbers)有完全不同的意思.
第33章 杂项
============
Nobody really knows what the Bourne shell's grammar is. Even
examination of the source code is little help.
Tom Duff
33.1. 交互式和非交互式的shells和脚本
------------------------------------交互式的shell在 tty终端从用户的输入中读取命令. 另一方面, shell能在启动时读取启动文
件,显示一个提示符并默认激活作业控制. 用户能交互地使用shell.
运行脚本的shell一般都是非交互的shell. 但脚本仍然可以存取它拥有的终端. 脚本里甚至可
以仿效成可交互的shell.
1 #!/bin/bash
2 MY_PROMPT='$ '
3 while :
4 do
5 echo -n "$MY_PROMPT"
6 read line
7 eval "$line"
http://www.818198.com Page 472
SHELL十三问
8 done
9
10 exit 0
11
12 # 这个例子脚本, 和上面的解释由
13 # Stéphane Chazelas 提供(再次感谢).
让我们考虑一个要求用户交互式输入的脚本,通常用read语句 (参考例子 11-3). 真正的情况
可能有些混乱.以现在假设的情况来说,交互式脚本被限制在一个tty设备上,它本身已经是从
一个控制终端或一个中被用户调用的.
初始化和启动脚本不必是非交互式的,因为它们必须不需要人为地干预地运行.许多管理和系
统维护脚本也同样是非交互式的.不多变的重复性的任务可以自动地由非交互式脚本完成.
非交互式的脚本可以在后台运行,但交互脚本在后台运行则会被挂起,等待永远不会到达的输
入.解决这个难点的办法可以写预料这种情况的脚本或是内嵌here document 的脚本来获取脚
本期望的输入,这样就可作为后台任务运行了.在最简单的情况,重定向一个文件给一个read
语句提供输入(read variable
如果脚本需要测试当前是否运行在交互shell中,一个简单的办法是找一下是否有提示符变量,
即$PS1是否设置了. (如果脚本需要用户输入数据,则脚本会显示一个提示符.)
1 if [ -z $PS1 ] # 没有提示符?
2 then
3 # 非交互式
4 ...
5 else
6 # 交互式
7 ...
8 fi
另一个办法是脚本可以测试是否在变量$-中出现了选项"i".
1 case $- in
2 *i*) # 交互式 shell
3 ;;
4 *) # 非交互式 shell
5 ;;
6 # (Courtesy of "UNIX F.A.Q.," 1993)
注意: 脚本可以使用-i选项强制在交互式模式下运行或脚本头用#!/bin/bash -i. 注意这样
可能会引起脚本古怪的行为或当没有错误出现时也会显示错误信息.
33.2. Shell 包装
----------------包装脚本是指嵌有一个系统命令和程序的脚本,也保存了一组传给该命令的参数. [1] 包装
脚本使原本很复杂的命令行简单化. 这对 sed 和 awk 特别有用.
sed 和 awk 命令一般从命令行上以 sed -e 'commands' 和 awk 'commands' 来调用. 把sed
和awk的命令嵌入到Bash脚本里使调用变得更简单, 并且也可多次使用. 也可以综合地利用
sed 和 awk 的功能, 例如管道(piping)连接sed 命令的输出到awk命令中. 保存为可执行的
文件, 你可以用脚本编写的或修改的调用格式多次的调用它, 而不必在命令行上重复键入复杂
的命令行.
Example 33-1 shell 包装
################################Start Script#######################################
1 #!/bin/bash
2
http://www.818198.com Page 473
SHELL十三问
3 # 这是一个把文件中的空行删除的简单脚本.
4 # 没有参数检查.
5 #
6 # 你可能想增加类似下面的代码:
7 #
8 # E_NOARGS=65
9 # if [ -z "$1" ]
10 # then
11 # echo "Usage: `basename $0` target-file"
12 # exit $E_NOARGS
13 # fi
14
15
16 # 就像从命令行调用下面的命令:
17 # sed -e '/^$/d' filename
18 #
19
20 sed -e /^$/d "$1"
21 # The '-e' 意味着后面跟的是编辑命令 (这是可选的).
22 # '^' 匹配行的开头, '$' 则是行的结尾.
23 # 这个表达式匹配行首和行尾之间什么也没有的行,
24 #+ 即空白行.
25 # 'd'是删除命令.
26
27 # 引号引起命令行参数就允许在文件名中使用空白字符和特殊字符
28 #
29
30 # 注意这个脚本不能真正的修改目标文件.
31 # 如果你需要保存修改,就要重定向到某个输出文件里.
32
33 exit 0
################################End Script#########################################
Example 33-2 稍微复杂一些的shell包装
################################Start Script#######################################
1 #!/bin/bash
2
3 # "subst", 把一个文件中的一个模式替换成一个模式的脚本
4 #
5 # 例如, "subst Smith Jones letter.txt".
6
7 ARGS=3 # 脚本要求三个参数.
8 E_BADARGS=65 # 传递了错误的参数个数给脚本.
9
10 if [ $# -ne "$ARGS" ]
11 # 测试脚本参数的个数 (这是好办法).
12 then
13 echo "Usage: `basename $0` old-pattern new-pattern filename"
14 exit $E_BADARGS
http://www.818198.com Page 474
SHELL十三问
15 fi
16
17 old_pattern=$1
18 new_pattern=$2
19
20 if [ -f "$3" ]
21 then
22 file_name=$3
23 else
24 echo "File \"$3\" does not exist."
25 exit $E_BADARGS
26 fi
27
28
29 # 这儿是实现功能的代码.
30
31 # -----------------------------------------------32 sed -e "s/$old_pattern/$new_pattern/g" $file_name
33 # -----------------------------------------------34
35 # 's' 在sed命令里表示替换,
36 #+ /pattern/表示匹配地址.
37 # The "g"也叫全局标志使sed会在每一行有$old_pattern模式出现的所有地方替换,
38 #+ 而不只是匹配第一个出现的地方.
39 # 参考'sed'的有关书籍了解更深入的解释.
40
41 exit 0 # 脚本成功调用会返回 0.
################################End Script#########################################
Example 33-3 写到日志文件的shell包装
################################Start Script#######################################
1 #!/bin/bash
2 # 普通的shell包装,执行一个操作并记录在日志里
3 #
4
5 # 需要设置下面的两个变量.
6 OPERATION=
7 # 可以是一个复杂的命令链,
8 #+ 例如awk脚本或是管道 . . .
9 LOGFILE=
10 # 不管怎么样,命令行参数还是要提供给操作的.
11
12
13 OPTIONS="$@"
14
15
16 # 记录操作.
17 echo "`date` + `whoami` + $OPERATION "$@"" >> $LOGFILE
18 # 现在, 执行操作.
http://www.818198.com Page 475
SHELL十三问
19 exec $OPERATION "$@"
20
21 # 在操作之前记录日志是必须的.
22 # 为什么?
################################End Script#########################################
Example 33-4 包装awk的脚本
################################Start Script#######################################
1 #!/bin/bash
2 # pr-ascii.sh: 打印 ASCII 码的字符表.
3
4 START=33 # 可打印的 ASCII 字符的范围 (十进制).
5 END=125
6
7 echo " Decimal Hex Character" # 表头.
8 echo " ------- --- ---------"
9
10 for ((i=START; i<=END; i++))
11 do
12 echo $i | awk '{printf(" %3d %2x %c\n", $1, $1, $1)}'
13 # 在这个上下文,不会运行Bash的内建printf命令:
14 # printf "%c" "$i"
15 done
16
17 exit 0
18
19
20 # Decimal Hex Character
21 # ------- --- ---------22 # 33 21 !
23 # 34 22 "
24 # 35 23 #
25 # 36 24 $
26 #
27 # . . .
28 #
29 # 122 7a z
30 # 123 7b {
31 # 124 7c |
32 # 125 7d }
33
34
35 # 把脚本的输出重定向到一个文件或是管道给more命令来查看:
36 #+ sh pr-asc.sh | more
################################End Script#########################################
Example 33-5 另一个包装awk的脚本
################################Start Script#######################################
1 #!/bin/bash
2
http://www.818198.com Page 476
SHELL十三问
3 # 给目标文件增加一列由数字指定的列.
4
5 ARGS=2
6 E_WRONGARGS=65
7
8 if [ $# -ne "$ARGS" ] # 检查命令行参数个数是否正确.
9 then
10 echo "Usage: `basename $0` filename column-number"
11 exit $E_WRONGARGS
12 fi
13
14 filename=$1
15 column_number=$2
16
17 # 传递shell变量给脚本的awk部分需要一点技巧.
18 # 方法之一是在awk脚本中使用强引用来引起bash脚本的变量
19 #
20 # $'$BASH_SCRIPT_VAR'
21 # ^ ^
22 # 这个方法在下面的内嵌的awk脚本中出现.
23 # 参考awk文档了解更多的细节.
24
25 # 多行的awk脚本调用格式为: awk ' ..... '
26
27
28 # 开始 awk 脚本.
29 # -----------------------------30 awk '
31
32 { total += $'"${column_number}"'
33 }
34 END {
35 print total
36 }
37
38 ' "$filename"
39 # -----------------------------40 # awk脚本结束.
41
42
43 # 把shell变量传递给awk变量可能是不安全的,
44 #+ 因此Stephane Chazelas提出了下面另外一种方法:
45 # ---------------------------------------46 # awk -v column_number="$column_number" '
47 # { total += $column_number
48 # }
49 # END {
50 # print total
http://www.818198.com Page 477
SHELL十三问
51 # }' "$filename"
52 # ---------------------------------------53
54
55 exit 0
################################End Script#########################################
对于要实现这些功能而只用一种多合一的瑞士军刀应该用Perl. Perl兼有sed和awk的能力, 并
且具有C的一个很大的子集. 它是标准的并支持面向对象编程的方方面面,甚至是很琐碎的东
西. 短的Perl脚本也可以嵌入到shell脚本中去,以至于有些人宣称Perl能够完全地代替shell
编程(本文作者对此持怀疑态度).
Example 33-6 把Perl嵌入Bash脚本
################################Start Script#######################################
1 #!/bin/bash
2
3 # Shell命令可以包含 Perl 脚本.
4 echo "This precedes the embedded Perl script within \"$0\"."
5 echo "==============================================================="
6
7 perl -e 'print "This is an embedded Perl script.\n";'
8 # 像sed脚本, Perl 也使用"-e"选项.
9
10 echo "==============================================================="
11 echo "However, the script may also contain shell and system commands."
12
13 exit 0
################################End Script#########################################
把Bash脚本和Perl脚本放在同一个文件是可能的. 依赖于脚本如何被调用, 要么是Bash部分被
执行,要么是Perl部分被执行.
Example 33-7 Bash 和 Perl 脚本联合使用
################################Start Script#######################################
1 #!/bin/bash
2 # bashandperl.sh
3
4 echo "Greetings from the Bash part of the script."
5 # 下面可以有更多的Bash命令.
6
7 exit 0
8 # 脚本的Bash部分结束.
9
10 # =======================================================
11
12 #!/usr/bin/perl
13 # 脚本的这个部分必须用-x选项来调用.
14
15 print "Greetings from the Perl part of the script.\n";
16 # 下面可以有更多的Perl命令.
17
18 # 脚本的Perl部分结束.
http://www.818198.com Page 478
SHELL十三问
################################End Script#########################################
bash$ bash bashandperl.sh
Greetings from the Bash part of the script.
bash$ perl -x bashandperl.sh
Greetings from the Perl part of the script.
注意事项:
[1] 事实上,相当数量的Linux软件工具包是shell包装脚本. 例如/usr/bin/pdf2ps,
/usr/bin/batch, 和 /usr/X11R6/bin/xmkmf.
33.3. 测试和比较: 另一种方法
----------------------------对于测试,[[ ]]结构可能比[ ]更合适.同样地,算术比较可能用(( ))结构更有用.
1 a=8
2
3 # 下面所有的比较是等价的.
4 test "$a" -lt 16 && echo "yes, $a < 16" # "与列表"
5 /bin/test "$a" -lt 16 && echo "yes, $a < 16"
6 [ "$a" -lt 16 ] && echo "yes, $a < 16"
7 [[ $a -lt 16 ]] && echo "yes, $a < 16" # 在[[ ]]和(( ))中不必用引号引起变量
8 (( a < 16 )) && echo "yes, $a < 16" #
9
10 city="New York"
11 # 同样,下面的所有比较都是等价的.
12 test "$city" \< Paris && echo "Yes, Paris is greater than $city" # 产生 ASCII 顺序.
13 /bin/test "$city" \< Paris && echo "Yes, Paris is greater than $city"
14 [ "$city" \< Paris ] && echo "Yes, Paris is greater than $city"
15 [[ $city < Paris ]] && echo "Yes, Paris is greater than $city" # 不需要用引号引起$city.
16
17 # 多谢, S.C.
33.4. 递归
----------脚本是否能 递归地 调用自己本身? 当然可以.
Example 33-8 递归调用自己本身的(无用)脚本
################################Start Script#######################################
1 #!/bin/bash
2 # recurse.sh
3
4 # 脚本能否递归地调用自己?
5 # 是的, 但这有什么实际的用处吗?
6 # (看下面的.)
7
8 RANGE=10
9 MAXVAL=9
10
11 i=$RANDOM
12 let "i %= $RANGE" # 产生一个从 0 到 $RANGE - 1 之间的随机数.
13
14 if [ "$i" -lt "$MAXVAL" ]
http://www.818198.com Page 479
SHELL十三问
15 then
16 echo "i = $i"
17 ./$0 # 脚本递归地调用再生成一个和自己一样的实例.
18 fi # 每个子脚本做的事都一样,
19 #+ 直到产生的变量 $i 和变量 $MAXVAL 相等.
20
21 # 用"while"循环代替"if/then"测试会引起错误.
22 # 解释为什么会这样.
23
24 exit 0
25
26 # 注:
27 # ----28 # 脚本要正确地工作必须有执行权限.
29 # 这是指用"sh"命令来调用这个脚本而没有设置正确权限导致的问题.
30 # 请解释原因.
################################End Script#########################################
Example 33-9 递归调用自己本身的(有用)脚本
################################Start Script#######################################
1 #!/bin/bash
2 # pb.sh: 电话本(phone book)
3
4 # 由Rick Boivie编写,已得到使用许可.
5 # 由ABS文档作者修改.
6
7 MINARGS=1 # 脚本需要至少一个参数.
8 DATAFILE=./phonebook
9 # 在当前目录下名为"phonebook"的数据文件必须存在
10 #
11 PROGNAME=$0
12 E_NOARGS=70 # 没有参数的错误值.
13
14 if [ $# -lt $MINARGS ]; then
15 echo "Usage: "$PROGNAME" data"
16 exit $E_NOARGS
17 fi
18
19
20 if [ $# -eq $MINARGS ]; then
21 grep $1 "$DATAFILE"
22 # 如果$DATAFILE文件不存在,'grep' 会打印一个错误信息.
23 else
24 ( shift; "$PROGNAME" $* ) | grep $1
25 # 脚本递归调用本身.
26 fi
27
28 exit 0 # 脚本在这儿退出.
29 # 因此Therefore, 从这行开始可以写没有#开头的的注释行
http://www.818198.com Page 480
SHELL十三问
30 #
31
32 # ------------------------------------------------------------------------33 "phonebook"文件的例子:
34
35 John Doe 1555 Main St., Baltimore, MD 21228 (410) 222-3333
36 Mary Moe 9899 Jones Blvd., Warren, NH 03787 (603) 898-3232
37 Richard Roe 856 E. 7th St., New York, NY 10009 (212) 333-4567
38 Sam Roe 956 E. 8th St., New York, NY 10009 (212) 444-5678
39 Zoe Zenobia 4481 N. Baker St., San Francisco, SF 94338 (415) 501-1631
40 # ------------------------------------------------------------------------41
42 $bash pb.sh Roe
43 Richard Roe 856 E. 7th St., New York, NY 10009 (212) 333-4567
44 Sam Roe 956 E. 8th St., New York, NY 10009 (212) 444-5678
45
46 $bash pb.sh Roe Sam
47 Sam Roe 956 E. 8th St., New York, NY 10009 (212) 444-5678
48
49 # 当超过一个参数传给这个脚本时,
50 #+ 它只打印包含所有参数的行.
################################End Script#########################################
Example 33-10 另一个递归调用自己本身的(有用)脚本
################################Start Script#######################################
1 #!/bin/bash
2 # usrmnt.sh, 由Anthony Richardson编写
3 # 得到允许在此使用.
4
5 # usage: usrmnt.sh
6 # 描述: 挂载设备, 调用者必须列在/etc/sudoers文件的MNTUSERS组里
7 #
8
9 # ----------------------------------------------------------10 # 这是一个用户挂载设备的脚本,它用sudo来调用自己.
11 # 只有拥有合适权限的用户才能用
12
13 # usermount /dev/fd0 /mnt/floppy
14
15 # 来代替
16
17 # sudo usermount /dev/fd0 /mnt/floppy
18
19 # 我使用相同的技术来处理我所有的sudo脚本,
20 #+ 因为我觉得它很方便.
21 # ----------------------------------------------------------22
23 # 如果 SUDO_COMMAND 变量没有设置,我们不能通过sudo来运行脚本本身.
24 #+ 传递用户的真实ID和组ID . . .
http://www.818198.com Page 481
SHELL十三问
25
26 if [ -z "$SUDO_COMMAND" ]
27 then
28 mntusr=$(id -u) grpusr=$(id -g) sudo $0 $*
29 exit 0
30 fi
31
32 # 如果我们以sudo来调用运行,就会运行这儿.
33 /bin/mount $* -o uid=$mntusr,gid=$grpusr
34
35 exit 0
36
37 # 附注 (由脚本作者加注):
38 # -------------------------------------------------39
40 # 1) Linux允许在/etc/fstab文件中使用"users"选项
41 # 以使任何用户能挂载可移动的介质.
42 # 但是, 在一个服务器上,
43 # 我只想有限的几个用户可以存取可移动介质.
44 # 我发现使用sudo可以有更多的控制.
45
46 # 2) 我也发现sudo能通过组更方便地达成目的.
47 #
48
49 # 3) 这个方法使给予任何想给合适权限的人使用mount命令
50 # 所以要小心使用.
51 # 你也可以开发类似的脚本mntfloppy, mntcdrom,和 mntsamba来使mount命令得到更好的控制
52 #
53 #
54 #
################################End Script#########################################
注意: 过多层次的递归调用会耗尽脚本的堆栈空间,会引起段错误.
33.5. 彩色脚本
--------------ANSI [1] 定义了屏幕属性的转义序列集合,例如粗体文本,背景和前景颜色. DOS批处理文
件(batch files) 一般使用ANSI的转义代码来控制色彩输出,Bash脚本也是这么做的.
Example 33-11 一个 "彩色的" 地址资料库
################################Start Script#######################################
1 #!/bin/bash
2 # ex30a.sh: ex30.sh的"彩色" 版本.
3 # 没有加工处理的地址资料库
4
5
6 clear # 清除屏幕.
7
8 echo -n " "
9 echo -e '\E[37;44m'"\033[1mContact List\033[0m"
10 # 白色为前景色,蓝色为背景色
http://www.818198.com Page 482
SHELL十三问
11 echo; echo
12 echo -e "\033[1mChoose one of the following persons:\033[0m"
13 # 粗体
14 tput sgr0
15 echo "(Enter only the first letter of name.)"
16 echo
17 echo -en '\E[47;34m'"\033[1mE\033[0m" # 蓝色
18 tput sgr0 # 把色彩设置为"常规"
19 echo "vans, Roland" # "[E]vans, Roland"
20 echo -en '\E[47;35m'"\033[1mJ\033[0m" # 红紫色
21 tput sgr0
22 echo "ones, Mildred"
23 echo -en '\E[47;32m'"\033[1mS\033[0m" # 绿色
24 tput sgr0
25 echo "mith, Julie"
26 echo -en '\E[47;31m'"\033[1mZ\033[0m" # 红色
27 tput sgr0
28 echo "ane, Morris"
29 echo
30
31 read person
32
33 case "$person" in
34 # 注意变量被引起来了.
35
36 "E" | "e" )
37 # 接受大小写的输入.
38 echo
39 echo "Roland Evans"
40 echo "4321 Floppy Dr."
41 echo "Hardscrabble, CO 80753"
42 echo "(303) 734-9874"
43 echo "(303) 734-9892 fax"
44 echo "[email protected]"
45 echo "Business partner & old friend"
46 ;;
47
48 "J" | "j" )
49 echo
50 echo "Mildred Jones"
51 echo "249 E. 7th St., Apt. 19"
52 echo "New York, NY 10009"
53 echo "(212) 533-2814"
54 echo "(212) 533-9972 fax"
55 echo "[email protected]"
56 echo "Girlfriend"
57 echo "Birthday: Feb. 11"
58 ;;
http://www.818198.com Page 483
SHELL十三问
59
60 # 稍后为 Smith 和 Zane 增加信息.
61
62 * )
63 # 默认选项Default option.
64 # 空的输入(直接按了回车) 也会匹配这儿.
65 echo
66 echo "Not yet in database."
67 ;;
68
69 esac
70
71 tput sgr0 # 把色彩重设为"常规".
72
73 echo
74
75 exit 0
################################End Script#########################################
Example 33-12 画盒子
################################Start Script#######################################
1 #!/bin/bash
2 # Draw-box.sh: 用ASCII字符画一个盒子.
3
4 # Stefano Palmeri编写,文档作者作了少量编辑.
5 # 征得作者同意在本书使用.
6
7
8 ######################################################################
9 ### draw_box 函数的注释 ###
10
11 # "draw_box" 函数使用户可以在终端上画一个盒子.
12 #
13 #
14 # 用法: draw_box ROW COLUMN HEIGHT WIDTH [COLOR]
15 # ROW 和 COLUMN 定位要画的盒子的左上角.
16 #
17 # ROW 和 COLUMN 必须要大于0且小于目前终端的尺寸.
18 #
19 # HEIGHT 是盒子的行数,必须 > 0.
20 # HEIGHT + ROW 必须 <= 终端的高度.
21 # WIDTH 是盒子的列数,必须 > 0.
22 # WIDTH + COLUMN 必须 <= 终端的宽度.
23 #
24 # 例如: 如果你当前终端的尺寸是 20x80,
25 # draw_box 2 3 10 45 是合法的
26 # draw_box 2 3 19 45 的 HEIGHT 值是错的 (19+2 > 20)
27 # draw_box 2 3 18 78 的 WIDTH 值是错的 (78+3 > 80)
28 #
http://www.818198.com Page 484
SHELL十三问
29 # COLOR 是盒子边框的颜色.
30 # 它是第5个参数,并且它是可选的.
31 # 0=黑色 1=红色 2=绿色 3=棕褐色 4=蓝色 5=紫色 6=青色 7=白色.
32 # 如果你传给这个函数错的参数,
33 #+ 它就会以代码65退出,
34 #+ 没有其他的信息打印到标准出错上.
35 #
36 # 在画盒子之前要清屏.
37 # 函数内不包含有清屏命令.
38 # 这使用户可以画多个盒子,甚至叠接多个盒子.
39
40 ### draw_box 函数注释结束 ###
41 ######################################################################
42
43 draw_box(){
44
45 #=============#
46 HORZ="-"
47 VERT="|"
48 CORNER_CHAR="+"
49
50 MINARGS=4
51 E_BADARGS=65
52 #=============#
53
54
55 if [ $# -lt "$MINARGS" ]; then # 如果参数小于4,退出.
56 exit $E_BADARGS
57 fi
58
59 # 搜寻参数中的非数字的字符.
60 # 能用其他更好的办法吗 (留给读者的练习?).
61 if echo $@ | tr -d [:blank:] | tr -d [:digit:] | grep . &> /dev/null; then
62 exit $E_BADARGS
63 fi
64
65 BOX_HEIGHT=`expr $3 - 1` # -1 是需要的,因为因为边角的"+"是高和宽共有的部分.
66 BOX_WIDTH=`expr $4 - 1` #
67 T_ROWS=`tput lines` # 定义当前终端长和宽的尺寸,
68 T_COLS=`tput cols` #
69
70 if [ $1 -lt 1 ] || [ $1 -gt $T_ROWS ]; then # 如果参数是数字就开始检查有效性.
71 exit $E_BADARGS #
72 fi
73 if [ $2 -lt 1 ] || [ $2 -gt $T_COLS ]; then
74 exit $E_BADARGS
75 fi
76 if [ `expr $1 + $BOX_HEIGHT + 1` -gt $T_ROWS ]; then
http://www.818198.com Page 485
SHELL十三问
77 exit $E_BADARGS
78 fi
79 if [ `expr $2 + $BOX_WIDTH + 1` -gt $T_COLS ]; then
80 exit $E_BADARGS
81 fi
82 if [ $3 -lt 1 ] || [ $4 -lt 1 ]; then
83 exit $E_BADARGS
84 fi # 参数检查完毕.
85
86 plot_char(){ # 函数内的函数.
87 echo -e "\E[${1};${2}H"$3
88 }
89
90 echo -ne "\E[3${5}m" # 如果传递了盒子边框颜色参数,则设置它.
91
92 # start drawing the box
93
94 count=1 # 用plot_char函数画垂直线
95 for (( r=$1; count<=$BOX_HEIGHT; r++)); do #
96 plot_char $r $2 $VERT
97 let count=count+1
98 done
99
100 count=1
101 c=`expr $2 + $BOX_WIDTH`
102 for (( r=$1; count<=$BOX_HEIGHT; r++)); do
103 plot_char $r $c $VERT
104 let count=count+1
105 done
106
107 count=1 # 用plot_char函数画水平线
108 for (( c=$2; count<=$BOX_WIDTH; c++)); do #
109 plot_char $1 $c $HORZ
110 let count=count+1
111 done
112
113 count=1
114 r=`expr $1 + $BOX_HEIGHT`
115 for (( c=$2; count<=$BOX_WIDTH; c++)); do
116 plot_char $r $c $HORZ
117 let count=count+1
118 done
119
120 plot_char $1 $2 $CORNER_CHAR # 画盒子的角.
121 plot_char $1 `expr $2 + $BOX_WIDTH` +
122 plot_char `expr $1 + $BOX_HEIGHT` $2 +
123 plot_char `expr $1 + $BOX_HEIGHT` `expr $2 + $BOX_WIDTH` +
124
http://www.818198.com Page 486
SHELL十三问
125 echo -ne "\E[0m" # 恢复最初的颜色.
126
127 P_ROWS=`expr $T_ROWS - 1` # 在终端的底部打印提示符.
128
129 echo -e "\E[${P_ROWS};1H"
130 }
131
132
133 # 现在, 让我们来画一个盒子.
134 clear # 清屏.
135 R=2 # 行
136 C=3 # 列
137 H=10 # 高
138 W=45 # 宽
139 col=1 # 颜色(红)
140 draw_box $R $C $H $W $col # 画盒子.
141
142 exit 0
143
144 # 练习:
145 # --------146 # 增加可以在盒子里打印文本的选项
################################End Script#########################################
最简单也可能是最有用的ANSI转义序列是加粗文本, \033[1m ... \033[0m. \033 触发转义序
列, 而 "[1" 启用加粗属性, 而"[0" 表示切换回禁用加粗状态. "m"则表示终止一个转义序列.
bash$ echo -e "\033[1mThis is bold text.\033[0m"
一种相似的转义序列可切换下划线效果 (在 rxvt 和 aterm 上).
bash$ echo -e "\033[4mThis is underlined text.\033[0m"
注意: echo使用-e选项可以启用转义序列.
其他的转义序列可用于更改文本或/和背景色彩.
bash$ echo -e '\E[34;47mThis prints in blue.'; tput sgr0
bash$ echo -e '\E[33;44m'"yellow text on blue background"; tput sgr0
bash$ echo -e '\E[1;33;44m'"BOLD yellow text on blue background"; tput sgr0
注意: 通常为淡色的前景色文本设置粗体效果是较好的.
tput sgr0 把终端设置恢复为原样. 如果省略这一句会使后续在该终端的输出仍为蓝色.
注意: 因为tput sgr0 在某些环境下不能恢复终端设置, echo -ne \E[0m 会是更好的选择.
可以在有色的背景上用下面的模板写有色彩的文本.
echo -e '\E[COLOR1;COLOR2mSome text goes here.'
"\E[" 开始转义序列. 分号分隔的数值"COLOR1" 和 "COLOR2" 指定前景色和背景色, 数值和
色彩的对应参见下面的表格. (数值的顺序不是有关系的,因为前景色和背景色数值都落在不
重叠的范围里.) "m"终止该转义序列, 然后文本以结束的转义指定的属性显示.
也要注意到用单引号引用了echo -e后面的余下命令序列.
下表的数值是在 rxvt 终端运行的结果. 具体效果可能在其他的各种终端上不一样.
table 33-1. 转义序列中数值和彩色的对应
http://www.818198.com Page 487
SHELL十三问
==========================
| 色彩 | 前景色 | 背景色|
| 黑 | 30 | 40 |
| 红 | 31 | 41 |
| 绿 | 32 | 42 |
| 黄 | 33 | 43 |
| 蓝 | 34 | 44 |
| 洋红 | 35 | 45 |
| 青 | 36 | 46 |
| 白 | 37 | 47 |
==========================
Example 33-13 显示彩色文本
################################Start Script#######################################
1 #!/bin/bash
2 # color-echo.sh: 用彩色来显示文本.
3
4 # 依照需要修改这个脚本.
5 # 这比手写彩色的代码更容易一些.
6
7 black='\E[30;47m'
8 red='\E[31;47m'
9 green='\E[32;47m'
10 yellow='\E[33;47m'
11 blue='\E[34;47m'
12 magenta='\E[35;47m'
13 cyan='\E[36;47m'
14 white='\E[37;47m'
15
16
17 alias Reset="tput sgr0" # 把文本属性重设回原来没有清屏前的
18 #
19
20
21 cecho () # Color-echo.
22 # 参数 $1 = 要显示的信息
23 # 参数 $2 = 颜色
24 {
25 local default_msg="No message passed."
26 # 不是非要一个本地变量.
27
28 message=${1:-$default_msg} # 默认的信息.
29 color=${2:-$black} # 如果没有指定,默认使用黑色.
30
31 echo -e "$color"
32 echo "$message"
33 Reset # 重设文本属性.
34
35 return
http://www.818198.com Page 488
SHELL十三问
36 }
37
38
39 # 现在,让我们试试.
40 # ----------------------------------------------------41 cecho "Feeling blue..." $blue
42 cecho "Magenta looks more like purple." $magenta
43 cecho "Green with envy." $green
44 cecho "Seeing red?" $red
45 cecho "Cyan, more familiarly known as aqua." $cyan
46 cecho "No color passed (defaults to black)."
47 # 缺失 $color (色彩)参数.
48 cecho "\"Empty\" color passed (defaults to black)." ""
49 # 空的 $color (色彩)参数.
50 cecho
51 # $message(信息) 和 $color (色彩)参数都缺失.
52 cecho "" ""
53 # 空的 $message (信息)和 $color (色彩)参数.
54 # ----------------------------------------------------55
56 echo
57
58 exit 0
59
60 # 练习:
61 # ---------62 # 1) 为'cecho ()'函数增加粗体的效果.
63 # 2) 增加可选的彩色背景.
################################End Script#########################################
Example 33-14 "赛马" 游戏
################################Start Script#######################################
1 #!/bin/bash
2 # horserace.sh: 非常简单的赛马模拟.
3 # 作者: Stefano Palmeri
4 # 已取得使用许可.
5
6 ################################################################
7 # 脚本目的:
8 # 使用转义字符和终端颜色.
9 #
10 # 练习:
11 # 编辑脚本使其更具有随机性,
12 #+ 设置一个假的赌场 . . .
13 # 嗯 . . . 嗯 . . . 这个开始使我想起了一部电影 . . .
14 #
15 # 脚本给每匹马一个随机的障碍.
16 # 不均等会以障碍来计算
17 #+ 并且用一种欧洲风格表达出来.
http://www.818198.com Page 489
SHELL十三问
18 # 例如: 机率(odds)=3.75 意味着如果你押1美元赢,
19 #+ 你可以赢得3.75美元.
20 #
21 # 脚本已经在GNU/Linux操作系统上测试过 OS,
22 #+ 测试终端有xterm 和 rxvt, 及 konsole.
23 # 测试机器有AMD 900 MHz 的处理器,
24 #+ 平均比赛时间是75秒.
25 # 在更快的计算机上比赛时间应该会更低.
26 # 所以, 如果你想有更多的悬念,重设USLEEP_ARG 变量的值.
27 #
28 # 由Stefano Palmeri编写.
29 ################################################################
30
31 E_RUNERR=65
32
33 # 检查 md5sum 和 bc 是不是安装了.
34 if ! which bc &> /dev/null; then
35 echo bc is not installed.
36 echo "Can\'t run . . . "
37 exit $E_RUNERR
38 fi
39 if ! which md5sum &> /dev/null; then
40 echo md5sum is not installed.
41 echo "Can\'t run . . . "
42 exit $E_RUNERR
43 fi
44
45 # 更改下面的变量值可以使脚本执行的更慢.
46 # 它会作为usleep的参数 (man usleep)
47 #+ 并且它的单位是微秒 (500000微秒 = 半秒).
48 USLEEP_ARG=0
49
50 # 如果脚本接收到ctrl-c中断,清除临时目录, 恢复终端光标和颜色
51 #
52 trap 'echo -en "\E[?25h"; echo -en "\E[0m"; stty echo;\
53 tput cup 20 0; rm -fr $HORSE_RACE_TMP_DIR' TERM EXIT
54 # 参考调试的章节了解'trap'的更多解释
55
56 # 给脚本设置一个唯一(实际不是绝对唯一的)的临时目录名.
57 HORSE_RACE_TMP_DIR=$HOME/.horserace-`date +%s`-`head -c10 /dev/urandom | md5sum | head -c30`
58
59 # 创建临时目录,并切换到该目录下.
60 mkdir $HORSE_RACE_TMP_DIR
61 cd $HORSE_RACE_TMP_DIR
62
63
64 # 这个函数把光标移动到行为 $1 列为 $2 然后打印 $3.
65 # 例如: "move_and_echo 5 10 linux" 等同于
http://www.818198.com Page 490
SHELL十三问
66 #+ "tput cup 4 9; echo linux", 但是用一个命令代替了两个.
67 # 注: "tput cup" 表示在终端左上角的 0 0 位置,
68 #+ echo 是在终端的左上角的 1 1 位置.
69 move_and_echo() {
70 echo -ne "\E[${1};${2}H""$3"
71 }
72
73 # 产生1-9之间伪随机数的函数.
74 random_1_9 () {
75 head -c10 /dev/urandom | md5sum | tr -d [a-z] | tr -d 0 | cut -c1
76 }
77
78 # 画马时模拟运动的两个函数.
79 draw_horse_one() {
80 echo -n " "//$MOVE_HORSE//
81 }
82 draw_horse_two(){
83 echo -n " "\\\\$MOVE_HORSE\\\\
84 }
85
86
87 # 取得当前的终端尺寸.
88 N_COLS=`tput cols`
89 N_LINES=`tput lines`
90
91 # 至少需要 20-行 X 80-列 的终端尺寸. 检查一下.
92 if [ $N_COLS -lt 80 ] || [ $N_LINES -lt 20 ]; then
93 echo "`basename $0` needs a 80-cols X 20-lines terminal."
94 echo "Your terminal is ${N_COLS}-cols X ${N_LINES}-lines."
95 exit $E_RUNERR
96 fi
97
98
99 # 开始画赛场.
100
101 # 需要一个80个字符的字符串,看下面的.
102 BLANK80=`seq -s "" 100 | head -c80`
103
104 clear
105
106 # 把前景和背景颜色设置成白色的.
107 echo -ne '\E[37;47m'
108
109 # 把光标移到终端的左上角.
110 tput cup 0 0
111
112 # 画六条白线.
113 for n in `seq 5`; do
http://www.818198.com Page 491
SHELL十三问
114 echo $BLANK80 # 线是用80个字符组成的字符串.
115 done
116
117 # 把前景色设置成黑色.
118 echo -ne '\E[30m'
119
120 move_and_echo 3 1 "START 1"
121 move_and_echo 3 75 FINISH
122 move_and_echo 1 5 "|"
123 move_and_echo 1 80 "|"
124 move_and_echo 2 5 "|"
125 move_and_echo 2 80 "|"
126 move_and_echo 4 5 "| 2"
127 move_and_echo 4 80 "|"
128 move_and_echo 5 5 "V 3"
129 move_and_echo 5 80 "V"
130
131 # 把前景色设置成红色.
132 echo -ne '\E[31m'
133
134 # 一些ASCII艺术.
135 move_and_echo 1 8 "..@@@..@@@@@...@@@@@.@...@..@@@@..."
136 move_and_echo 2 8 ".@...@...@.......@...@...@.@......."
137 move_and_echo 3 8 ".@@@@@...@.......@...@@@@@.@@@@...."
138 move_and_echo 4 8 ".@...@...@.......@...@...@.@......."
139 move_and_echo 5 8 ".@...@...@.......@...@...@..@@@@..."
140 move_and_echo 1 43 "@@@@...@@@...@@@@..@@@@..@@@@."
141 move_and_echo 2 43 "@...@.@...@.@.....@.....@....."
142 move_and_echo 3 43 "@@@@..@@@@@.@.....@@@@...@@@.."
143 move_and_echo 4 43 "@..@..@...@.@.....@.........@."
144 move_and_echo 5 43 "@...@.@...@..@@@@..@@@@.@@@@.."
145
146
147 # 把前景和背景颜色设为绿色.
148 echo -ne '\E[32;42m'
149
150 # 画11行绿线.
151 tput cup 5 0
152 for n in `seq 11`; do
153 echo $BLANK80
154 done
155
156 # 把前景色设为黑色.
157 echo -ne '\E[30m'
158 tput cup 5 0
159
160 # 画栅栏.
161 echo "++++++++++++++++++++++++++++++++++++++\
http://www.818198.com Page 492
SHELL十三问
162 ++++++++++++++++++++++++++++++++++++++++++"
163
164 tput cup 15 0
165 echo "++++++++++++++++++++++++++++++++++++++\
166 ++++++++++++++++++++++++++++++++++++++++++"
167
168 # 把前景和背景色设回白色.
169 echo -ne '\E[37;47m'
170
171 # 画3条白线.
172 for n in `seq 3`; do
173 echo $BLANK80
174 done
175
176 # 把前景色设为黑色.
177 echo -ne '\E[30m'
178
179 # 创建9个文件来保存障碍物.
180 for n in `seq 10 7 68`; do
181 touch $n
182 done
183
184 # 设置脚本要画的马的类型为第一种类型.
185 HORSE_TYPE=2
186
187 # 为每匹马创建位置文件和机率文件.
188 #+ 在这些文件里保存了该匹马当前的位置,
189 #+ 类型和机率.
190 for HN in `seq 9`; do
191 touch horse_${HN}_position
192 touch odds_${HN}
193 echo \-1 > horse_${HN}_position
194 echo $HORSE_TYPE >> horse_${HN}_position
195 # 给马定义随机的障碍物.
196 HANDICAP=`random_1_9`
197 # 检查random_1_9函数是否返回了有效值.
198 while ! echo $HANDICAP | grep [1-9] &> /dev/null; do
199 HANDICAP=`random_1_9`
200 done
201 # 给马定义最后的障碍的位置.
202 LHP=`expr $HANDICAP \* 7 + 3`
203 for FILE in `seq 10 7 $LHP`; do
204 echo $HN >> $FILE
205 done
206
207 # 计算机率.
208 case $HANDICAP in
209 1) ODDS=`echo $HANDICAP \* 0.25 + 1.25 | bc`
http://www.818198.com Page 493
SHELL十三问
210 echo $ODDS > odds_${HN}
211 ;;
212 2 | 3) ODDS=`echo $HANDICAP \* 0.40 + 1.25 | bc`
213 echo $ODDS > odds_${HN}
214 ;;
215 4 | 5 | 6) ODDS=`echo $HANDICAP \* 0.55 + 1.25 | bc`
216 echo $ODDS > odds_${HN}
217 ;;
218 7 | 8) ODDS=`echo $HANDICAP \* 0.75 + 1.25 | bc`
219 echo $ODDS > odds_${HN}
220 ;;
221 9) ODDS=`echo $HANDICAP \* 0.90 + 1.25 | bc`
222 echo $ODDS > odds_${HN}
223 esac
224
225
226 done
227
228
229 # 打印机率.
230 print_odds() {
231 tput cup 6 0
232 echo -ne '\E[30;42m'
233 for HN in `seq 9`; do
234 echo "#$HN odds->" `cat odds_${HN}`
235 done
236 }
237
238 # 在起跑线上画马.
239 draw_horses() {
240 tput cup 6 0
241 echo -ne '\E[30;42m'
242 for HN in `seq 9`; do
243 echo /\\$HN/\\" "
244 done
245 }
246
247 print_odds
248
249 echo -ne '\E[47m'
250 # 等待回车按键开始赛马.
251 # 转义序列'\E[?25l'禁显了光标.
252 tput cup 17 0
253 echo -e '\E[?25l'Press [enter] key to start the race...
254 read -s
255
256 # 禁用了终端的常规显示功能.
257 # 这避免了赛跑时不小心按了按键键入显示字符而弄乱了屏幕.
http://www.818198.com Page 494
SHELL十三问
258 #
259 stty -echo
260
261 # --------------------------------------------------------262 # 开始赛跑.
263
264 draw_horses
265 echo -ne '\E[37;47m'
266 move_and_echo 18 1 $BLANK80
267 echo -ne '\E[30m'
268 move_and_echo 18 1 Starting...
269 sleep 1
270
271 # 设置终点线的列数.
272 WINNING_POS=74
273
274 # 记录赛跑开始的时间.
275 START_TIME=`date +%s`
276
277 # COL 是由下面的"while"结构使用的.
278 COL=0
279
280 while [ $COL -lt $WINNING_POS ]; do
281
282 MOVE_HORSE=0
283
284 # 检查random_1_9函数是否返回了有效值.
285 while ! echo $MOVE_HORSE | grep [1-9] &> /dev/null; do
286 MOVE_HORSE=`random_1_9`
287 done
288
289 # 取得随机取得的马的类型和当前位置.
290 HORSE_TYPE=`cat horse_${MOVE_HORSE}_position | tail -1`
291 COL=$(expr `cat horse_${MOVE_HORSE}_position | head -1`)
292
293 ADD_POS=1
294 # 检查当前的位置是否是障碍物的位置.
295 if seq 10 7 68 | grep -w $COL &> /dev/null; then
296 if grep -w $MOVE_HORSE $COL &> /dev/null; then
297 ADD_POS=0
298 grep -v -w $MOVE_HORSE $COL > ${COL}_new
299 rm -f $COL
300 mv -f ${COL}_new $COL
301 else ADD_POS=1
302 fi
303 else ADD_POS=1
304 fi
305 COL=`expr $COL + $ADD_POS`
http://www.818198.com Page 495
SHELL十三问
306 echo $COL > horse_${MOVE_HORSE}_position # 保存新位置.
307
308 # 选择要画的马的类型.
309 case $HORSE_TYPE in
310 1) HORSE_TYPE=2; DRAW_HORSE=draw_horse_two
311 ;;
312 2) HORSE_TYPE=1; DRAW_HORSE=draw_horse_one
313 esac
314 echo $HORSE_TYPE >> horse_${MOVE_HORSE}_position # 保存当前类型.
315
316 # 把前景色设为黑,背景色设为绿.
317 echo -ne '\E[30;42m'
318
319 # 把光标位置移到新的马的位置.
320 tput cup `expr $MOVE_HORSE + 5` `cat horse_${MOVE_HORSE}_position | head -1`
321
322 # 画马.
323 $DRAW_HORSE
324 usleep $USLEEP_ARG
325
326 # 当所有的马都越过15行的之后,再次打印机率.
327 touch fieldline15
328 if [ $COL = 15 ]; then
329 echo $MOVE_HORSE >> fieldline15
330 fi
331 if [ `wc -l fieldline15 | cut -f1 -d " "` = 9 ]; then
332 print_odds
333 : > fieldline15
334 fi
335
336 # 取得领头的马.
337 HIGHEST_POS=`cat *position | sort -n | tail -1`
338
339 # 把背景色重设为白色.
340 echo -ne '\E[47m'
341 tput cup 17 0
342 echo -n Current leader: `grep -w $HIGHEST_POS *position | cut -c7`" "
343
344 done
345
346 # 取得赛马结束的时间.
347 FINISH_TIME=`date +%s`
348
349 # 背景色设为绿色并且启用闪动的功能.
350 echo -ne '\E[30;42m'
351 echo -en '\E[5m'
352
353 # 使获胜的马闪动.
http://www.818198.com Page 496
SHELL十三问
354 tput cup `expr $MOVE_HORSE + 5` `cat horse_${MOVE_HORSE}_position | head -1`
355 $DRAW_HORSE
356
357 # 禁用闪动文本.
358 echo -en '\E[25m'
359
360 # 把前景和背景色设为白色.
361 echo -ne '\E[37;47m'
362 move_and_echo 18 1 $BLANK80
363
364 # 前景色设为黑色.
365 echo -ne '\E[30m'
366
367 # 闪动获胜的马.
368 tput cup 17 0
369 echo -e "\E[5mWINNER: $MOVE_HORSE\E[25m"" Odds: `cat odds_${MOVE_HORSE}`"\
370 " Race time: `expr $FINISH_TIME - $START_TIME` secs"
371
372 # 恢复光标和最初的颜色.
373 echo -en "\E[?25h"
374 echo -en "\E[0m"
375
376 # 恢复回显功能.
377 stty echo
378
379 # 删除赛跑的临时文件.
380 rm -rf $HORSE_RACE_TMP_DIR
381
382 tput cup 19 0
383
384 exit 0
################################End Script#########################################
参考 例子 A-22.
注意: 然而,有一个主要的问题,那就是ANSI 转义序列是不可移植的. 在一些终端运行的
很好的代码可能在另外一些终端上可能运行地很糟糕. 在彩色脚本作者终端上运行的
很好的脚本可能在另外一些终端上就产生不可阅读的输出了. 这给彩色脚本的用处大
大打了个折扣,而很可能使这些技术变成一个暗机关或只是一个玩具而已.
Moshe Jacobson的颜色工具(http://runslinux.net/projects.html#color)能相当容易地使用
ANSI转义序列. 它用清晰和较有逻辑的语法来代替刚才讨论的难用的结构.
Henry/teikedvl 也同样开发了一个软件包来简化彩色脚本的一些操作
(http://scriptechocolor.sourceforge.net/).
注意事项:
[1] 当然,ANSI是American National Standards Institute(美国国家标准组织)的缩
写. 这个令人敬畏的组织建立和维护着许多技术和工业的标准.
33.6. 优化
----------大多数shell脚本处理不复杂的问题时会有很快的解决办法. 正因为这样,优化脚本速度不是
一个问题. 考虑这样的情况, 一个脚本处理很重要的任务, 虽然它确实运行的很好很正确,但
http://www.818198.com Page 497
SHELL十三问
是处理速度太慢. 用一种可编译的语言重写它可能不是非常好的选择. 最简单的办法是重写使
这个脚本效率低下的部分. 这个代码优化的原理是否同样适用于效率低下的shell脚本?
检查脚本中的循环. 反复执行操作的时间消耗增长非常的快. 如果可能, 可以从循环中删除时
间消耗的操作.
优先使用内建(builtin)命令而不是系统命令. 内建命令执行起来更快并且一般调用时不会
产生新的子shell.
避免不需要的命令, 特别是管道(pipe).
1 cat "$file" | grep "$word"
2
3 grep "$word" "$file"
4
5 # 上面的命令行有同样的效果,
6 #+ 但第二个运行的更有效率,因为它不产生新的子进程.
cat 命令似乎特别常在脚本中被滥用.
用time和times工具去了解计算花费的时间. 考虑用C甚至是汇编重写关键的消耗时间的部分.
尝试最小化文件I/O. Bash在文件处理上不是特别地有效率, 所以要考虑在脚本中使用更合适
地工具来处理, 比如说awk 或 Perl.
采用结构化的思想来写脚本, 使各个模块能够依据需要组织和合并起来.一些适用于高级语言
的优化技术也可以用在脚本上 , 但有些技术, 比如说循环优化, 几乎是不相关的. 上面的讨
论, 依据经验来判断.
怎样优化减少执行时间的优秀脚本示例, 请参考例子 12-42.
33.7. 各种小技巧
----------------* 为了记录在一个实际的会话期或多个会话期内运行的用户脚本,可以加下面的代码到每
个你想追踪记录的脚本里. 这会记录下连续的脚本名记录和调用的次数.
1 # 添加(>>)下面几行到你想追踪记录的脚本末尾处.
2
3 whoami>> $SAVE_FILE # 记录调用脚本的用户.
4 echo $0>> $SAVE_FILE # 记录脚本名.
5 date>> $SAVE_FILE # 记录日期和时间.
6 echo>> $SAVE_FILE # 空行作为分隔行.
7
8 # 当然, SAVE_FILE 变量应在~/.bashrc中定义并导出(export)
9 #+ (变量值类似如 ~/.scripts-run)
* >> 操作符可以在文件尾添加内容. 如果你想在文件头添加内容,那应该怎么办?
1 file=data.txt
2 title="***This is the title line of data text file***"
3
4 echo $title | cat - $file >$file.new
5 # "cat -" 连接标准输出的内容和$file的内容.
6 # 最后的结果就是生成了一个新文件,
7 #+ 文件的头添加了 $title 的值,后跟$file的内容.
这是早先例子 17-13中的简化变体. 当然, , sed 也可以办到.
* 脚本也可以像内嵌到另一个shell脚本的普通命令一样调用, 如 Tcl 或 wish 脚本, 甚至
可以是Makefile. 它们可以作为外部shell命令用C语言的 system() 函数调用, 例如.,
system("script_name");.
* 把内嵌的 sed 或 awk 脚本的内容赋值给一个变量可以增加包装脚本(shell wrapper)
的可读性. 参考 例子 A-1 和 例子 11-18.
http://www.818198.com Page 498
SHELL十三问
* 把你最喜欢和最有用的定义和函数放在一些文件中. 当需要的使用的时候, 在脚本中使用
dot (.) 或 source 命令来"包含(include)"这些"库文件"的一个或多个.
1 # 脚本库
2 # ------ ------- 3
4 # 注:
5 # 本文件没有"#!"开头.
6 # 也没有真正做执行动作的代码.
7
8
9 # 有用的变量定义
10
11 ROOT_UID=0 # Root用户的 $UID 值是0.
12 E_NOTROOT=101 # 非root用户出错代码.
13 MAXRETVAL=255 # 函数最大的的返回值(正值).
14 SUCCESS=0
15 FAILURE=-1
16
17
18
19 # 函数
20
21 Usage () # "Usage:" 信息(即帮助信息).
22 {
23 if [ -z "$1" ] # 没有传递参数.
24 then
25 msg=filename
26 else
27 msg=$@
28 fi
29
30 echo "Usage: `basename $0` "$msg""
31 }
32
33
34 Check_if_root () # 检查是不是root在运行脚本.
35 { # 取自例子"ex39.sh".
36 if [ "$UID" -ne "$ROOT_UID" ]
37 then
38 echo "Must be root to run this script."
39 exit $E_NOTROOT
40 fi
41 }
42
43
44 CreateTempfileName () # 创建一个"唯一"的临时文件.
45 { # 取自例子"ex51.sh".
46 prefix=temp
http://www.818198.com Page 499
SHELL十三问
47 suffix=`eval date +%s`
48 Tempfilename=$prefix.$suffix
49 }
50
51
52 isalpha2 () # 测试字符串是不是都是字母组成的.
53 { # 取自例子"isalpha.sh".
54 [ $# -eq 1 ] || return $FAILURE
55
56 case $1 in
57 *[!a-zA-Z]*|"") return $FAILURE;;
58 *) return $SUCCESS;;
59 esac # Thanks, S.C.
60 }
61
62
63 abs () # 绝对值.
64 { # 注意: 最大的返回值 = 255.
65 E_ARGERR=-999999
66
67 if [ -z "$1" ] # 要传递参数.
68 then
69 return $E_ARGERR # 返回错误.
70 fi
71
72 if [ "$1" -ge 0 ] # 如果非负的值,
73 then #
74 absval=$1 # 绝对值是本身.
75 else # 否则,
76 let "absval = (( 0 - $1 ))" # 改变它的符号.
77 fi
78
79 return $absval
80 }
81
82
83 tolower () # 把传递的字符串转为小写
84 { #
85
86 if [ -z "$1" ] # 如果没有传递参数,
87 then #+ 打印错误信息
88 echo "(null)" #+ (C风格的void指针的错误信息)
89 return #+ 然后从函数中返回.
90 fi
91
92 echo "$@" | tr A-Z a-z
93 # 转换传递过来的所有参数($@).
94
http://www.818198.com Page 500
SHELL十三问
95 return
96
97 # 用命令替换功能把函数的输出赋给变量.
98 # 例如:
99 # oldvar="A seT of miXed-caSe LEtTerS"
100 # newvar=`tolower "$oldvar"`
101 # echo "$newvar" # 一串混合大小写的字符转换成了全部小写字符
102 #
103 # 练习: 重写这个函数把传递的参数变为大写
104 # ... toupper() [容易].
105 }
* 在脚本中添加特殊种类的注释开头标识有助于条理清晰和可读性.
1 ## 表示注意.
2 rm -rf *.zzy ## "rm"命令的"-rf"组合选项非常的危险,
3 ##+ 尤其是对通配符而言.
4
5 #+ 表示继续上一行.
6 # 这是第一行
7 #+ 这是多行的注释,
8 #+ 这里是最后一行.
9
10 #* 表示标注.
11
12 #o 表示列表项.
13
14 #> 表示另一个观点.
15 while [ "$var1" != "end" ] #> while test "$var1" != "end"
* if-test 结构的一种聪明用法是用来注释一块代码块.
1 #!/bin/bash
2
3 COMMENT_BLOCK=
4 # 给上面的变量设置某个值就会产生讨厌的结果
5 #
6
7 if [ $COMMENT_BLOCK ]; then
8
9 Comment block -- 10 =================================
11 This is a comment line.
12 This is another comment line.
13 This is yet another comment line.
14 =================================
15
16 echo "This will not echo."
17
18 Comment blocks are error-free! Whee!
19
20 fi
http://www.818198.com Page 501
SHELL十三问
21
22 echo "No more comments, please."
23
24 exit 0
把这种方法和使用here documents来注释代码块作一个比较.
* 测试$? 退出状态变量, 因为一个脚本可能想要测试一个参数是否只包含数字,以便后面
可以把它当作一个整数.
1 #!/bin/bash
2
3 SUCCESS=0
4 E_BADINPUT=65
5
6 test "$1" -ne 0 -o "$1" -eq 0 2>/dev/null
7 # 整数要么等于零要么不等于零.
8 # 2>/dev/null 可以抑制错误信息.
9
10 if [ $? -ne "$SUCCESS" ]
11 then
12 echo "Usage: `basename $0` integer-input"
13 exit $E_BADINPUT
14 fi
15
16 let "sum = $1 + 25" # 如果$1不是整数就会产生错误.
17 echo "Sum = $sum"
18
19 # 任何变量,而不仅仅命令行参数可用这种方法来测试.
20
21 exit 0
* 0 - 255 范围的函数返回值是个严格的限制. 用全局变量和其他方法常常出问题. 函数内
返回值给脚本主体的另一个办法是让函数写值到标准输出(通常是用echo) 作为"返回值",
并且将其赋给一个变量. 这实际是命令替换(command substitution)的变体.
Example 33-15 返回值技巧
################################Start Script#######################################
1 #!/bin/bash
2 # multiplication.sh
3
4 multiply () # 传递乘数.
5 { # 能接受多个参数.
6
7 local product=1
8
9 until [ -z "$1" ] # 直到所有参数都处理完毕...
10 do
11 let "product *= $1"
12 shift
13 done
14
15 echo $product # 不会打印到标准输出,
http://www.818198.com Page 502
SHELL十三问
16 } #+ 因为要把它赋给一个变量.
17
18 mult1=15383; mult2=25211
19 val1=`multiply $mult1 $mult2`
20 echo "$mult1 X $mult2 = $val1"
21 # 387820813
22
23 mult1=25; mult2=5; mult3=20
24 val2=`multiply $mult1 $mult2 $mult3`
25 echo "$mult1 X $mult2 X $mult3 = $val2"
26 # 2500
27
28 mult1=188; mult2=37; mult3=25; mult4=47
29 val3=`multiply $mult1 $mult2 $mult3 $mult4`
30 echo "$mult1 X $mult2 X $mult3 X $mult4 = $val3"
31 # 8173300
32
33 exit 0
################################End Script#########################################
相同的技术也可用在字符串中. 这意味着函数可以"返回"一个非数字的值.
1 capitalize_ichar () # 把传递来的参数字符串的第一个字母大写
2 { #
3
4 string0="$@" # 能接受多个参数.
5
6 firstchar=${string0:0:1} # 第一个字符.
7 string1=${string0:1} # 余下的字符.
8
9 FirstChar=`echo "$firstchar" | tr a-z A-Z`
10 # 第一个字符转换成大写字符.
11
12 echo "$FirstChar$string1" # 打印到标准输出.
13
14 }
15
16 newstring=`capitalize_ichar "every sentence should start with a capital letter."`
17 echo "$newstring" # Every sentence should start with a capital letter.
用这种办法甚至可能"返回" 多个值.
Example 33-16 整型还是string?
################################Start Script#######################################
1 #!/bin/bash
2 # sum-product.sh
3 # 函数可以"返回"多个值.
4
5 sum_and_product () # 计算所传参数的总和与乘积.
6 {
7 echo $(( $1 + $2 )) $(( $1 * $2 ))
8 # 打印每个计算的值到标准输出,各值用空格分隔开.
http://www.818198.com Page 503
SHELL十三问
9 }
10
11 echo
12 echo "Enter first number "
13 read first
14
15 echo
16 echo "Enter second number "
17 read second
18 echo
19
20 retval=`sum_and_product $first $second` # 把函数的输出赋值给变量.
21 sum=`echo "$retval" | awk '{print $1}'` # 把第一个域的值赋给sum变量.
22 product=`echo "$retval" | awk '{print $2}'` # 把第二个域的值赋给product变量.
23
24 echo "$first + $second = $sum"
25 echo "$first * $second = $product"
26 echo
27
28 exit 0
################################End Script#########################################
* 下一个技巧是传递数组给函数的技术, 然后 "返回" 一个数组给脚本.
用 变量替换(command substitution)把数组的所有元素用空格分隔开来并赋给一个变量
就可以实现给函数传递数组. 用先前介绍的方法函数内echo一个数组并"返回此值",然后
调用命令替换用 ( ... ) 操作符赋值给一个数组.
Example 33-17 传递和返回数组
################################Start Script#######################################
1 #!/bin/bash
2 # array-function.sh: 传递一个数组给函数并且...
3 # 从函数"返回"一个数组
4
5
6 Pass_Array ()
7 {
8 local passed_array # 局部变量.
9 passed_array=( `echo "$1"` )
10 echo "${passed_array[@]}"
11 # 列出新数组中的所有元素
12 #+ 新数组是在函数内声明和赋值的.
13 }
14
15
16 original_array=( element1 element2 element3 element4 element5 )
17
18 echo
19 echo "original_array = ${original_array[@]}"
20 # 列出最初的数组元素.
21
http://www.818198.com Page 504
SHELL十三问
22
23 # 下面是传递数组给函数的技巧.
24 # **********************************
25 argument=`echo ${original_array[@]}`
26 # **********************************
27 # 把原数组的所有元素用空格分隔开合成一个字符串并赋给一个变量
28 #
29 #
30 # 注意:只是把数组本身传给函数是不会工作的.
31
32
33 # 下面是允许数组作为"返回值"的技巧.
34 # *****************************************
35 returned_array=( `Pass_Array "$argument"` )
36 # *****************************************
37 # 把函数的输出赋给数组变量.
38
39 echo "returned_array = ${returned_array[@]}"
40
41 echo "============================================================="
42
43 # 现在,再试一次Now, try it again,
44 #+ 尝试在函数外存取(列出)数组.
45 Pass_Array "$argument"
46
47 # 函数本身可以列出数组,但...
48 #+ 函数外存取数组被禁止.
49 echo "Passed array (within function) = ${passed_array[@]}"
50 # 因为变量是函数内的局部变量,所以只有NULL值.
51
52 echo
53
54 exit 0
################################End Script#########################################
在例子 A-10中有一个更精心制作的给函数传递数组的例子.
* 利用双括号结构,使在for 和 while 循环中可以使用C风格的语法来设置和增加变量. 参
考例子 10-12 和 例子 10-17.
* 在脚本开头设置 path 和 umask 增加脚本的"可移植性" -- 在某些把 $PATH 和 umask
弄乱的系统里也可以运行.
1 #!/bin/bash
2 PATH=/bin:/usr/bin:/usr/local/bin ; export PATH
3 umask 022 # 脚本的创建的文件有 755 的权限设置.
4
5 # 多谢Ian D. Allen提出这个技巧.
* 一个有用的脚本技术是:重复地把一个过滤器的输出回馈(用管道)给另一个相同过滤器,
但过滤器有不同的参数和/或选项. 尤其对 tr 和 grep 更合适.
1 # 取自例子"wstrings.sh".
2
http://www.818198.com Page 505
SHELL十三问
3 wlist=`strings "$1" | tr A-Z a-z | tr '[:space:]' Z | \
4 tr -cs '[:alpha:]' Z | tr -s '\173-\377' Z | tr Z ' '`
Example 33-18 anagrams游戏
################################Start Script#######################################
1 #!/bin/bash
2 # agram.sh: 用anagrams玩游戏.
3
4 # 寻找 anagrams ...
5 LETTERSET=etaoinshrdlu
6 FILTER='.......' # 最小有多少个字母?
7 # 1234567
8
9 anagram "$LETTERSET" | # 找出这串字符中所有的 anagrams ...
10 grep "$FILTER" | # 至少7个字符,
11 grep '^is' | # 以'is'开头
12 grep -v 's$' | # 不是复数的(指英文单词复数)
13 grep -v 'ed$' # 不是过去式的(当然也是英文单词)
14 # 可以加许多组合条件和过滤器.
15
16 # 使用 "anagram" 软件
17 #+ 它是作者 "yawl" 单词列表软件包的一部分.
18 # http://ibiblio.org/pub/Linux/libs/yawl-0.3.2.tar.gz
19 # http://personal.riverusers.com/~thegrendel/yawl-0.3.2.tar.gz
20
21 exit 0 # 代码结束.
22
23
24 bash$ sh agram.sh
25 islander
26 isolate
27 isolead
28 isotheral
29
30
31
32 # 练习:
33 # ---------34 # 修改这个脚本使 LETTERSET 能作为命令行参数来接受.
35 # 能够传递参数给第 11 - 13 行的过滤器(就像 $FILTER),
36 #+ 以便能靠传递参数来指定一种功能.
37
38 # 参考agram2.sh了解些微不同的anagram的一种方法
39 #
################################End Script#########################################
See also Example 27-3, Example 12-22, and Example A-9.
* 使用"匿名的 here documents" 来注释代码块,这样避免了对代码块的每一块单独用#来
注释了. 参考例子 17-11.
* 当依赖某个命令脚本在一台没有安装该命令的机器上运行时会出错. 使用 whatis 命令可
http://www.818198.com Page 506
SHELL十三问
以避免此问题.
1 CMD=command1 # 第一选择First choice.
2 PlanB=command2 # 第二选择Fallback option.
3
4 command_test=$(whatis "$CMD" | grep 'nothing appropriate')
5 # 如果'command1'没有在系统里发现 , 'whatis'会返回:
6 #+ "command1: nothing appropriate."
7 #
8 # 另一种更安全的办法是:
9 # command_test=$(whereis "$CMD" | grep \/)
10 # 但后面的测试判断应该翻转过来,
11 #+ 因为$command_test只有当系统存在$CMD命令时才有内容.
12 #
13 # (Thanks, bojster.)
14
15
16 if [[ -z "$command_test" ]] # 检查命令是否存在.
17 then
18 $CMD option1 option2 # 调用command1.
19 else # 否则,
20 $PlanB #+ 调用command2.
21 fi
* 在发生错误的情况下 if-grep test 可能不会返回期望的结果,因为文本是打印在标准出
错而不是标准输出上.
1 if ls -l nonexistent_filename | grep -q 'No such file or directory'
2 then echo "File \"nonexistent_filename\" does not exist."
3 fi
把标准出错重定向到标准输出上可以修改这个.
1 if ls -l nonexistent_filename 2>&1 | grep -q 'No such file or directory'
2 # ^^^^
3 then echo "File \"nonexistent_filename\" does not exist."
4 fi
5
6 # 多谢Chris Martin指出.
* The run-parts 命令很容易依次运行一组命令脚本,特别是和 cron 或 at 组合起来.
* 在shell脚本里能调用 X-Windows 的窗口小部件将多么美好. 已经存在有几种工具包实现
这个了, 它们称为Xscript, Xmenu, 和 widtools. 头两个已经不再维护. 幸运地是仍然
可以从这儿下载widtools.
注意: widtools (widget tools) 工具包要求安装了 XForms 库. 另外, 它的 Makefile
在典型的Linux系统上安装前需要做一些合适的编辑. 最后, 提供的6个部件有3个
不能工作 (事实上会发生段错误).
dialog 工具集提供了shell脚本使用一种称为"对话框"的窗口部件. 原始的 dialog 软件
包工作在文本模式的控制台下, 但它的后续软件 gdialog, Xdialog, 和 kdialog 使用基
于X-Windows的窗口小部件集.
Example 33-19 在shell脚本中调用的窗口部件
################################Start Script#######################################
1 #!/bin/bash
2 # dialog.sh: 使用 'gdialog' 窗口部件.
http://www.818198.com Page 507
SHELL十三问
3 # 必须在你的系统里安装'gdialog'才能运行此脚本.
4 # 版本 1.1 (04/05/05 修正)
5
6 # 这个脚本的灵感源自下面的文章.
7 # "Scripting for X Productivity," by Marco Fioretti,
8 # LINUX JOURNAL, Issue 113, September 2003, pp. 86-9.
9 # Thank you, all you good people at LJ.
10
11
12 # 在窗口中的输入错误.
13 E_INPUT=65
14 # 输入窗口显示的尺寸.
15 HEIGHT=50
16 WIDTH=60
17
18 # 输出文件名 (由脚本名构建而来).
19 OUTFILE=$0.output
20
21 # 把这个脚本的内容显示在窗口中.
22 gdialog --title "Displaying: $0" --textbox $0 $HEIGHT $WIDTH
23
24
25
26 # 现在,保存输入到输出文件中.
27 echo -n "VARIABLE=" > $OUTFILE
28 gdialog --title "User Input" --inputbox "Enter variable, please:" \
29 $HEIGHT $WIDTH 2>> $OUTFILE
30
31
32 if [ "$?" -eq 0 ]
33 # 检查退出状态是一个好习惯.
34 then
35 echo "Executed \"dialog box\" without errors."
36 else
37 echo "Error(s) in \"dialog box\" execution."
38 # 或者, 点击"Cancel", 而不是"OK" 按钮.
39 rm $OUTFILE
40 exit $E_INPUT
41 fi
42
43
44
45 # 现在,我们重新取得并显示保存的变量.
46 . $OUTFILE # 'Source' 保存的文件(即执行).
47 echo "The variable input in the \"input box\" was: "$VARIABLE""
48
49
50 rm $OUTFILE # 清除临时文件.
http://www.818198.com Page 508
SHELL十三问
51 # 有些应用可能需要保留这些文件.
52
53 exit $?
################################End Script#########################################
其他的在脚本中使用窗口的工具还有 Tk 或 wish (Tcl 派生物), PerlTk (Perl 的 Tk
扩展), tksh (ksh 的 Tk 扩展), XForms4Perl (Perl 的 XForms 扩展), Gtk-Perl
(Perl 的 Gtk 扩展), 或 PyQt (Python 的 Qt 扩展).
* 为了对复杂的脚本做多次的版本修订管理, 可以使用 rcs 软件包.
使用这个软件包的好处之一是会自动地升级ID头标识.在 rcs 的co命令处理一些预定义的
关键字参数替换,例如,代替脚本里头#$Id$的,如类似下面的行:
1 #$Id: hello-world.sh,v 1.1 2004/10/16 02:43:05 bozo Exp $
33.8. 安全话题
--------------
33.8.1. 被感染的脚本
--------------------有一个简短的关于脚本安全的介绍是适当的. 脚本程序可能会包含蠕虫病毒,特洛伊木马, 或
是其他的病毒. 由于这些原因, 决不要以root身份运行脚本 (或允许它被插入到系统的
/etc/rc.d里的启动脚本中) 除非你确定这是值得信赖的源码或你已经很小心地分析过了脚本
并确信它不会有什么危害.
Bell实验室及其他地方的病毒研究人员, 包括 M. Douglas McIlroy, Tom Duff, 和
Fred Cohen 已经调查过了shell脚本病毒的实现. 他们认为写脚本病毒甚至对于新手来说也是
很容易的,就像“脚本小子(script kiddie)”也能容易地写出. [1]
这也是学习脚本编程的原因之一:学习读懂脚本和理解脚本可以保护你的系统免受骇客攻击或
破坏.
33.8.2. 隐藏Shell脚本源码
-------------------------为了安全, 使脚本不可读是有必要的. 如果有软件可以把脚本转化成相应的二进制执行文件就
好了. Francisco Rosales的 shc - 通用的Shell脚本编译器(generic shell script
compiler) 可以出色地完成目标.
不幸地, 依照发表在2005年十月的Linux Journal杂志上的一篇文章, 二进制文件可以,至少
在某些情况下能被恢复回脚本的原始代码. 但不管怎么说,这对大多数技术不高超的骇客来说
仍然是一个保持脚本安全的有效的方法.
注意事项:
[1] 参考 Marius van Oers 的文章(Unix Shell Scripting Malware),和列在参考书目
(bibliography)的参考资料.
33.9. 移植话题
--------------这本书是关于在GNU/Linux系统下的Bash编程. 但同样,sh 和 ksh 用户也能在这儿得到许多
有用的价值.
以现在的情况来看,许多种shell和脚本语言都尽力使自己符合 POSIX 1003.2 标准. 用
--posix 选项调用Bash或在脚本开头插入 set -o posix 就能使Bash能以很接近这个标准的方
式运行. 在脚本开头用
1 #!/bin/sh
比用
1 #!/bin/bash
会更好.注意在Linux和一些UNIX风格的系统里/bin/sh是/bin/bash的一个链接(link), 并且
如果脚本以/bin/sh调用时会禁用Bash的扩展功能.
大多数的Bash脚本能不作修改就能运行在 ksh下, 反之亦然, 因为 Chet Ramey 辛勤地把 ksh
http://www.818198.com Page 509
SHELL十三问
的属性移植到了最新的Bash版本.
在商业的 UNIX 机器上, 使用了GNU扩展属性的标准命令的脚本可能不会工作. 这个问题在最
近几年已经有所改观了, 因为GNU软件包完美地代替了在这些"大块头的"UNIX运行的相应工具.
源码分发 给传统UNIX上加快了这种趋势.
Bash 有传统的 Bourne shell 缺乏的一些属性. 下面是其中一些:
* 一些扩展的 调用选项(invocation options)
* 使用 $( ) 结构来完成命令替换(Command substitution)
* 一些 字符串处理(string manipulation) 操作符
* 进程替换(Process substitution)
* Bash的 内建(builtins) 命令
参考 Bash F.A.Q. 查看完整的列表.
33.10. 在Windows下进行Shell编程
-------------------------------使用其他操作系统用户希望能运行UNIX类型的脚本能在他们的系统上运行, 因此也希望能在这
本书里能学到这方面的知识. 来自Cygnus的 Cygwin 软件结合来自Mortice Kern的MKS软件包
(MKS utilities)可以给Windows添加shell脚本的兼容.
已经有正式宣布Windows的将来版本会包含Bash风格的命令行和脚本能力,但目前为止还没有
结果.
第34章 Bash, 版本 2 和 3
=========================
34.1. Bash, 版本2
-----------------当前运行在你的机器里的Bash版本号是版本 2.xx.y 或 3.xx.y.
bash$ echo $BASH_VERSION
2.05.b.0(1)-release
经典的Bash版本2编程语言升级版增加了数组变量, [1] 字符串和参数扩展, 和间接变量引用
的更好的方法,及其他的属性.
Example 34-1 字符串扩展
################################Start Script#######################################
1 #!/bin/bash
2
3 # 字符串扩展.
4 # Bash版本2引入的特性.
5
6 # 具有$'xxx'格式的字符串
7 #+ 将会解释里面的标准的转义字符.
8
9 echo $'Ringing bell 3 times \a \a \a'
10 # 可能在一些终端只能响铃一次.
11 echo $'Three form feeds \f \f \f'
12 echo $'10 newlines \n\n\n\n\n\n\n\n\n\n'
13 echo $'\102\141\163\150' # Bash
14 # 八进制相等的字符.
15
16 exit 0
################################End Script#########################################
Example 34-2 间接变量引用 - 新方法
################################Start Script#######################################
http://www.818198.com Page 510
SHELL十三问
1 #!/bin/bash
2
3 # 间接变量引用.
4 # 这有点像C++的引用属性.
5
6
7 a=letter_of_alphabet
8 letter_of_alphabet=z
9
10 echo "a = $a" # 直接引用.
11
12 echo "Now a = ${!a}" # 间接引用.
13 # ${!variable} 形式比老的"eval var1=\$$var2"更高级
14
15 echo
16
17 t=table_cell_3
18 table_cell_3=24
19 echo "t = ${!t}" # t = 24
20 table_cell_3=387
21 echo "Value of t changed to ${!t}" # 387
22
23 # 这在用来引用数组或表格的成员时非常有用,
24 #+ 或用来模拟多维数组.
25 # 如果有可索引的选项 (类似于指针运算)
26 #+ 会更好. 唉.
27
28 exit 0
################################End Script#########################################
Example 34-3 使用间接变量引用的简单数据库应用
################################Start Script#######################################
1 #!/bin/bash
2 # resistor-inventory.sh
3 # 使用间接变量引用的简单数据库应用.
4
5 # ============================================================== #
6 # 数据
7
8 B1723_value=470 # 值
9 B1723_powerdissip=.25 # 是什么
10 B1723_colorcode="yellow-violet-brown" # 色彩带宽
11 B1723_loc=173 # 它们存在哪儿
12 B1723_inventory=78 # 有多少
13
14 B1724_value=1000
15 B1724_powerdissip=.25
16 B1724_colorcode="brown-black-red"
17 B1724_loc=24N
http://www.818198.com Page 511
SHELL十三问
18 B1724_inventory=243
19
20 B1725_value=10000
21 B1725_powerdissip=.25
22 B1725_colorcode="brown-black-orange"
23 B1725_loc=24N
24 B1725_inventory=89
25
26 # ============================================================== #
27
28
29 echo
30
31 PS3='Enter catalog number: '
32
33 echo
34
35 select catalog_number in "B1723" "B1724" "B1725"
36 do
37 Inv=${catalog_number}_inventory
38 Val=${catalog_number}_value
39 Pdissip=${catalog_number}_powerdissip
40 Loc=${catalog_number}_loc
41 Ccode=${catalog_number}_colorcode
42
43 echo
44 echo "Catalog number $catalog_number:"
45 echo "There are ${!Inv} of [${!Val} ohm / ${!Pdissip} watt] resistors in stock."
46 echo "These are located in bin # ${!Loc}."
47 echo "Their color code is \"${!Ccode}\"."
48
49 break
50 done
51
52 echo; echo
53
54 # 练习:
55 # ---------56 # 1) 重写脚本,使其从外部文件里读数据.
57 # 2) 重写脚本,用数组代替间接变量引用
58 #
59 # 用数组会更简单明了
60
61
62 # 注:
63 # -----64 # Shell脚本除了最简单的数据应用,其实并不合适数据库应用,
65 #+ 它过多地依赖实际工作的环境和命令.
http://www.818198.com Page 512
SHELL十三问
66 # 写数据库应用更好的还是用一门自然支持数据结构的语言,
67 #+ 如 C++ 或 Java (或甚至是 Perl).
68
69 exit 0
################################End Script#########################################
Example 34-4 用数组和其他的小技巧来处理四人随机打牌
################################Start Script#######################################
1 #!/bin/bash
2
3 # Cards:
4 # 处理四人打牌.
5
6 UNPICKED=0
7 PICKED=1
8
9 DUPE_CARD=99
10
11 LOWER_LIMIT=0
12 UPPER_LIMIT=51
13 CARDS_IN_SUIT=13
14 CARDS=52
15
16 declare -a Deck
17 declare -a Suits
18 declare -a Cards
19 # 用一个三维数据来描述数据会更容易实现也更明了一些.
20 #
21 # 可能Bash将来的版本会支持多维数组.
22
23
24 initialize_Deck ()
25 {
26 i=$LOWER_LIMIT
27 until [ "$i" -gt $UPPER_LIMIT ]
28 do
29 Deck[i]=$UNPICKED # 把整副牌的每张牌都设为没人持牌.
30 let "i += 1"
31 done
32 echo
33 }
34
35 initialize_Suits ()
36 {
37 Suits[0]=C #梅花
38 Suits[1]=D #方块
39 Suits[2]=H #红心
40 Suits[3]=S #黑桃
41 }
http://www.818198.com Page 513
SHELL十三问
42
43 initialize_Cards ()
44 {
45 Cards=(2 3 4 5 6 7 8 9 10 J Q K A)
46 # 另一种初始化数组的方法.
47 }
48
49 pick_a_card ()
50 {
51 card_number=$RANDOM
52 let "card_number %= $CARDS"
53 if [ "${Deck[card_number]}" -eq $UNPICKED ]
54 then
55 Deck[card_number]=$PICKED
56 return $card_number
57 else
58 return $DUPE_CARD
59 fi
60 }
61
62 parse_card ()
63 {
64 number=$1
65 let "suit_number = number / CARDS_IN_SUIT"
66 suit=${Suits[suit_number]}
67 echo -n "$suit-"
68 let "card_no = number % CARDS_IN_SUIT"
69 Card=${Cards[card_no]}
70 printf %-4s $Card
71 # 优雅地打印各张牌.
72 }
73
74 seed_random () # 随机产生牌上数值的种子.
75 { # 如果你没有这么做会有什么发生?
76 seed=`eval date +%s`
77 let "seed %= 32766"
78 RANDOM=$seed
79 # 其他的产生随机用的种子的方法还有什么W?
80 #
81 }
82
83 deal_cards ()
84 {
85 echo
86
87 cards_picked=0
88 while [ "$cards_picked" -le $UPPER_LIMIT ]
89 do
http://www.818198.com Page 514
SHELL十三问
90 pick_a_card
91 t=$?
92
93 if [ "$t" -ne $DUPE_CARD ]
94 then
95 parse_card $t
96
97 u=$cards_picked+1
98 # 改回1步进的索引(临时的). 为什么?
99 let "u %= $CARDS_IN_SUIT"
100 if [ "$u" -eq 0 ] # 内嵌的 if/then 条件测试.
101 then
102 echo
103 echo
104 fi
105 # Separate hands.
106
107 let "cards_picked += 1"
108 fi
109 done
110
111 echo
112
113 return 0
114 }
115
116
117 # 结构化编程:
118 # 整个程序逻辑模块化.
119
120 #================
121 seed_random
122 initialize_Deck
123 initialize_Suits
124 initialize_Cards
125 deal_cards
126 #================
127
128 exit 0
129
130
131
132 # 练习 1:
133 # 把这个脚本完整地做注释.
134
135 # 练习 2:
136 # 增加一个处理例程 (函数) 来以花色排序打印出每个人手中的牌.
137 # 如果你高兴,可增加你喜欢的各种酷的代码.
http://www.818198.com Page 515
SHELL十三问
138
139 # 练习 3:
140 # 简化和理顺脚本的逻辑.
################################End Script#########################################
注意事项:
[1] Chet Ramey 承诺会在Bash的未来版本中实现关联数组(associative arrays)
(一个Perl特性). 到了版本3,这个特性还没有实现.
34.2. Bash版本3
---------------在2004年7月27日, Chet Ramey 发布了Bash的第三版本. 它修复了许多bug并加入了一些新的
属性.
增加的一些属性有:
* 新的,更特别的不可移植的 {a..z} 花括号扩展(brace expansion) 操作符.
1 #!/bin/bash
2
3 for i in {1..10}
4 # 比下面的更简单并且更易于理解
5 #+ for i in $(seq 10)
6 do
7 echo -n "$i "
8 done
9
10 echo
11
12 # 1 2 3 4 5 6 7 8 9 10
* ${!array[@]} 操作符, 它扩展给定的数组(array)的所有元素下标.
1 #!/bin/bash
2
3 Array=(element-zero element-one element-two element-three)
4
5 echo ${Array[0]} # 元素0
6 # 数组的第一个元素.
7
8 echo ${!Array[@]} # 0 1 2 3
9 # 数组所有的下标.
10
11 for i in ${!Array[@]}
12 do
13 echo ${Array[i]} # element-zero
14 # element-one
15 # element-two
16 # element-three
17 #
18 # 在数组里的所有元素.
19 done
* =~ 正则表达式(Regular Expression) 匹配操作符在双方括号(double brackets)
测试表达式中使用. (Perl也有一个相似的操作符.)
1 #!/bin/bash
http://www.818198.com Page 516
SHELL十三问
2
3 variable="This is a fine mess."
4
5 echo "$variable"
6
7 if [[ "$variable" =~ "T*fin*es*" ]]
8 # 在双方括号([[]])里用=~操作符进行正则匹配.
9 then
10 echo "match found"
11 # match found
12 fi
或, 更有用的用法:
1 #!/bin/bash
2
3 input=$1
4
5
6 if [[ "$input" =~ "[1-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][0-9][0-9]" ]]
7 # NNN-NN-NNNN
8 # 每个N是一个数字.
9 # 但, 开头的第一个数字不能是 0.
10 then
11 echo "Social Security number."
12 # 处理 SSN.
13 else
14 echo "Not a Social Security number!"
15 # 或者, 要求正确的输入.
16 fi
还有一个使用 =~ 操作符的例子, 参考例子 A-28.
注意: 升级到Bash版本3使原来在早先版本可以工作的少部分脚本不能工作了. 要重新测试
原来的脚本看是否它们仍然可以工作!
确实发生不能工作的情况, 在 Advanced Bash Scripting Guide 里的部分脚本代码
不得不修复 (例如,例子 A-20 和 例子 9-4).
第35章 后记
============
35.1. 作者后记
--------------
doce ut discas
(Teach, that you yourself may learn.)
我怎么会写这本脚本编程的书?这有一个很奇怪的故事.时间回到前几年,那时我准备学习
shell脚本编程--这最好的办法莫过于读一本这方面的好书了.我一直在找一本能覆盖这个
主题方方面面的指南参考书.我也在找一本能把难点说得清楚容易并能用实际的代码和代码注
释解释这些难以理解的细节. [1] 事实上,我在找一本非常满意的书, 或者是类似的东西.
不幸的是,这是不存在的.如果我想要一本,那我不得不自己写一本.于是,它就写出来了.
这使我想起一个关于疯子教授的故事.下面的事像笨蛋那样荒谬.当这本书上架之前,对于所
有的在图书馆的书使他有了想写一本书的主意.他就这样做了,开始了这项好处多多的工作.
他争分夺秒地开始完成和现在这本书标题很像的书,他每天都很快地奔跑回家来做这件事.当
几年之后他死掉了,他有了写几千本书保存下来,可能会被和其他的破书一块放在书架上.可
http://www.818198.com Page 517
SHELL十三问
能他写的书不是那么的好,但这有什么关系呢?这是一个活在幻想里的一个朋友.即使他没有
被幻想给迷惑驱使...但我忍不住地钦佩这个老笨蛋.
注意事项:
[1] 这是声名狼藉使人郁闷到死的技术.
35.2. 关于作者
--------------这家伙到底是谁?
作者没有外交特权,不是被强迫写作的. [1] 这本书有点违背他的其他的主要工作,
HOW-2 Meet Women: The Shy Man's Guide to Relationships. 他另外也写了
Software-Building HOWTO. 近来, 他尝试写一些虚构的短篇小说.
自1995年成为一个Linux用户以来(Slackware 2.2, kernel 1.2.1), 作者已经发表了一些软件
包,包括 cruft 一次性加密软件包(one-time pad encryption utility), 软件 mcalc 可
用做计算器, 软件judge是Scrabble拼字游戏的自动求解包, 和软件包yawl 一起组成猜词表.
他从一台CDC 3800上使用FORTRAN IV开始他的编程之旅, 但那种日子一点也不值得怀念.
作者和他的妻子还有他们的狗隐居在一个偏远的地方,他幻想着人性是善良的.
注意事项:
[1] 这些谁可以做,谁不可以做...拿到一个MCSE证书.
35.3. 哪里可以取得帮助?
-----------------------作者不是太忙(并且心情不错)的话,会回答一般性的脚本编程问题.但是,如果你的特定应
用的脚本不工作,建议你最好把问题发到 comp.os.unix.shell 新闻组去.
35.4. 制作这本书的工具
----------------------
35.4.1 硬件
-----------一个运行着Red Hat 7.1/7.3的IBM Thinkpad, model 760XL(P166, 104 meg RAM)的笔记本.
是的,它非常的缓慢并且还有一个更人胆战心惊的键盘,但它总比一根铅笔加一个巨大的写字
板好多了.
升级: 升级到了运行着FC3的 770Z Thinkpad (P2-366, 192 meg RAM)笔记本.谁想捐赠一个
更新的一点笔记本给这个快饿死的作者
35.4.2 软件和排版软件
--------------------- 1. Bram Moolenaar的功能强大的SGML软件 vim文本编辑器.
2. OpenJade, 一个把SGML文档转换为其他格式的DSSSL翻译引擎.
3. Norman Walsh的DSSSL排版框架.
4. 由Norman Walsh和Leonard Muellner (O'Reilly, ISBN 1-56592-580-7)写的最权威的
指南:DocBook. 它仍然是任何一个想写Docbook SGML格式的书的标准参考书.
todo
====
12.22 第2部分翻译结束.阶段性总结.
12.02 本打算自己看,做做笔记就算了,如果要大家看,就不行了.补全前3章所有未译的地方.
01.06 所有的没翻译或翻译不对的地方标记
01.08 一些朋友提出想要,可惜没译完,最近效率比较低,手里还有些其它的活:(.
丑媳妇总要见公婆,还是放了吧.
01.13 11章结束
01.14 12章开始, 翻译html版本.
01.19 开始12.4节.
http://www.818198.com Page 518
SHELL十三问
02.16 开始12.6节.
03.23 12章终于结束了...
04.15 前3部分终于结束了, 历时4个多月:(, 后边的部分就交给
05.15 终于完成了, 没有黄毅兄的帮助, 真是不敢想象, 恐怕就要流产了
接下来就要搞sgml版本的了, 唉... 到底走弯路了...
[]
awk基础入门(1)
文章整理: 文章来源: 网络
Awk是一种非常好的语言,同时有一个非常奇怪的名称。在本系列文章中,DanielRobbins 将使您迅速掌握 awk编程技巧。
随着本系列的进展,将讨论更高级的主题,最后将演示一个真正的高级awk 演示程序。
捍卫 awk
在本系列文章中,我将使您成为精通 awk 的编码人员。我承认,awk 并没有一个非常好听且又非常“时髦”的名字。
awk 的 GNU 版本(叫作 gawk)听起来非常怪异。那些不熟悉这种语言的人可能听说过 "awk",并可能认为它是一组落伍
且过时的混乱代码。它甚至会使最博学的 UNIX 权威陷于错乱的边缘(使他不断地发出 "kill -9!" 命令,就象使用咖啡机一
样)。
的确,awk 没有一个动听的名字。但它是一种很棒的语言。awk 适合于文本处理和报表生成,它还有许多精心设计的特
性,允许进行需要特殊技巧程序设计。与某些语言不同,awk 的语法较为常见。它借鉴了某些语言的一些精华部分,如 C
语言、python 和 bash(虽然在技术上,awk 比 python 和 bash 早创建)。awk 是那种一旦学会了就会成为您战略编码库
的主要部分的语言。
第一个 awk
让我们继续,开始使用 awk,以了解其工作原理。在命令行中输入以下命令:
$ awk '{ print }' /etc/passwd
[]
awk基础入门(2)
文章整理: 文章来源: 网络
如您所见,awk 打印出 /etc/passwd 文件的第一和第三个字段,它们正好分别是用户名和用户标识字段。现在,当脚本运行
时,它并不理想 -- 在两个输出字段之间没有空格!如果习惯于使用 bash 或 python 进行编程,那么您会指望 print $1 $3 命
令在两个字段之间插入空格。然而,当两个字符串在 awk 程序中彼此相邻时,awk 会连接它们但不在它们之间添加空格。
以下命令会在这两个字段中插入空格:
$ awk -F":" '{ print $1 " " $3 }' /etc/passwd
[]
awk基础入门(3)
文章整理: 文章来源: 网络
条件语句
awk 还提供了非常好的类似于 C 语言的 if 语句。如果您愿意,可以使用 if 语句重写前一个脚本:
{ if ( $5 ~ /root/ ) { print $3 }}
[]
--------------------制作工具:小说下载阅读器 http://www.mybook66.com
http://www.818198.com Page 519