shell 循环(完整指南)

shell 循环入门:让重复任务自动化起来

你有没有遇到过这样的场景?需要对 100 个文件分别执行相同的操作,比如重命名、检查权限、修改内容。如果手动一个个操作,不仅耗时,还容易出错。这时候,shell 循环就是你的救星。

想象一下,你每天早上要打开 5 个不同的终端窗口,运行 5 个不同的启动脚本。如果每次都要手动敲一遍命令,那得多累。而通过 shell 循环,你只需写一次代码,就能自动帮你完成全部操作。这就像给你的命令装上“自动播放”功能,省时又高效。

shell 循环的核心思想是:让计算机重复执行一段代码,直到满足某个条件为止。无论是处理文件、遍历列表,还是监控系统状态,shell 循环都能派上大用场。它让你从繁琐的重复劳动中解脱出来,专注于更重要的逻辑设计。

接下来,我们将从最基础的 for 循环开始,逐步深入到 while、until 以及各种实用技巧,让你真正掌握 shell 循环的精髓。

for 循环:按列表逐个执行任务

for 循环是 shell 循环中最常用的一种,它的结构非常直观:遍历一个集合中的每个元素,对每个元素执行一次指定的操作

在 shell 中,for 循环的基本语法如下:

for 变量名 in 列表
do
    命令1
    命令2
    # 可以添加更多命令
done

这里的“列表”可以是字符串、文件名、数字序列等。变量名用来临时存储当前正在处理的元素。

举个实际例子:假设你有一个名为 users.txt 的文件,里面每行是一个用户名,你想为每个用户创建一个目录。

for username in $(cat users.txt)
do
    # 检查目录是否已存在,避免重复创建
    if [ ! -d "/home/$username" ]; then
        # 创建用户主目录
        mkdir /home/$username
        # 设置权限为 755,确保安全
        chmod 755 /home/$username
        echo "已为用户 $username 创建目录"
    else
        echo "用户 $username 的目录已存在,跳过"
    fi
done

这段脚本的执行过程就像“传话游戏”:从文件中读取第一个用户名,执行创建目录的命令,然后换下一个,直到所有用户都处理完毕。

你也可以用 for 循环来生成数字序列。比如创建 10 个测试文件:

for i in {1..10}
do
    # 使用 touch 命令创建文件,文件名为 test_1.txt, test_2.txt, ...
    touch test_$i.txt
    echo "已创建文件 test_$i.txt"
done

这个 {1..10} 语法是 Bash 的扩展,代表从 1 到 10 的连续整数。它比手动写 10 个命令高效得多。

⚠️ 注意:在 for 循环中使用 $(cat file) 时,如果文件中有空格或特殊字符,可能会导致错误。推荐使用 while read 配合文件输入,更安全。

while 循环:当条件为真时持续执行

while 循环的逻辑是:只要条件成立,就一直执行循环体内的命令。它适用于那些不知道具体执行次数,但知道“什么时候该停”的场景。

语法结构如下:

while 条件表达式
do
    命令1
    命令2
done

条件表达式通常是一个测试命令,比如检查文件是否存在、变量是否等于某个值、或某个服务是否运行。

举个例子:你想监控某个日志文件,当文件大小超过 100KB 时,就发一条提醒。

log_file="/var/log/app.log"
threshold=102400  # 100KB

while [ $(du -b "$log_file" | cut -f1) -lt "$threshold" ]
do
    # 每 5 秒检查一次
    sleep 5
    echo "当前日志大小:$(du -h "$log_file" | cut -f1)"
done

echo "警告:日志文件大小已超过 $threshold 字节!"

这个循环就像一个“守门员”:每隔 5 秒检查一次日志大小,只要还没超过 100KB,就继续等待。一旦超过,就跳出循环并发出警告。

另一个常见用途是实现用户输入验证:

choice=""

while [[ "$choice" != "yes" && "$choice" != "no" ]]
do
    echo "请输入 yes 或 no:"
    read choice
    # 将输入转为小写,避免大小写问题
    choice=$(echo "$choice" | tr '[:upper:]' '[:lower:]')
done

echo "你选择了:$choice"

这个例子展示了 while 循环在交互式脚本中的强大作用——它能确保用户输入符合预期,避免程序因无效输入崩溃。

until 循环:直到条件为真才停止

until 循环和 while 循环正好相反。它的逻辑是:只要条件为假,就继续执行;当条件为真时,循环结束

语法如下:

until 条件表达式
do
    命令1
    命令2
done

