高级Bash脚本编程指南
一本深入学习shell脚本艺术的书籍
版本 6.0.35
2009年6月29日
作者Mendel Cooper
该教程假设你以前没有脚本知识或者编程知识,但是如果你具备该知识的话很快就能达到中级或者高级水平。整个书中涵盖了UNIX®的特有智慧和知识。你可以把它当成自学教材、手册或者shell脚本技术的参考资料。练习题和例子中的注释能够引起读者的积极性,前提是真正学习脚本的唯一途径就是写脚本。
本书也适合作为教材来讲解一般的介绍编程概念。
第一部分:介绍
Shell是一种命令解释器。它不仅仅是操作系统内核和用户之间的连接器,更是功能强大的编程语言。一个shell程序,被叫做一个脚本,是一个很容易使用的工具,可以通过系统调用、工具、实用程序或者编译过的二进制连接在一起。几乎所有的UNIX指令、实用程序和工具都可以通过shell脚本调用。如果这些还不够,那么shell本身类似于testing和loop循环结构的命令可以增加脚本的强力支持及灵活性。Shell脚本更加适合管理系统进程以及其他重复工作的进程,而不需要那些多余的完全成熟的结构化变成语言。
目录
1. Shell Programming!
2. Starting Off With a Sha-Bang
第一章 shell 编程
对于任何想精通系统管理的人来说,掌握shell脚本知识是必不可少的,即使他们之前没有真正的写过脚本。想想Linux机器的启动过程,它是通过执行/etc/rc.d 目录下的脚本去恢复系统配置及建立相关服务的。详细的了解这些启动脚本对于分析系统是非常重要的,同时还可能需要修改它。
掌握脚本并不难,因为这些脚本都可以分割成小的片段去学习,并且这些小的片段又是相对独立的操作[1]。Shell语法很简单也很直观,类似于把一些实用程序在命令行连接起来调用,而且只用到很少的规则。绝大多少短小的脚本第一次就可以很好的运行,即使调试一个比较长的脚本也是比较直观的。
在20世纪70年代, BASIC语言适合在早期的微机上编写程序。10年后,Bash脚本作为Linux或者UNIX的基础知识被用在更强大的机器上。
Shell脚本在复杂的应用程序模型设计的简捷方法。项目开发的第一阶段,使用脚本完成部分功通常都是很有用的。使用这种方法,在使用C,C++,Java,Perl或者Python语言完成程序编码之前,通过对应用程序结构测试、演示就能够反映程序的主要缺陷。
Shell脚本遵循经典的UNIX体系,把复杂的项目拆分成简单的单元,并且组件和程序之间相互连接。序贯观点认为这种方法比较好,至少比使用新一代语言更加完美的解决问题,例如Perl语言,就是尝试所有人用之去做所有事情,但是代价就是强迫你使用这种语言思考解决问题的方法。
根据Herbert Mayer的理论,“一种有用的语言需要数组、指针、以及泛型结构来创建数据结构”。根据这个标准,Shell脚本就不那么“有用”,或许不能……
我们将开始用Bash,Bourne-Again Shell的首字母缩写组合,也是Stephen Bourne的经典Bourne Shell。Bash已经成了主流UNIX的shell脚本,本书绝大部分原则涵盖了其他shell,比如Korn Shell,Bash也包含了一些Korn Shell的特性,同时也包含了一些C Shell的变种。(注意C Shell编程由于存在内在的问题不被推荐,这在1993年10月已经由Tom Christiansen 在网络上公告了)
第二章 与Sha-Bang一起出发
一个简单的例子,一个脚本无非是将一些系统命令列在一个文件中。最起码的用处就是,在调用特殊的有顺序的命令时,可以节省工作。
例 2-1 清除:一个清除/var/log 目录下日志文件的脚本
1 # Cleanup 2 # Run as root, of course. 3 4 cd /var/log 5 cat /dev/null > messages 6 cat /dev/null > wtmp 7 echo "Logs cleaned up." |
这没有什么异常的,只不过是在控制台命令行或者终端窗口一个接一个的调用一些命令,好处是不用每次都去重新敲命令。这个脚本就是一个程序——一个工具——能够很容易修改或者定制。
例 2-2 清除:一个改进的清除脚本
1 #!/bin/bash 2 # Proper header for a Bash script. 3 4 # Cleanup, version 2 5 6 # Run as root, of course. 7 # Insert code here to print error message and exit if not root. 8 9 LOG_DIR=/var/log 10 # Variables are better than hard-coded values. 11 cd $LOG_DIR 12 13 cat /dev/null > messages 14 cat /dev/null > wtmp 15 16 17 echo "Logs cleaned up." 18 19 exit # The right and proper method of "exiting" from a script. |
现在让我们开始看一个真正的脚本,这样我们能够做更多事情……
例 2-3 清除:一个增强的和普遍的清除脚本的版本
1 #!/bin/bash 2 # Cleanup, version 3 3 4 # Warning: 5 # ------- 6 # This script uses quite a number of features that will be explained 7 #+ later on. 8 # By the time you've finished the first half of the book, 9 #+ there should be nothing mysterious about it. 10 11 12 13 LOG_DIR=/var/log 14 ROOT_UID=0 # Only users with $UID 0 have root privileges. 15 LINES=50 # Default number of lines saved. 16 E_XCD=86 # Can't change directory? 17 E_NOTROOT=87 # Non-root exit error. 18 19 20 # Run as root, of course. 21 if [ "$UID" -ne "$ROOT_UID" ] 22 then 23 echo "Must be root to run this script." 24 exit $E_NOTROOT 25 fi 26 27 if [ -n "$1" ] 28 # Test whether command-line argument is present (non-empty). 29 then 30 lines=$1 31 else 32 lines=$LINES # Default, if not specified on command-line. 33 fi 34 35 36 # Stephane Chazelas suggests the following, 37 #+ as a better way of checking command-line arguments, 38 #+ but this is still a bit advanced for this stage of the tutorial. 39 # 40 # E_WRONGARGS=85 # Non-numerical argument (bad argument format). 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 #* Skip ahead to "Loops" chapter to decipher all this. 49 50 51 cd $LOG_DIR 52 53 if [ `pwd` != "$LOG_DIR" ] # or if [ "$PWD" != "$LOG_DIR" ] 54 # Not in /var/log? 55 then 56 echo "Can't change to $LOG_DIR." 57 exit $E_XCD 58 fi # Doublecheck if in right directory before messing with log file. 59 60 # Far more efficient is: 61 # 62 # cd /var/log || { 63 # echo "Cannot change to necessary directory." >&2 64 # exit $E_XCD; 65 # } 66 67 68 69 70 tail -n $lines messages > mesg.temp # Save last section of message log file. 71 mv mesg.temp messages # Becomes new log directory. 72 73 74 # cat /dev/null > messages 75 #* No longer needed, as the above method is safer. 76 77 cat /dev/null > wtmp # ': > wtmp' and '> wtmp' have the same effect. 78 echo "Logs cleaned up." 79 80 exit 0 81 # A zero return value from the script upon exit indicates success 82 #+ to the shell. |
因为你也不希望清除所有的系统日志,因此这个改进的脚本保留了最后的部分完整日志。你可以不断的发现新的方法完善上面的脚本,提高效力。
***
脚本开头的#!符号告诉系统这个文件的命令需要命令解释器来解释。#!实际上就是1个2字节的幻数,表示这一个特殊的标记,表明该文件类型,或者表示本例是可知性shell脚本(键入man magic 来获取详更详细的丰富的主题)。紧跟这sha-bang的是路径名,这个路径在脚本中是解释命令是否是一个shell,或者是一个编程语言,或者是实用程序。然后命令解释器从第一行(sha-bang下面的一行)开始执行脚本中的命令,执行过程中忽略注释。
1 #!/bin/sh 2 #!/bin/bash 3 #!/usr/bin/perl 4 #!/usr/bin/tcl 5 #!/bin/sed -f 6 #!/usr/awk -f |
2.1 调用脚本
脚本写好后,可以通过 sh 脚本名 或者 bash 脚本名 来调用它(不推荐使用 sh <脚本名 调用,因为不能有效的读取脚本中的标准输入)更有效的方法是直接赋予脚本本身可执行权限。
或者:
chmod 555 scriptname (赋予任何人 读/执行 权限)
或者:
chmod +rx scriptname(赋予任何人 读/执行 权限)
chmod u+rx scriptname (赋予脚本自身读/执行 权限)
当脚本是可执行的,就可以通过 ./脚本名字 来测试它。若果脚本是以“sha-bang”行开始的,调用脚本需要正确的命令解释器来运行。
最后一步,测试和调试之后你可能想把它移到/usr/local/bin目录(当然,移动需要以root用户才可以),这样的话脚本就可以在系统中被所有用户执行,然后用户就可以通过在命令行简单键入脚本名 [回车] 来调用脚本。
2.2 初步练习
1. 系统管理员常常需要写脚本使得系统一些公共进程能够自动执行,举一些例子说明这类脚本的用途。
2. 写一个脚本调用一些程序显示出系统的日期和时间,列出所有登录过的用户,显示出系统的正常运行时间,然后脚本把这些信息保存到日志文件。
第二部分 基础知识
目录
3. 特殊字符
4. 变量和参数介绍
5. 引用
6. 退出和退出状态
7. 测试
8. 操作符相关主题
第三章 特殊字符
什么是特殊字符?如果按照字面意思是一个词转意了,然后就成了特殊字符。
在脚本或其他地方找特殊字符
#
注释,以 # 符号开始的行是注释(#!是例外),不会被执行
1 # This line is a comment. |
注释也可以跟在命令结尾后面
1 echo "A comment will follow." # 注释. 2 # ^ 注意 #前面有空格 |
注释也可以在行的空格后面
1 # A tab precedes this comment. |
注释也可能嵌在管道符里面
1 initial=( `cat "$startfile" | sed -e '/#/d' | tr -d '/n' |/ 2 # Delete lines containing ‘#’ 注释字符 3 sed –e `s//.//. /g’ –e ‘s/_/_ /g’` ) 4 # Excerpted from life.sh script |
同一行上命令不能跟在注释后面,没有办法结束注释接着开始新的命令,要开始新命令就需要另起一行。
当然在echo语句中,引用或者转码中的“#”不能作为注释的。同样的,#号出现在特定的参数替换结构中和数字常量表达式中,也不作为注释的。
1 echo "The # 不是注释" 2 echo 'The # 不是注释' 3 echo The /# 不是注释 4 echo The # 注释 5 6 echo ${PATH#*:} # 参数替换,不是注释 7 echo $(( 2#101011 )) # 基数转换,不是注释 8 9 # Thanks, S.C. |
标准的应用字符和转意字符(“ ‘ /)也可以转意“#”号,固定的匹配操作也可以用“#”号。
;
命令分隔符【分号】。允许两条或者多条命令在同一行。
1 echo hello; echo there 2 3 4 if [ -x "$filename" ]; then # 注意分号后面的空格 5 #+ ^^ 6 echo "File $filename exists."; cp $filename $filename.bak 7 else # ^^ 8 echo "File $filename not found."; touch $filename 9 fi; echo "File test complete." |
注意“;”有时候需要被转意。
;;
终止选择【双分号】
1 case “$variable” in 2 abc) echo “/$variable = abc” ;; 3 xyz) echo “/$variable = xyz” ;; 4 esac |
;;&,;&
终止选择【第4版以后的Bash】
.
“.”命令【句号】,相当于来源【参考例子14-22】,这是一个bash内建函数。
“.”作为文件名的组成部分,是隐藏文件的前缀,当执行ls命令时不能显示出文件名。
[root@localhost tmp]# touch .hidden-file [root@localhost tmp]# ls -l total 100 -rw------- 1 root root 9248 Jul 13 14:34 grub-install.img.fV3362 -rw------- 1 root root 0 Jul 13 14:34 grub-install.log.vE3363 -rw-r--r-- 1 root root 73406 Aug 18 2008 netconfig-0.8.24-1.2.2.1.i386.rpm [root@localhost tmp]#
[root@localhost tmp]# ls -al total 136 drwxrwxrwx 4 root root 4096 Jul 14 16:35 . drwxr-xr-x 25 root root 4096 Jul 9 11:57 .. drwxrwxrwt 2 root root 4096 Jan 9 2002 .font-unix -rw------- 1 root root 9248 Jul 13 14:34 grub-install.img.fV3362 -rw------- 1 root root 0 Jul 13 14:34 grub-install.log.vE3363 -rw-r--r-- 1 root root 0 Jul 14 16:35 .hidden-file drwxrwxrwt 2 root root 4096 Jan 9 2002 .ICE-unix -rw-r--r-- 1 root root 73406 Aug 18 2008 netconfig-0.8.24-1.2.2.1.i386.rpm [root@localhost tmp]# |
作为目录名,“.”表示当前目录,“..”则表示上一级目录
bash$ pwd /home/bozo/projects
bash$ cd . bash$ pwd /home/bozo/projects
bash$ cd .. bash$ pwd /home/bozo/ |
“.”经常出现在移动命令中,由于这个原因,“.”表示的是当前目录。
bash$ cp /home/zozo/current_work/junk/* . |
把junk目录下所有文件复制到$PWD目录(即当前目录)。
. 字符匹配。“.”作为正则表达式的一部分,匹配单个字符。
“
部分引用【双引号】。“STRING”保留了STRING中大多数特殊字符,详细见第5章。
‘
全部引用【单引号】。‘STRING’保留了STRING中的所有特殊字符,‘STRING’的引用是“STRING”的较强形式。参见第5章。
,
逗号操作符。逗号操作符连接着连续的算术操作,所有的都是可以计算的,只有最后一个被返回。
1 let “t2 = ((a=9,15/3()” #set “a = 2 9” and “t2 = 15 / 3” |
逗号操作符也可以连接字符串。
1 for file in /{,usr/}bin/*calc 2 # ^ 查找所有以“calc”结尾的可执行文件 3 #+ 在 /bin 目录和 /usr/bin 目录 4 do 5 if [ -x “$file” ] 6 then 7 echo $file 8 fi 9 done 10 下面是查找结果 11 # /bin/ipcalc 12 # /usr/bin/kcalc 13 # /usr/bin/oidcalc 14 # /usr/bin/oocalc 15 16 17 # Thank you, Rory Winston, for pointing this out. |
” ’
参数替代小写字母(第4版bash中增加)。
/
换码【反斜杠】。单字符引用
/x 引用x 字符,它引用x 等同于‘X’。“/”常常被“ 和‘ 引用,所以这些表达简洁。
参考第5章,更详细的说明反斜杠。
/
文件名路径分隔符【向前斜线】。分隔开文件名名字中的各级路径(如/home/bozo/projects/Makefile)。
这也是算数运算中的除法操作。
`
命令替换。`command` 结构把命令正确的输出给变量。这也是通常所说的backquotes或者backticks。
:
空命令【冒号】。这个shell命令等同于“NOP”(没有操作,一个空操作)。它通常被认为是shell内建的“真”的同义词,“:”指令是bash内部的函数,退出状态是true(即0)。
1 : 2 Echo $? # 0 |
死循环,例如:
1 while : 2 do 3 operation-1 4 operation-2 5 … 6 operation –n 7 done 8 9 # 与下面相同 10 # while true 11 # do 12 … 13 done |
在if/then 例子中占位符
1 if condition 2 then : # Do nothing and branch ahead 3 else # or else … 4 take-some-action 5 fi |
在预期的二元操作中提供占位符。
1 : ${username=`whoami `} 2 # ${username=`whoami `} 如果不以:开始就报错 3 # 除非“username”是一个命令或者内建函数 … |
用替换参数计算变量中的字符串(例 9-16)。
1 :${HOSTNAME} ${USER?} ${MAIL?} 2 # Prints errot message 3 #+ if one or more essential environmental variables not set. |
变量扩展/参数替换
在和重定向符号>相结合时,在不改变文件权限的情况下清空文件内容,如果文件不存在则创建文件。
1 : > data.xxx # “data.xxx” 文件变成空 2 3 # 效果同 cat /dev/null > data.xxx # 然而这不能 产生一个新进程,因为“:”是内建函数 |
也可以参考例 15-15
在和重定向操作符>>连接时,对已有的目标文件不起作用(: >> target_file),如果文件不存在则创建文件。
这使用语规则的文件,不包含管道符、连接符和某些特殊的文件。
也可能用在注释行的开始,尽管这中情况不被推荐。使用#注释,不对该行剩余内容错误检测,所以注释内容可以写任何东西。然而,使用“:”情况则不同:
1 : This is a comment that generates an error, ( if [ 4x –eq 3 ] ). |
“:”在/etc/passwd中和在$PATH变量中是字段分隔符。
Bash$ echo $PATH /usr/local/bin:/bin:/usr/bin:/usr/X11R6/bin:/usr/sbin:/usr/games |
!
取反(或否定)。!操作反置了适当命令的退出状态(参考例6-2),意思刚好和实验的意思相反。举个例子,它能够将等于(=)变成不等于(!=),!是Bash脚本的一个关键字。
在不同的环境中,!还用在变量引用中。
在命令行中,!还能够调用历史记录,注意,在脚本中历史记录是隐藏的。
*
通用字符【星号】。*作为通配符用在文件名的替换中,替换给定目录的所有文件明。
Bash$ echo * abs-book.sgml add-drive.sh agram.sh alias.sh |
在正则表达式中,*可以替代任何数字字符(或者0)。
*
算数运算。在算数运算中,*表示乘法运算。
双星代表幂运算或者在文件名替换中代表补充的文件名。
?
假设操作。在某些表达式中,?代表假设条件。
在双括号结构中,?可以作为C语言风格中的三元操作元素,如 ?:
1 (( vr0 = var1<98?9:21 )) 2 # ^ ^ 3 4 # if [ “$var1” –lt 98 ] 5 # then 6 # var0=9 7 # else 8 # var0=21 9 # fi |
在 参数替换中,?表示假设变量是否是设定的。
?
通配符。在文件名替换中,?代表一个单个的字符,也代表正则表达式中的一个字符。
$
变量替换。(属于变量)
1 var1=5 2 var2=23skidoo 3 4 echo $var1 # 5 5 echo $var2 # 23skidoo |
一个变量名字的$前缀表示跟着的是变量的值。
$
行尾。在正则表达式中,文本的末尾常常有个$符号。
${}
替换参数。
$*,$@
定位参数。
$?
退出变量。$?变量保持一个命令、一个函数或者脚本本身处于退出状态。
$$
进程序号。$$变量保存了一个进程出现在脚本中的序号。
()
命令组。
1 (a=hello; echo $a)
|
括号中的命令列表作为subshell运行。
括号中的变量,只在内subshell中运行,对括号外的脚本程序不起作用,父进程,也就是脚本本身不能读取子进程中创建的变量,也就是在subshell中创建的变量。
1 a=123 2 ( a=321; ) 3 4 echo "a = $a" # a = 123 5 # "a" within parentheses acts like a local variable.
|
数组初始化。
1 Array=(element1 element2 element3)
|
{xxx,yyy,zzz,…}
大括号。
1 echo /"{These,words,are,quoted}/" # " 前缀和后缀 2 # 输出的结果如 "These" "words" "are" "quoted" 3 4 5 cat {file1,file2,file3} > combined_file 6 # 将file1, file2, file3合并成combined_file. 7 8 cp file22.{txt,backup} 9 # 复制"file22.txt" 到"file22.backup"
|
一个命令可能对大括号中以逗号分开的文件列表规格起作用[3]。文件名扩展(文件名替换)适用于大括号中的文件规格。
大括号中不允许有空格,除非括号中的空格是引用或者转码。
echo {file1,file2}/ :{/ A," B",' C'}
file1 : A file1 : B file1 : C file2 : A file2 : B file2 : C #上面语句的结果
{a..z}
大括号扩展。
1 echo {a..z} # 结果是 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 z 2 # 返回 a 到 z 之间的字母 3 4 echo {0..3} # 0 1 2 3 5 # 返回 0 到 3 之间的数字
|
{}
代码块【花括号】。也被当作构造函数组,事实上创建了一个匿名函数(一个没有名字的函数)。然而,与“标准”函数不同的是,代码块中的变量对于脚本来说还是可见的。
bash$ { local a; a=123; } bash: local: can only be used in a function
|
1 a=123 2 { a=321; } 3 echo "a = $a" # a = 321 结果是代码组里面的 4 5 # Thanks, S.C.
|
大括号中的代码输入/输出重定向。
例 3-1 代码 I/O 重定向
1 #!/bin/bash 2 # 从 /etc/fstab中读行 3 4 File=/etc/fstab 5 6 { 7 read line1 8 read line2 9 } < $File 10 11 echo "First line in $File is:" 12 echo "$line1" 13 echo 14 echo "Second line in $File is:" 15 echo "$line2" 16 17 exit 0 18 19 # 现在考虑一下,你怎样分析单独的每一行? 20 # 提示: 可以使用 awk, 或者其他方式 . . . 21 # . . . Hans-Joerg Diers 建议使用bash内建函数 "set" |
例 3-2 将代码块的输出结果保存在一个文件中
1 #!/bin/bash 2 # rpm-check.sh 3 4 # 查寻一个rpm文件描述、清单 5 #+ 无论这个包能不能安装 6 # 将输出结果保存在一个文件中. 7 # 8 # 这个脚本距离说明了代码块 9 10 SUCCESS=0 11 E_NOARGS=65 12 13 if [ -z "$1" ] 14 then 15 echo "Usage: `basename $0` rpm-file" 16 exit $E_NOARGS 17 fi 18 19 { # Begin code block. 20 echo 21 echo "Archive Description:" 22 rpm -qpi $1 # Query description. 23 echo 24 echo "Archive Listing:" 25 rpm -qpl $1 # Query listing. 26 echo 27 rpm -i --test $1 # Query whether rpm file can be installed. 28 if [ "$?" -eq $SUCCESS ] 29 then 30 echo "$1 can be installed." 31 else 32 echo "$1 cannot be installed." 33 fi 34 echo # End code block. 35 } > "$1.test" # Redirects output of everything in block to file. 36 37 echo "Results of rpm test in file $1.test" 38 39 # See rpm man page for explanation of options. 40 41 exit 0 |
和()命令不同的是{}封装的代码常常不能创建subshell。
{}
文本占位标志。{}用在xargs –i(替换字符串操作)之后表示文本占位。{}在输出文本中表示占位。
1 ls . | xargs -i -t cp ./{} $1 2 # ^^ ^^ 3 4 # From "ex42.sh" (copydir.sh) example.
|