资源描述
高级Bash脚本编程指南
一个对脚本编程技术的深入讨论
1:简介
Shell是一个命令解释器。它是操作系统内核和用户之间的绝缘层,也是一种功能强大的编程语言。一个Shell程序,通常被称作为脚本,它是由系统调用,命令工具,软件包和已编译的二进制包"结合" 起来的容易使用的工具。事实上,shell脚本可以调用整个UNIX系统命令,软件包和工具。如果这还不太够的话,Shell的内部命令,如测试和循环结构,都使得Shell脚本更强大和更有弹性。Shell脚本在管理系统任务中表现得非常出色,在对日常反复性的处理工作中避免使用那些结构复杂的程序语言。
2:为什么要用Shell编程?
脚本应用知识对希望精通系统管理的任何个人说是必需的,他实际上不想写一个脚本程序。一般说一个Linux机器启动后,它会执行在/etc/rc.d 目录下的Shell脚本重建系统环境并且启动各种服务。理解这些启动脚本的细节对分析系统运作并修改它是有很大意义的。
编写shell脚本并不难学,因为脚本内建的功能集(check?)和他们只要学相当少的shell的操作符和选项。语法简单易懂,像在命令行上调用和连接软件包那样容易,它只有少量的 "规则" 需要掌握。大多数短小的脚本第一次就工作的很好,较长的脚本调试也相当的容易。
shell脚本是个复杂应用原型的"quick and dirty" 方法。在项目开发中用shell编程实现个有限的功能性子集是有用的开始。用这种方法去测试应用程序的结构和模块组合,可以在实际地用C,C++,Java或者Perl进行编程之前发现主要的设计缺陷。
Shell编程遵从经典UNIX哲学:把复杂的问题分成简单的小问题,然后再把各部分功能组合起来解决复杂问题。这和用新一代高级的多用途的语言,例如Perl,试图成为所有人处理所有事情的语言但是所付出的代价是强迫改变你的思维方法来适应这种工具,大多数人认为这是一个更好的或者至少感觉上更令人能接受的方法。
什么时候不适合使用Shell编程:
· 资源紧张的项目,特别是速度是重要因素的地方(排序,散序,等等)
· 程序要进行很复杂的数学计算,特别是浮点计算,任意精度的计算,或者是复数计算(应该用C++或是FORTRAN代替)
· 要求交叉编译平台的可移植性(使用C或者是Java代替)
· 需要结构化编程的复杂应用(需要变量类型检查和函数原型等等)
· 对于影响系统全局性的关键任务应用。
· 安全非常重要。你必须保证系统完整性和抵抗入侵,攻击和恶意破坏。
· 项目由连串的依赖的各个部分组成。
· 多种文件操作要求(Bash被限制成文件顺序存取,并且是以相当笨拙,效率低下的逐行的存取方式)
· 需要良好的多维数组支持。
· 需要类似链表或树这样的数据结构。
· 需要产生或操作图象或图形用户界面。
· 需要直接存取系统硬件。
· 需要端口号或是socket I/O。
· 需要使用可重用的函数库或接口。
· 所有的私有的不开源的应用程序(Shell脚本的源代码是直接可读,能被所有人看到的)
如果你需要有上面的任意一种应用,请考虑其他的更强大的脚本语言――Perl,Tcl,Python,Ruby,或者可能是其他更高级的编译型语言,例如C,C++或者是Java。尽管如此,使用Shell脚本来构造应用原型仍然是一个有用的开发步骤。
我们将会使用Bash,它是 "Bourne-Again shell" 的首字母缩写,并且是Setphen Bourne写的经典的Bourne shell的同义词。Bash已经变成了所有令人喜欢的UNIX上shell编程 事实的上的 这本书的大多数脚本技术能很好的应用到其他的Shell当中,比如说Korn Shell,Bash借用了它的一些特性, 还有C Shell和他的不同之处。(注意:C shell编程不是交付一定的内在问题,这点已由Tom Christiansen在1993年10月在 Usenet post 被指出了)
接下来是的是一篇脚本的指南。它由许多的例子来引出Shell的许多特性。这些已经被测试过的例子不仅能工作,并且可能的话某些甚至能用在真正的应用中。读者能让源码文件(scriptname.sh或是 scriptname.bash)的这些例子真正地运行起来, 给他们增加运行权限(chmod u+rx scriptname), 然后运行他们看看运行结果。如果你没有源码包,你仍然可以从HTML, pdf, 或是text 格式版本中复制粘贴代码。注意这些脚本可能在他们被详细解释前提前展示一些特性,这时读者可以暂时忽略这些特性。
3:从一个Sha-Bang(#!)开始
在最简单的情况下,脚本程序不过是存储在一个文件里的系统命令列表。这至少让你执行它时不必重新按顺序键入相同功能的命令序列。
例子 2-1. cleanup: 一个清空/var/log目录下的日志文件的脚本
1 # Cleanup
2 # 必须以root用户运行.
3
4 cd /var/log
5 cat /dev/null > messages
6 cat /dev/null > wtmp
7 echo "Logs cleaned up.
这没有什么不同寻常的,它不过是一组可以容易地从控制台或xterm(译者注:一种图形虚拟控制台). )中顺序键入的命令集。用一种脚本代替这组命令的用处是使你不必每次执行相同任务时都重复地顺序键入它们。脚本变成了一个工具, 并且它也很容易地在一个实际项目被修改或者定制。
例子 2-2. cleanup: 一个改进版的cleanup脚本
1 #!/bin/bash
2 # Bash脚本正确的头部.
3
4 # Cleanup, 版本 2
5
6 # 需要以root运行.
7 # 如果不是root用户,在此处添加错误信息打印代码和退出代码.
8
9 LOG_DIR=/var/log
10 # 使用变量比使用硬编码(hard-coded)更好。
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 # 这是从一个脚本中退出正确合适的方法
现在它看起来像一个真正的脚本了。但下面我们将做的更好…
例子 2-3. cleanup: 一个上面脚本的增强版,但不能处理错误
1 #!/bin/bash
2 # Cleanup, 版本 3
3
4 # 注意:
5 # -------
6 # 这个脚本使用了相当多的特性,这些我们稍后将会解释.
7 #
8 # 到那时,你已经学了这本书的一半了,你将不会再对shell感觉神秘了。
9 #
10
11
12
13 LOG_DIR=/var/log
14 ROOT_UID=0 # 只有用户ID变量$UID值为0的用户才有root权限.
15 LINES=50 # 默认的行数
16 E_XCD=66 # 不能进入到目录时的退出代码值
17 E_NOTROOT=67 # 不是root用户时退出的代码值
18
19
20 # 必须以root用户运行,以下进行检测
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 # 测试是否提供了命令行参数(即是测试命令行参数至少有一个参数)
29 then
30 lines=$1
31 else
32 lines=$LINES # Default, if not specified on command line.
33 fi
34
35
36 # Stephane Chazelas建议,
37 #+ 下面是一种更好的检测命令行参数的方法,
38 #+ 但是对于现在来说还是有些高级。
39 #
因为你可能并不希望把整个系统日志都清空,所以这个版本的cleanup保留了日志中最后的几行日志记录。如果你继续努力地学下去,将会发现更多精练的写法来代替上面的代码。
在脚本开头的 sha-bang ( #!) 是告诉系统这个文件是由特定命令解释器解释的一组命令。 那个 #! 实际上是两个字节的魔数, 魔数是指定文件类型的特殊记号,在此是表示这是一个可执行的shell脚本(键入 man magic可了解更多的信息)。紧跟着#!的是一个路径名.这个路径名是解释这个脚本内命令的命令解释器程序的路径:可能是一个shell,也可能是一个编程语言或者是一个软件包程序。这个命令解释器能执行脚本内的命令语句。它从脚本开头(即从#!所在行的下一行)起执行,但是忽略注释行。
1 #!/bin/sh
2 #!/bin/bash
3 #!/usr/bin/perl
4 #!/usr/bin/tcl
5 #!/bin/sed –f
6 #!/usr/awk –f
上面每一个脚本头行都是不同的命令解释器,如果第一行是/bin/sh, 那就是默认的Shell(Linux系统中bash是默认的shell),否则的话就是其他的解释器. 如果使用#!/bin/sh/bin/sh(因为大多不同的商业UNIX都使用Bourne shell为默认shell)可以使脚本能够移植到非Linux的机器上,虽然这样做你将不能使用Bash许多特有的属性。但这样做的脚本遵循 POSIX sh标准.
值得注意的是,在"#!"后面提供的路径必须是正确的,否则你运行脚本只会收到通常像"Command not found"那样的错误信息。
如果脚本程序只是由一组普通的系统命令而没有使用Shell内置命令的话#!将被忽略。上面的第二个例子被要求以#!,开头是因为变量赋值(lines=50),这就使用了Shell的特有的语句。再次提醒使用#!/bin/sh将调用默认的命令解释器,这在Linux系统上是/bin/bash.
这份指南鼓励使用模块化的方法来写脚本。留意记录像“模板”的代码片断以备将来的脚本使用。最后你能生成一个很好的可扩展的例程库。下面的代码片断可以测试脚本是否被正确的数目参数调用。
1 E_WRONG_ARGS=65
2 script_parameters="-a -h -m -z"
3 # -a = all, -h = help, etc.
4
5 if [ $# -ne $Number_of_expected_args ]
6 then
7 echo "Usage: `basename $0` $script_parameters"
8 # `basename $0`是指脚本名称(译者:这个内容在后面章节会讲).
9 exit $E_WRONG_ARGS
10 fi
很多时候,你会写一个执行实际功能的脚本。本章的第一个脚本就是一个例子。以后它可能会使你记起把这个脚本扩展以完成类似的任务。使用变量代替固定的字符串常量是好的办法,像这样的办法还有用函数代替反复使用的代码块。
2.1. 运行脚本
写完一个脚本,你能够运行它用命令:sh scriptname, 另外也也可以用bash scriptname. 来执行(不推荐使用:sh <scriptname, 因为这样会禁止脚本从标准输入里读数据)。更为方便的是你可以使用chmod命令来使脚本自身变为可执行的.
你可以:
chmod 555 scriptname (使每个人都有读和执行的权限)
也可以:
chmod +rx scriptname (使每个人都有读和执行的权限)
chmod u+rx scriptname (仅仅使脚本文件拥有者有读和执行的权限)
在给脚本加上执行权限之后,你可以很容易地使用./scriptname. 来执行它。如果脚本以"#!"行开头,将会调用正确的命令解释器来执行它.
最后,把脚本测试并调试完后,如果想把脚本给系统中所有其他的用户使用,你应该把脚本移到目录/usr/local/bin 中(当然,这必须要有root的权限),这样只需简单地在命令行输入scriptname [回车]就能执行脚本了。
第三章:特殊字符
在脚本或其他别的地方出现的特殊字符
#
注释. 以一个#开头的行 (#!是例外) 是注释行.
1 # 这是一行注释.
注释也可以出现在一个命令语句的后面。
1 echo "A comment will follow." # 这里可以添加注释.
2 # ^ 注意在#前面可以有空白符 #注释行前面也可以有空白字符.
1 #注意这个注释行的开头是一个TAB键.
在同一行中,命令不会跟在一个注释的后面。因为这种情况下没有办法分辨注释的结尾,命令只能放在同一行的行首。用另外的一个新行开始下一个注释。
当然了,在echo命令给出的一个转义的#字符并不会开始一个注释。同样地,出现在一些参数代换结构和在数值常量表达式中的#字符也同样不会开始一个注释。
1 echo "The # here does not begin a comment."
2 echo 'The # here does not begin a comment.'
3 echo The \# here does not begin a comment.
4 echo The # here begins a comment.
5
6 echo ${PATH#*:} # 前面的#是参数代换,不是注释.
7 echo $(( 2#101011 )) # 基本转换,不是注释.
8
9 # 多谢, S.C.
标准的引用和转义 符("'\)可以转义#。
当然,模式匹配操作也可以使用#,而不必把它当做注释的开始。
;
命令分割符[分号]. 分割符允许在同一行里有两个或更多的命令.
1 echo hello; echo there
2
3
4 if [ -x "$filename" ]; then # 注意:"if" and "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."
注意”;”有时需要转义.
;;
case语句分支的结束符[双分号].
1 case "$variable" in
2 abc) echo "\$variable = abc" ;;
3 xyz) echo "\$variable = xyz" ;;
4 esac.
"点"命令[圆点]. 等同于source (参考例子 11-20).这是一个bash的内建命令.
.
"点", 作为一个文件名的组成部分.当点(.)以一个文件名为前缀时,起作用使该文件变成了隐藏文件。这种隐藏文件ls一般是不会显示出来的。[译者注:除非你加了选项-a]
bash$ touch .hidden-file
bash$ ls -l
total 10
-rw-r--r-- 1 bozo 4034 Jul 18 22:04 data1.addressbook
-rw-r--r-- 1 bozo 4602 May 25 13:58 data1.addressbook.bak
-rw-r--r-- 1 bozo 877 Dec 17 2000 employment.addressbook
bash$ ls -al
total 14
drwxrwxr-x 2 bozo bozo 1024 Aug 29 20:54 ./
drwx------ 52 bozo bozo 3072 Aug 29 20:51 ../
-rw-r--r-- 1 bozo bozo 4034 Jul 18 22:04 data1.addressbook
-rw-r--r-- 1 bozo bozo 4602 May 25 13:58 data1.addressbook.bak
-rw-r--r-- 1 bozo bozo 877 Dec 17 2000 employment.addressbook
-rw-rw-r-- 1 bozo bozo 0 Aug 29 20:54 .hidden-file
作为目录名时,单个点(.)表示当前目录,两个点(..)表示上一级目录(译者注:或称为父目录)。
bash$ pwd
/home/bozo/projects
bash$ cd .
bash$ pwd
/home/bozo/projects
bash$ cd ..
bash$ pwd
/home/bozo/
单点(.)文件名常常被当作文件移动命令的目的路径.
bash$ cp /home/bozo/current_work/junk/* .
.
点(.)字符匹配.作为正则表达式的一部分,匹配字符时,单点(.)表示匹配任意一个字符。
"
部分引用[双引号]. "STRING"的引用会使STRING里的特殊字符能够被解释。请参考第五章.
'
完全引用[单引号]. 'STRING'能引用STRING里的所有字符(包括特殊字符也会被原样引用). 这是一个比使用双引号(“)更强的引用。 参考第5章.
,
逗号操作符[逗号]. 逗号操作符用于连接一连串的数学表达式。这一串的数学表达式每一个都被求值,但只有最后一个被返回。(译者注:换句话说,就是整个表达式的值取决于最后一个表达式的值。)
1 let "t2 = ((a = 9, 15 / 3))" # Set "a = 9" and "t2 = 15 / 3"\
转义符[后斜杠].用于单个字符的引用机制。
\X "转义"字符为X.它有"引用"X的作用,也等同于直接在单引号里的'X'.\符也可以用于引用双引号(")和单引号('),这时双引号和单引号就表示普通的字符,而不是表示引用了。
参考第五章对转义字符的更深入的解释。
/
文件路径的分隔符[前斜杠]. 分隔一个文件路径的各个部分。(就像/home/bozo/projects/Makefile).
它也是算术操作符中的除法.
`
命令替换.`command` 结构使字符(`)[译者注:这个字符不是单引号,而是在标准美国键盘上的ESC键下面,在字符1左边,在TAB键上面的那个键,要特别留心]引住的命令(command)执行结果能赋值给一个变量。它也被称为后引号(backquotes)或是斜引号(backticks).
:
空命令[冒号]. 这个命令意思是空操作(即什么操作也不做). 它一般被认为是和shell的内建命令true是一样的。冒号":" 命令是Bash自身内建的, and its它的退出状态码是真(即0)。[译者注:shell中真用数字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 : # 什么也不做的分支
3 else
4 take-some-action
5 fi
在必须要有两元操作的地方作为一个分隔符, 参考例子 8-2和默认参数.
1 : ${username=`whoami`}
2 # ${username=`whoami`} 如果没有开头的:,将会出错
3 # 除非"username"是一个外部命令或是内建命令...
在here document中的一个命令作为一个分隔符. 参考例子 17-10.
在参数替换中为字符串变量赋值 (就像例子 9-14).
1 : ${HOSTNAME?} ${USER?} ${MAIL?}
2 # 如果列出的一个或多个基本的环境变量没有设置,
3 #+ 将打印出错信息。
展开阅读全文