在Linux系统管理和自动化脚本编写中,循环结构是shell编程的核心武器。我从业十余年,处理过无数服务器维护任务,可以说90%的重复性工作都能用循环语句优雅解决。不同于其他编程语言,shell循环有着独特的语法特性和使用场景,特别适合处理文件批量操作、日志分析和系统监控等任务。
shell中主要有三种循环结构:for循环、while循环和until循环。每种结构都有其最佳适用场景:
初学者常犯的错误是强行用一种循环解决所有问题。实际上,根据任务特点选择合适的循环类型,能让脚本更高效、更易维护。比如处理当前目录下所有日志文件,for循环就是最自然的选择;而监控某个进程是否结束,while或until循环会更合适。
重要提示:在编写循环语句时,务必注意设置合理的退出条件,否则容易产生无限循环。特别是在生产环境中,一个失控的循环可能会耗尽系统资源。
for循环的标准语法格式如下:
bash复制for 变量 in 值列表
do
命令序列
done
这种结构在处理文件集合时特别有用。例如,我们需要批量重命名当前目录下所有.txt文件:
bash复制#!/bin/bash
count=1
for file in *.txt
do
mv "$file" "document_${count}.txt"
((count++))
done
这个脚本中,*.txt会展开为当前目录所有txt文件列表,for循环依次处理每个文件。注意变量引用要加双引号,这是为了避免文件名中含有空格时出现问题——这是我早期踩过的坑。
bash还支持类似C语言的for循环语法,这在需要精确控制循环次数时非常方便:
bash复制for ((初始值; 条件判断; 值变化))
do
命令序列
done
例如生成乘法表:
bash复制#!/bin/bash
for ((i=1; i<=9; i++))
do
for ((j=1; j<=i; j++))
do
echo -n "${j}x${i}=$((i*j)) "
done
echo
done
这种循环结构的特点是:
现代bash支持数组操作,结合for循环可以处理复杂数据结构:
bash复制#!/bin/bash
servers=("web01" "web02" "db01" "cache01")
for server in "${servers[@]}"
do
ping -c 1 "$server" > /dev/null && echo "$server: alive" || echo "$server: down"
done
这里有几个关键点:
"${servers[@]}"展开为所有数组元素&&和||替代if-else使代码更简洁while循环的语法结构为:
bash复制while 条件测试
do
命令序列
done
一个经典的例子是读取文件内容:
bash复制#!/bin/bash
while IFS= read -r line
do
echo "Processing: $line"
# 其他处理逻辑
done < "input.txt"
这个模式有几个要点:
IFS=防止行首尾空白被trim-r选项避免反斜杠转义< "input.txt"将文件重定向到循环有时我们需要故意创建无限循环,通过内部条件退出:
bash复制#!/bin/bash
while true
do
read -p "Enter command (q to quit): " input
[[ "$input" == "q" ]] && break
# 处理命令
echo "Executing: $input"
done
这种模式常用于交互式脚本。注意:
true是始终返回成功的命令break立即退出当前循环continue跳过本次迭代while循环的条件部分可以组合多个测试:
bash复制#!/bin/bash
count=0
max_attempts=5
server="example.com"
while [[ "$count" -lt "$max_attempts" ]] && ! ping -c 1 "$server" > /dev/null
do
((count++))
echo "Attempt $count failed, retrying..."
sleep 3
done
这个脚本会尝试ping服务器,最多重试5次。条件部分使用了:
-lt数值比较!逻辑非&&逻辑与until循环与while循环逻辑相反,当条件为假时执行循环:
bash复制until 条件测试
do
命令序列
done
典型应用是等待某个条件满足:
bash复制#!/bin/bash
until systemctl is-active --quiet nginx
do
echo "Waiting for nginx to start..."
sleep 1
done
echo "Nginx is now running!"
until循环特别适合服务监控场景:
bash复制#!/bin/bash
service="mysql"
timeout=60
interval=5
elapsed=0
until systemctl is-active --quiet "$service"
do
[[ "$elapsed" -ge "$timeout" ]] && {
echo "Service $service failed to start within $timeout seconds"
exit 1
}
echo "Waiting for $service to start..."
sleep "$interval"
((elapsed+=interval))
done
这个脚本增加了超时控制,避免无限等待。
break和continue可以带参数控制跳出循环的层数:
bash复制#!/bin/bash
for i in {1..5}
do
echo "Outer loop: $i"
for j in {1..5}
do
[[ "$j" -eq 3 ]] && continue 2 # 跳到外层循环下一次
[[ "$i" -eq 4 && "$j" -eq 2 ]] && break 2 # 直接退出两层循环
echo "Inner loop: $j"
done
done
在大规模循环中,性能优化很重要:
避免在循环内部调用外部命令
bash复制# 不好
for file in *
do
size=$(stat -c %s "$file")
echo "$file: $size"
done
# 更好
for file in *
do
echo "$file: $(stat -c %s "$file")"
done
使用内置字符串操作替代外部命令
bash复制# 不好
for host in "${hosts[@]}"
do
domain=$(echo "$host" | cut -d. -f2-)
done
# 更好
for host in "${hosts[@]}"
do
domain="${host#*.}"
done
减少循环次数,使用更高效的通配符
bash复制# 不好
for file in *
do
[[ "$file" == *.log ]] || continue
done
# 更好
for file in *.log
do
# 直接处理.log文件
done
下面是一个综合应用各种循环的日志分析脚本:
bash复制#!/bin/bash
log_dir="/var/log/app"
output_file="analysis_$(date +%Y%m%d).csv"
error_count=0
warning_count=0
echo "timestamp,level,message" > "$output_file"
for log_file in "$log_dir"/*.log
do
echo "Processing $log_file..."
while IFS= read -r line
do
if [[ "$line" == *"ERROR"* ]]; then
((error_count++))
echo "$(date +%T),ERROR,${line//,/;}" >> "$output_file"
elif [[ "$line" == *"WARNING"* ]]; then
((warning_count++))
echo "$(date +%T),WARNING,${line//,/;}" >> "$output_file"
fi
done < "$log_file"
done
echo "Analysis complete. Found $error_count errors and $warning_count warnings."
另一个实用的系统监控脚本:
bash复制#!/bin/bash
threshold=90
interval=60
log_file="memory_monitor.log"
until [[ "$(free | awk '/Mem/{printf("%d"), $3/$2*100}')" -lt "$threshold" ]]
do
echo "===== $(date) =====" >> "$log_file"
echo "Memory usage exceeds threshold" >> "$log_file"
ps -eo pid,ppid,cmd,%mem,%cpu --sort=-%mem | head -10 >> "$log_file"
sleep "$interval"
done
这个脚本会持续监控内存使用情况,直到使用率低于阈值为止。
循环体内部定义的变量默认是全局的,这可能引发意外行为:
bash复制for i in {1..5}
do
temp=$i
done
echo "$temp" # 输出5,而不是undefined
如果需要局部变量,可以使用子shell:
bash复制for i in {1..5}
do
(
temp=$i
# 这里的temp是局部的
)
done
当文件名包含空格或特殊字符时,需要特别注意:
bash复制# 危险:空格会导致分词
for file in $(ls)
do
echo "$file"
done
# 安全:使用通配符
for file in *
do
echo "$file"
done
调试循环脚本的几个实用技巧:
set -x开启调试模式echo语句显示变量值bash -n script.sh检查语法错误例如:
bash复制#!/bin/bash
set -x # 开启调试
for i in {1..3}
do
echo "Processing item $i"
# 其他命令
done
set +x # 关闭调试
使用&和wait实现简单并行:
bash复制for server in "${servers[@]}"
do
{
ping -c 1 "$server" > "${server}.log"
} &
done
wait # 等待所有后台任务完成
对于大文件处理,可以使用命名管道避免内存问题:
bash复制mkfifo mypipe
grep "ERROR" huge.log > mypipe &
while IFS= read -r line
do
# 处理每一行
done < mypipe
rm mypipe
使用trap捕获信号并处理错误:
bash复制#!/bin/bash
trap 'echo "Script interrupted"; exit 1' INT TERM
for i in {1..10}
do
sleep 1
# 如果收到中断信号,trap会处理
done
根据任务特点选择循环类型:
在实际脚本开发中,我通常会先写伪代码确定循环逻辑,再实现具体细节。测试时要特别注意边界条件,比如空输入、单元素等情况。