Shell 流程控制

1. 流程控制概述

1.1 流程控制简介

Shell 脚本默认从上到下顺序执行,在程序运行中,会遇到很多种情况,对应不同情况执行对应的操作,例如对于一批数据需要进行执行重复工作,这些都需要我们使用特定的流程控制语句来实现,我们想要程序完成预定的操作,就需要熟练掌握流程控制语句,不同的流程控制语句有不同的适应场景。

1.2 为什么要用流程控制

流程控制是每种编程语言控制逻辑走向和执行次序的重要组成部分,流程控制可以说是一门语言的 “经脉”,其控制着程序的运行走向,所以熟练掌握流程控制语句才能更好的控制整个脚本的运行结果,来完成我们的需求。

2. Shell 流程控制操作

2.1 条件语句

顾名思义,就是满足特定条件执行对应操作,按照顺序从上到下,条件语句 if 通常需要与 test 命令配合使用,当满足条件则执行 then 后的 command,否则继续往下运行执行对应的 command,条件语句 if 是 Shell 编程中最基础的条件判断流程语句。

2.1.1 单分支 if 语句

顾名思义就是只有一个 if 语句块包含的语句,condition 为正确则执行 then 内的命令,语法:

if condition
then
    command1 
    command2
    ...
    commandN 
fi

对于 sshd 进程是否存在,可以使用单分支 if 语句来判断,例如:

if [ $(ps -ef |grep /usr/sbin/sshd|grep -v grep|wc -l) -eq 1 ];then 
	echo "sshd server exist"
fi

2.1.2 双分支 if 语句

多分支 if 语句存在 else 情况,语法:

if condition
then
    command1 
    command2
    ...
    commandN
else
    command
fi

统一判断 sshd 服务,可以在 else 中进行一些列操作,例如:

if [ $(ps -ef |grep /usr/sbin/sshd|grep -v grep|wc -l) -eq 1 ];then 
	echo "sshd server exist"
else
	service sshd start && echo "启动sshd服务"
fi

2.1.3 多分支 if 语句

顾名思义有多个 if 条件,在此利用 elif 来表示,注意最后有一个 else 结尾。

if condition1;then
    command1
elif condition2;then 
    command2
    ...
...
elif conditionN,then
		commandN
else
    command
fi

我们的 linux 系统有多个版本,可以利用多分支 if 语句来进行判断,例如:

#! /bin/bash

sys_version=$(rpm -q centos-release|cut -d- -f3)

if [ $sys_version -eq 6 ];then
	echo "sysversion is $sys_version"
elif [ $sys_version -eq 7 ];then
	echo "sysversion is $sys_version"
else
	echo "sysversion is ${sys_version}"
fi

2.2 循环语句

对于一批数据,我们需要对其重复进行操作的时候,就需要利用循环语句来操作。

2.2.1 for 循环

for 循环语句通常应用在可预估数量的一批对象操作中,默认 for 循环的取值列表是以 $IFS 分割,默认 $IFS 为空白符,如果我们有其他需求可以更改,语法为:

for var in item1 item2 ... itemN
do
    command1
    command2
    ...
    commandN
done

通过 for 循环每次遍历一个后面跟的对象,在 do…done 操作块中对对象进行一些列操作。

例如我们来求和 1-10 的和:

SUM=0
for num in $(seq 1 10)
do
		let SUM=${SUM}+${num}
done
echo "1-10的和为:${SUM}"

当然在 for 循环语句里面也可以配合 if 条件判断或其他流程控制语句进行操作。

在此我们举例修改 $IFS 的应用场景,首选备份默认当前的 $IFS,之后为其赋值新的 $IFS:,在对 /etc/passwd 进行操作完成后,恢复之前的 $IFS, 在此我们就利用改变 $IFS 对 /etc/passwd 的单个字段进行了变量操作。

#!/bin/bash
OLD_IFS=$IFS
IFS=":"
for i in $(head -1 /etc/passwd); do
echo $i
done
IFS=${OLD_IFS}
[root@xuel-terraform-cvm-0 ~]# bash 1.sh
root
x
0
0
root
/root
/bin/bash
[root@xuel-terraform-cvm-0 ~]# cat /etc/passwd |head -1
root:x:0:0:root:/root:/bin/bash

for 循环如果条件永远满足则,一直执行内部的命令。

for (( ; ; ))

2.2.2 while 循环

while 循环同样为循环,与 for 循环功能一样,利用 for 循环的语句同样也可以使用 while 循环完成,但是 while 循环通常用于处理未知数量对象的操作,语法:

while 条件表达式:do
    command
done

while 通常与 test 语句配合使用,如果条件表达式成立,则一直执行。

例如求和打印 1-5 个数:

#!/bin/bash
N=0
while [ $N -lt 5 ]; do
  let N++
  echo $N
done

