SHELL脚本编程1开场白如果把shell命令比作盖房子的砖瓦,那shell脚本就是用一块块砖瓦建起来的房子了。我们可以通过一些约定的格式来将那些小巧的命令组合起来,实现更加自动化更加智能的所谓shell脚本。所谓约定的格式,其实就是shell脚本的语法规则,就像C语言一样,将很多语句按照一定的规则组合起来形成一个程序。但是这里要强调的是,C语言编写出来的程序是需要经过编译器编译,生成另一个称为ELF格式的文件之后才能执行的,但是shell脚本是不需要编译而可以直接执行的,这种脚本语言称为解释型语言。废话少说,下面来各个击破。2脚本格式要把shell命令放到一个“脚本”当中,有一个要求:脚本的第一行必须写成类似这样的格式:#!/bin/bash聪明如你一定立即明白,这是给系统指定一款shell解释器,来解释下面所出现的命令的。比如,你的第一个最简单的脚本first_script.sh,也许是这样的:vincent@ubuntu:~/ch01/1.3$catfirst_script.sh-n1#!/bin/bash23echo"hello!"这个脚本指定一款在/bin/下名字叫bash的shell解释器,来解释接下来的任何命令。如果你的系统用的是其他的解释器,就要将/bin/bash改成相应的名字。注意,脚本文件缺省是没有执行权限的,要使得脚本可以执行必须给他添加权限:vincent@ubuntu:~/ch01/1.3$chmod+xfirst_script.shvincent@ubuntu:~/ch01/1.3$./first_script.shecho“hello!”hello!vincent@ubuntu:~$3变量shell脚本是一种弱类型语言,在脚本当中使用变量不需要也无法指定变量的“类型”。缺省状态下,shell脚本的变量都是字符串,即一连串的单词列表。下面将shell中关于变量的技术点各个击破:1,变量的定义和赋值myname=”MichaelJackson”
请严重注意:赋值号的两边没有空格!在SHELL脚本中,任何时候要给变量赋值,赋值号两边一定不能有空格。另外,变量名也有类似于C语言那样的规定:只能包含英文字母和数字,且不能以数字开头。2,变量的引用使用变量时,需要在变量的前面加一个美元符号:$myname这表示对变量的引用,比如:vincent@ubuntu:~$echo$myname这样就把myname的值打印出来了。3,变量的种类SHELL脚本中有这么几种变量:A普通的用户自定义变量,比如上面的myname。B系统预定义好的环境变量,比如PATH。C命令行变量,比如$#、$*等。系统的环境变量可以通过如下命令来查看:vincent@ubuntu:~$env……PATH=/usr/lib/lightdm/lightdm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binDESKTOP_SESSION=gnome-classicPWD=/mnt/hgfs/codes/shell/structGNOME_KEYRING_PID=8070LANG=en_US.UTF-8MANDATORY_PATH=/usr/share/gconf/gnome-classic.mandatory.pathUBUNTU_MENUPROXY=libappmenu.soGDMSESSION=gnome-classicSHLVL=1HOME=/home/vincentGNOME_DESKTOP_SESSION_ID=this-is-deprecatedLOGNAME=vincent……可以看到,系统中的环境变量有很多,每个环境变量都用大写字母表示,比如PATH。每个环境变量都有一个值,就是等号右边的字符串。根据环境变量的不同,它们各自的含义不同。设置环境变量:以PATH环境变量为例子,如果想要将至修改为dir/,只需执行以下命令:vincent@ubuntu:~$exportPATH=dir/当然,PATH环境变量的作用是保存系统中可执行程序或脚本的所在路径,因此它的值都是一些以分号隔开的目录,我们经常的使用办法是:不改变其原有的值,而给它再增加一个我们自己需要设置的目录dir/,因此更有用的命令可能是类似于以下这样的:vincent@ubuntu:~$exportPATH=$PATH:dir/
还有,在命令行敲下如上的命令只会在当前的shell中临时有效,如果要永久有效,就必须将命令exportPATH=$PATH:dir/写入~/.bashrc中。然后执行vincent@ubuntu:~$source~/.bashrc使之生效。而SHELL脚本中的所谓命令行变量,指的是在脚本内部使用用户从命令行中传递进来的参数,例如:vincent@ubuntu:~$./example.shabcd1234这里的脚本名叫example.sh,咱们在执行他的时候顺便给了他两个参数,分别是abcd和1234,要访问这两个参数以及相关的其他值,就必须使用命令行变量,以下是具体情况:(以命令./example.shabcd1234为例子)1,$#:代表命令行参数个数,即22,$*:代表所有的参数,即abcd12343,$@:同上4,$n:第n个参数,比如$1即abcd,而$2就是1234SHELL脚本中还有几个跟命令行变量形式很类似的特殊变量,他们是:1,$?:代表最后一个命令执行之后的返回值2,$$:代表当前shell的进程号PID(事实上以上变量前面的$是变量的引用符,他们的真正的名字是紧跟$后面的那个字符)4特殊符号们SHELL脚本有好几种特殊的符号,各自有各自的神通,他们分别是:引号、竖杠(管道)、和大于小于号(重定向),以下分别进行各个击破。第一,引号。引号有三种,他们是:双引号“”、单引号‘’、反引号(抑音符)``1,双引号的作用是将一些“单词”括起来形成单个的“值”。比如:myname=“MichaelJackson”在此变量的定义中如果没有双引号将会报错,因为这个字符串有两个单词,第二个单词会被认为是一个命令,但显然不对,因为Jackson不是命令而只是myname的一部分。双引号所包含的内容还可以包括对变量的引用,比如:fruit=applemytree=“$ftruittree”由于对别的变量进行了引用,因此mytree的最终值是appletree双引号所包含的内容还可以是一个命令,比如:today=“todayis`date`”请注意:date是一个shell命令,用来获得系统的当前时间,因为此命令出现在双引
号内部,默认情况下脚本会把它当做一个普通的单词而不是命令,要让脚本识别出该命令必须用一对反引号(``)包含他。2,单引号以上对双引号的分析间接也澄清了单引号的作用了:如果一个字符串被单引号所包含,那么其内部的任何成分都将被视为普通的字符,而不是变量的引用或者命令,比如:var=’$myname,today:`date`’这个变量var的值不会引用myname也不会执行date。3,反引号的作用就是在双引号中标识出命令。编写以下脚本,可以立即理解这三个引号的区别:#!/bin/bashvar=calenderecho"var:date"#直接打印出var和dateecho"$var:`date`"#打印出变量var的值,以及命令date的执行结果echo'$var:`date`'#打印出$var:`date`第二:竖杠|(管道)。SHELL命令的一大优点是秉承了UNIX/LINUX的哲学:小而美。一个个小巧而精致的命令,各自完成各自的功能,不罗嗦,不繁杂。但有时候,我们需要他们相互协作,共同完成任务,就像采购负责买菜,回来交给洗涮工加工,再交给厨师烹饪,再交给服务员端给客人,我们常常需要将一个命令所达成的结果,给到另一个命令进行再加工,这时候就需要用到管道。例如:vincent@ubuntu:~$ls-l|wc管道就像水管一样,将前面的命令的执行结果输送给后面的命令。ls-l负责收集当前目录下的文件的信息,然后将这些文件名作为结果输送到管道,wc这个命令接着从管道中把他们读取出来,并计算出行数、单词个数和总字符数。管道不仅可以连接两个命令,也可以连接多个命令,类似于这样:vincent@ubuntu:~$ccat/etc/passwd|awk-F=/'{print$1}'|wc这就将三个命令连接起来,每个命令的输出都作为下一个命令的输入,连接起来就能完成强大的功能。第三:大于号>和小于号<(重定向)。每一个进程在刚开始运行的时候,系统都会为他们默认地打开了三个文件,他们分别是标准输入、标准输出、标准出错,其文件描述符和对应设备关系如下图所示:
图1-36缺省打开的三个设备文件这三个标准文件对应两个硬件设备:标准输入是键盘,标准输出和标准出错是显示器(是的,显示器设备被打开了两次,第一次打开为行缓冲类型的标准输出,第二次打开为不缓冲类型的标准出错)。绝大多数的SHELL命令,默认的输入输出都是这三个文件。比如,当我们执行命令ls的时候,他会默认地将结果打印到显示器上,就是因为ls本来就被设计为将结果往1号描述符(即标准输出,当执行成功的时候)或者2号描述符(即标准出错,当执行失败的时候)。而当我们打开普通文件的时候,系统也会帮我们产生一系列后续的数字(文件描述符)来表示这些文件,比如我们紧跟着打开了文件a.txt和b.doc,图1-37再打开两个普通文件之后此时进程的的描述符情况如下:第一:假如需要将ls命令的成功的输出结果(本来会被默认地输送到1号文件描述符的信息)重定向到a.txt文件中去,方法如下:vincent@ubuntu:~$ls1>a.txt图1-38重定向1号文件描述符第二:假如需要将ls命令的失败的输出结果(本来会被默认地输送到2号文件描述符的信
息)重定向到a.txt文件中去,方法如下:vincent@ubuntu:~$lsnotexist2>a.txt(notexist是一个不存在的文件,所以ls命令执行会失败)图1-39重定向2号文件描述符第三:重定向标准输入也是类似的,比如直接执行echo命令,他将会默认地从标准输入(即键盘)读取信息,然后打印出来。但是我们可以将标准输入重定向为b.doc文件:vincent@ubuntu:~$echo0&25字符串处理SHELL中对字符串的处理,除了使用1.2.3节介绍的神器sed和awk,在某些比较简单的场合,其实有更简便的办法。1,计算一个字符串的字符个数:vincent@ubuntu:~$var="appletree"vincent@ubuntu:~$echo"${#var}"102,删除一个字符串左边部分字符:vincent@ubuntu:~$path="/etc/rc0.d/K20openbsd-inetd"
vincent@ubuntu:~$level=${path#/etc/rc[0-9].d/[SK]}vincent@ubuntu:~$echo$level20openbsd-inetd3,删除一个字符串右边部分字符:vincent@ubuntu:~$path="/etc/rc0.d/K20openbsd-inetd"vincent@ubuntu:~$level=${path#/etc/rc[0-9].d/[SK]}vincent@ubuntu:~$level=${level%%[a-zA-Z]*}vincent@ubuntu:~$echo$level20注意:两个%%表示贪婪匹配,具体含义是:使用通配符[a-zA-Z]*从右向左“尽可能多地”匹配字符(贪婪原则)。如果只写一个%,则无贪婪原则,那么[a-zA-Z]*将按照最少原则匹配,即匹配0个字符(因为方括号星号*的含义是0个或多个字符)。这个道理对于删除左边字符的井号#也是适用的:双井号##代表从左到右的贪婪匹配。6测试语句有一个叫test的命令,专门用来实现所谓的测试语句,测试语句可以测试很多不同的情形,比如:vincent@ubuntu:~$test-efile以上语句用以判断文件file是否存在,如果存在返回0,否则返回1。那么除了可以判断一个文件存在与否,还有没有别的功能呢?晒出以下列表给各位看官鉴赏:语句含义说明test-efile判断文件file是否存在存在返回0,否则返回1test-rfile判断文件file是否可读可读返回0,否则返回1test-wfile判断文件file是否可写可写返回0,否则返回1test-xfile判断文件file是否可执行可执行返回0,否则返回1test-dfile判断文件file是否是目录是目录返回0,否则返回1test-ffile判断文件file是否是普通文件是普通文件返回0,否则返回1test-sfile判断文件file是否非空非空返回0,否则返回1tests1=s2判断字符串s1和s2是否相同相同返回0,否则返回1tests1!=s2判断字符串s1和s2是否不同不同返回0,否则返回1tests1s2判断字符串s1是否大于s2s1大于s2返回0,否则返回1test-ns判断字符串s长度是否为非0s长度为非0返回0,否则返回1test-zs判断字符串s长度是否为0s长度为0返回0,否则返回1testn1-eqn2判断数值n1是否等于n2n1等于n2返回0,否则返回1testn1-nen2判断数值n1是否不等于n2n1不等于n2返回0,否则返回1testn1-gtn2判断数值n1是否大于n2n1大于n2返回0,否则返回1testn1-gen2判断数值n1是否大于等于n2n1大于等于n2返回0,否则返回1testn1-ltn2判断数值n1是否小于n2n1小于n2返回0,否则返回1
testn1-len2判断数值n1是否小于等于n2n1小于等于n2返回0,否则返回1图1-41test语句举个例子,假如要判断一个文件file是否存在,而且可读,如果都满足的话就将其显示在屏幕上,脚本可以写成这样:#!/bin/bashiftest-efile&&test-rfilethencatfilefi以上代码中,我们依靠test语句来决定是否要执行cat命令,这是脚本语言中最简单的条件判断语句,根C语言的if-else结构很类似,只不过C语言的if语句跟着的是一对儿括号,看起来更加直观,其实,脚本也可以使用括号来代替test语句,看起来更顺眼:#!/bin/bashif[-efile]&&[-rfile]thencatfilefi语法上讲这两种写法完全等价,因此方括号[]其实就是test语句,但是从可读性来看,显然方括号的写法更容易理解,这里一定要特别注意的是:方括号的左右两边都必须有空格!7脚本语法单元跟C语言很类似,SHELL脚本也需要一套基本单元来控制整个逻辑的执行,包括所谓的控制流(就是常见的分支控制和循环控制)、函数、数值处理等。废话少说,以下各个击破。分支控制事实上前一小节的范例已经为我们展示了脚本中的分支控制语句,现将之摘抄下来:1if[-efile]&&[-rfile]2then3catfile4fi代码中的分支语句的语法要点有这么几处:1,每一个if语句都有一个fi(即倒过来写的if)作为结束标记。2,分支结构中使用then作为起始语句。3,当if语句后面的语句执行结果为真(即为0)时,then以下的语句才会被执行。当然,if语句还可以跟else配对使用,跟C语言类似,比如:1if[-efile]&&[-rfile]2then3catfile#如果文件存在且可读,则显示该文件内容4elif[-efile]5then