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连接服务器,因此可能就会有在脚本或命令执行期间不能中断的需求,若中断,则会前功尽弃,更要命的是会破坏系统数据。

下面是防止脚本执行中断的几个可行方法:

  1. 使用sh /server/scripts/while_01.sh & 命令,即使用 & 在后台运行脚本。

  2. 使用nohup /server/scripts/uptime.sh & 命令,即使用 nohup 加&在后台运行脚本。

  3. 利用 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 本章小结

  1. while 循环结构及相关语句综合实践小结
    • while 循环的特长是执行守护进程,以及实现我们希望循环持续执行不退出的应用,适合用于频率小于1分钟的循环处理,其他的while 循环几乎都可以被后面即将要讲到的for循环及定时任务crond功能所替代。
    • case语句可以用if语句来替换,而在系统启动脚本时传入少量固定规则字符串的情况下,多用case语句,其他普通判断多用if语句。
    • 一句话场景下,if语句、for语句最常用,其次是while(守护进程)、case(服务启动脚本)。
  2. Shell脚本中各个语句的使用场景
    • 条件表达式,用于简短的条件判断及输出(文件是否存在,字符串是否为空等)
    • if取值判断,多用于不同值数量较少的情况。
    • for最常用于正常的循环处理中while多用于守护进程、无限循环(要加sleep和usleep控制频率)场景。
    • case多用于服务启动脚本中,打印菜单可用select语句,不过很少见,一般用cat的here文档方法来替代。
    • 函数的作用主要是使编码逻辑清晰,减少重复语句开发。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