对于任何想适当精通一些系统管理知识的人来说,掌握shell脚本知识都是最基本的,即使这些人可能并不打算真正的编写一些脚本.想一下Linux机器的启动过程,在这个过程中,必将运行 /etc/rc.d目录下的脚本来存储系统配置和建立服务.详细的理解这些启动脚本对于分析系统的行为是非常重要的,并且有时候可能必须修改它.
Bash是"Bourne-Again shell"首字母的缩写,也是Stephen Bourne的经典 的Bourne shell的一个双关语,(译者:说实话,我一直搞不清这个双关语是什么意思,为什么叫 "Bourn-Again shell",这其中应该有个什么典故吧,哪位好心,告诉我一下^^).Bash已经成为了 所有UNIX中shell脚本的事实上的标准了 。
C Shell和它的变种.(注意:C Shell 编程是不被推荐的,因为一些特定的内在问题,Tom Christiansen在1993年10月指出了这个问题 请在http://www.etext.org/Quartz/computer/unix/csh.harmful.gz中查看具体内容.)
现在,让我们看一下一个真正意义的脚本.而且我们可以走得更远...
Example 2-3. cleanup:一个增强的和广义的删除logfile的脚本
################################Start Script#######################################
1 #!/bin/bash
2 # 清除, 版本 3
3
4 # Warning:
5 # -------
6 # 这个脚本有好多特征,这些特征是在后边章节进行解释的,大概是进行到本书的一半的
7 # 时候,
8 # 你就会觉得它没有什么神秘的了.
9 #
10
11
12
13 LOG_DIR=/var/log
14 ROOT_UID=0 # $UID为0的时候,用户才具有根用户的权限
15 LINES=50 # 默认的保存行数
16 E_XCD=66 # 不能修改目录?
17 E_NOTROOT=67 # 非根用户将以error退出 这些常数是liinux下定义的么?有什么具体含义呢?
18
19
20 # 当然要使用根用户来运行
21 if [ "$UID" -ne "$ROOT_UID" ] -ne 表示 非?
22 then
23 echo "Must be root to run this script."
24 exit $E_NOTROOT
25 fi 注意语法格式
26
27 if [ -n "$1" ] $1表示脚本的第一个参数
28 # 测试是否有命令行参数(非空).
29 then
30 lines=$1
31 else
32 lines=$LINES # 默认,如果不在命令行中指定
33 fi
34
35
36 # Stephane Chazelas 建议使用下边
37 #+ 的更好方法来检测命令行参数.
38 #+ 但对于这章来说还是有点超前.
39 #
40 # E_WRONGARGS=65 # 非数值参数(错误的参数格式)
41 #
42 # case "$1" in 注意语法格式
43 # "" ) lines=50;;
44 # *[!0-9]*) echo "Usage: `basename $0` file-to-cleanup"; exit $E_WRONGARGS;;
45 # * ) lines=$1;;
46 # esac
47 #
48 #* 直到"Loops"的章节才会对上边的内容进行详细的描述.
49
50
51 cd $LOG_DIR
52
53 if [ `pwd` != "$LOG_DIR" ] # 或者 if[ "$PWD" != "$LOG_DIR" ] 上面的-ne和这里的可以互换不?有什么异同呢?
54 # 不在 /var/log中?
55 then
56 echo "Can't change to $LOG_DIR."
57 exit $E_XCD
58 fi # 在处理log file之前,再确认一遍当前目录是否正确.
59
60 # 更有效率的做法是
61 #
62 # cd /var/log || {
63 # echo "Cannot change to necessary directory." >&2
64 # exit $E_XCD;
65 # }
66
67
68
69
70 tail -$lines messages > mesg.temp # 保存log file消息的最后部分.
71 mv mesg.temp messages # 变为新的log目录.
72
73
74 # cat /dev/null > messages
75 #* 不再需要了,使用上边的方法更安全.
76
77 cat /dev/null > wtmp # ': > wtmp' 和 '> wtmp'具有相同的作用
78 echo "Logs cleaned up."
79
80 exit 0
81 # 退出之前返回0,返回0表示成功.
82 #
################################End Script#########################################
echo ${PATH#*:} # 参数替换,不是一个注释 ,这里有什么语法规则呢?
echo $(( 2#101011 )) # 数制转换,不是一个注释
if [ -x "$filename" ]; then # 注意:"if"和"then"需要分隔 , 为啥?
case "$variable" in 注意语法格式
abc) echo ""$variable = abc" ;;
xyz) echo ""$variable = xyz" ;;
esac
.命令经常作为一个文件移动命令的目的地
bash$ cp /home/bozo/current_work/junk/* .
1 a=123
2 ( a=321; )
3
4 echo "a = $a" # a = 123
5 # 在圆括号中a变量,更像是一个局部变量.
代码块.又被称为内部组.事实上,这个结构创建了一个匿名的函数.但是与函数不同的
是,在其中声明的变量,对于脚本其他部分的代码来说还是可见的.如:
bash$
{
local a;
a= 123;
}
bash中的local申请的变量只能够用在函数中.
#!/bin/bash
echo "$1"
echo
File=io_output
{
read line1
read line2
} < $File 把文件中前两行读入到line1.line2变量中
echo "line1:$line1"
echo "line2:$line2"
exit 0
{} "; 路径名.一般都在find命令中使用.这不是一个shell内建命令.
注意: ";"用来结束find命令序列的-exec选项.
[] test.
test的表达式将在[]中.
值得注意的是[是shell内建test命令的一部分,并不是/usr/bin/test中的扩展命令
的一个连接.
[[]] test.
test表达式放在[[]]中.(shell关键字)
具体查看[[]]结构的讨论.
[] 数组元素
Array[1]=slot_1
echo ${Array[1]}
[] 字符范围
在正则表达式中使用,作为字符匹配的一个范围
(()) 数学计算的扩展
在(())结构中可以使用一些数字计算.
具体参阅((...))结构.
>&>>&>><
重定向.
scriptname >filename 重定向脚本的输出到文件中.覆盖文件原有内容.
command &>filename 重定向stdout和stderr到文件中
command >&2 重定向command的stdout到stderr
scriptname >>filename 重定向脚本的输出到文件中.添加到文件尾端,如果没有文件,
则创建这个文件.
进程替换,具体见"进程替换部分",跟命令替换极其类似.
(command)>
<(command)
<和> 可用来做字符串比较
<和> 可用在数学计算比较
<< 重定向,用在"here document"
<<< 重定向,用在"here string"
<,> ASCII比较
1 veg1=carrots
2 veg2=tomatoes
3
4 if [[ "$veg1" < "$veg2" ]]
5 then
6 echo "Although $veg1 precede $veg2 in the dictionary,"
7 echo "this implies nothing about my culinary preferences."
8 else
9 echo "What kind of dictionary are you using, anyhow?"
10 fi
"<,"> 正则表达式中的单词边界.如:
bash$grep '"<the">' textfile
| 管道.分析前边命令的输出,并将输出作为后边命令的输入.这是一种产生命令链的
好方法.
1 echo ls -l | sh
2 # 传递"echo ls -l"的输出到shell中,
3 #+ 与一个简单的"ls -l"结果相同.
4
5
6 cat *.lst | sort | uniq
7 # 合并和排序所有的".lst"文件,然后删除所有重复的行.
管道是进程间通讯的一个典型办法,将一个进程的stdout放到另一个进程的stdin中.
标准的方法是将一个一般命令的输出,比如cat或echo,传递到一个过滤命令中(在这个
过滤命令中将处理输入),得到结果,如:
cat $filename1 | $filename2 | grep $search_word
当然输出的命令也可以传递到脚本中.如:
################################Start Script#######################################
1 #!/bin/bash
2 # uppercase.sh : 修改输出,全部转换为大写
3
4 tr 'a-z' 'A-Z'
5 # 字符范围必须被""引用起来
6 #+ 来阻止产生单字符的文件名.
7
8 exit 0
################################End Script#########################################
现在让我们输送ls -l的输出到一个脚本中.
bash$ ls -l | ./uppercase.sh
-RW-RW-R-- 1 BOZO BOZO 109 APR 7 19:49 1.TXT
-RW-RW-R-- 1 BOZO BOZO 109 APR 14 16:48 2.TXT
-RW-R--R-- 1 BOZO BOZO 725 APR 20 20:56 DATA-FILE
注意:管道中的一个进程的stdout必须被下一个进程作为stdin读入.否则,数据流会阻
塞,并且管道将产生非预期的行为.
如:
1 cat file1 file2 | ls -l | sort
2 #从"cat file1 file2"中的输出并没出现
作为子进程的运行的管道,不能够改变脚本的变量.
1 variable="initial_value"
2 echo "new_value" | read variable
3 echo "variable = $variable" #variable = initial_value
如果管道中的某个命令产生了一个异常,并中途失败,那么这个管道将过早的终止.
这种行为被叫做a broken pipe,并且这种状态下将发送一个SIGPIPE信号.
>| 强制重定向(即使设置了noclobber选项--就是-C选项).这将强制的覆盖一个现存文件.
|| 或-逻辑操作.
& 后台运行命令.一个命令后边跟一个&,将表示在后台运行.
bash$sleep 10 &
[1] 850
[1]+ Done sleep 10
在一个脚本中,命令和循环都可能运行在后台.
注意:在一个脚本内后台运行一个命令,有可能造成这个脚本的挂起,等待一个按键
响应.幸运的是,我们可以在Example 11-24附近,看到这个问题的解决办法.
&& 与-逻辑操作.
- 选项,前缀.在所有的命令内如果想使用选项参数的话,前边都要加上"-".
COMMAND -[Option1][Option2][...]
ls -al
sort -dfu $filename
set -- $variable
1 if [ $file1 -ot $file2 ]
2 then
3 echo "File $file1 is older than $file2."
4 fi
5
6 if [ "$a" -eq "$b" ]
7 then
8 echo "$a is equal to $b."
9 fi
10
11 if [ "$c" -eq 24 -a "$d" -eq 47 ]
12 then
13 echo "$c equals 24 and $d equals 47."
14 fi
- 用于重定向 stdin 或 stdout.
注意:在一个脚本内后台运行一个命令,有可能造成这个脚本的挂起,等待一个按键
响应.
1 variable1_=$1_ # 而不是 variable1=$1
2 # 这将阻止一个错误,即使在调用时没使用这个位置参数.
注意:如果一个脚本以|(管道字符)结束.那么一个"(转义符),就不用非加上不可了.
但是一个好的shell脚本编写风格,还是应该在行尾加上",以增加可读性.
exit命令被用来结束脚本,就像C语言一样.他也会返回一个值来传给父进程,父进程会判断是否
可用.
每个命令都会返回一个exit状态(有时候也叫return状态).成功返回0,如果返回一个非0值,通
常情况下都会被认为是一个错误码.一个编写良好的UNIX命令,程序,和工具都会返回一个0作为
退出码来表示成功,虽然偶尔也会有例外.
同样的,脚本中的函数和脚本本身都会返回退出状态.在脚本或者是脚本函数中执行的最后的命
令会决定退出状态.在脚本中,exit nnn命令将会把nnn退出码传递给shell(nnn必须是10进制数
0-255).
当一个脚本以不带参数exit来结束时,脚本的退出状态就由脚本中最后执行命令来决定.
$?读取最后执行命令的退出码.函数返回后,$?给出函数最后执行的那条命令的退出码.这种给
函数返回值的方法是Bash的方法.对于脚本来说也一样.总之,一般情况下,0为成功,非0失败W.
注意: !逻辑非操作,将会反转test命令的结果,并且这会影响exit状态.
注意事项:
特定的退出码都有预定的含义(见附录D),用户不应该在自己的脚本中指定他.
Bash具有test命令,不同的[]和()操作,和
if/then结构.
一个if/then结构可以测试命令的返回值是否为0(因为0表示成功),如果是的话,执行更多命令.
有一个专用命令"["(左中括号,特殊字符).这个命令与test命令等价,但是出于效率上的考虑,
它是一个内建命令.这个命令把它的参数作为比较表达式或是文件测试,并且根据比较的结果,
返回一个退出码.
在版本2.02的Bash中,推出了一个新的[[...]]扩展test命令.因为这种表现形式可能对某些语
言的程序员来说更加熟悉.注意"[["是一个关键字,并不是一个命令.
Bash把[[ $a -lt $b ]]看作一个单独的元素,并且返回一个退出码.
((...))和let...结果也能够返回一个退出码,当它们所测试的算术表达式的结果为非0的时候,
他们的退出码将返回0.这些算术扩展(见第15章)结构被用来做算术比较.
注意:当if和then在一个条件测试的同一行中的话,必须使用";"来终止if表达式.if和then都是
关键字.关键字(或者命令)作为一个表达式的开头,并且在一个新的表达式开始之前,必须
结束上一个表达式.
1 if [ -x "$filename" ]; then
使用if test condition-true这种形式和if[condition-true]这种形式是等价的.向我们前边
所说的"["是test的标记.并且以"]"结束.在if/test中并不应该这么严厉,但是新版本的Bash
需要它.
注意:test命令是Bash的内建命令,用来测试文件类型和比较字符串.因此,在Bash脚本中,test
并不调用/usr/bin/test的二进制版本(这是sh-utils工具包的一部分).同样的,[并不调用
/usr/bin/[,被连接到/usr/bin/test.
bash$ type test
test is a shell builtin
bash$ type '['
[ is a shell builtin
bash$ type '[['
[[ is a shell keyword
bash$ type ']]'
]] is a shell keyword
bash$ type ']'
bash: type: ]: not found
[[]]结构比Bash的[]更加灵活,这是一个扩展的test命令,从ksh88继承过来的.
注意:在[[]]结构中,将没有文件扩展或者是单词分离,但是会发生参数扩展和命令替换.
1 file=/etc/passwd
2
3 if [[ -e $file ]]
4 then
5 echo "Password file exists."
6 fi
注意:使用[[]],而不是[],能够阻止脚本中的许多逻辑错误.比如,尽管在[]中将给出一个错误, 但是&&,||,<>操作还是能够工作在一个[[]]test之中.
注意:在if后边,test命令和[]或[[]]都不是必须的.如下:
1 dir=/home/bozo
2
3 if cd "$dir" 2>/dev/null; then # "2>/dev/null" hides error message.
4 echo "Now in $dir."
5 else
6 echo "Can't change to $dir."
7 fi
if命令将返回if后边的命令的退出码.
与此相似,当在一个在使用与或列表结构的时候,test或中括号的使用,也并不一定非的有if不可
1 var1=20
2 var2=22
3 [ "$var1" -ne "$var2" ] && echo "$var1 is not equal to $var2"
4
5 home=/home/bozo
6 [ -d "$home" ] || echo "$home directory does not exist."
(())结构扩展并计算一个算术表达式的结果.如果表达式的结果为0,它将返回1作为退出码,或
者是"false".而一个非0表达式的结果将返回0作为退出码,或者是"true".
文件测试操作
返回true如果...
-e 文件存在
-a 文件存在
这个选项的效果与-e相同.但是它已经被弃用了,并且不鼓励使用
-f file是一个regular文件(不是目录或者设备文件)
-s 文件长度不为0
-d 文件是个目录
-b 文件是个块设备(软盘,cdrom等等)
-c 文件是个字符设备(键盘,modem,声卡等等)
-p 文件是个管道
-h 文件是个符号链接
-L 文件是个符号链接
-S 文件是个socket
-t 关联到一个终端设备的文件描述符
这个选项一般都用来检测是否在一个给定脚本中的stdin[-t0]或[-t1]是一个终端
-r 文件具有读权限(对于用户运行这个test)
-w 文件具有写权限(对于用户运行这个test)
-x 文件具有执行权限(对于用户运行这个test)
-g set-group-id(sgid)标志到文件或目录上
如果一个目录具有sgid标志,那么一个被创建在这个目录里的文件,这个目录属于创建
这个目录的用户组,并不一定与创建这个文件的用户的组相同.对于workgroup的目录
共享来说,这非常有用.见<<UNIX环境高级编程中文版>>第58页.
-u set-user-id(suid)标志到文件上
如果运行一个具有root权限的文件,那么运行进程将取得root权限,即使你是一个普通
用户.[1]这对于需要存取系统硬件的执行操作(比如pppd和cdrecord)非常有用.如果
没有suid标志的话,那么普通用户(没有root权限)将无法运行这种程序.
见<<UNIX环境高级编程中文版>>第58页.
-rwsr-xr-t 1 root 178236 Oct 2 2000 /usr/sbin/pppd
对于设置了suid的文件,在它的权限标志中有"s".
-k 设置粘贴位,见<<UNIX环境高级编程中文版>>第65页.
对于"sticky bit",save-text-mode标志是一个文件权限的特殊类型.如果设置了这
个标志,那么这个文件将被保存在交换区,为了达到快速存取的目的.如果设置在目录
中,它将限制写权限.对于设置了sticky bit位的文件或目录,权限标志中有"t".
drwxrwxrwt 7 root 1024 May 19 21:26 tmp/
如果一个用户并不时具有stick bit位的目录的拥有者,但是具有写权限,那么用户只
能在这个目录下删除自己所拥有的文件.这将防止用户在一个公开的目录中不慎覆盖
或者删除别人的文件,比如/tmp(当然root或者是目录的所有者可以随便删除或重命名
其中的文件).
-O 你是文件的所有者.
-G 文件的group-id和你的相同.
-N 从文件最后被阅读到现在,是否被修改.
f1 -nt f2
文件f1比f2新
f1 -ot f2
f1比f2老
f1 -ef f2
f1和f2都硬连接到同一个文件.
! 非--反转上边测试的结果(如果条件缺席,将返回true)