在Linux系统管理和自动化运维领域,Shell脚本的循环结构就像瑞士军刀中的主刀——看似简单却几乎无处不在。我处理过的服务器故障中,约40%的修复脚本都依赖循环结构实现批量操作。不同于其他编程语言的花式循环语法,Shell循环以其直白的表达方式和与系统命令的无缝结合,成为系统管理员日常工作的利器。
循环结构的本质是模式化重复操作,但Shell环境下其特殊之处在于:
for file in *.log)for user in $(cut -d: -f1 /etc/passwd))初学者常陷入的误区是过度关注语法细节,而忽略了Shell循环真正的威力在于它与系统命令的组合。比如用一行循环实现日志归档:
bash复制for log in /var/log/*.log; do
gzip -c "$log" > "/backup/${log##*/}.gz"
done
标准列表遍历:
bash复制for item in apple orange banana; do
echo "Processing $item"
done
这种结构在处理已知枚举值时最为高效,但在实际工作中更常见的用法是结合通配符或命令替换:
bash复制# 处理当前目录所有.jpg文件
for img in *.jpg; do
convert "$img" -resize 800x600 "resized_${img}"
done
# 处理命令输出
for pid in $(pgrep -u apache); do
renice +5 "$pid"
done
C风格for循环:
bash复制for ((i=0; i<10; i++)); do
echo "Iteration $i"
done
这种结构特别适合需要精确控制循环次数或基于数字序列的操作。比如批量创建用户:
bash复制for ((user_num=1; user_num<=50; user_num++)); do
useradd "training${user_num}" -m -s /bin/bash
done
无限循环变体:
bash复制for ((;;)); do
monitor_service
sleep 60
done
while循环的核心在于其条件判断的灵活性,这使得它成为处理动态数据的首选:
bash复制# 逐行处理文件
while IFS= read -r line; do
[[ "$line" =~ ^# ]] && continue
process_line "$line"
done < config.cfg
# 监控直到条件满足
while [[ $(df -h / | awk 'NR==2 {print $5}' | tr -d '%') -lt 90 ]]; do
echo "Disk space OK"
sleep 300
done
关键技巧:使用
IFS= read -r处理文本时,一定要保留IFS和-r参数,这是避免特殊字符和空格处理问题的黄金法则。
until循环在需要"持续执行直到某条件成立"的场景下表现出色:
bash复制until mysqladmin ping -h"$DB_HOST" --silent; do
echo "Waiting for database..."
sleep 2
done
与while循环的区别在于:
这在服务启动等待、依赖检查等场景特别有用。
break的高级用法:
bash复制for dir in /opt/*; do
[[ ! -d "$dir" ]] && continue
for file in "$dir"/*.conf; do
if grep -q "DEBUG_MODE=true" "$file"; then
echo "Found in $file"
break 2 # 跳出两层循环
fi
done
done
continue的妙用:
bash复制while read -r line; do
[[ -z "$line" ]] && continue # 跳过空行
[[ "$line" == \#* ]] && continue # 跳过注释
process "$line"
done < input.txt
减少子shell生成:
bash复制# 低效写法
for file in $(find . -name "*.tmp"); do...
# 高效写法
find . -name "*.tmp" -print0 | while IFS= read -r -d '' file; do...
大文件处理技巧:
bash复制while IFS= read -r line; do
process_line "$line" &
(( $(jobs | wc -l) >= 8 )) && wait # 控制并发数
done < large_file.txt
wait
循环次数预估:
bash复制total_files=$(find /data -type f | wc -l)
processed=0
find /data -type f | while read -r file; do
process_file "$file"
((processed++))
echo -ne "Progress: $((processed*100/total_files))%\r"
done
用户账户批量管理:
bash复制# 创建带密码的用户
while IFS=: read -r user pass; do
if id "$user" &>/dev/null; then
echo "$user exists"
else
useradd -m "$user" && echo "$pass" | passwd --stdin "$user"
fi
done < user_list.txt
日志轮转自动化:
bash复制# 保留最近7天的日志
find /var/log/app -name "*.log" -mtime +7 | while read -r log; do
gzip "$log"
mv "$log.gz" "/archive/$(date +%Y%m%d)_${log##*/}.gz"
done
CSV文件处理:
bash复制while IFS=, read -r name email phone; do
# 跳过表头
[[ "$name" == "Name" ]] && continue
# 数据清洗
phone=${phone//[!0-9]/}
echo "$name,$email,$phone" >> cleaned.csv
done < contacts.csv
多服务器并行操作:
bash复制for server in $(<server_list); do
(
ssh "admin@$server" "
sudo apt update
sudo apt upgrade -y
" >"${server}_upgrade.log" 2>&1 &
)
done
wait # 等待所有后台任务完成
使用set命令调试:
bash复制#!/bin/bash
set -x # 开启调试
for i in {1..3}; do
echo "Processing item $i"
done
set +x # 关闭调试
日志记录模式:
bash复制exec 3>&1 4>&2 # 保存标准输出/错误描述符
exec >script.log 2>&1 # 重定向所有输出到日志
for task in "${tasks[@]}"; do
echo "[$(date)] Starting $task"
if ! process_task "$task"; then
echo "[$(date)] ERROR: Failed on $task" >&3 # 仍然输出到原终端
fi
done
exec 1>&3 2>&4 # 恢复标准输出/错误
错误重试机制:
bash复制max_retries=3
for ((attempt=1; attempt<=max_retries; attempt++)); do
if perform_operation; then
break
else
echo "Attempt $attempt failed"
((attempt == max_retries)) && exit 1
sleep $((attempt * 5))
fi
done
循环安全模式:
bash复制# 防止空变量导致意外循环
files=(*.data) # 使用数组
if (( ${#files[@]} == 0 )); then
echo "No files found" >&2
exit 1
fi
for file in "${files[@]}"; do
[[ ! -f "$file" ]] && continue
process_safe "$file"
done
目录树处理:
bash复制while IFS= read -r -d '' dir; do
echo "Processing directory: $dir"
while IFS= read -r -d '' file; do
process_file "$file"
done < <(find "$dir" -maxdepth 1 -type f -print0)
done < <(find /data -type d -print0)
并行任务队列:
bash复制max_jobs=4
task_queue=(task1 task2 task3 task4 task5 task6)
for task in "${task_queue[@]}"; do
(
execute_task "$task"
) &
# 控制并发数
while (( $(jobs -p | wc -l) >= max_jobs )); do
sleep 0.1
done
done
wait
命名管道实现生产者-消费者:
bash复制mkfifo task_queue
# 生产者
for i in {1..10}; do
echo "Task$i" > task_queue
done
echo "DONE" > task_queue
# 消费者
while read -r task; do
[[ "$task" == "DONE" ]] && break
process_task "$task"
done < task_queue
rm task_queue
测试不同循环处理10000次迭代的时间消耗:
bash复制# for循环
time for ((i=0; i<10000; i++)); do :; done
# while循环
time i=0; while ((i++ < 10000)); do :; done
# seq + for
time for i in $(seq 1 10000); do :; done
典型结果对比:
重要发现:在迭代次数超过1000时,应避免使用命令生成数字序列,直接使用C风格循环或while循环。
"$var"nullglob避免无匹配时的字面解释bash复制shopt -s nullglob
for file in *.nonexistent; do ... # 不会进入循环
"${array[@]}"而非${array[*]}while IFS= read -r而非for line in $(cat file)sleep避免CPU占用100%bash复制trap 'echo "Interrupted"; exit 1' INT TERM
while true; do ...; done
| 特性 | Bash | Zsh | Ksh | Dash |
|---|---|---|---|---|
| C风格for循环 | ✓ | ✓ | ✓ | ✗ |
| {1..10}范围扩展 | ✓ | ✓ | ✓ | ✗ |
| 关联数组遍历 | ✓ | ✓ | ✓ | ✗ |
| select循环菜单 | ✓ | ✓ | ✓ | ✗ |
数字序列生成:
bash复制# 非Bash环境兼容写法
if [ -n "$BASH_VERSION" ]; then
seq={1..10}
else
seq=$(seq 1 10)
fi
for i in $seq; do ... done
数组遍历兼容方案:
bash复制# 确保数组定义兼容
if [ -n "$BASH_VERSION" ]; then
declare -a items=("one" "two")
else
set -A items one two
fi
# 通用遍历方式
i=0
while [ $i -lt ${#items[@]} ]; do
echo "${items[$i]}"
((i++))
done
以下是一个完整的日志分析案例,展示循环结构在实际工作中的综合应用:
bash复制#!/bin/bash
# 日志分析工具 - 统计各状态码出现频率
LOG_DIR="/var/log/nginx"
OUTPUT_FILE="status_report_$(date +%Y%m%d).csv"
declare -A status_codes # 关联数组存储统计结果
# 第一阶段:原始日志处理
find "$LOG_DIR" -name "access.log*" | while read -r logfile; do
echo "Processing $logfile"
# 处理压缩日志
case "$logfile" in
*.gz) cmd="zcat";;
*) cmd="cat";;
esac
# 提取状态码
$cmd "$logfile" | while read -r line; do
status=$(awk '{print $9}' <<<"$line")
[[ -z "$status" ]] && continue
# 统计频次
((status_codes["$status"]++))
done
done
# 第二阶段:生成报告
{
echo "HTTP Status Code,Count"
for code in "${!status_codes[@]}"; do
echo "$code,${status_codes[$code]}"
done | sort -nr -t, -k2 # 按频次降序
} > "$OUTPUT_FILE"
echo "Report generated: $OUTPUT_FILE"
这个脚本展示了:
过滤器模式:
bash复制# 只处理符合条件的项目
for item in "${items[@]}"; do
[[ ! "$item" =~ ^[A-Z] ]] && continue # 过滤非大写开头的项
process_item "$item"
done
转换器模式:
bash复制# 数组元素转换
declare -a new_array
for original in "${original_array[@]}"; do
new_array+=("${original//old/new}")
done
分发器模式:
bash复制# 根据类型分发处理
for file in *; do
case "${file##*.}" in
txt) handle_text "$file";;
csv) handle_spreadsheet "$file";;
*) handle_other "$file";;
esac
done
长循环重构前:
bash复制for user in $(getent passwd | cut -d: -f1); do
home=$(getent passwd "$user" | cut -d: -f6)
if [[ -d "$home" ]]; then
size=$(du -sh "$home" | cut -f1)
echo "$user,$size" >> report.csv
if [[ "$size" > "1G" ]]; then
send_alert "$user exceeds quota"
fi
fi
done
重构后(函数化):
bash复制get_user_home() {
getent passwd "$1" | cut -d: -f6
}
check_quota() {
local size=$(du -sh "$1" | cut -f1)
echo "$2,$size" >> report.csv
[[ "$size" > "1G" ]] && send_alert "$2 exceeds quota"
}
for user in $(getent passwd | cut -d: -f1); do
home=$(get_user_home "$user")
[[ -d "$home" ]] && check_quota "$home" "$user"
done
重构要点: