鸟哥的 Linux 私房菜Shell Scripts篇(四)
12.4 条件判断式
只要讲到『程式』的话,那么条件判断式,亦即是『 if then 』这种判别式肯定一定要学习的!因为很多时候,我们都必须要依据某些资料来判断程式该如何进行。举例来说,我们在上头的ans_yn.sh 讨论输入回应的范例中不是有练习当使用者输入Y/N时,必须要执行不同的讯息输出吗?简单的方式可以利用&&与|| ,但如果我还想要执行一堆指令呢?那真的得要if then来帮忙啰~底下我们就来聊一聊!
12.4.1 利用if .... then
这个if .... then 是最常见的条件判断式了~简单的说,就是当符合某个条件判断的时候, 就予以进行某项工作就是了。这个if ... then 的判断还有多层次的情况!我们分别介绍如下:
- 单层、简单条件判断式
如果你只有一个判断式要进行,那么我们可以简单的这样看:
if [条件判断式]; then
当条件判断式成立时,可以进行的指令工作内容;
fi <==将if反过来写,就成为fi啦!结束if之意!
|
至于条件判断式的判断方法,与前一小节的介绍相同啊!较特别的是,如果我有多个条件要判别时,除了ans_yn.sh那个案例所写的,也就是『将多个条件写入一个中括号内的情况』之外,我还可以有多个中括号来隔开喔!而括号与括号之间,则以&&或||来隔开,他们的意义是:
- && 代表AND ;
- || 代表or ;
所以,在使用中括号的判断式中, && 及|| 就与指令下达的状态不同了。举例来说, ans_yn.sh 里面的判断式可以这样修改:
[ "${yn}" == "Y" -o "${yn}" == "y" ]
上式可替换为
[ "${yn}" == "Y" ] || [ "${ yn}" == "y" ]
之所以这样改,很多人是习惯问题!很多人则是喜欢一个中括号仅有一个判别式的原因。好了, 现在我们来将ans_yn.sh 这个脚本修改成为if ... then 的样式来看看:
[dmtsai@study bin]$ cp ans_yn.sh ans_yn-2.sh <==用复制来修改的比较快! [dmtsai@study bin]$ vim ans_yn-2.sh #!/bin/bash # Program: # This program shows the user's choice # History: # 2015/07/16 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH read -p "Please input (Y/N): " yn if [ "${yn}" == "Y" ] || [ "${yn}" == "y" ]; then echo "OK, continue" exit 0 fi if [ "${yn}" == "N" ] || [ "${yn}" == "n" ]; then echo "Oh, interrupt!" exit 0 fi echo "I don't know what your choice is" && exit 0 |
不过,由这个例子看起来,似乎也没有什么了不起吧?原本的ans_yn.sh 还比较简单呢~ 但是如果以逻辑概念来看,其实上面的范例中,我们使用了两个条件判断呢!明明仅有一个${yn} 的变数,为何需要进行两次比对呢?此时,多重条件判断就能够来测试测试啰!
- 多重、复杂条件判断式
在同一个资料的判断中,如果该资料需要进行多种不同的判断时,应该怎么作?举例来说,上面的ans_yn.sh脚本中,我们只要进行一次${yn}的判断就好(仅进行一次if ),不想要作多次if的判断。此时你就得要知道底下的语法了:
#一个条件判断,分成功进行与失败进行(else)
if [条件判断式]; then
当条件判断式成立时,可以进行的指令工作内容;
else
当条件判断式不成立时,可以进行的指令工作内容;
fi
|
如果考虑更复杂的情况,则可以使用这个语法:
#多个条件判断(if ... elif ... elif ... else)分多种不同情况执行
if [条件判断式一]; then
当条件判断式一成立时,可以进行的指令工作内容;
elif [条件判断式二]; then
当条件判断式二成立时,可以进行的指令工作内容;
else
当条件判断式一与二均不成立时,可以进行的指令工作内容;
fi
|
你得要注意的是, elif 也是个判断式,因此出现elif 后面都要接then 来处理!但是else 已经是最后的没有成立的结果了, 所以else 后面并没有then 喔!好!我们来将ans_yn-2.sh 改写成这样:
[dmtsai@study bin]$ cp ans_yn-2.sh ans_yn-3.sh [dmtsai@study bin]$ vim ans_yn-3.sh #!/bin/bash # Program: # This program shows the user's choice # History: # 2015/07/16 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH read -p "Please input (Y/N): " yn if [ "${yn}" == "Y" ] || [ "${yn}" == "y" ]; then echo "OK, continue" elif [ "${yn}" == "N" ] || [ "${yn}" == "n" ]; then echo "Oh, interrupt!" else echo "I don't know what your choice is" fi |
是否程式变得很简单,而且依序判断,可以避免掉重复判断的状况,这样真的很容易设计程式的啦!^_^!好了,让我们再来进行另外一个案例的设计。一般来说,如果你不希望使用者由键盘输入额外的资料时,可以使用上一节提到的参数功能($1)!让使用者在下达指令时就将参数带进去!现在我们想让使用者输入『 hello 』这个关键字时,利用参数的方法可以这样依序设计:
- 判断$1 是否为hello,如果是的话,就显示"Hello, how are you ?";
- 如果没有加任何参数,就提示使用者必须要使用的参数下达法;
- 而如果加入的参数不是hello ,就提醒使用者仅能使用hello 为参数。
整个程式的撰写可以是这样的:
[dmtsai@study bin]$ vim hello-2.sh #!/bin/bash # Program: # Check $1 is equal to "hello" # History: # 2015/07/16 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH if [ "${1}" == "hello" ]; then echo "Hello, how are you ?" elif [ "${1}" == "" ]; then echo "You MUST input parameters, ex> {${0} someword}" else echo "The only parameter is 'hello', ex> {${0} hello}" fi |
然后你可以执行这支程式,分别在$1 的位置输入hello, 没有输入与随意输入, 就可以看到不同的输出啰~是否还觉得挺简单的啊!^_^。事实上, 学到这里,也真的很厉害了~好了,底下我们继续来玩一些比较大一点的计画啰~
我们在第十章已经学会了grep 这个好用的玩意儿,那么多学一个叫做netstat的指令,这个指令可以查询到目前主机有开启的网路服务埠口(service ports),相关的功能我们会在伺服器架设篇继续介绍,这里你只要知道,我可以利用『 netstat -tuln』来取得目前主机有启动的服务,而且取得的资讯有点像这样:
[dmtsai@study ~]$ netstat -tuln Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN tcp6 0 0 :::22 :::* LISTEN tcp6 0 0 ::1:25 :::* LISTEN udp 0 0 0.0.0.0:123 0.0.0.0:* udp 0 0 0.0.0.0:5353 0.0.0.0:* udp 0 0 0.0.0.0:44326 0.0.0.0:* udp 0 0 127.0.0.1:323 0.0.0.0:* udp6 0 0 :::123 :::* udp6 0 0 ::1:323 :::* #封包格式本地IP:埠口远端IP:埠口是否监听 |
上面的重点是『Local Address (本地主机的IP与埠口对应)』那个栏位,他代表的是本机所启动的网路服务!IP的部分说明的是该服务位于那个介面上,若为127.0.0.1 则是仅针对本机开放,若是0.0.0.0 或::: 则代表对整个Internet 开放(更多资讯请参考伺服器架设篇的介绍)。每个埠口(port) 都有其特定的网路服务,几个常见的port 与相关网路服务的关系是:
- 80: WWW
- 22: ssh
- 21: ftp
- 25: mail
- 111: RPC(远端程序呼叫)
- 631: CUPS(列印服务功能)
假设我的主机有兴趣要侦测的是比较常见的port 21, 22, 25及80 时,那我如何透过netstat 去侦测我的主机是否有开启这四个主要的网路服务埠口呢?由于每个服务的关键字都是接在冒号『 : 』后面, 所以可以藉由撷取类似『 :80 』来侦测的!那我就可以简单的这样去写这个程式喔:
[dmtsai@study bin]$ vim netstat.sh #!/bin/bash # Program: # Using netstat and grep to detect WWW,SSH,FTP and Mail services. # History: # 2015/07/16 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH # 1. 先作一些告知的动作而已~ echo "Now, I will detect your Linux server's services!" echo -e "The www, ftp, ssh, and mail(smtp) will be detected! \n" # 2. 开始进行一些测试的工作,并且也输出一些资讯啰! testfile=/dev/shm/netstat_checking.txt netstat -tuln > ${testfile} #先转存资料到记忆体当中!不用一直执行netstat testing=$(grep ":80 " ${testfile}) #侦测看port 80在否? if [ "${testing}" != "" ]; then echo "WWW is running in your system." fi testing=$(grep ":22 " ${testfile}) #侦测看port 22在否? if [ "${testing}" != "" ]; then echo "SSH is running in your system." fi testing=$(grep ":21 " ${testfile}) #侦测看port 21在否? if [ "${testing}" != "" ]; then echo "FTP is running in your system." fi testing=$(grep ":25 " ${testfile}) #侦测看port 25在否? if [ "${testing}" != "" ]; then echo "Mail is running in your system." fi |
实际执行这支程式你就可以看到你的主机有没有启动这些服务啦!是否很有趣呢?条件判断式还可以搞的更复杂!举例来说,在台湾当兵是国民应尽的义务,不过,在当兵的时候总是很想要退伍的!那你能不能写个脚本程式来跑,让使用者输入他的退伍日期,让你去帮他计算还有几天才退伍?
由于日期是要用相减的方式来处置,所以我们可以透过使用date 显示日期与时间,将他转为由1970-01-01 累积而来的秒数, 透过秒数相减来取得剩余的秒数后,再换算为日数即可。整个脚本的制作流程有点像这样:
- 先让使用者输入他们的退伍日期;
- 再由现在日期比对退伍日期;
- 由两个日期的比较来显示『还需要几天』才能够退伍的字样。
似乎挺难的样子?其实也不会啦,利用『date --date="YYYYMMDD" +%s』转成秒数后,接下来的动作就容易的多了!如果你已经写完了程式,对照底下的写法试看看:
[dmtsai@study bin]$ vim cal_retired.sh #!/bin/bash # Program: # You input your demobilization date, I calculate how many days before you demobilize. # History: # 2015/07/16 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH # 1. 告知使用者这支程式的用途,并且告知应该如何输入日期格式? echo "This program will try to calculate :" echo "How many days before your demobilization date..." read -p "Please input your demobilization date (YYYYMMDD ex>20150716): " date2 # 2.测试一下,这个输入的内容是否正确?利用正规表示法啰~ date_d=$(echo ${date2} |grep '[0-9]\{8\}') #看看是否有八个数字 if [ "${date_d}" == "" ]; then echo "You input the wrong date format...." exit 1 fi # 3.开始计算日期啰~ declare -i date_dem=$(date --date="${date2}" +%s) #退伍日期秒数 declare -i date_now=$(date +%s) #现在日期秒数 declare -i date_total_s=$((${date_dem}-${date_now})) #剩余秒数统计 declare -i date_d=$((${date_total_s}/60/60/24)) #转为日数 if [ "${date_total_s}" -lt "0" ]; then #判断是否已退伍 echo "You had been demobilization before: " $((-1*${date_d})) " ago" else declare -i date_h=$(($((${date_total_s}-${date_d}*60*60*24))/60/60)) echo "You will demobilize after ${date_d} days and ${date_h} hours." fi |
瞧一瞧,这支程式可以帮你计算退伍日期呢~如果是已经退伍的朋友,还可以知道已经退伍多久了~哈哈!很可爱吧~脚本中的date_d变数宣告那个/60/60/24是来自于一天的总秒数(24小时*60分*60秒) 。瞧~全部的动作都没有超出我们所学的范围吧~ ^_^还能够避免使用者输入错误的数字,所以多了一个正规表示法的判断式呢~这个例子比较难,有兴趣想要一探究竟的朋友,可以作一下课后练习题 关于计算生日的那一题喔!~加油!
12.4.2 利用case ..... esac 判断
上个小节提到的『 if .... then .... fi 』对于变数的判断是以『比对』的方式来分辨的,如果符合状态就进行某些行为,并且透过较多层次(就是elif ...)的方式来进行多个变数的程式码撰写,譬如 hello-2.sh那个小程式,就是用这样的方式来撰写的啰。好,那么万一我有多个既定的变数内容,例如hello-2.sh当中,我所需要的变数就是"hello"及空字串两个,那么我只要针对这两个变数来设定状况就好了,对吧?那么可以使用什么方式来设计呢?呵呵~就用case ... in .... esac吧~,他的语法如下:
case $变数名称in <==关键字为case ,还有变数前有钱字号
"第一个变数内容" ) <==每个变数内容建议用双引号括起来,关键字则为小括号)
程式段
;; <==每个类别结尾使用两个连续的分号来处理!
"第二个变数内容" )
程式段
;;
* ) <==最后一个变数内容都会用*来代表所有其他值
不包含第一个变数内容与第二个变数内容的其他程式执行段
exit 1
;;
esac <==最终的case结尾!『反过来写』思考一下!
|
要注意的是,这个语法以case (实际案例之意) 为开头,结尾自然就是将case 的英文反过来写!就成为esac 啰!不会很难背啦!另外,每一个变数内容的程式段最后都需要两个分号(;;) 来代表该程式段落的结束,这挺重要的喔!至于为何需要有* 这个变数内容在最后呢?这是因为,如果使用者不是输入变数内容一或二时, 我们可以告知使用者相关的资讯啊!废话少说,我们拿hello-2.sh 的案例来修改一下,他应该会变成这样喔:
[dmtsai@study bin]$ vim hello-3.sh #!/bin/bash # Program: # Show "Hello" from $1.... by using case .... esac # History: # 2015/07/16 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH case ${1} in "hello") echo "Hello, how are you ?" ;; "") echo "You MUST input parameters, ex> {${0} someword}" ;; *) #其实就相当于万用字元,0~无穷多个任意字元之意! echo "Usage ${0} {hello}" ;; esac |
在上面这个hello-3.sh的案例当中,如果你输入『 sh hello-3.sh test 』来执行,那么萤幕上就会出现『Usage hello-3.sh {hello}』的字样,告知执行者仅能够使用hello喔~这样的方式对于需要某些固定字串来执行的变数内容就显的更加的方便呢!这种方式你真的要熟悉喔!这是因为早期系统的很多服务的启动scripts都是使用这种写法的(CentOS 6.x以前)。虽然CentOS 7已经使用systemd,不过仍有数个服务是放在/etc/init.d/目录下喔!例如有个名为netconsole的服务在该目录下,那么你想要重新启动该服务,是可以这样做的(请注意,要成功执行,还是得要具有root身份才行!一般帐号能执行,但不会成功!):
/etc/init.d/netconsole restart
重点是那个restart 啦!如果你使用『 less /etc/init.d/netconsole 』去查阅一下,就会看到他使用的是case 语法, 并且会规定某些既定的变数内容,你可以直接下达/etc/init.d/ netconsole , 该script 就会告知你有哪些后续接的变数可以使用啰~方便吧!^_^
一般来说,使用『 case $变数in 』这个语法中,当中的那个『 $变数』大致有两种取得的方式:
- 直接下达式:例如上面提到的,利用『 script.sh variable 』的方式来直接给予$1这个变数的内容,这也是在/etc/init.d目录下大多数程式的设计方式。
- 互动式:透过read这个指令来让使用者输入变数的内容。
这么说或许你的感受性还不高,好,我们直接写个程式来玩玩:让使用者能够输入one, two, three , 并且将使用者的变数显示到萤幕上,如果不是one, two, three时,就告知使用者仅有这三种选择。
[dmtsai@study bin]$ vim show123.sh #!/bin/bash # Program: # This script only accepts the flowing parameter: one, two or three. # History: # 2015/07/17 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH echo "This program will print your selection !" # read -p "Input your choice: " choice #暂时取消,可以替换! # case ${choice} in #暂时取消,可以替换! case ${1} in #现在使用,可以用上面两行替换! "one") echo "Your choice is ONE" ;; "two") echo "Your choice is TWO" ;; "three") echo "Your choice is THREE" ;; *) echo "Usage ${0} {one|two|three}" ;; esac |
此时,你可以使用『 sh show123.sh two 』的方式来下达指令,就可以收到相对应的回应了。上面使用的是直接下达的方式,而如果使用的是互动式时,那么将上面第10, 11 行的"#" 拿掉, 并将12 行加上注解(#),就可以让使用者输入参数啰~这样是否很有趣啊?
12.4.3 利用function 功能
什么是『函数(function)』功能啊?简单的说,其实,函数可以在shell script当中做出一个类似自订执行指令的东西,最大的功能是,可以简化我们很多的程式码~举例来说,上面的show123.sh当中,每个输入结果one, two, three其实输出的内容都一样啊~那么我就可以使用function来简化了!function的语法是这样的:
function fname () {
程式段
}
|
那个fname就是我们的自订的执行指令名称~而程式段就是我们要他执行的内容了。要注意的是,因为shell script的执行方式是由上而下,由左而右,因此在shell script当中的function的设定一定要在程式的最前面,这样才能够在执行时被找到可用的程式段喔(这一点与传统程式语言差异相当大!初次接触的朋友要小心!)!好~我们将show123.sh改写一下,自订一个名为printit的函数来使用喔:
[dmtsai@study bin]$ vim show123-2.sh #!/bin/bash # Program: # Use function to repeat information. # History: # 2015/07/17 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH function printit(){ echo -n "Your choice is " #加上-n可以不断行继续在同一行显示 } echo "This program will print your selection !" case ${1} in "one") printit ; echo ${1} | tr 'az' 'AZ' #将参数做大小写转换! ;; "two") printit ; echo ${1} | tr 'az' 'A-Z' ;; "three") printit ; echo ${1} | tr 'az' 'A-Z' ;; *) echo "Usage ${0} {one|two|three}" ;; esac |
以上面的例子来说,鸟哥做了一个函数名称为printit ,所以,当我在后续的程式段里面, 只要执行printit 的话,就表示我的shell script 要去执行『 function printit .... 』里面的那几个程式段落啰!当然啰,上面这个例子举得太简单了,所以你不会觉得function 有什么好厉害的, 不过,如果某些程式码一再地在script 当中重复时,这个function 可就重要的多啰~ 不但可以简化程式码,而且可以做成类似『模组』的玩意儿,真的很棒啦!
另外,function也是拥有内建变数的~他的内建变数与shell script很类似,函数名称代表示$0 ,而后续接的变数也是以$1, $2...来取代的~这里很容易搞错喔~因为『 function fname() {程式段} 』内的$0, $1...等等与shell script的$0是不同的。以上面show123-2.sh来说,假如我下达:『 sh show123-2.sh one 』这表示在shell script内的$1为"one"这个字串。但是在printit()内的$1则与这个one无关。我们将上面的例子再次的改写一下,让你更清楚!
[dmtsai@study bin]$ vim show123-3.sh #!/bin/bash # Program: # Use function to repeat information. # History: # 2015/07/17 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH function printit(){ echo "Your choice is ${1}" #这个$1必须要参考底下指令的下达 } echo "This program will print your selection !" case ${1} in "one") printit 1 #请注意, printit指令后面还有接参数! ;; "two") printit 2 ;; "three") printit 3 ;; *) echo "Usage ${0} {one|two|three}" ;; esac |
在上面的例子当中,如果你输入『 sh show123-3.sh one 』就会出现『 Your choice is 1 』的字样~ 为什么是1 呢?因为在程式段落当中,我们是写了『 printit 1 』那个1 就会成为function 当中的$1 喔~ 这样是否理解呢?function 本身其实比较困难一点,如果你还想要进行其他的撰写的话。不过,我们仅是想要更加了解shell script 而已,所以,这里看看即可~了解原理就好啰~ ^_^