10.Shell 脚本 while 与 until 语句全解析:语法结构、条件判断及批量任务循环实践
Shell脚本中while和until循环是重要的流程控制结构。while循环在条件满足时执行循环体,常用于守护进程;until循环则在条件不满足时执行。两者语法结构相似,但逻辑相反。文章通过计算阶乘、猴子吃桃等示例展示了两种循环的应用,并提供了企业级实战案例,如监控系统负载、并发控制等。特别介绍了后台运行脚本的方法(nohup、screen等)和并发任务控制技巧,包括使用wait指令等待后台任务
10. while 循环和 until 循环的应用实践
循环语句命令常用于重复执行一条指令或一组指令,直到条件不再满足时停止,Shell脚本语言的循环语句常见的有while、until、for及select循环语句。
while 循环语句主要用来重复执行一组命令或语句,在企业实际应用中,常用于守护进程或持续运行的程序,除此以外,大多数循环都会用后文即将讲解的for循环语句。
10.1 当型和直到型循环语法
10.1.1 while 循环语句
while 循环语句的基本语法为:
while <条件表达式>
do
指令...
done
提示:注意代码缩进。
while 循环语句会对紧跟在while命令后的条件表达式进行判断:
- 如果该条件表达式成立,则执行while 循环体里的命令或语句(即语法中do和done之间的指令),每一次执行到done时就会重新判断while条件表达式是否成立,直到条件表达式不成立时才会跳出while 循环体。
- 如果一开始条件表达式就不成立,那么程序就不会进入循环体(即语法中do和done之间的部分)中执行命令了。
为了便于大家记忆,下面是某女生写的while条件语句的中文形象描述:
# 如果男朋友努力工作,则继续相处
while 男朋友努力工作
do
继续相处
done
while 循环执行流程对应的逻辑图如下:

10.1.2 until 循环语句
until 循环语句的语法为:
until <条件表达式>
do
指令...
done
until 循环语句的用法与while 循环语句的用法类似,区别是until会在条件表达式不成立时,进入循环执行指令;条件表达式成立时,终止循环。
为了便于大家记忆,下面是某女生写的until条件语句的中文形象描述:
# 如果男朋友不努力工作,就不继续相处
until 男朋友不努力工作
do
继续相处
done
until 循环执行流程对应的逻辑图如下:

