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. 判断 $1 是否为 hello,如果是的话,就显示 "Hello, how are you ?";
  2. 如果没有加任何参数,就提示使用者必须要使用的参数下达法;
  3. 而如果加入的参数不是 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 detect! \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 累积而来的秒数, 通过秒数相减来取得剩余的秒数后,再换算为日数即可。整个脚本的制作流程有点像这样:

  1. 先让使用者输入他们的退伍日期;
  2. 再由现在日期比对退伍日期;
  3. 由两个日期的比较来显示“还需要几天”才能够退伍的字样。

似乎挺难的样子?其实也不会啦,利用“ 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

瞧一瞧,这支程序可以帮你计算退伍日期呢~如果是已经退伍的朋友, 还可以知道已经退伍多久了~哈哈!很可爱吧~脚本中的 dated 变量宣告那个 /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 'a-z' 'A-Z'  # 将参数做大小写转换!
    ;;
  "two")
    **printit**; echo ${1} | tr 'a-z' 'A-Z'
    ;;
  "three")
    **printit**; echo ${1} | tr 'a-z' 'A-Z'
    ;;
  *)
    echo "Usage ${0} {one|two|three}"
    ;;
esac

以上面的例子来说,鸟哥做了一个函数名称为 printit ,所以,当我在后续的程序段里面, 只要执行 printit 的话,就表示我的 shell script 要去执行“ function printit .... ” 里面的那几个程序段落啰!当然啰,上面这个例子举得太简单了,所以你不会觉得 function 有什么好厉害的, 不过,如果某些程序码一再地在 script 当中重复时,这个 function 可就重要的多啰~ 不但可以简化程序码,而且可以做成类似“模块”的玩意儿,真的很棒啦!

鸟哥的图示

Tips 建议读者可以使用类似 vim 的编辑器到 /etc/init.d/ 目录下去查阅一下你所看到的文件, 并且自行追踪一下每个文件的执行情况,相信会更有心得!

另外, 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 而已,所以,这里看看即可~了解原理就好啰~ ^_^