在实际工作中,会遇到某项任务重复执行,或者需要重复执行的命令中,只有个别参数不同。比如,测试主机连通性的ping命令,创建批量用户等操作。
这些任务的共同点就是简单且重复,循环语句就可以帮助解决工作中这种难题,提高工作效率,节省大量代码,同时也会相应想节省内存。
本文提到的有 5 中循环,分别是 for、while、until、case、select
for 循环语句
语句结构
for 变量名 in 取值列表
do
命令序列
done
变量名:自定义一个不存在的变量名
取值列表:为变量名赋值,多个取值列表之间用空格隔开
命令序列:位于do…done之间,用来执行重复性任务,也叫循环体
for语句的执行流程,变量名从取值列表中,获取第一个变量值,并执行do…done之间的命令序列,然后获取取值列表中第二个变量值,执行命令序列,直到取值列表中的值全部获取结束。最后done结束循环。
如:检测主机连通性
#!/bin/bash
HOST="192.168.1."
for PING in `seq 254`
do
ping -c 3 -i 0.2 -w 3 ${HOST}${PING}
done
# seq 254可以将取值列表定义为1-254,根据实际情况自己改变即可。
for语句结构(C格式)
for (( 变量;条件;自增/自减运算 ))
do
命令序列
done
如:检测ip为例
PING变量的起始值为1,每循环一次加1,直到大于等于10结束
#!/bin/bash
HOST="192.168.1."
for (( PING=1;PING<10;PING++ ))
do
ping -c 3 -i 0.2 -w 3 ${HOST}${PING}
done
C格式的for循环也可以指定多个变量
for (( 变量1,变量2;条件;自增/自减运算 ))
do
命令序列
done
如:
for (( a=0,b=9;a<10,b>0;a++,b-- ))
do
echo $a $b
done
# 输出为
0 9
1 8
2 7
3 6
4 5
5 4
6 3
7 2
8 1
无限循环
应该有很多人知道,while true
可以做到无限循环,但不知道 for 也可以做到,知道就可以,不建议使用,格式如下
for ((;;))
do
命令序列
done
循环控制
以下循环控制的命令,while
循环同样适用
sleep
控制循环的节奏:当执行一个无限循环会消耗服务器硬件资源的脚本时,可以通过控制循环的节奏,缓解资源消耗
只需要在循环中加入sleep的命令即可
#!/bin/bash
IP="172.16.182.63"
UP_LOG="/var/log/ping_up.log"
DOWN_LOG="/var/log/ping_down.log"
for ((;;));do
DATE=`date "+%F %H:%M:%S"`
ping -c1 $IP > /dev/null
if [ $? -eq 0 ];then
echo -e "$DATE HOST $IP is \033[32mUp\033[0m" >> $UP_LOG
else
echo -e "$DATE HOST $IP is \033[31mDown\033[0m" >> $DOWN_LOG
fi
sleep 60
done
每隔60秒检测一遍主机是否存活
continue
跳过某次循环
跳过某次循环,继续执行下一次循环,表示循环体内 continue
下面的代码不执行。
很多人可能会对跳过循环有误解,我刚学习的时候,不知道是老师没讲明白,还是我没听明白,反正感觉 continue
和 break
是一个功能。在整个循环的过程中,遇到某个特殊的变量值,不想让它使用该值执行命令,则使用continue
如:还是以ping主机为例
# 脚本
NET="172.16.182."
IP=`seq 10`
for i in $IP;do
if [ $i -eq 4 ];then
continue
fi
IP=${NET}$i
DATE=`date "+%F %H:%M:%S"`
ping -c1 $IP > /dev/null
if [ $? -eq 0 ];then
echo -e "$DATE HOST:$IP is \033[32mUp\033[0m"
else
echo -e "$DATE HOST:$IP is \033[31mDown\033[0m"
fi
done
# 执行结果
# 可以看到结果中是没有172.16.182.4的,是因为在循环到值为4时被跳过了
2021-04-15 10:27:32 HOST:172.16.182.1 is Down
2021-04-15 10:27:35 HOST:172.16.182.2 is Down
2021-04-15 10:27:38 HOST:172.16.182.3 is Down
2021-04-15 10:27:41 HOST:172.16.182.5 is Down
2021-04-15 10:27:44 HOST:172.16.182.6 is Down
2021-04-15 10:27:47 HOST:172.16.182.7 is Down
2021-04-15 10:27:50 HOST:172.16.182.8 is Down
2021-04-15 10:27:53 HOST:172.16.182.9 is Up
2021-04-15 10:27:53 HOST:172.16.182.10 is Up
break
跳出循环
也就是终止循环,满足判断条件后,break打断,不再继续后面的所有循环
还以上面的代码为例
# 脚本,将以上代码中的continue改为break
NET="172.16.182."
IP=`seq 10`
for i in $IP;do
if [ $i -eq 4 ];then
break
fi
IP=${NET}$i
DATE=`date "+%F %H:%M:%S"`
ping -c1 $IP > /dev/null
if [ $? -eq 0 ];then
echo -e "$DATE HOST:$IP is \033[32mUp\033[0m"
else
echo -e "$DATE HOST:$IP is \033[31mDown\033[0m"
fi
done
# 结果,可以看到满足break条件后,循环结束
2021-04-15 10:44:06 HOST:172.16.182.1 is Down
2021-04-15 10:44:09 HOST:172.16.182.2 is Down
2021-04-15 10:44:12 HOST:172.16.182.3 is Down
while 循环语句
for 循环的循环条件是手动申明的,而 while 循环,循环条件是根据条件判断来决定的,for 一般不会出现死循环,而 while 循环就比较容易出现死循环的情况。
for 循环用来操作已知次数的变量体,while 循环的循环次数不受人为控制,只有根据条件判断来决定是否结束循环的场景
语句结构
while [ 条件表达式 ]
do
命令序列
done
while [ 1 -gt 2 ] 或者 (( 1 > 2 ))
do
命令序列
done
条件表达式:用来决定循环次数
命令序列:位于do…done之间,用来执行重复性任务,也叫循环体
示例
打印输出1-5
num=1
while [ $num -le 5 ]; do
echo $num
let num++
done
比较运算
字符串比较
read -p "输入quit则退出循环:" choose
while [ $choose != "quit" ]; do
echo "你输入的是:$choose"
read -p "输入quit则退出循环:" choose
done
逻辑运算
当有多个条件时,使用逻辑运算
a=1
b=2
c=3
d=4
e=5
# 必须满足所有条件
while (( $a < $b )) && [ $c -gt $d ] && [ $e -ge $e ]; do
echo "yes"
done
# 只满足其中一个条件
while (( $a < $b )) || [ $c -gt $d ] || [ $e -ge $e ]; do
echo "yes"
done
文件类型判断
和 if 语句的判断方式时一样的
# 该路径不是目录时条件成立
while [ ! -d /tmp/xxx ]; do
echo "非目录"
sleep 1
done
特殊条件
- 冒号(:):适用于死循环,条件永远成立
- true:和冒号作用一样
- false:条件永远不成立,也表示永远不循环
while true; do
echo "yes"
done
不要轻易尝试死循环,尤其是在脚本中,除非测试过程中,在死循环中加入判断来退出循环。
while 循环与 for 循环一样可以使用循环控制语句(continue/break/sleep)
实战示例
这两天我就正好使用 while 写了个循环,主要用来保存生产中用到更新版本的包的数量,因为不可能把所有构建的包都保存,但是为了版本回滚,也不可能都删除,否则重新构建也是耗时的。每次构建结束后,将包存储在一个地方,并根据想要保留的版本数量和实际包的数量作比较,条件成立则删除多余的包,反之就会退出循环
keep_version=2 # 想要保留的版本
num=$(ls /data/backup | wc -l)
# 将现有包的数量与想要保留包的数量作比较
while (( "$num" > "$keep_version" )); do
# ls -rt可以将文件根据时间排序,第一个为时间最远的文件
OldFile=$(ls -rt /data/backup | grep "*.war" | head -1)
echo "date 删除多余旧版本: $OldFile"
# 删除多出来的旧版本
rm "$OldFile"
(( num-- ))
# 直到*.war的包只剩2个,则循环停止,脚本退出
done
until 循环语句
一个可能失传的循环语句,甚至有的人学都不会学到,与 while 循环语句结构一样,但是条件与之相反,条件不成立时才记执行循环体
语句结构
until [ 条件表达式 ]
do
命令序列
done
until [ 1 -gt 2 ] 或者 (( 1 > 2 ))
do
命令序列
done
示例
当 num 变量值为20时,退出循环,until 条件为 num 小于10,意思就是 num 大于10的时候是不成立的,则会开始循环,如果 num 起始值就小于 10,将不会开始循环。
num=11
until [ $num -lt 10 ]; do
echo $num
let num++
if [ $num -gt 20 ];then
break
fi
done
说实话,一般没什么人用这个循环,了解就行
case 循环语句
在运维的学习过程中,见到过使用 命令+参数
的方式启动服务,比如:mysqld start
,其实很多关于服务的启动脚本都是用到了 case 循环,一般用于选择性执行对应参数的命令。
语句结构
case 变量 in
参数1)
命令
;;
参数2)
命令
;;
*)
# 不符合以上条件的都将执行此处的命令
命令
;;
esac
每个参数必须以右括号结束,命令结尾以双分号结束,case 中的 * 和 if 中的 else 作用类似。
举一个上面说到的示例
#!/bin/bash
case $1 in
start)
nginx && echo "nginx服务启动"
;;
stop)
nginx -s quit && echo "nginx服务停止"
;;
restart)
nginx -s reload && echo "nginx服务重启成功"
;;
*)
echo "Usage: $0 {start|stop|restart}"
;;
esac
以上就是一个nginx启动的简单脚本,根据需求活学活用即可。
参数中支持的正则有:*、?、[ ]、[.-.]、|
#!/bin/bash
case $1 in
[0-9]) # 表示0-9任意单个数字
echo "数字"
;;
[a-zA-Z]?) # [a-zA-Z]表示任意单个大小写字母,?表示任意单个字符
echo "字母+任意单个字符"
;;
feiyi|mupei) # 表示feiyi或者mupei
echo Blog
;;
*) # 不符合以上,则执行这里
echo "Usage: $0 {start|stop|restart}"
esac
我写过一个脚本是用来发布版本的,可以参考
set -eu
deploy_module=$1
echo "正在发布: ${deploy_module}"
read -p "请复制 jenkins 构建后输出 ${deploy_module} 的文件URL到此处: " -r pack_name
case "${deploy_module}" in
"nuxt-app")
cd /data/"${deploy_module}"
wget "$pack_name"
pack_name=$(ls -- *.tar.gz)
tar zxf "$pack_name" -C nuxt-app && rm -f "$pack_name"
echo "前端静态页面更新完成"
;;
"exchange-vue-server-home")
cd /data/"${deploy_module}"/"${deploy_module}"
wget "$pack_name"
pack_name=$(ls -- *.tar.gz)
tar zxf "$pack_name" -C app && rm -f "$pack_name"
npm run stop-"${deploy_module}" && sleep 5 && \
npm run start-"${deploy_module}" && echo "${deploy_module} 发版成功"
;;
"exchange-vue-server-ex"|"exchange-h5-server-ex")
cd /data/"${deploy_module}"/"${deploy_module}"
wget "$pack_name"
pack_name=$(ls -- *.tar.gz)
tar zxf "$pack_name" -C app/dist && rm -f "$pack_name"
npm run stop-"${deploy_module}" && sleep 5 && \
npm run start-"${deploy_module}" && echo "${deploy_module} 发版成功"
;;
*)
echo "使用方法:"
echo -e "\t sh $0 nuxt-app"
echo -e "\t sh $0 exchange-vue-server-ex"
echo -e "\t sh $0 exchange-h5-server-ex"
echo -e "\t sh $0 exchange-vue-server-home"
;;
esac
select 循环语句
select 是一个格式类似于 for 循环的语句,使用起来类似 case 循环的语句,它本身还是个无限循环,但又和 while 不一样,只会每次让你选择,直至 ctrl + c
打断,或者循环中加入判断执行 exit 或者 break 等循环控制命令。
语句结构
select 变量名 in 取值列表
do
命令序列
done
示例
select num in 1 2 3 4 5 quit
do
if [ $num = "quit" ];then
exit 0
fi
echo $num
done
# 输出如下
1) 1
2) 2
3) 3
4) 4
5) 5
6) quit
#?
#?
是个提示符,在这里选择对应的序号即可。如果没有判断来做退出循环,则会一直让你进行选择。
将取值列表也可以换为数组,与上方示例效果一致
NUM=(1 2 3 4 5 quit)
select num in ${NUM[@]}
do
if [ $num = "quit" ];then
exit 0
fi
echo $num
done
也可以通过定义 PS3
的值来改变提示符 #?
。
NUM=(1 2 3 4 5 quit)
PS3="请输入你想要的选项前的序号:"
select num in ${NUM[@]}
do
if [ $num = "quit" ];then
exit 0
fi
echo $num
done
# 输出如下
1) 1
2) 2
3) 3
4) 4
5) 5
6) quit
请输入你想要的选项前的序号:
搭配 while 的无限循环和 break 可以做到,每次显示选项菜单,原理就是每次都通过 break 结束 select 循环,再通过 while 循环开始再一次的 select 循环。
NUM=(1 2 3 4 5 quit)
PS3="请输入你想要的选项前的序号:"
while true;do
select num in ${NUM[@]}
do
if [ $num = "quit" ];then
exit 0
fi
echo $num
break
done
done
总结
各种循环的套用,可以自己尝试结合使用,没有正确的,只有合适的,能达到目的,怎么样都对