10.2 当型和直到型循环的基本范例
示例1: 竖向打印54321
while格式:
#!/bin/bash
i=5
while ((i>0))
do
echo $i
((i--))
done
until格式:
#!/bin/bash
i=5
until ((i==0))
do
echo $i
((i--))
done
示例2: 计算1+2+3+…+99+100的和
#!/bin/bash
i=1
sum=0
while ((i<=100))
do
((sum+=i))
# let sum=sum+i
((i++))
# let i++
done
echo "1+2+3+...+99+100=$sum"
示例3: 计算5的阶乘
#!/bin/bash
i=1
sum=1
while ((i<=5))
do
((sum*=i))
((i++))
done
echo "5的阶乘为:$sum"
示例4: 猴子吃桃。
- 猴子第一天摘下若干个桃子,当即吃了一半,还不过瘾,又多吃了一个。
- 第二天早上又将第一天剩下的桃子吃掉一半,又多吃了一个。
- 以后每天早上都吃了前一天剩下的一半零一个。
- 到第 10 天早上想再吃时,发现只剩下一个桃子了。
问:一共有多少个桃子?
解题方法1:while 循环9次
#!/bin/bash
# 当天桃子数量,第一天为1
today=1
# 前一天桃子数量
lastday=0
# 只需要迭代9次
i=1
while ((i<=9))
do
# 计算上一天桃子数量
lastday=$[(today+1)*2]
# 把上一天的数量当作今天的数量
today=${lastday}
((i++))
done
echo "猴子第一天摘的桃子数量是:$today。"
解题方法2: 函数递归调用
#!/bin/bash
function sum (){
if [[ $1 = 1 ]];then
echo $1
else
echo $[ ($(sum $[$1 -1]) + 1)*2 ]
fi
}
echo "猴子第一天摘的桃子数量是:$(sum 10)。"
示例5: 系统随机产生一个50以内数字,猜出该数字,并记录猜测次数。
#!/bin/bash
# 生成随机数字,并保存到文件/tmp/number
random_num=$[ RANDOM%50+1 ]
echo "${random_num}" >> /tmp/number
# 记录猜测次数
i=0
while true
do
read -p "猜一猜系统产生的50以内随机数是:" num
if ((num>=1 && num<=50));then
((i++))
if [ $num -eq ${random_num} ];then
echo "恭喜你,第$i次猜对了!"
# 清理临时文件
rm -f /tmp/number
exit
else
echo -n "第$i次猜测,加油。"
[ $num -gt ${random_num} ] && echo "太大了,往小猜。" || echo "太小了,往大猜。"
fi
else
echo "请输入一个介于1-50之间的数字。"
fi
done
在实际工作中,一般会通过客户端SSH连接服务器,因此可能就会有在脚本或命令执行期间不能中断的需求,若中断,则会前功尽弃,更要命的是会破坏系统数据。
下面是防止脚本执行中断的几个可行方法:
-
使用
sh /server/scripts/while_01.sh &命令,即使用&在后台运行脚本。 -
使用
nohup /server/scripts/uptime.sh &命令,即使用nohup加&在后台运行脚本。 -
利用
screen保持会话,然后再执行命令或脚本,即使用screen保持当前会话状态。
10.3 让 Shell 脚本在后台运行的知识
脚本运行的相关用法和说明:
sh whilel.sh &,把脚本whilel.sh放到后台执行(在后台运行脚本时常用的方法)。ctl+c,停止执行当前脚本或任务。ctl+z,暂停执行当前脚本或任务。bg,把当前脚本或任务放到后台执行。bg可以理解为background。fg,把当前脚本或任务放到前台执行,如果有多个任务,可以使用fg加任务编号调出对应的脚本任务,如fg 2,调出第二个脚本任务。fg可以理解为frontground。jobs,查看当前执行的脚本或任务。kill,关闭执行的脚本任务,即以“kill %任务编号”的形式关闭脚本。任务编号,可以通过jobs命令获得。
示例1: 让所有 CPU 满负荷工作
#!/bin/bash
cpu_count=$(lscpu|grep '^CPU(s)'|awk '{print $2}')
i=1
while ((i<=${cpu_count}))
do
{
while :
do
((1+1))
done
} &
((i++))
done
注意:
- { comand1;command2;command3; …; } &,将多个命令放到后台运行
- {} 内部两侧有空格。
- 最后一个命令后有分号。
并发控制
示例1: 消耗完所有CPU
[laoma@shell ~]$ vim cpu_load
#!/bin/bash
while true
do
((1+1))
done
[laoma@shell ~]$ vim multi_cpu_load
#!/bin/bash
cpu_count=$(lscpu | awk '/^CPU\(s\):/ { print $2}')
i=1
while [ $i -le 2 ]
do
bash /home/laoma/cpu_load &
((i++))
done
示例2: 控制并发数量不能超过CPU数量
[laoma@shell ~]$ vim cpu_load
#!/bin/bash
while true
do
((1+1))
done &
# 只允许改进程运行10s
pid=$!
sleep 10 && kill -9 $pid
[laoma@shell ~]$ vim multi_cpu_load
#!/bin/bash
cpu_count=$(lscpu | awk '/^CPU\(s\):/ { print $2}')
while true
do
bash /home/laoma/cpu_load &
# 控制并发数量不能大于cpu数量
while true
do
jobs=$(jobs -l |wc -l)
if [ $jobs -ge $cpu_count ];then
# 如果并发数大于cpu数量,则sleep 3s后继续检测
sleep 3
else
# 如果并发数小于cpu数量,则退出当前while 循环的并发检测
break
fi
done
done
wait 指令
等后台任务运行完成。
#!/bin/bash
> /tmp/sleep
i=1
while [ $i -le 10 ]
do
( sleep $i && echo sleep $i >> /tmp/sleep )&
((i++))
done
# 等待前面后台任务运行完成后再运行wait后指令
wait
cat /tmp/sleep
10.4 企业生产实战:while 循环语句实践
示例1: 每隔2秒输出一次系统负载(负载是系统性能的基础重要指标)情况。
[laoma@shell ~]$ cat while1.sh
#!/bin/bash
while true
do
uptime
sleep 2
done
[laoma@shell ~]$ bash while1.sh
17:45:08 up 8:39, 2 users, load average: 0.00, 0.01, 0.05
17:45:10 up 8:39, 2 users, load average: 0.00, 0.01, 0.05
17:45:12 up 8:39, 2 users, load average: 0.00, 0.01, 0.05
17:45:14 up 8:39, 2 users, load average: 0.00, 0.01, 0.05
^C
# 按 ctrl+c 停止运行
[laoma@shell ~]$ cat while2.sh
#!/bin/bash
while true
do
uptime >> /tmp/loadaverage.log
sleep 2
done
# 放到后台运行
[laoma@shell ~]$ bash while2.sh &
[laoma@shell ~]$ tail -f while2.sh
示例2: 后台检测sshd服务,如果未运行,则重启sshd服务。
while格式:
#!/bin/bash
while true
do
systemctl is-active sshd.service &>/dev/null
if [ $? -ne 0 ];then
systemctl restart sshd.service &>/dev/null
fi
sleep 5
done
until格式:
#!/bin/bash
until false
do
systemctl is-active sshd.service &>/dev/null
if [ $? -ne 0 ];then
systemctl restart sshd.service &>/dev/null
fi
sleep 5
done
示例3: 使用while守护进程的方式监控网站,每隔3秒确定一次网站是否正常。
[laoma@shell scripts]$ cat check_url.sh
#!/bin/bash
if [ $# -ne 1 ];then
echo "Usage: $0 url"
exit 1
fi
url="$1"
while true
do
if curl -o /dev/null -s --connect-timeout 5 $url;then
echo $url is ok.
else
echo $url is error.
fi
sleep 3
done
执行结果
[laoma@shell scripts]$ bash check_url.sh
Usage: check_url.sh url
[laoma@shell scripts]$ bash check_url.sh www.baidu.com
www.baidu.com is ok.
www.baidu.com is ok.
^C
[laoma@shell scripts]$ bash check_url.sh www.baidu.co
www.baidu.co is error.
www.baidu.co is error.
^C
示例4: 手机发信息平台:
- 每发一次短信(输出当前余额)花费1角5分钱
- 当余额低于1角5分钱时就不能再发短信了,提示“余额不足,请充值”
- 用户充值后可以继续发短信
#!/bin/bash
# 默认金额
money=0.5
# 保存消息的文件
msg_file=/tmp/message
# 清空消息文件
> $msg_file
# 手机操作菜单
function print_menu () {
cat << EOF
1. 查询余额
2. 发送消息
3. 充值
4. 退出
EOF
}
# 检查数字函数
function check_digit () {
expr $1 + 1 &> /dev/null && return 0 || return 1
}
# 显示余额函数
function check_money_all () {
echo "余额为:$money。"
}
# 检查余额是否充足
function check_money () {
new_money=$(echo "$money*100"|bc|cut -d . -f1)
if [ ${new_money} -lt 15 ];then
echo "余额不足,请充值。"
# 余额不足,返回值为1
return 1
else
# 余额充足,返回值为0
return 0
fi
}
# 充值函数
function chongzhi () {
read -p "充值金额(单位:元):" chongzhi_money
while true
do
check_digit $chongzhi_money
if [ $? -eq 0 ] && [ ${chongzhi_money} -ge 1 ];then
money=$( echo "($money+${chongzhi_money})"|bc)
echo "当前余额为:$money"
return 0
else
read -p "重新输入充值金额:" chongzhi_money
fi
done
}
# 发送消息函数
function send_msg () {
# 检查余额是否充足
check_money
# 返回值与0比较,判定余额是否充足
if [ $? -eq 0 ];then
read -p "msg: " message
echo "$message" >> ${msg_file}
# bc计算器计算的结果,如果值小于1,则前面的0省略
new_money=$(echo "scale=2;($money*100-15)" | bc |cut -d. -f1 )
if [ ${new_money} -ge 100 ];then
money=$(echo "scale=2;${new_money}/100" | bc )
else
money=0$(echo "scale=2;${new_money}/100" | bc )
fi
echo "当前余额为:$money"
fi
}
# 主程序
while true
do
print_menu
echo
read -p "请输入你的选择:" choice
clear
case $choice in
1)
check_money_all
;;
2)
send_msg
;;
3)
chongzhi
;;
4)
exit
;;
*)
echo "只能从1、2、3、4中选择。"
;;
esac
echo
done
10.5 while 循环按行读文件的方式总结
示例:读取/etc/hosts内容
[laoma@shell scripts]$ cat /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
10.1.8.88 laoma-shell
- 方式1:采用exec读取文件,然后进入while 循环处理。
#!/bin/bash
exec < /etc/hosts
while read line
do
echo $line
done
- 方式2:使用cat读取文件内容,然后通过管道进入while 循环处理。
#!/bin/bash
cat /etc/hosts | while read line
do
echo $line
done
- 方式3:在while 循环结尾done处通过输入重定向指定读取的文件。
#!/bin/bash
while read line
do
echo $line
done < /etc/hosts
- 方式4:定义shell分隔符为换行符
#!/bin/bash
IFS=$'\n'
for line in $(cat /etc/hosts)
do
echo $line
done
10.6 企业级生产高级实战案例
写一个Shell脚本解决类DDoS攻击的生产案例。
-
示例1:请根据Web日志,监控某个IP短时内PV达到一个阈值,即调用防火墙命令封掉对应的IP。防火墙命令:“iptables -I INPUT -s IP地址 -j DROP”
分析题目
分析Web日志,可以每分钟或每小时分析一次,这里给出按小时处理的方法。可以将日志按小时进行分割,分成不同的文件,根据分析结果把PV数高的单IP封掉。
例如,每小时单IP的PV数超过500,则即刻封掉,这里简单地把日志的每一行近似看作一个PV,实际工作中需要计算实际页面的数量,而不是请求页面元素的数量,另外,很多公司都是以NAT形式上网的,因此每小时单IP的PV数超过多少就会被封掉,还要根据具体的情况具体分析,本题仅给出一个实现的案例,读者使用时需要考虑自身网站的业务去使用。
参考答案
#!/bin/bash logfile=$1 while true do awk '{print $1}' $logfile | grep -v "^$" | sort |uniq -c > /tmp/tmp.log exec < /tmp/tmp.log while read line do # 获取IP ip=$(echo $line |awk '{print $2}') # 获取对应数量 count=$(echo $line |awk '{print $1}') # 如果数量超过500,而且当前防火墙列表中没有封该IP,则调用iptables封掉该IP if [ $count -gt 500 ] && [ $(iptables -L -n|grep "$ip" |wc -l) -lt 1 ];then iptables -I INPUT -s $ip -j DROP # 记录IP地址 echo "$ip is dropped" >> /tmp/droplist_$(date +%F).log fi done # 1小时统计一次 sleep 3600 done -
示例2:请根据系统网络连接数,监控某个IP的并发连接数,如果连接数达到100,即调用防火墙命令封掉对应的IP。防火墙命令:“iptables -I INPUT -s IP地址 -j DROP”
分析题目
首先要分析单IP占网络连接数的情况,即取当前网络连接状态为ESTABLISHED的行数,然后分析对应客户端列不同IP连接数量的排序,对排序比较高的IP进行封堵。
参考答案
#!/bin/bash while true do ss -t | grep ESTAB|awk '{print $4}' | cut -d: -f1 | sort |uniq -c > /tmp/tmp.log exec < /tmp/tmp.log while read line do # 获取IP ip=$(echo $line |awk '{print $2}') # 获取对应数量 count=$(echo $line |awk '{print $1}') # 如果数量超过500,而且当前防火墙列表中没有封该IP,则调用iptables封掉该IP if [ $count -gt 500 ] && [ $(iptables -L -n|grep "$ip" |wc -l) -lt 1 ];then iptables -I INPUT -s $ip -j DROP # 记录IP地址 echo "$ip is dropped" >> /tmp/droplist_$(date +%F).log fi done # 10秒统计一次 sleep 10 done
10.7 本章小结
- while 循环结构及相关语句综合实践小结
- while 循环的特长是执行守护进程,以及实现我们希望循环持续执行不退出的应用,适合用于频率小于1分钟的循环处理,其他的while 循环几乎都可以被后面即将要讲到的for循环及定时任务crond功能所替代。
- case语句可以用if语句来替换,而在系统启动脚本时传入少量固定规则字符串的情况下,多用case语句,其他普通判断多用if语句。
- 一句话场景下,if语句、for语句最常用,其次是while(守护进程)、case(服务启动脚本)。
- Shell脚本中各个语句的使用场景
- 条件表达式,用于简短的条件判断及输出(文件是否存在,字符串是否为空等)
- if取值判断,多用于不同值数量较少的情况。
- for最常用于正常的循环处理中while多用于守护进程、无限循环(要加sleep和usleep控制频率)场景。
- case多用于服务启动脚本中,打印菜单可用select语句,不过很少见,一般用cat的here文档方法来替代。
- 函数的作用主要是使编码逻辑清晰,减少重复语句开发。
更多推荐


所有评论(0)