# 条件判断

在程序中,没有条件判断 if then 方式的话,在执行多条指令的时候,就会很麻烦。

# 利用 if...then

# 单层、简单条件判断

if [ 表达式 ]; then
	当条件成立时,可以进行的指令工作内容
fi
1
2
3

至于表达式的编码,与上一章的 test 一致,但是有一个特别的是,可以使用 &&|| 来连接多个中括号,在这里他们的含义就是表示 并且 和 或者 的意思

所以在使用中括号的时候, &&|| 与指令状态下的含义不同。比如:

[ "${yn}" == "Y" -o "${yn}" == "y" ]
可以替换为下面的方式
[ "${yn}" == "Y" ] || [ "${yn}" == "y" ]
1
2
3

这样就很方便维护了,一个中括号一个表达式。那么将这个 script 修改为 if...then 的形式如下

[mrcode@study bin]$ vi ans_yn.sh

#!/bin/bash
# Program:
#       This program shows the user's choice
# History:
#       2020/01/20              mrcode          first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

read -p "请输入 Y/N:" yn
# [ "${yn}" == "Y" -o "${yn}" == "y" ] && echo "Ok,continue" && exit 0
if [ "${yn}" == "Y" ] || [ "${yn}" == "y" ]; then
        echo "Ok,continue"
        exit 0
fi

# [ "${yn}" == "N" -o "${yn}" == "n" ] && echo "Oh,interrupt!" && exit 0
if [ "${yn}" == "N" ] || [ "${yn}" == "n" ]; then
        echo "Oh,interrupt!"
        exit 0
fi
echo "I don't know what your choice is" && exit 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

此方式只是在代码组织上更偏向于笔者所学的 JAVA 语言了,对于变量的判定还可以使用如下的多重判断来达到效果

# 多重、复杂条件判断

简单说,上述实例对于变量 ${yn} 使用了两次 if,那么可以使用如下方式简化

if [ 条件表达式 ]; then
	做点啥
else
	做点啥
fi
1
2
3
4
5

更复杂的情况,增加 elseif ,如下

if [ 条件表达式 ]; then
	做点啥
elif [ 条件表达式 ]; then
	做点啥
else
	做点啥
fi
1
2
3
4
5
6
7

改写 ans_yn.sh 脚本如下

#!/bin/bash
# Program:
#       This program shows the user's choice
# History:
#       2020/01/20              mrcode          first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

read -p "请输入 Y/N:" yn
# [ "${yn}" == "Y" -o "${yn}" == "y" ] && echo "Ok,continue" && exit 0
if [ "${yn}" == "Y" ] || [ "${yn}" == "y" ]; then
        echo "Ok,continue"
        exit 0
else
        echo "Oh,interrupt!"
        exit 0
fi
echo "I don't know what your choice is" && exit 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

另一个范例知识,上一节提到参数功能($1),让用户在下达指令的时候将参数带进去,让用户输入 hello 关键词,利用参数的方法可以如下设计:

  1. 判断 $1 是否为 hello ,如果是,则显示「Hello, how ary you?」
  2. 如果无参数,则提示使用者必须要使用的参数下达方法
  3. 如果参数不是 hello,则提示使用者仅能使用 hello 为参数
#!/bin/bash
# Program:
#       直接携带参数提示
# History:
#       2020/01/20              mrcode          first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

if [ "$1" == "hello" ]; then
	echo "Hello, how ary you?"
elif [ -z "$1" ]; then
	echo "请携带参数"
else
	echo "只能携带参数 hello"
fi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

测试输出

[mrcode@study bin]$ ./hello-2.sh 
请携带参数
[mrcode@study bin]$ ./hello-2.sh hello
Hello, how ary you?
[mrcode@study bin]$ ./hello-2.sh hellox
只能携带参数 hello
1
2
3
4
5
6

书上例子如下:

#!/bin/bash
# Program:
#       Chek $1 is equal to "hello"
# History:
#       2020/01/20              mrcode          first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

if [ "$1" == "hello" ]; then
	echo "Hello, how ary you?"
elif [ "$1" == "" ]; then
	echo "You MUST input parameters, ex> {${0} someword}"
else
	echo "The only parameter is 'hello', ex> {${0} hello}"
fi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

信息输出如下

[mrcode@study bin]$ ./hello-2.sh 
You MUST input parameters, ex> {./hello-2.sh someword}
[mrcode@study bin]$ ./hello-2.sh hell
The only parameter is 'hello', ex> {./hello-2.sh hello}
[mrcode@study bin]$ ./hello-2.sh hello
Hello, how ary you?
1
2
3
4
5
6

笔者点评:这个显示很棒,错误提示告知了用户要怎么做


那么深入练习。

在第十章学习了 grep 指令,现在多了解一个 netstat 指令,可以查询到目前主机有开启的网络服务端口(service ports),相关功能会在 服务器架设篇 继续介绍;这里只需要知道 netstat -tuln可以取得目前主机有启动的服务,而且取得的信息类似下面这样

[mrcode@study bin]$ netstat -tuln
Active Internet connections (only servers)
# 封包格式			 本地 IP:端口			  远程 IP:端口				是否监听
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 127.0.0.1:631           0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:6010          0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:6011          0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN     
tcp        0      0 192.168.122.1:53        0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN     
tcp6       0      0 ::1:631                 :::*                    LISTEN     
tcp6       0      0 ::1:25                  :::*                    LISTEN     
tcp6       0      0 ::1:6010                :::*                    LISTEN     
tcp6       0      0 ::1:6011                :::*                    LISTEN     
tcp6       0      0 :::111                  :::*                    LISTEN     
tcp6       0      0 :::22                   :::*                    LISTEN     
udp        0      0 0.0.0.0:48829           0.0.0.0:*                          
udp        0      0 192.168.122.1:53        0.0.0.0:*                          
udp        0      0 0.0.0.0:67              0.0.0.0:*                          
udp        0      0 0.0.0.0:111             0.0.0.0:*                          
udp        0      0 127.0.0.1:323           0.0.0.0:*                          
udp        0      0 0.0.0.0:672             0.0.0.0:*                          
udp        0      0 0.0.0.0:5353            0.0.0.0:*                          
udp6       0      0 :::111                  :::*                               
udp6       0      0 ::1:323                 :::*                               
udp6       0      0 :::672                  :::*  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

重点关注 Local Address 字段(本地主机 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」来检测。那么程序如下

下面是笔者写的脚本

vim netstat.sh

#!/bin/bash
# Program:
#       
# History:
#       2020/01/20              mrcode          first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

echo "现在开始检测当前主机上的服务"
echo -e "www、ftp、mail、www 服务将被检测 \n"
# 将 local Address 字段截取出来
datas=$(netstat -tuln | awk '{print $4}')
testing=$(grep ":80" ${datas}) 
if [ ! -z "${testing}" ]; then
	echo "www"
fi
testing=$(grep ":22" ${datas}) 
if [ ! -z "${testing}" ]; then
	echo "ssh"
fi
testing=$(grep ":21" ${datas}) 
if [ ! -z "${testing}" ]; then
	echo "ftp"
fi
testing=$(grep ":25" ${datas}) 
if [ ! -z "${testing}" ]; then
	echo "mail"
fi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

不过很遗憾,grep 后只能跟一个文件路径。那么正确的做法如下

#!/bin/bash
# Program:
#       Using netstat and grep to detect www⽀~Assh⽀~Aftp and mail services
# History:
#       2020/01/20              mrcode          first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

# 1. 佅~H佑~J潟¥彎¥䷾K彝¥襾A佁~Z亾@举H
echo "潎°作¨廾@妾K梾@派K弾S佉~M主彜º䷾J潚~D彜~M佊¡"
echo -e "www⽀~Aftp⽀~Amail⽀~Awww 彜~M佊¡対F被梾@派K \n"

# 2. 达[蠾L佈¤孾Z佒~L信彁¯轾S佇º 
# 対F local Address 嬾W段彈ª住~V佇º彝¥﻾L并潔~_彈~P彖~G件
testfile=/dev/shm/netstat_checking.txt
netstat -tuln | awk '{print $4}' > ${testfile}

testing=$(grep ":80" ${testfile})
if [ "${testing}" != "" ]; then
        echo "www is running in you system. "
fi

testing=$(grep ":22" ${testfile})
if [ ! -z "${testing}" ]; then
        echo "ssh  is running in you system. "
fi

testing=$(grep ":21" ${testfile})
if [ ! -z "${testing}" ]; then
        echo "ftp  is running in you system. "
fi
testing=$(grep ":25" ${testfile})
if [ ! -z "${testing}" ]; then
        echo "mail  is running in you system. "
fi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

输出信息如下

[mrcode@study bin]$ ./netstat.sh 
现在开始检测当前主机上的服务
www、ftp、mail、www 服务将被检测 

ssh  is running in you system. 
mail  is running in you system.
1
2
3
4
5
6

条件判断还可以更复杂,比如:在台湾当兵是国民应尽的义务,不过,在当兵的时候总是很想退伍,那么写个脚本程序来实现:让用户输入他的退伍日期,计算出还有多少天才退伍?的功能

那么思路如下:

  1. 用户输入自己的退伍日期
  2. 由现在的日期对比退伍日期
  3. 由两个日期的比较来显示「还需要几天」才能够退伍的字样

温馨提示:日期可以使用 date --date="YYYYMMDD" +%s 来取得指定日期的秒数,再利用秒数相减,再计算到天

笔者从现在开始,就不再贴出自己写的代码了,先自己写,然后对照书上的,最后部分修改成书上的展示

vim cal_retired.sh
  
#!/bin/bash
# Program:
#       You input you demobilization date,I calculate how many days before you demobilize.
# History:
#       2020/01/20              mrcode          first relese
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>20200112):" date2