也可以利用 read 读入文件,例如我们来读入一个写有 ip 或域名列表的文件,来判断该文件内的域名或 IP 网络是否可达。

#!/bin/bash
#function:check url
filename=urllist.txt
for url in $(cat $filename)
do
  status=`curl -I --connect-timeout 5 $url -s|awk '/HTTP/{print $2}'`
  if [[ $status == "200" ]];then
          echo "Url:$url is ok!   status is $status"
  else
          echo "Url:$url is error! status is $status"
  fi
done

编写 urllist.txt

[root@xuel-terraform-cvm-0 ~]# cat urllist.txt
baidu.com
114.114.114.114
[root@xuel-terraform-cvm-0 ~]# bash urlcheck.sh
Url:baidu.com is ok!   status is 200
Url:114.114.114.114 is error! status is

如果 while 的判断条件为永远为 true,则称为无限循环,会一直执行内部的操作,例如:

while :
do
    command
done

或者
while true
do
    command
done

2.2.3 until 循环

until 循环与 while 循环刚好相反,其也有一定的应用场景,其为条件表达式为 true 时停止,否则一直运行,语法:

until 条件表达式
do
    command
done

例如我们使用 until 来打印 1-5 数字:

NUM=0
until [ ${NUM} -ge 5 ]
do 
	let NUM++
	echo $NUM
done

2.2.4 break 与 continue

与上面三个循环语句不同的是,break 为跳出循环,continue 则为不执行下一次操作,直接跳到下一次循环。

我们可以利用 break 来跳出终止循环。

  • break
#!/bin/bash

N=0
while true; do
    let N++
    if [ $N -eq 5 ]; then
    break
    fi
    echo $N
done
  • continue
#!/bin/bash
N=0
while [ $N -lt 5 ]; do
    let N++
    if [ $N -eq 3 ]; then
        continue
    fi
    echo $N
done

利用 continue 来跳过特定的条件操作。

2.3 选择语句

2.3.1 case 语句

选择语句 case 可以在特定的几个条件中选择某一个进行执行,其他 case 可以利用 if 多分支来替代。

case 模式名	in
    模式 1)
        命令
        ;;
    模式 2)
        命令
        ;;
    *)
        不符合以上模式执行的命令
esac

例如我们服务的启动操作脚本就是利用 case 语句来,当用户的输入与模式名相匹配则执行对应的命令。

#!/bin/bash
case $1 in
    start)
        echo "start."   
        ;;
    stop)
        echo "stop."
        ;;
    restart)
        echo "restart."
        ;;
    *)
        echo "Usage: $0 {start|stop|restart}"
esac

3. 实例

3.1 需求

编写一个脚本,来对当前系统 PATH 目录下的二进制执行文件进行,或者对指定全盘进行 md5 扫描,后期可以配合定时任务来监控文件是否变化,用于文件权限及内容的管控。

3.2 思路

文件扫描可以使用 md5sum 工具执行,对目录遍历需要使用 for 循环,现在执行方式可以使用 case 来操作,其中一些操作会涉及到 sed 的命令,后期我们会针对这些命令进行单独章节详细讲解,在本需求案例中着重思考 Shell 中流程控制的作用。

3.3 实现

  • 具体实现 shell 脚本
#!/bin/bash
# Description: count file scripts
# Auth: kaliarch
# Email: kaliarch@163.com
# function: count file
# Date: 2020-03-28 14:00
# Version: 1.0

# 定义文件扫描目录

SCAN_DIR=`echo $PATH |sed 's/:/ /g'`
SCAN_CMD=`which md5sum`
SCAN_FILE_FAIL="/tmp/scan_$(date +%F%H%m)_fall.txt"
SCAN_FILE_BIN="/tmp/scan_$(date +%F%H%m)_bin.txt"

scan_fall_disk() {
	echo "正在全盘扫描,请稍等!文件路径:$SCAN_FILE_FALL"
	find / -type f ! -path "/proc/*" -exec $SCAN_CMD {} ;>> $SCAN_FILE_FAIL 2>/dev/null
	echo "扫描完成,可利用以下命令后期对文件进行校验"
	echo "$SCAN_CMD -c $SCAN_FILE_FAIL |grep -v 'OK$'"
}

scan_bin() {
	echo "正在扫描$PATH可执行文件,请稍等,文件路径:$SCAN_FILE_BIN"
	for file in $SCAN_DIR
	do
		find $file -type f -exec $SCAN_CMD {} ;>> $SCAN_FILE_BIN 2>/dev/null
	done
	echo "扫描完成,可利用以下命令后期对文件进行校验"
	echo "$SCAN_CMD -c $SCAN_FILE_BIN |grep -v 'OK$'"
}

