Shell 脚本中的循环与流程控制:从基础到复杂逻辑实现 循环和流程控制是 Shell 脚本实现复杂逻辑的核心机制。无论是批量处理文件、迭代数组元素,还是实现条件分支,都离不开灵活的流程控制结构。本文将系统介绍 Shell 中的循环类型、条件判断和流程控制技巧,帮助你编写高效、清晰的脚本逻辑。 一、条件判断:Shell 中的逻辑分支 条件判断是流程控制的基础,Shell 提供了多种方式实现条件检查:
- 基本 if 语句结构 bash #!/bin/bash
基本 if 结构
age=25 if [ $age -ge 18 ]; then echo "已成年" fi
if-else 结构
if [ $age -ge 65 ]; then echo "已退休" else echo "未退休" fi
if-elif-else 结构
score=85 if [ $score -ge 90 ]; then echo "优秀" elif [ $score -ge 80 ]; then echo "良好" elif [ $score -ge 60 ]; then echo "及格" else echo "不及格" fi 2. 条件表达式的类型 Shell 支持多种条件表达式,用于不同场景的判断: bash #!/bin/bash
file="example.txt" str="hello" num=10
文件判断
if [ -f "file 是普通文件"
fi
if [ -d "/tmp" ]; then # 目录存在 echo "/tmp 是目录" fi
if [ -r "file 可读"
fi
字符串判断
if [ -z "$str" ]; then # 字符串为空 echo "字符串为空" else echo "字符串不为空" fi
if [ "$str" = "hello" ]; then # 字符串相等 echo "字符串匹配" fi
数值判断
if [ num 大于 5"
fi
if [ num 等于 10"
fi
3. 高级条件表达式:[[]] 与 (( ))
Bash 提供了更强大的条件表达式语法:
bash
#!/bin/bash
[[ ]] 支持模式匹配和正则表达式
name="John Doe" if [[ $name == J* ]]; then # 模式匹配(以J开头) echo "姓名以J开头" fi
email="john@example.com"
if [[ ]]; then
echo "邮箱格式有效"
fi
(( )) 支持更直观的算术运算
num=15 if (( num > 10 && num < 20 )); then # 算术表达式 echo "$num 在 10-20 之间" fi
变量自增
(( num++ )) echo "自增后: $num" 二、循环结构:重复执行的艺术 Shell 提供了多种循环结构,适用于不同的迭代场景:
- for 循环:遍历列表元素 bash #!/bin/bash
遍历直接列出的元素
for fruit in apple banana cherry date; do echo "水果: $fruit" done
遍历数组
fruits=(apple banana cherry date) for fruit in "${fruits[@]}"; do echo "数组中的水果: $fruit" done
遍历命令输出
echo -e "\n当前目录中的.sh文件:" for file in $(ls *.sh); do echo "- $file" done
C风格的for循环
echo -e "\n计数到5:" for (( i=1; i<=5; i++ )); do echo "计数: $i" done 2. while 循环:条件满足时持续执行 bash #!/bin/bash
基本while循环
count=1 echo "计数到3:" while [ $count -le 3 ]; do echo "计数: $count" (( count++ )) # 计数自增 done
读取文件内容
echo -e "\n读取文件内容:" while IFS= read -r line; do echo "行内容: $line" done < "example.txt" # 从文件读取
无限循环(配合break使用)
echo -e "\n输入q退出:" while true; do read -p "请输入内容: " input if [ "$input" = "q" ]; then break # 退出循环 fi echo "你输入了: $input" done 3. until 循环:条件不满足时持续执行 bash #!/bin/bash
until循环:条件为假时执行
count=5 echo "从5倒数到1:" until [ $count -eq 0 ]; do echo "计数: $count" (( count-- )) done
结合break和continue
echo -e "\n跳过偶数:" num=0 until [ $num -gt 10 ]; do (( num++ )) if (( num % 2 == 0 )); then continue # 跳过偶数 fi echo "奇数: $num" done 4. 循环控制:break 与 continue bash #!/bin/bash
break:跳出整个循环
echo "找到3就停止:" for i in {1..5}; do if [ $i -eq 3 ]; then break fi echo "数字: $i" done
continue:跳过当前迭代
echo -e "\n跳过3:" for i in {1..5}; do if [ $i -eq 3 ]; then continue fi echo "数字: $i" done
嵌套循环中的break
echo -e "\n嵌套循环:" for i in {1..3}; do echo "外部循环: $i" for j in {1..3}; do if [ $j -eq 2 ]; then break # 只跳出内部循环 fi echo " 内部循环: $j" done done 三、循环与条件的组合:实现复杂逻辑 将循环与条件判断结合,可以实现更复杂的业务逻辑:
- 批量文件处理 bash #!/bin/bash
批量处理目录中的图片文件
image_dir="./images" output_dir="./resized_images"
创建输出目录
mkdir -p "$output_dir"
遍历所有图片文件
for file in "file" ] || continue
# 获取文件名(不含路径)
filename=$(basename "$file")# 检查文件是否已经处理过
if [ -f "$output_dir/$filename" ]; thenecho "已处理: $filename,跳过"continue
fi# 处理图片(这里使用convert工具,需要imagemagick)
echo "处理: $filename"
convert "$file" -resize 800x600 "$output_dir/$filename" || {echo "处理 $filename 失败" >&2continue
}
done
echo "批量处理完成" 2. 数据过滤与转换 bash #!/bin/bash
过滤并转换数据文件
input_file="raw_data.txt" output_file="processed_data.txt"
清空输出文件
"$output_file"
echo "开始处理数据..." line_count=0 valid_count=0
读取并处理每一行
while IFS= read -r line; do (( line_count++ ))
# 跳过空行
if [ -z "$line" ]; thencontinue
fi# 跳过注释行
if [[ $line =~ ^# ]]; thencontinue
fi# 提取需要的字段(假设用逗号分隔)
id=$(echo "$line" | cut -d ',' -f 1)
value=$(echo "$line" | cut -d ',' -f 3)# 验证ID格式
if ! [[ $id =~ ^[A-Z]{2}[0-9]{4}$ ]]; thenecho "警告: 行 $line_count 包含无效ID: $id" >&2continue
fi# 验证数值范围
if ! [[ $value =~ ^[0-9]+(\.[0-9]+)?$ ]] || (( $(echo "$value > 100" | bc -l) )); thenecho "警告: 行 $line_count 包含无效值: $value" >&2continue
fi# 处理有效数据(转换为整数)
rounded_value=$(printf "%.0f" "$value")# 写入输出文件
echo "$id,$rounded_value" >> "$output_file"
(( valid_count++ ))
done < "$input_file"
echo "处理完成: 共 $line_count 行,有效 $valid_count 行" echo "结果保存到: $output_file" 四、高级流程控制:函数与循环的协同 将循环逻辑封装到函数中,可以提高代码的可维护性:
- 循环函数:处理可复用逻辑 bash #!/bin/bash
检查目录中的文件是否符合命名规范
check_filename_conventions() { local dir="$1" local pattern="$2" local error_count=0
# 验证目录存在
if [ ! -d "$dir" ]; thenecho "错误: 目录 $dir 不存在" >&2return 1
fiecho "检查 $dir 中的文件命名规范..."# 遍历目录中的文件
while IFS= read -r -d '' file; dofilename=$(basename "$file")# 检查文件名是否匹配模式if ! [[ $filename =~ $pattern ]]; thenecho "不符合规范: $filename" >&2(( error_count++ ))fi
done < <(find "$dir" -type f -print0) # 使用-print0处理含空格的文件名echo "检查完成,发现 $error_count 个不符合规范的文件"
return $error_count
}
主程序
if [ $# -ne 1 ]; then echo "用法: $0 <目录>" exit 1 fi
检查图片文件(假设规范是小写字母、数字和下划线,.jpg或.png结尾)
check_filename_conventions "'
根据检查结果采取行动
if [ $? -eq 0 ]; then echo "所有文件符合命名规范" else echo "请修正不符合规范的文件命名" >&2 # exit 1 # 如需严格检查,可取消注释 fi 2. 递归循环:处理层级结构 bash #!/bin/bash
递归计算目录大小
calculate_dir_size() { local dir="$1" local total_size=0 local item
# 遍历目录中的所有项目
for item in "$dir"/*; do# 跳过不存在的项目(空目录)[ -e "$item" ] || continueif [ -d "$item" ]; then# 递归处理子目录subdir_size=$(calculate_dir_size "$item")total_size=$(( total_size + subdir_size ))elif [ -f "$item" ]; then# 累加文件大小filesize=$(stat -c %s "$item")total_size=$(( total_size + filesize ))fi
done# 显示当前目录大小(转换为KB)
echo "$dir: $(( total_size / 1024 )) KB"
return $total_size
}
主程序
if [ $# -ne 1 ] || [ ! -d "$1" ]; then echo "用法: $0 <目录>" exit 1 fi
echo "计算目录大小(递归):" total=$(calculate_dir_size "$1") echo "总大小: $(( total / 1024 / 1024 )) MB" 五、企业级脚本中的流程控制模式 在大型脚本中,合理组织循环和条件判断能显著提升可读性和可维护性: bash #!/bin/bash
企业级备份脚本:展示复杂流程控制
set -euo pipefail
配置
BACKUP_DIR="/var/backups" SOURCES=("/home" "/etc" "/var/www") RETENTION_DAYS=7 LOG_FILE="/var/log/backup_script.log"
日志函数
log() {
local level=$1
local message=(date +"%Y-%m-%d %H:%M:%S")
echo "[
level]
LOG_FILE"
echo "[$level] $message" # 同时输出到控制台
}
检查依赖工具
check_dependencies() { log "INFO" "检查必要工具..." local dependencies=("rsync" "gzip" "find")
for dep in "${dependencies[@]}"; doif ! command -v "$dep" >/dev/null 2>&1; thenlog "ERROR" "缺少必要工具: $dep"exit 1fi
done
}
执行备份
perform_backup() {
local source=(basename "
BACKUP_DIR/
(date +%Y%m%d).tar.gz"
log "INFO" "开始备份 $source 到 $backup_path"# 创建压缩备份
if tar -czf "$backup_path" -C "$(dirname "$source")" "$(basename "$source")"; thenlog "INFO" "$source 备份成功"return 0
elselog "ERROR" "$source 备份失败"# 清理失败的备份文件if [ -f "$backup_path" ]; thenrm "$backup_path"fireturn 1
fi
}
清理旧备份
cleanup_old_backups() { log "INFO" "清理 $RETENTION_DAYS 天前的旧备份..."
# 查找并删除旧备份
find "$BACKUP_DIR" -type f -name "*.tar.gz" \-mtime +"$RETENTION_DAYS" -print0 | while IFS= read -r -d '' file; dolog "INFO" "删除旧备份: $file"rm "$file"
done
}
主函数
main() { log "INFO" "===== 备份脚本开始执行 ====="
# 检查依赖
check_dependencies# 检查备份目录
if [ ! -d "$BACKUP_DIR" ]; thenlog "INFO" "创建备份目录: $BACKUP_DIR"mkdir -p "$BACKUP_DIR"
fi# 备份每个源目录
local failed=0
for source in "${SOURCES[@]}"; doif [ ! -d "$source" ]; thenlog "WARN" "源目录 $source 不存在,跳过"continuefiif ! perform_backup "$source"; thenfailed=$(( failed + 1 ))fi
done# 清理旧备份
cleanup_old_backups# 总结结果
if [ $failed -eq 0 ]; thenlog "INFO" "所有备份任务成功完成"
elselog "ERROR" "有 $failed 个备份任务失败"exit 1
filog "INFO" "===== 备份脚本执行结束 ====="
}
启动主函数
main "(check_validation "
is_valid" = "true" ])
健壮性考虑
循环处理文件时,始终检查文件是否存在
处理用户输入的循环需设置超时或最大迭代次数
嵌套循环层级不超过 3 层,否则考虑重构
常见陷阱避免
注意文件名中的空格和特殊字符(始终用引号包裹变量)
for 循环遍历命令输出时注意字段分隔符(IFS)
管道中的 while 循环在子 shell 中执行,变量修改不会影响外部
掌握这些循环和流程控制技巧,能让你编写的 Shell 脚本从简单的命令堆砌升级为结构化的程序。良好的流程控制设计不仅能实现复杂逻辑,还能保证脚本的可读性和可维护性,这在企业级脚本开发中尤为重要。记住,最适合的控制结构是既能清晰表达逻辑,又能让其他开发者轻松理解的结构。