Shell 循环实战:while 与 until 的趣味编程之旅

Shell 循环实战:while 与 until 的趣味编程之旅

1. 循环基础:当型和直到型

1.1 while 循环:条件成立就继续

bash

复制代码
# 基本语法
while <条件表达式>
do
  指令...
done

形象记忆(某女生版):

bash

复制代码
# 如果男朋友努力工作,就继续相处
while 男朋友努力工作
do
  继续相处
done

执行流程:先检查条件,成立就执行循环体,直到条件不成立为止。

1.2 until 循环:条件不成立才继续

bash

复制代码
# 基本语法
until <条件表达式>
do
  指令...
done

形象记忆

bash

复制代码
# 直到男朋友不努力工作,就不继续相处
until 男朋友不努力工作
do
  继续相处 
done

执行流程:先检查条件,不成立就执行循环体,直到条件成立为止。

2. 基础示例:从简单到有趣

示例1:倒数打印 5-1

while 版本

bash

复制代码
#!/bin/bash
i=5
while ((i>0))
do
  echo $i
  ((i--))  # i减1
done

until 版本

bash

复制代码
#!/bin/bash
i=5
until ((i==0))  # 直到i等于0停止
do
  echo $i
  ((i--))
done

示例2:计算1到100的和

bash

复制代码
#!/bin/bash
i=1
sum=0
while ((i<=100))
do
  ((sum+=i))  # 累加
  ((i++))     # i加1
done
echo "1+2+3+...+99+100=$sum"

示例3:计算5的阶乘

bash

复制代码
#!/bin/bash
i=1
sum=1
while ((i<=5))
do
  ((sum*=i))  # 连乘
  ((i++))
done
echo "5的阶乘为:$sum"

示例4:猴子吃桃问题

猴子每天吃一半加一个桃子,第10天只剩1个,问最初有多少桃子?

解法1:while 循环

bash

复制代码
#!/bin/bash
today=1     # 第10天的桃子数
i=1         # 循环计数器

while ((i<=9))  # 循环9次(第9天到第1天)
do
  # 计算上一天的桃子数:今天桃子数加1再乘以2
  lastday=$[(today+1)*2]
  today=${lastday}  # 更新为前一天的数量
  ((i++))
done
echo "猴子第一天摘的桃子数量是:$today。"

解法2:函数递归(高级技巧)

bash

复制代码
#!/bin/bash
function sum (){
  if [[ $1 = 1 ]];then
    echo $1
  else
    # 递归调用:前一天的桃子数是(后一天+1)*2
    echo $[ ($(sum $[$1 -1]) + 1)*2 ]
  fi
}
echo "猴子第一天摘的桃子数量是:$(sum 10)。"

示例5:猜数字游戏

bash

复制代码
#!/bin/bash
# 生成1-50的随机数
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

3. 后台运行与并发控制

3.1 后台运行脚本的方法

  1. 直接后台运行sh script.sh &
  2. 不挂断运行nohup sh script.sh &
  3. 会话保持 :使用 screentmux 工具

3.2 控制后台任务的命令

  • &:后台运行
  • ctrl+c:停止当前任务
  • ctrl+z:暂停当前任务
  • bg:将任务放到后台运行
  • fg:将任务拉到前台运行
  • jobs:查看后台任务列表
  • kill %n:结束编号为n的后台任务

示例:让所有CPU满负荷工作

bash

复制代码
#!/bin/bash
# 获取CPU核心数
cpu_count=$(lscpu|grep '^CPU(s)'|awk '{print $2}')
i=1

while ((i<=${cpu_count}))
do
  {
    # 无限循环计算,消耗CPU
    while :
    do
      ((1+1))
    done
  } &  # 放到后台运行
  ((i++))
done

示例:控制并发数量(不超过CPU数)

bash

复制代码
#!/bin/bash
# 获取CPU核心数
cpu_count=$(lscpu | awk '/^CPU\(s\):/ { print $2}')

while true
do
  # 启动一个CPU负载任务
  bash cpu_load_script.sh &
  
  # 控制并发数量
  while true
  do
    jobs=$(jobs -l |wc -l)  # 当前后台任务数
    
    if [ $jobs -ge $cpu_count ];then
      sleep 3  # 如果任务数已达CPU数,等待3秒
    else
      break    # 还有空余,退出等待循环
    fi
  done
done

使用wait等待后台任务完成

bash

复制代码
#!/bin/bash
> /tmp/sleep  # 清空临时文件
i=1

while [ $i -le 10 ]
do
  # 每个任务睡眠不同时间然后写入文件
  ( sleep $i && echo "sleep $i" >> /tmp/sleep ) &
  ((i++))
done

wait  # 等待所有后台任务完成
cat /tmp/sleep  # 显示结果

4. 实战应用:系统监控与服务管理

示例1:每隔2秒输出系统负载

bash

复制代码
#!/bin/bash
while true
do
  uptime  # 显示系统负载
  sleep 2
done

示例2:SSHD服务监控与自动重启

while版本

bash

复制代码
#!/bin/bash
while true
do 
  # 检查sshd是否活跃
  systemctl is-active sshd.service &>/dev/null
  
  if [ $? -ne 0 ];then 
    echo "SSHD服务未运行,正在重启..."
    systemctl restart sshd.service &>/dev/null
  fi
  
  sleep 5  # 每5秒检查一次
done

until版本

bash

复制代码
#!/bin/bash
until false  # 一直循环直到false(永远不会发生)
do 
  systemctl is-active sshd.service &>/dev/null
  
  if [ $? -ne 0 ];then 
    systemctl restart sshd.service &>/dev/null
  fi
  
  sleep 5
done

示例3:网站可用性监控

bash

复制代码
#!/bin/bash

