循环是Shell脚本编程中实现重复执行的核心结构。通过循环,我们可以高效地处理大量数据、自动化重复任务和执行批量操作。
基于列表的循环,适合遍历数组、文件列表等。
基于条件的循环,条件为真时持续执行。
基于条件的循环,条件为假时持续执行。
提前退出循环,跳出当前循环结构。
跳过当前迭代,继续下一次循环。
循环内部包含其他循环,处理复杂数据结构。
循环是Shell脚本实现自动化和批量处理的关键。通过合理的循环结构,脚本可以高效处理文件、数据、用户输入等,大大提高工作效率和脚本的实用性。
for循环是基于列表的循环,适合遍历数组、文件列表、数字序列等。
#!/bin/bash
# for循环示例
echo "=== 基本for循环 ==="
# 遍历单词列表
for fruit in apple banana orange grape; do
echo "水果: $fruit"
done
echo -e "\n=== 遍历数组 ==="
fruits=("apple" "banana" "orange" "grape")
for fruit in "${fruits[@]}"; do
echo "数组元素: $fruit"
done
echo -e "\n=== 遍历数字序列 ==="
# 方法1: 使用序列表达式
for i in {1..5}; do
echo "数字: $i"
done
# 方法2: 使用seq命令
for i in $(seq 1 5); do
echo "seq数字: $i"
done
# 方法3: 指定步长
for i in {1..10..2}; do
echo "步长为2: $i"
done
echo -e "\n=== 遍历文件 ==="
# 遍历当前目录的所有.sh文件
for file in *.sh; do
if [ -f "$file" ]; then
echo "Shell脚本: $file"
# 可以在这里处理文件
fi
done
echo -e "\n=== 遍历命令输出 ==="
# 遍历ls命令的输出
for item in $(ls /var/log/ | head -5); do
echo "日志文件/目录: $item"
done
echo -e "\n=== C风格for循环 ==="
# C风格for循环
for ((i=1; i<=5; i++)); do
echo "C风格循环: $i"
done
# 复杂的C风格循环
for ((i=0, j=10; i<=j; i++, j--)); do
echo "i=$i, j=$j"
done
echo -e "\n=== 遍历参数 ==="
# 遍历所有命令行参数
echo "参数个数: $#"
for arg in "$@"; do
echo "参数: $arg"
done
# 使用$*(不推荐,可能有问题)
for arg in $*; do
echo "参数(\$*): $arg"
done
echo -e "\n=== 遍历带空格的字符串 ==="
# 处理包含空格的字符串
names="Alice Bob Charlie"
# 错误的方式(会被空格分割)
echo "错误方式:"
for name in $names; do
echo "名字: $name"
done
# 正确的方式(使用数组)
echo -e "\n正确方式:"
name_array=("Alice" "Bob" "Charlie")
for name in "${name_array[@]}"; do
echo "名字: $name"
done
echo -e "\n=== 遍历目录 ==="
# 遍历目录及其子目录(需要Bash 4.0+)
for file in /var/log/*; do
if [ -d "$file" ]; then
echo "目录: $file"
elif [ -f "$file" ]; then
echo "文件: $file"
fi
done
echo -e "\n=== 使用IFS控制分割 ==="
# 修改IFS(内部字段分隔符)
data="apple,banana,orange"
OLD_IFS="$IFS"
IFS=','
for item in $data; do
echo "CSV项: $item"
done
IFS="$OLD_IFS" # 恢复原来的IFS
echo -e "\n=== 无限for循环 ==="
# 无限循环(需要break退出)
# for ((;;)); do
# echo "这是一个无限循环"
# sleep 1
# done
"${array[@]}"而不是"${array[*]}"遍历数组while循环是基于条件的循环,只要条件为真就持续执行。
#!/bin/bash
# while循环示例
echo "=== 基本while循环 ==="
counter=1
while [ $counter -le 5 ]; do
echo "计数: $counter"
((counter++))
done
echo -e "\n=== 读取文件行 ==="
# 方法1: 使用read和重定向
filename="example.txt"
# 创建示例文件
echo -e "第一行\n第二行\n第三行" > "$filename"
echo "读取文件内容:"
while read line; do
echo "行内容: $line"
done < "$filename"
echo -e "\n=== 读取命令输出 ==="
# 读取命令的每一行输出
echo "进程信息:"
ps aux | head -5 | while read line; do
echo "进程: $line"
done
echo -e "\n=== 使用管道和子shell ==="
# 注意:在管道中的while循环会在子shell中运行
counter=1
echo -e "a\nb\nc" | while read item; do
echo "项目 $counter: $item"
((counter++))
done
echo "计数器值(子shell中修改不会影响父shell): $counter"
echo -e "\n=== 无限while循环 ==="
# 无限循环的几种方式
# 方式1: 使用true命令
# while true; do
# echo "无限循环..."
# sleep 1
# done
# 方式2: 使用冒号命令(比true更高效)
# while :; do
# echo "无限循环..."
# sleep 1
# done
# 方式3: 使用永远为真的条件
# while [ 1 -eq 1 ]; do
# echo "无限循环..."
# sleep 1
# done
echo -e "\n=== 使用break退出循环 ==="
counter=1
while [ $counter -le 10 ]; do
if [ $counter -eq 5 ]; then
echo "在计数5时退出循环"
break
fi
echo "计数: $counter"
((counter++))
done
echo -e "\n=== 使用continue跳过迭代 ==="
counter=0
while [ $counter -lt 5 ]; do
((counter++))
if [ $counter -eq 3 ]; then
echo "跳过计数3"
continue
fi
echo "计数: $counter"
done
echo -e "\n=== 处理用户输入 ==="
# 简单的菜单系统
while true; do
echo "=== 菜单 ==="
echo "1. 显示当前时间"
echo "2. 显示系统信息"
echo "3. 退出"
read -p "请选择 [1-3]: " choice
case $choice in
1)
echo "当前时间: $(date)"
;;
2)
echo "系统信息: $(uname -a)"
;;
3)
echo "再见!"
break
;;
*)
echo "无效选择,请重新输入"
;;
esac
echo
done
echo -e "\n=== 监控文件变化 ==="
# 监控文件变化的简单示例
watch_file="watch.txt"
echo "初始内容" > "$watch_file"
last_mtime=$(stat -c %Y "$watch_file")
echo "监控文件 $watch_file 的变化 (Ctrl+C 退出)"
while true; do
current_mtime=$(stat -c %Y "$watch_file" 2>/dev/null)
if [ "$current_mtime" != "$last_mtime" ]; then
echo "文件已修改: $(date)"
echo "新内容: $(cat "$watch_file")"
last_mtime="$current_mtime"
fi
sleep 2
done
# 清理示例文件
rm -f "$filename" "$watch_file"
break可以提前退出循环continue可以跳过当前迭代until循环与while循环相反,只要条件为假就持续执行。
特点:条件为真时执行循环
特点:条件为假时执行循环
#!/bin/bash
# until循环示例
echo "=== 基本until循环 ==="
counter=1
until [ $counter -gt 5 ]; do
echo "计数: $counter"
((counter++))
done
echo -e "\n=== 等待条件满足 ==="
# 等待文件创建
target_file="target.txt"
rm -f "$target_file"
echo "等待文件 $target_file 被创建..."
until [ -f "$target_file" ]; do
echo "文件还不存在,等待 1 秒..."
sleep 1
done
echo "文件已创建!"
# 在后台创建文件
(sleep 3; touch "$target_file") &
echo -e "\n=== 等待服务启动 ==="
# 等待端口可用(模拟)
port_available=false
attempt=1
max_attempts=5
until [ "$port_available" = true ] || [ $attempt -gt $max_attempts ]; do
echo "尝试检查端口 (尝试 $attempt/$max_attempts)..."
# 模拟端口检查(随机成功)
if [ $((RANDOM % 3)) -eq 0 ]; then
port_available=true
echo "端口现在可用!"
else
echo "端口还不可用,等待 2 秒..."
sleep 2
fi
((attempt++))
done
if [ "$port_available" = false ]; then
echo "错误: 在 $max_attempts 次尝试后端口仍然不可用"
fi
echo -e "\n=== 等待用户输入特定值 ==="
# 等待用户输入特定值
valid_input=false
until [ "$valid_input" = true ]; do
read -p "请输入 'yes' 或 'no': " user_input
case $user_input in
yes|YES|Yes)
echo "你选择了: yes"
valid_input=true
;;
no|NO|No)
echo "你选择了: no"
valid_input=true
;;
*)
echo "无效输入,请重新输入"
;;
esac
done
echo -e "\n=== 超时控制 ==="
# 带超时的等待
timeout=10
start_time=$(date +%s)
service_ready=false
echo "等待服务准备就绪 (超时: ${timeout}秒)..."
until [ "$service_ready" = true ]; do
current_time=$(date +%s)
elapsed=$((current_time - start_time))
if [ $elapsed -ge $timeout ]; then
echo "错误: 在 ${timeout} 秒后服务仍未就绪"
break
fi
# 模拟服务检查
if [ $((RANDOM % 4)) -eq 0 ]; then
service_ready=true
echo "服务已就绪! 耗时: ${elapsed}秒"
else
echo "服务还未就绪,等待 1 秒... (已等待 ${elapsed}秒)"
sleep 1
fi
done
echo -e "\n=== 处理多个条件 ==="
# 多个条件的until循环
counter=0
max_count=8
target_value=5
until [ $counter -ge $max_count ] || [ $counter -eq $target_value ]; do
echo "计数器: $counter"
((counter++))
done
if [ $counter -eq $target_value ]; then
echo "达到目标值: $target_value"
else
echo "达到最大计数: $max_count"
fi
echo -e "\n=== 与while循环对比 ==="
echo "while循环示例:"
count=1
while [ $count -le 3 ]; do
echo "while: $count"
((count++))
done
echo -e "\nuntil循环示例:"
count=1
until [ $count -gt 3 ]; do
echo "until: $count"
((count++))
done
# 清理
rm -f "$target_file"
使用break和continue控制循环的执行流程。
break - 退出循环立即退出当前循环,继续执行循环后面的代码。
continue - 跳过当前迭代跳过当前循环迭代的剩余代码,直接开始下一次迭代。
#!/bin/bash
# 循环控制示例
echo "=== break基本用法 ==="
for i in {1..10}; do
if [ $i -eq 5 ]; then
echo "在 i=5 时退出循环"
break
fi
echo "i = $i"
done
echo "循环结束"
echo -e "\n=== continue基本用法 ==="
for i in {1..5}; do
if [ $i -eq 3 ]; then
echo "跳过 i=3"
continue
fi
echo "i = $i"
done
echo -e "\n=== break n (退出多层循环) ==="
# 退出指定层数的循环
for i in {1..3}; do
echo "外层循环 i=$i"
for j in {1..3}; do
echo " 内层循环 j=$j"
if [ $j -eq 2 ]; then
echo " 退出两层循环"
break 2 # 退出两层循环
fi
done
done
echo -e "\n=== continue n (跳过多层迭代) ==="
# 跳过指定层数的迭代
for i in {1..3}; do
echo "外层循环 i=$i"
for j in {1..3}; do
if [ $j -eq 2 ]; then
echo " 跳过外层循环的剩余迭代"
continue 2 # 跳过外层循环的当前迭代
fi
echo " 内层循环 j=$j"
done
done
echo -e "\n=== 在while循环中使用break ==="
count=1
while true; do # 无限循环
echo "计数: $count"
if [ $count -eq 5 ]; then
echo "达到目标计数,退出循环"
break
fi
((count++))
done
echo -e "\n=== 在until循环中使用continue ==="
count=0
until [ $count -gt 5 ]; do
((count++))
if [ $count -eq 3 ]; then
echo "跳过计数3"
continue
fi
echo "当前计数: $count"
done
echo -e "\n=== 实际应用:查找文件 ==="
# 在目录树中查找文件
search_file="target.txt"
found=false
# 创建测试目录结构
mkdir -p test_dir/sub1 test_dir/sub2
echo "content" > "test_dir/sub1/file1.txt"
echo "content" > "test_dir/sub2/$search_file"
echo "content" > "test_dir/file2.txt"
echo "查找文件: $search_file"
for dir in test_dir/*; do
if [ -d "$dir" ]; then
echo "搜索目录: $dir"
for file in "$dir"/*; do
if [ -f "$file" ] && [ "$(basename "$file")" = "$search_file" ]; then
echo "找到文件: $file"
found=true
break 2 # 找到文件,退出两层循环
fi
done
fi
done
if [ "$found" = false ]; then
echo "未找到文件: $search_file"
fi
echo -e "\n=== 实际应用:处理数据 ==="
# 处理数据,跳过无效条目
data=("正常数据1" "" "正常数据2" "无效数据" "正常数据3")
echo "处理数据列表:"
for item in "${data[@]}"; do
# 跳过空字符串
if [ -z "$item" ]; then
echo "跳过空数据"
continue
fi
# 跳过包含"无效"的数据
if [[ "$item" == *"无效"* ]]; then
echo "跳过无效数据: $item"
continue
fi
echo "处理数据: $item"
done
echo -e "\n=== 实际应用:菜单系统 ==="
# 带退出选项的菜单系统
while true; do
echo "=== 系统菜单 ==="
echo "1. 显示磁盘使用情况"
echo "2. 显示内存使用情况"
echo "3. 显示进程信息"
echo "4. 退出系统"
read -p "请选择选项 [1-4]: " choice
case $choice in
1)
echo "磁盘使用情况:"
df -h | head -5
;;
2)
echo "内存使用情况:"
free -h
;;
3)
echo "进程信息 (前5个):"
ps aux | head -6
;;
4)
echo "退出系统..."
break
;;
*)
echo "无效选项,请重新选择"
continue # 跳过本次迭代,重新显示菜单
;;
esac
echo
read -p "按回车键继续..."
echo
done
# 清理测试文件
rm -rf test_dir
break提前退出不必要的循环迭代continue跳过不符合条件的迭代break n和continue n可以控制多层循环break和continue通过实际脚本示例展示Shell循环的综合应用。
使用各种循环结构实现文件的批量处理和管理。
#!/bin/bash
# 文件批量处理工具 - 循环综合示例
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 配置
BACKUP_DIR="backups"
LOG_FILE="file_processor.log"
# 初始化
init() {
mkdir -p "$BACKUP_DIR"
echo "$(date): 文件处理器启动" >> "$LOG_FILE"
}
# 记录日志
log() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
case $level in
"ERROR") echo -e "${RED}[ERROR]${NC} $message" ;;
"WARNING") echo -e "${YELLOW}[WARNING]${NC} $message" ;;
"INFO") echo -e "${BLUE}[INFO]${NC} $message" ;;
"SUCCESS") echo -e "${GREEN}[SUCCESS]${NC} $message" ;;
*) echo "[$level] $message" ;;
esac
}
# 批量重命名文件
batch_rename() {
local pattern="$1"
local prefix="$2"
local counter=1
log "INFO" "开始批量重命名文件,模式: $pattern, 前缀: $prefix"
# 使用for循环遍历匹配的文件
for file in $pattern; do
if [ -f "$file" ]; then
local extension="${file##*.}"
local new_name="${prefix}_${counter}.${extension}"
if mv "$file" "$new_name" 2>/dev/null; then
log "SUCCESS" "重命名: $file → $new_name"
((counter++))
else
log "ERROR" "重命名失败: $file"
fi
fi
done
log "INFO" "批量重命名完成,共处理 $((counter-1)) 个文件"
}
# 批量转换文件编码
convert_encoding() {
local pattern="$1"
local from_encoding="$2"
local to_encoding="$3"
local converted=0
local failed=0
log "INFO" "开始转换文件编码: $from_encoding → $to_encoding"
# 使用while循环读取find命令输出
find . -name "$pattern" -type f | while read file; do
local backup_file="$BACKUP_DIR/$(basename "$file").backup"
# 创建备份
if cp "$file" "$backup_file" 2>/dev/null; then
log "INFO" "创建备份: $backup_file"
else
log "ERROR" "备份失败: $file"
((failed++))
continue
fi
# 转换编码
if iconv -f "$from_encoding" -t "$to_encoding" "$file" > "${file}.converted" 2>/dev/null; then
if mv "${file}.converted" "$file" 2>/dev/null; then
log "SUCCESS" "转换成功: $file"
((converted++))
else
log "ERROR" "移动转换文件失败: $file"
((failed++))
fi
else
log "ERROR" "转换失败: $file"
((failed++))
fi
done
log "INFO" "编码转换完成: $converted 成功, $failed 失败"
}
# 文件大小统计
file_statistics() {
local pattern="${1:-*}"
local total_size=0
local file_count=0
declare -A size_by_extension
log "INFO" "开始文件统计,模式: $pattern"
# 使用for循环统计文件
for file in $pattern; do
if [ -f "$file" ]; then
local size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null)
local extension="${file##*.}"
# 如果没有扩展名,使用"无"
if [ "$file" = "$extension" ]; then
extension="无"
fi
((total_size += size))
((file_count++))
((size_by_extension["$extension"] += size))
log "INFO" "文件: $file, 大小: $size 字节, 扩展名: .$extension"
fi
done
# 显示统计结果
echo -e "\n${BLUE}=== 文件统计结果 ===${NC}"
echo "总文件数: $file_count"
echo "总大小: $total_size 字节 ($((total_size/1024)) KB)"
if [ $file_count -gt 0 ]; then
echo -e "\n${GREEN}按扩展名统计:${NC}"
for ext in "${!size_by_extension[@]}"; do
local size=${size_by_extension["$ext"]}
local percentage=$((size * 100 / total_size))
echo " .$ext: $size 字节 ($percentage%)"
done
fi
}
# 监控目录变化
monitor_directory() {
local directory="$1"
local interval="${2:-5}"
if [ ! -d "$directory" ]; then
log "ERROR" "目录不存在: $directory"
return 1
fi
log "INFO" "开始监控目录: $directory, 间隔: ${interval}秒"
# 获取初始文件列表
declare -A initial_files
while read -r file; do
initial_files["$file"]=1
done < <(find "$directory" -type f 2>/dev/null)
# 持续监控
while true; do
# 获取当前文件列表
declare -A current_files
while read -r file; do
current_files["$file"]=1
done < <(find "$directory" -type f 2>/dev/null)
# 检查新增文件
for file in "${!current_files[@]}"; do
if [ -z "${initial_files[$file]}" ]; then
log "INFO" "新增文件: $file"
fi
done
# 检查删除文件
for file in "${!initial_files[@]}"; do
if [ -z "${current_files[$file]}" ]; then
log "WARNING" "删除文件: $file"
fi
done
# 更新文件列表
initial_files=()
for file in "${!current_files[@]}"; do
initial_files["$file"]=1
done
# 等待指定间隔
sleep "$interval"
done
}
# 批量下载管理器
batch_download() {
local url_list_file="$1"
local max_concurrent="${2:-3}"
local downloaded=0
local failed=0
if [ ! -f "$url_list_file" ]; then
log "ERROR" "URL列表文件不存在: $url_list_file"
return 1
fi
log "INFO" "开始批量下载,最大并发数: $max_concurrent"
# 读取URL列表
urls=()
while IFS= read -r url || [ -n "$url" ]; do
urls+=("$url")
done < "$url_list_file"
total_urls=${#urls[@]}
log "INFO" "总共发现 $total_urls 个URL"
# 使用for循环处理URL
for ((i=0; i<total_urls; i++)); do
local url="${urls[$i]}"
local filename=$(basename "$url")
# 如果文件已存在,跳过
if [ -f "$filename" ]; then
log "WARNING" "文件已存在,跳过: $filename"
continue
fi
log "INFO" "下载进度: $((i+1))/$total_urls - $filename"
# 使用wget下载(后台执行)
if wget -q "$url" -O "$filename" 2>/dev/null; then
log "SUCCESS" "下载成功: $filename"
((downloaded++))
else
log "ERROR" "下载失败: $filename"
((failed++))
fi
# 控制并发数
local current_jobs=$(jobs -r | wc -l)
while [ "$current_jobs" -ge "$max_concurrent" ]; do
sleep 1
current_jobs=$(jobs -r | wc -l)
done
done
# 等待所有后台任务完成
wait
log "INFO" "批量下载完成: $downloaded 成功, $failed 失败"
}
# 主菜单
main_menu() {
while true; do
echo -e "\n${BLUE}=== 文件批量处理工具 ===${NC}"
echo "1. 批量重命名文件"
echo "2. 批量转换文件编码"
echo "3. 文件统计"
echo "4. 目录监控"
echo "5. 批量下载"
echo "6. 退出"
read -p "请选择操作 [1-6]: " choice
case $choice in
1)
read -p "请输入文件模式 (例如: *.txt): " pattern
read -p "请输入文件名前缀: " prefix
batch_rename "$pattern" "$prefix"
;;
2)
read -p "请输入文件模式 (例如: *.txt): " pattern
read -p "请输入源编码 (例如: GBK): " from_enc
read -p "请输入目标编码 (例如: UTF-8): " to_enc
convert_encoding "$pattern" "$from_enc" "$to_enc"
;;
3)
read -p "请输入文件模式 (默认: *): " pattern
file_statistics "${pattern:-*}"
;;
4)
read -p "请输入要监控的目录: " directory
read -p "请输入监控间隔(秒,默认: 5): " interval
monitor_directory "$directory" "${interval:-5}" &
monitor_pid=$!
echo "监控已启动 (PID: $monitor_pid),按回车键停止..."
read
kill "$monitor_pid" 2>/dev/null
log "INFO" "目录监控已停止"
;;
5)
read -p "请输入URL列表文件: " url_file
read -p "请输入最大并发数 (默认: 3): " max_conn
batch_download "$url_file" "${max_conn:-3}"
;;
6)
echo -e "${GREEN}感谢使用文件处理工具,再见!${NC}"
break
;;
*)
echo -e "${RED}无效选择,请重新输入${NC}"
;;
esac
echo
read -p "按回车键继续..."
done
}
# 脚本入口
main() {
init
echo -e "${GREEN}文件批量处理工具已启动${NC}"
log "INFO" "应用程序启动"
main_menu
log "INFO" "应用程序退出"
}
# 参数处理
case "${1:-}" in
"rename")
batch_rename "$2" "$3"
;;
"convert")
convert_encoding "$2" "$3" "$4"
;;
"stats")
file_statistics "$2"
;;
"monitor")
monitor_directory "$2" "$3"
;;
"download")
batch_download "$2" "$3"
;;
"help"|"-h"|"--help")
echo "用法: $0 [command]"
echo "命令:"
echo " rename [pattern] [prefix] 批量重命名文件"
echo " convert [pattern] [from] [to] 转换文件编码"
echo " stats [pattern] 文件统计"
echo " monitor [dir] [interval] 监控目录变化"
echo " download [file] [max_conn] 批量下载"
echo " help 显示帮助信息"
echo ""
echo "示例:"
echo " $0 rename '*.txt' doc"
echo " $0 convert '*.txt' GBK UTF-8"
echo " $0 stats '*.sh'"
echo " $0 monitor /tmp 10"
echo " $0 download urls.txt 5"
;;
*)
main
;;
esac
./file_processor.sh - 启动交互式菜单./file_processor.sh rename '*.txt' document - 批量重命名txt文件./file_processor.sh convert '*.txt' GBK UTF-8 - 转换文件编码./file_processor.sh stats '*.sh' - 统计sh文件./file_processor.sh help - 显示帮助信息
"${array[@]}"遍历数组break提前退出不必要的循环无限循环必须有明确的退出条件。在生产脚本中,应该为无限循环设置超时机制,并提供用户中断的方式(如Ctrl+C)。监控类脚本应该记录状态并支持优雅退出。