clear
echo "##########################################"
echo "#                                        #"
echo "#        利用md5sum对文件进行校验        #"
echo "#                                        #"
echo "##########################################"
echo "1: 全盘扫描"
echo "2: bin path扫描"
echo "3: EXIT"
# 选择扫描方式
read -p "Please input your choice:" method
case $method in 
1)
	scan_fall_disk;;
2)
	scan_bin;;
3)
        echo "you choce channel!" && exit 1;;
*)
	echo "input Error! Place input{1|2|3}" && exit 0;;
esac
  • 测试
[root@xuel-terraform-cvm-0 ~]# bash file_scan.sh
##########################################
#                                        #
#        利用md5sum对文件进行校验        #
#                                        #
##########################################
1: 全盘扫描
2: bin path扫描
3: EXIT
Please input your choice:2
正在扫描/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin可执行文件,请稍等,文件路径:/tmp/scan_2020-03-271703_bin.txt
扫描完成,可利用以下命令后期对文件进行校验
/usr/bin/md5sum -c /tmp/scan_2020-03-271703_bin.txt |grep -v 'OK$'
[root@xuel-terraform-cvm-0 ~]# /usr/bin/md5sum -c /tmp/scan_2020-03-271703_bin.txt |grep -v 'OK$'
/sbin/mii-tool: 确定
/sbin/wipefs: 确定
/sbin/blkdiscard: 确定
/sbin/rtmon: 确定
/sbin/shutdown: 确定
/sbin/aureport: 确定
/sbin/plymouthd: 确定
/sbin/udevd: 确定
/sbin/lvmetad: 确定
/sbin/e2undo: 确定
...

当我们输入数字 2 的时候即对 bin 路径执行扫描,扫描完成后会生成对应的扫描文件,可以执行 /usr/bin/md5sum -c /tmp/scan_2020-03-271703_bin.txt |grep -v 'OK$' 来进行后期文件校验。

[root@xuel-terraform-cvm-0 ~]# bash file_scan.sh
##########################################
#                                        #
#        利用md5sum对文件进行校验           #
#                                        #
##########################################
1: 全盘扫描
2: bin path扫描
3: EXIT
Please input your choice:3
you choce channel!

当输入 3 程序退出,在此就是使用 case 来完成。

[root@xuel-terraform-cvm-0 ~]# bash file_scan.sh
##########################################
#                                        #
#        利用md5sum对文件进行校验        	 #
#                                        #
##########################################
1: 全盘扫描
2: bin path扫描
3: EXIT
Please input your choice:1
正在全盘扫描,请稍等!文件路径:
扫描完成,可利用以下命令后期对文件进行校验
/usr/bin/md5sum -c /tmp/scan_2020-03-271703_fall.txt |grep -v 'OK$'
[root@xuel-terraform-cvm-0 ~]# /usr/bin/md5sum -c /tmp/scan_2020-03-271703_fall.txt |grep -v 'OK$' |more
/sys/devices/platform/uevent: 确定
/sys/devices/platform/power/control: 确定
/sys/devices/platform/power/wakeup: 确定
/sys/devices/platform/pcspkr/uevent: 确定
/sys/devices/platform/pcspkr/modalias: 确定
/sys/devices/platform/pcspkr/power/control: 确定
/sys/devices/platform/pcspkr/power/wakeup: 确定
/sys/devices/platform/platform-framebuffer.0/uevent: 确定
/sys/devices/platform/platform-framebuffer.0/modalias: 确定
/sys/devices/platform/platform-framebuffer.0/power/control: 确定
/sys/devices/platform/platform-framebuffer.0/power/wakeup: 确定
/sys/devices/platform/serial8250/uevent: 确定
/sys/devices/platform/serial8250/modalias: 确定
/sys/devices/platform/serial8250/power/control: 确定
/sys/devices/platform/serial8250/power/wakeup: 确定
/sys/devices/platform/serial8250/tty/ttyS1/uevent: 确定
/sys/devices/platform/serial8250/tty/ttyS1/dev: 确定
/sys/devices/platform/serial8250/tty/ttyS1/power/cont

4. 注意事项

  • 对于条件语句中的多分支 if 语句,结尾肯定是有一个 else 来完成,需要注意此点;
  • 在编写流程控制时候,可以根据自己的习惯对于 command 中的操作进行缩进,可以为几个空格,也可以为一个 tab 键,建议使用一个制表符;
  • 流程控制语句可以相互嵌套,例如 for 循环中可以有 if 条件判断,if 添加判断中也可以添加 for 循环等,根据需求具体灵活运用;
  • case 语句与 if 多分支语句一样,不同点在与 case 语句只能判断一种条件关系,而 if 多分支可以对多种条件进行判断。

5. 小结

流程控制语句是我们 Shell 编写的经络,利用它来控制程序的走向,需要我们能熟练掌握最常用的流程控制语句,并知道其对应的适应场景,灵活配合使用。