if [ $# -ne 1 ];then
  echo "Usage: $0 url"
  exit 1
fi

url="$1"

while true
do
  # 检查网站是否可访问(5秒超时)
  if curl -o /dev/null -s --connect-timeout 5 $url;then
    echo "$(date): $url is ok."
  else
    echo "$(date): $url is error."
  fi
  
  sleep 3  # 每3秒检查一次
done

5. 高级实战:手机短信平台模拟

bash

复制代码
#!/bin/bash

# 初始化变量
money=0.5                    # 初始余额(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 元。"
}

# 检查余额是否足够发送一条消息(0.15元)
function check_money () {
  # 将元转换为分进行比较
  new_money=$(echo "$money*100"|bc|cut -d . -f1)
  
  if [ ${new_money} -lt 15 ];then
    echo "余额不足,请充值。"
    return 1  # 余额不足
  else
    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 "请输入有效金额(至少1元):" chongzhi_money 
    fi
  done
}

# 发送消息功能
function send_msg () {
  # 检查余额是否充足
  check_money
  
  if [ $? -eq 0 ];then  # 余额充足
    read -p "请输入消息内容:" message
    echo "$message" >> ${msg_file}  # 保存消息

    # 扣除费用(0.15元)
    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)
      echo "谢谢使用,再见!"
      exit
      ;;
    *)
      echo "无效选择,请从1、2、3、4中选择。" 
      ;;
  esac
  
  echo
done

6. 文件读取的四种方式

以读取 /etc/hosts 文件为例:

方式1:使用exec重定向

bash

复制代码
#!/bin/bash
exec < /etc/hosts  # 将文件内容重定向到标准输入
while read line
do
  echo $line
done

方式2:使用管道

bash

复制代码
#!/bin/bash
cat /etc/hosts | while read line
do
  echo $line
done

方式3:在循环结尾重定向

bash

复制代码
#!/bin/bash
while read line
do
  echo $line
done < /etc/hosts  # 在done处重定向

方式4:设置分隔符并使用for循环

bash

复制代码
#!/bin/bash
IFS=$'\n'  # 设置换行符为字段分隔符
for line in $(cat /etc/hosts)
do
  echo $line
done

7. 企业级实战:网络安全防护

示例1:防止Web服务的DDoS攻击

bash

复制代码
#!/bin/bash
logfile=$1  # 日志文件作为参数

while true
do
  # 分析日志:提取IP并统计访问次数
  awk '{print $1}' $logfile | grep -v "^$" | sort | uniq -c > /tmp/tmp.log
  
  # 处理统计结果
  exec < /tmp/tmp.log
  while read line
  do
    ip=$(echo $line | awk '{print $2}')      # 提取IP
    count=$(echo $line | awk '{print $1}')   # 提取访问次数
    
    # 如果单IP访问超过500次且未被封禁
    if [ $count -gt 500 ] && [ $(iptables -L -n | grep "$ip" | wc -l) -lt 1 ];then
      iptables -I INPUT -s $ip -j DROP  # 封禁IP
      echo "$(date): $ip 已被封禁(访问次数: $count)" >> /tmp/droplist_$(date +%F).log
    fi
  done
  
  sleep 3600  # 每小时检查一次
done

示例2:监控网络连接数并封禁异常IP

bash

复制代码
#!/bin/bash
while true
do
  # 获取已建立连接的IP及连接数
  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=$(echo $line | awk '{print $2}')
    count=$(echo $line | awk '{print $1}')
    
    # 如果单IP连接数超过100且未被封禁
    if [ $count -gt 100 ] && [ $(iptables -L -n | grep "$ip" | wc -l) -lt 1 ];then
      iptables -I INPUT -s $ip -j DROP
      echo "$(date): $ip 已被封禁(连接数: $count)" >> /tmp/droplist_$(date +%F).log
    fi
  done
  
  sleep 10  # 每10秒检查一次
done

8. 本章小结与使用场景

8.1 各循环结构的使用场景

  • while循环:最适合守护进程和需要持续运行的场景,频率低于1分钟的监控任务
  • until循环:与while类似,但条件判断逻辑相反
  • for循环(后续章节):最适合已知循环次数的常规处理
  • case语句:适合服务启动脚本中的多分支选择
  • if语句:最常用的条件判断结构

8.2 一句话应用场景

  • 条件判断:if 和条件表达式 [ ]

  • 固定次数循环:for

  • 守护进程/无限循环:while + sleep

  • 服务脚本选择:case

  • 代码复用:函数

    如果单IP连接数超过100且未被封禁

    if [ $count -gt 100 ] && [ (iptables−L−n∣grep"(iptables -L -n | grep "(iptables−L−n∣grep"ip" | wc -l) -lt 1 ];then

    iptables -I INPUT -s ip−jDROPecho"ip -j DROP echo "ip−jDROPecho"(date): $ip 已被封禁(连接数: KaTeX parse error: Expected group after '_' at position 25: ...> /tmp/droplist_̲(date +%F).log

    fi

    done

    sleep 10 # 每10秒检查一次

    done

    8. 本章小结与使用场景

    8.1 各循环结构的使用场景

    • while循环:最适合守护进程和需要持续运行的场景,频率低于1分钟的监控任务
    • until循环:与while类似,但条件判断逻辑相反
    • for循环(后续章节):最适合已知循环次数的常规处理
    • case语句:适合服务启动脚本中的多分支选择
    • if语句:最常用的条件判断结构

    8.2 一句话应用场景

    • 条件判断:if 和条件表达式 [ ]
    • 固定次数循环:for
    • 守护进程/无限循环:while + sleep
    • 服务脚本选择:case
    • 代码复用:函数

    希望这篇轻松易懂的教程帮助你掌握了while和until循环的用法!记得多动手实践,才能真正掌握这些技巧。