它适合用于“等待某个事件发生”的场景。比如,等待某个服务启动、某个文件出现、或某个端口被占用。

举个例子:你想等待一个网络服务启动,比如 Nginx。你可以写一个脚本,持续检查服务是否已运行,直到它成功启动。

service_name="nginx"

until systemctl is-active --quiet "$service_name"
do
    # 每隔 2 秒检查一次
    sleep 2
    echo "正在等待 $service_name 启动..."
done

echo "$service_name 已成功启动!"

这里的 systemctl is-active --quiet nginx 会返回 0(成功)表示服务正在运行,非 0 表示未运行。until 循环会持续执行,直到这个命令返回 0,也就是服务启动成功。

另一个实际应用是等待某个文件被创建:

target_file="/tmp/data.json"

until [ -f "$target_file" ]
do
    sleep 1
    echo "等待 $target_file 出现..."
done

echo "文件 $target_file 已创建,继续处理..."

这在自动化部署或数据同步流程中非常有用。你不需要知道文件什么时候生成,只需告诉脚本“等它出现”,它就会自动等待。

循环控制:break 与 continue 的妙用

在实际使用中,我们常常需要在循环过程中“提前退出”或“跳过当前迭代”。这时,breakcontinue 就派上用场了。

  • break:立即退出整个循环。
  • continue:跳过当前这次循环,直接进入下一次。

举个例子:遍历一个用户列表,但只想处理前 3 个用户。

users=("alice" "bob" "charlie" "david" "eve")

count=0

for user in "${users[@]}"
do
    # 如果已经处理了 3 个,就退出循环
    if [ $count -ge 3 ]; then
        break
    fi

    # 处理当前用户
    echo "正在处理用户:$user"
    # 模拟处理时间
    sleep 1
    count=$((count + 1))
done

echo "处理完成,共处理了 $count 个用户"

在这个例子中,break 让程序在处理完前 3 个用户后立即停止,不再继续遍历剩余的用户。

再看 continue 的使用场景:你想处理所有用户,但跳过名字以 "d" 开头的用户。

for user in "${users[@]}"
do
    # 如果用户名以 d 开头,跳过本次循环
    if [[ "$user" == d* ]]; then
        continue
    fi

    echo "正在处理用户:$user"
done

continue 让脚本跳过 david,不执行后面的命令,直接进入下一轮。

这两个控制语句让你的循环逻辑更灵活,能应对复杂的业务需求。

实战案例:批量处理日志文件

现在我们综合运用前面的知识,来完成一个真实项目中的任务:批量压缩并备份过去 7 天的日志文件

假设日志文件按日期命名,格式为 app.log.2024-04-01,我们想把最近 7 天的文件打包成一个压缩包。

backup_dir="/backup/logs"
archive_name="logs_$(date +%Y%m%d).tar.gz"

mkdir -p "$backup_dir"

temp_list=$(mktemp)

for i in {1..7}
do
    # 计算前 i 天的日期
    date_str=$(date -d "$i days ago" +%Y-%m-%d)
    # 构造日志文件名
    log_file="app.log.$date_str"
    # 如果文件存在,就加入列表
    if [ -f "$log_file" ]; then
        echo "$log_file" >> "$temp_list"
    fi
done

if [ ! -s "$temp_list" ]; then
    echo "未找到过去 7 天的日志文件,备份跳过。"
    rm -f "$temp_list"
    exit 0
fi

tar -czf "$backup_dir/$archive_name" -T "$temp_list"

echo "已成功备份 $(( $(wc -l < "$temp_list") )) 个日志文件到 $backup_dir/$archive_name"

rm -f "$temp_list"

这个脚本展示了 shell 循环的强大之处:

  • 使用 for 循环生成日期
  • 用 while 或 if 判断文件是否存在
  • 使用 break 提前退出
  • 结合 tar 实现批量处理

它将原本可能需要手动操作的复杂任务,简化为一条命令就能完成。

结语

shell 循环不仅是命令行工具的“加速器”,更是自动化运维的基石。它让你从重复劳动中解放出来,把精力投入到更有价值的问题解决中。

无论是 for 的精准遍历、while 的持续监控,还是 until 的等待机制,每一种循环都有其适用场景。掌握它们,你就能写出高效、健壮的脚本,真正实现“一次编写,无限复用”。

记住,写脚本不是为了炫技,而是为了让机器替你干活。当你熟练运用 shell 循环后,你会发现,那些曾经让你头疼的重复任务,如今只需几行代码就能轻松搞定。