# 2. 测试判定,输入内容是否正确,使用正则表达式
date_d=$(echo ${date2} | grep '[0-9]\{8\}')             # 匹配 8 位数的字符串
if [ -z "${date_d}" ]; then
        # 您输入了错误的日期格式
        echo "You input the wrong date format..."
        exit 1
fi

# 3. 开始计算日期
declare -i date_dem=$(date --date="${date_d}" +%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
		# 这里是用 -1 乘,得到是正数,标识已经退伍多少天了
        echo "You had been demobilization before:  $((-1*${date_d}))  ago"
else
        # 这里使用 总秒数 - 转换为日的变量(这里只是转换为了天),剩余数据转成小时
        # 则计算到 n 天 n 小时
        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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

测试输出

[mrcode@study bin]$ ./cal_retired.sh 
This program will try to calculate :
How many days before your demobilization date...
Please input your demobilization date (YYYYMMDD ex>20200112):20200120	# 输入当天
You had been demobilization before:  0  ago

[mrcode@study bin]$ ./cal_retired.sh 
This program will try to calculate :
How many days before your demobilization date...
Please input your demobilization date (YYYYMMDD ex>20200112):20200119	# 输入前一天
You had been demobilization before:  1  ago

[mrcode@study bin]$ ./cal_retired.sh 
This program will try to calculate :
How many days before your demobilization date...
Please input your demobilization date (YYYYMMDD ex>20200112):20200121	# 输入明天
You will demobilize after 0 days and 8 hours.

[mrcode@study bin]$ ./cal_retired.sh 
This program will try to calculate :
How many days before your demobilization date...
Please input your demobilization date (YYYYMMDD ex>20200112):2020^H^H3	# 输入错误的格式
You input the wrong date format...

[mrcode@study bin]$ ./cal_retired.sh 
This program will try to calculate :
How many days before your demobilization date...
Please input your demobilization date (YYYYMMDD ex>20200112):20300120	# 输入10 年后
You will demobilize after 3652 days and 8 hours.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

笔者总结:

  • 本例结合了 grep 查找符合条件的参数,如果完全不符合,则为空白返回了
  • 结合了 declare -i 定义整数变量
  • 使用了 $(($(()))) 嵌套指令执行语法
  • 该范例还是有难度的,难点在于 用正则检查输入参数 和 计算 天 并计算小时

# 利用 case...esac 判断

作为 JAVA 程序员,这个不用多解释,直接看语法

case $变量名称 in		# 关键词为 case 还有 变量前的 $ 符号
	“变量内容 1)		# 每个变量内容建议用双引号括起来,关键词则为小括号
	 	程序段
	 ;;				  # 使用两个连续的分号来结尾
	“变量内容 2)
	 	程序段
	 ;;
	*)			 	  # 最后一个变量内容需要用 * 来代表所有其他值
		程序段
	 ;;
esac				  # 最终的 case 结尾,就是反过来拼写的字符 esac
1
2
3
4
5
6
7
8
9
10
11

将上面 ./hello-2.sh 的例子使用该语法修改

./hello-3.sh 

#!/bin/bash
# Program:
#       直接携带参数提示
# History:
#       2020/01/20              mrcode          first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

case $1 in
	"hello")
		echo "Hello, how ary you?"
	;;
	"")
		echo "请携带参数"
	;;
	*)
		echo "只能携带参数 hello"
	;;
esac
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

此种判定方式,针对于判定字符串时会更加的方便,CentOS6.x 以前系统很多服务的启动都是使用使用这种写法写的。虽然 CentOS7 已经使用 systemd ,不过任然有数个服务时放在 /etc/init.d 目录下的、比如有个名为 netconsole 的服务在该目录下

# 重新启动该服务
# 注意该服务需要使用 root 身份才行,一般账户可以执行,但是不会成功
/etc/init.d/netconsole restart
1
2
3

查阅该文件,找到文件末尾为发现以下的内容,这里就判定了输入的参数,使用的就是 case 语法

case "$1" in
    stop) stop ;;
    status) status ;;
    start|restart|reload|force-reload) restart ;;
    condrestart) condrestart ;;
    *) usage ;;
esac
1
2
3
4
5
6
7

所以对于脚本的编写,可以参考这些已经有的,看看人家是怎么写的

一般来说,使用「case $变量 in」语法,那个变量大致有两种取得方式:

  • 直接下达:利用 script.sh variable 方式直接给 $1 变量,这也是在 /etc/init.d 目录下大多数程序的设计方式
  • 交互式:通过 read 指令让用户输入变量内容

下面来演示下:

  • 用户输入 one、two、three 并显示在屏幕上
  • 如果不是以上变量,那么提示用户只有这三种选择
vim show123.sh

#!/bin/bash
# Program:
#       
# History:
#       2020/01/20              mrcode          first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

# 如需要让用户交互性输入,那么可以用这两行代替:case "$1" in
# read -p "请输入您的选择:" choice
# case "${choice}" in

case "$1" in
	"one") echo $1 ;;
	"two") echo $1 ;;
	"three") echo $1 ;;
	*) echo "只能输入 one、two、three" ;;
esac
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

测试输出

[mrcode@study bin]$ ./show123.sh 
只能输入 one、two、three
[mrcode@study bin]$ ./show123.sh one
one
[mrcode@study bin]$ ./show123.sh two
two
[mrcode@study bin]$ ./show123.sh three
three
[mrcode@study bin]$ ./show123.sh three111
只能输入 one、two、three
1
2
3
4
5
6
7
8
9
10

# 利用 function 功能

函数功能,不用多说,可以被复用,优化程序结构,语法如下

function fname(){
	程序段
}
1
2
3

TIP

由于 shell script 执行方式是由上而下,由左而右,因此 function 的代码一定要在程序的最前面

下面将 show123.sh 改写成使用 function 方式

vim show123-2.sh
#!/bin/bash
# Program:
#       
# History:
#       2020/01/20              mrcode          first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

# 如需要让用户交互性输入,那么可以用这两行代替:case "$1" in
# read -p "请输入您的选择:" choice
# case "${choice}" in

function printit(){
	echo -n "Your choice is " # -n 可以不断行连续在同一行显示
}

case "$1" in
	"one") printit; echo $1 ;;
	"two") printit; echo $1 | tr 'a-z' 'A-z' ;;  # 转换为大写
	"three") printit; echo $1 ;;
	*) echo "只能输入 one、two、three" ;;
esac
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

输出信息

[mrcode@study bin]$ ./show123-2.sh one
Your choice is one
[mrcode@study bin]$ vim show123-2.sh
[mrcode@study bin]$ ./show123-2.sh tow
只能输入 one、two、three
[mrcode@study bin]$ ./show123-2.sh two
Your choice is TWO
1
2
3
4
5
6
7

上述代码,做了一个打印部分重复信息的功能,这个例子比较简单,当在程序中有大量重复,和大量逻辑的时候,就会体现出来了

同样,function 也可以有参数变量,改写成有参数调用函数

vim show123-3.sh

#!/bin/bash
# Program:
#       
# History:
#       2020/01/20              mrcode          first relese
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

function printit(){
        echo "Your choice is ${1}" # 在函数域中,的参数变量,与外部的不一致
}

case "$1" in
        "one") printit 1 ;;
        "two") printit 2 ;;
        "three") printit $1 ;;
        *) echo "只能输入 one、two、three" ;;
esac
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

测试如下

[mrcode@study bin]$ ./show123-3.sh one
Your choice is 1			
[mrcode@study bin]$ ./show123-3.sh two
Your choice is 2 	# 可以看到,这里给定参数 1,那么在里面获取 ${1},的时候就获取到了

[mrcode@study bin]$ ./show123-3.sh three		
Your choice is three	# 在外部给定的是脚本中的变量 $1, 在内部也能获取到变量的具体内容
[mrcode@study bin]$ ./show123-3.sh threex
只能输入 one、two、three
1
2
3
4
5
6
7
8
9