目录
[2.1 列表遍历(最常用)](#2.1 列表遍历(最常用))
[2.2 实战:批量重命名文件](#2.2 实战:批量重命名文件)
[2.3 C语言风格的for循环](#2.3 C语言风格的for循环)
[2.4 seq命令的使用](#2.4 seq命令的使用)
[3.1 基本语法](#3.1 基本语法)
[3.2 最经典的用法:逐行读取文件](#3.2 最经典的用法:逐行读取文件)
[3.3 实战:分析日志文件](#3.3 实战:分析日志文件)
[3.4 等待条件满足](#3.4 等待条件满足)
[3.5 无限循环](#3.5 无限循环)
[5.1 break:跳出整个循环](#5.1 break:跳出整个循环)
[5.2 continue:跳过本次循环](#5.2 continue:跳过本次循环)
[5.3 break n:跳出多层循环](#5.3 break n:跳出多层循环)
一、引言:为什么要学循环?
想象以下场景:
-
把100个
.jpg文件全部重命名为.png -
检查50台服务器是否在线
-
读取一个日志文件的每一行,提取包含"ERROR"的行
-
等待数据库启动完成后再执行下一步操作
如果手动逐一处理,不仅低效,还容易出错。循环结构就是为解决这类重复性任务而生的。
Shell提供了三种循环:
-
for:已知循环次数,遍历一个列表 -
while:循环次数未知,满足条件就继续 -
until:循环次数未知,直到条件成立才停止
二、for循环:遍历的艺术
2.1 列表遍历(最常用)
bash
for 变量 in 列表; do
命令
done
最简单的例子:
bash
#!/bin/bash
for name in zhangsan lisi wangwu; do
echo "Hello, ${name}!"
done
输出:
text
Hello, zhangsan!
Hello, lisi!
Hello, wangwu!
列表的多种写法:
bash
# 花括号展开(数字序列)
for i in {1..5}; do
echo "第${i}次执行"
done
# 花括号展开(带步长)
for i in {0..10..2}; do
echo $i # 0, 2, 4, 6, 8, 10
done
# 花括号展开(字母序列)
for letter in {a..e}; do
echo $letter
done
# 通配符展开(遍历文件)
for file in /var/log/*.log; do
echo "处理文件: ${file}"
done
# 命令替换(遍历命令输出)
for user in $(cat /etc/passwd | cut -d: -f1); do
echo "用户: ${user}"
done
2.2 实战:批量重命名文件
bash
#!/bin/bash
# 将当前目录下所有.txt文件重命名为.md
count=0
for file in *.txt; do
# 如果没有.txt文件,for会原样返回"*.txt"
if [[ ! -f "$file" ]]; then
echo "没有找到.txt文件"
exit 0
fi
# 用字符串操作去掉.txt后缀,加上.md
new_name="${file%.txt}.md"
mv "$file" "$new_name"
echo "重命名: ${file} → ${new_name}"
((count++))
done
echo "共重命名 ${count} 个文件"
2.3 C语言风格的for循环
Bash也支持类似C语言的for语法:
bash
for (( 初始值; 条件; 步进 )); do
命令
done
bash
#!/bin/bash
# 计算1到100的和
sum=0
for (( i=1; i<=100; i++ )); do
sum=$((sum + i))
done
echo "1到100的和: ${sum}" # 5050
适用场景:循环次数需要精确控制,或步进逻辑较复杂时。
2.4 seq命令的使用
seq生成序列,比花括号展开更灵活------支持变量代替硬编码数字:
bash
# 基本用法
seq 1 5 # 1 2 3 4 5
seq 1 2 10 # 1 3 5 7 9(步长2)
seq -w 01 10 # 01 02 ... 10(等宽补齐)
# 在for中使用(花括号不支持变量展开,seq支持)
start=1
end=100
for i in $(seq $start $end); do
echo $i
done
seq与花括号的关键区别 :花括号必须在变量展开之前 被解释,所以{1..${end}}会失败;而seq支持动态参数。当范围边界是变量时,必须用seq。
三、while循环:条件驱动的重复
3.1 基本语法
bash
while 条件; do
命令
done
条件为真时持续循环,条件变假时退出。
3.2 最经典的用法:逐行读取文件
bash
#!/bin/bash
# 逐行读取文件并处理
while IFS= read -r line; do
echo "读取到: ${line}"
done < /etc/passwd
参数解释:
-
IFS=:清空IFS,保留行首行尾空格 -
read -r:-r禁止反斜杠转义,原样读取 -
< /etc/passwd:输入重定向,把文件内容喂给while循环
3.3 实战:分析日志文件
假设有一个Nginx访问日志access.log,提取所有返回500状态码的行:
bash
#!/bin/bash
error_count=0
LOG_FILE="/var/log/nginx/access.log"
if [[ ! -f "$LOG_FILE" ]]; then
echo "日志文件不存在: ${LOG_FILE}"
exit 1
fi
while IFS= read -r line; do
# 用字符串处理提取状态码(日志中状态码通常在"HTTP/1.1" 后面)
if [[ "$line" =~ \ 500\ ]]; then
echo "500错误: ${line}"
((error_count++))
fi
done < "$LOG_FILE"
echo "共发现 ${error_count} 条500错误"
3.4 等待条件满足
bash
#!/bin/bash
# 等待MySQL服务就绪(最多等待30秒)
max_wait=30
waited=0
echo "等待MySQL服务启动..."
while ! mysqladmin ping -u root --silent 2>/dev/null; do
sleep 2
((waited+=2))
echo " 已等待 ${waited} 秒..."
if [[ $waited -ge $max_wait ]]; then
echo "超时!MySQL未在${max_wait}秒内启动"
exit 1
fi
done
echo "MySQL已就绪!"
3.5 无限循环
bash
# 持续监控
while true; do
df -h / | tail -1
sleep 5
done
# 也可用 : (冒号是Shell内置的恒真命令)
while :; do
echo "按Ctrl+C退出"
sleep 1
done
四、until循环:反向条件的while
until和while是镜像关系------until在条件为假 时继续循环,条件变真时退出。
bash
until 条件; do
命令
done
bash
#!/bin/bash
# 等待服务停止
until ! systemctl is-active --quiet nginx; do
echo "Nginx仍在运行,等待停止..."
sleep 2
done
echo "Nginx已停止"
五、break与continue:流程控制
5.1 break:跳出整个循环
bash
#!/bin/bash
# 查找第一个包含"error"的日志文件
for file in /var/log/*.log; do
echo "检查: ${file}"
if grep -q "error" "$file" 2>/dev/null; then
echo "在 ${file} 中找到error!"
break # 找到就停止搜索
fi
done
5.2 continue:跳过本次循环
bash
#!/bin/bash
# 只处理数字命名的文件,跳过其他
for file in *; do
if [[ ! "$file" =~ ^[0-9]+$ ]]; then
continue # 不是纯数字,跳过
fi
echo "处理数字文件: ${file}"
done
5.3 break n:跳出多层循环
bash
#!/bin/bash
# 二维搜索
for i in {1..10}; do
for j in {1..10}; do
if [[ $((i * j)) -eq 56 ]]; then
echo "找到: ${i} × ${j} = 56"
break 2 # 跳出两层循环
fi
done
done
六、综合实战:批量服务器巡检脚本
把循环、条件判断、变量处理整合起来:
bash
#!/bin/bash
# 批量服务器巡检脚本
SERVERS=(
"192.168.1.10"
"192.168.1.20"
"192.168.1.30"
"10.0.0.5"
)
LOG_FILE="server_check_$(date +%Y%m%d).log"
echo "=== 服务器巡检 $(date) ===" | tee "$LOG_FILE"
total=${#SERVERS[@]}
online=0
offline=0
for server in "${SERVERS[@]}"; do
# 用ping测试连通性(ping 1次,超时1秒)
if ping -c 1 -W 1 "$server" &>/dev/null; then
echo "[✓] ${server} - 在线" | tee -a "$LOG_FILE"
((online++))
else
echo "[✗] ${server} - 离线" | tee -a "$LOG_FILE"
((offline++))
fi
done
echo "" | tee -a "$LOG_FILE"
echo "巡检完成:共${total}台,在线${online}台,离线${offline}台" | tee -a "$LOG_FILE"
# 如果存在离线服务器,退出码返回1(便于集成到监控系统)
if [[ $offline -gt 0 ]]; then
exit 1
fi
这个脚本可直接用于日常运维,也可以集成到crontab中定时执行。
七、循环选择速查表
| 场景 | 推荐循环 | 原因 |
|---|---|---|
| 遍历固定列表(文件、数字、字符串) | for ... in |
简洁直观 |
| 精确控制循环次数 | for ((...)) |
类似C语法,灵活 |
| 逐行读取文件 | while read |
经典范式,省内存 |
| 等待某条件成立 | while |
条件控制灵活 |
| 等待某条件不成立 | until |
语义清晰 |
| 无限循环 | while true |
通用写法 |
八、常见陷阱与注意事项
陷阱一:通配符在没有匹配文件时不展开
bash
# 如果当前目录没有.log文件
for file in *.log; do
echo "$file"
done
# 结果输出:*.log(字符串原样返回,而不是跳过循环)
解决方案:
bash
shopt -s nullglob # 启用:无匹配时返回空
for file in *.log; do
[ -f "$file" ] && echo "$file"
done
陷阱二:管道会创建子Shell,循环内的变量修改会丢失
bash
count=0
cat /etc/passwd | while read line; do
((count++))
done
echo $count # 输出0!因为while在子Shell中运行,变量修改丢失
解决方案 :改用输入重定向(done < file)或进程替换(done < <(command))。
陷阱三:for循环读取文件会一次性加载所有内容到内存
大文件不要用for line in $(cat huge_file),改用while read逐行处理。
九、本篇小结
三种循环语法:
| 循环 | 语法 | 适用场景 |
|---|---|---|
for ... in |
for var in list; do ... done |
遍历列表、文件 |
for (()) |
for ((i=0; i<n; i++)); do ... done |
精确次数控制 |
while |
while 条件; do ... done |
逐行读文件、等待条件 |
until |
until 条件; do ... done |
反向while |
流程控制:
-
break:跳出循环 -
continue:跳过本次循环 -
break n:跳出n层循环
循环选择优先级:
text
已知列表 → for ... in
已知次数 → for (())
逐行处理 → while read
条件等待 → while/until
动手练习
bash
#!/bin/bash
# 练习:统计每个文件的扩展名数量
declare -A ext_count # 关联数组(需要Bash 4+)
for file in *; do
if [[ -f "$file" ]]; then
ext="${file##*.}"
# 没有扩展名的文件
[[ "$ext" = "$file" ]] && ext="(无扩展名)"
((ext_count["$ext"]++))
fi
done
echo "文件扩展名统计:"
for ext in "${!ext_count[@]}"; do
echo " .${ext}: ${ext_count[$ext]} 个"
done
十、下篇预告
循环让脚本能批量处理,但脚本本身也变得越来越长。如何把重复使用的代码块封装起来,让脚本更模块化、更易维护?
下一篇我们将学习函数与模块化,包括:
-
函数的定义与调用
-
参数传递(
$1、$2在函数中的使用) -
返回值(
returnvsecho) -
如何用
source引入公共函数库
学会函数后,你将能编写出结构清晰、可复用的专业级Shell脚本。
延伸思考 :while IFS= read -r line是逐行读文件的金标准写法,但每个部分都有其用意。IFS=防止行首空格被吞,-r防止反斜杠转义,read line从标准输入读取一行。少一个参数都可能在某些边界情况下出错。这就是Shell编程的特点------看似随意,其实处处有讲究。