流程控制是Shell脚本实现复杂逻辑的核心。本章将详细介绍Shell中的条件语句(if-elif-else)、多分支语句(case)以及各种循环结构(for、while、until),同时深入讲解循环的高级用法、信号处理和超时控制等实用场景,帮助你全面掌握Shell脚本的流程控制能力。
5.1 if-elif-else条件语句
5.1.1 基本语法
Shell中的if语句使用fi作为结束标记,语法结构如下:
bash
# 单分支if
if 条件; then
命令
fi
# 双分支if-else
if 条件; then
命令1
else
命令2
fi
# 多分支if-elif-else
if 条件1; then
命令1
elif 条件2; then
命令2
elif 条件3; then
命令3
else
命令4
fi
5.1.2 条件表达式写法
bash
#!/bin/bash
# 使用 [ ] 进行条件判断
name="zhangsan"
if [ "$name" = "zhangsan" ]; then
echo "名字匹配"
fi
# 使用 [[ ]] 进行条件判断(推荐,支持模式匹配)
name="zhangsan"
if [[ $name == zhang* ]]; then
echo "名字以zhang开头"
fi
# 使用 (( )) 进行数学比较
a=10
b=20
if (( a < b )); then
echo "a小于b"
fi
# 结合多个条件
age=25
if [ $age -ge 18 ] && [ $age -le 60 ]; then
echo "年龄在18到60之间"
fi
# 使用 -a 和 -o(在 [ ] 中)
if [ $age -ge 18 -a $age -le 60 ]; then
echo "年龄在18到60之间"
fi
# 在 [[ ]] 中使用 && 和 ||
if [[ $age -ge 18 && $age -le 60 ]]; then
echo "年龄在18到60之间"
fi
5.1.3 嵌套if语句与多层条件
bash
#!/bin/bash
# 嵌套if语句
num=75
if [ $num -ge 60 ]; then
if [ $num -ge 90 ]; then
echo "优秀"
elif [ $num -ge 80 ]; then
echo "良好"
else
echo "及格"
fi
else
echo "不及格"
fi
# 使用 && 和 || 简化嵌套if
# 等价于上面的嵌套if
[[ $num -ge 90 ]] && echo "优秀" && exit
[[ $num -ge 80 ]] && echo "良好" && exit
[[ $num -ge 60 ]] && echo "及格" || echo "不及格"
5.1.4 if语句的退出状态
if语句实际上是根据命令的退出状态来决定分支执行的:
bash
#!/bin/bash
# if 后跟命令(而非条件表达式)
if grep -q "error" /var/log/syslog 2>/dev/null; then
echo "发现错误"
fi
# if后跟函数返回值
check_service() {
systemctl is-active nginx >/dev/null 2>&1
}
if check_service; then
echo "Nginx服务正在运行"
else
echo "Nginx服务未运行"
fi
# 使用命令的退出状态进行判断
if ! command -v git >/dev/null 2>&1; then
echo "git未安装"
fi
5.2 case多分支语句
5.2.1 基本语法
case语句类似于其他语言中的switch-case,用于多分支选择:
bash
case 变量 in
模式1)
命令1
;; # 类似于break
模式2)
命令2
;;
模式3)
命令3
;;
*) # 默认分支,类似于default
命令4
;;
esac # case的反写
5.2.2 使用通配符模式
bash
#!/bin/bash
# 使用通配符
echo "请输入一个字符:"
read char
case $char in
[a-z])
echo "你输入了小写字母"
;;
[A-Z])
echo "你输入了大写字母"
;;
[0-9])
echo "你输入了数字"
;;
*)
echo "你输入了其他字符"
;;
esac
5.2.3 多个模式匹配
bash
#!/bin/bash
# 多个模式用 | 分隔
echo "请输入月份(1-12):"
read month
case $month in
1|2|3)
echo "春天"
;;
4|5|6)
echo "夏天"
;;
7|8|9)
echo "秋天"
;;
10|11|12)
echo "冬天"
;;
*)
echo "无效的月份"
;;
esac
5.2.4 case实战应用
bash
#!/bin/bash
# 1. 系统服务管理
service_action() {
local action=$1
local service=$2
case $action in
start)
echo "启动 $service"
;;
stop)
echo "停止 $service"
;;
restart)
echo "重启 $service"
;;
status)
echo "查看 $service 状态"
;;
*)
echo "未知操作: $action"
return 1
;;
esac
}
# 2. 文件处理
process_file() {
local filename=$1
local ext="${filename##*.}"
case $ext in
txt)
echo "处理文本文件"
;;
jpg|jpeg|png|gif)
echo "处理图片文件"
;;
mp4|avi|mkv)
echo "处理视频文件"
;;
sh)
echo "处理Shell脚本"
;;
*)
echo "未知文件类型"
;;
esac
}
# 3. 菜单驱动
show_menu() {
echo "1. 显示日期"
echo "2. 显示日历"
echo "3. 显示当前目录"
echo "4. 退出"
}
while true; do
show_menu
read -p "请选择: " choice
case $choice in
1) date ;;
2) cal ;;
3) pwd ;;
4) echo "再见"; break ;;
*) echo "无效选择" ;;
esac
done
5.3 for循环
5.3.1 列表for循环
遍历一系列值:
bash
#!/bin/bash
# 基本列表for循环
for i in 1 2 3 4 5; do
echo "计数器: $i"
done
# 使用花括号生成序列
for i in {1..5}; do
echo "计数器: $i"
done
# 指定步长(使用seq)
for i in $(seq 1 2 10); do
echo "奇数: $i"
done
# 负数步长
for i in $(seq 10 -2 1); do
echo "倒序偶数: $i"
done
5.3.2 C风格for循环
类似C语言的for循环语法:
bash
#!/bin/bash
# C风格for循环
for ((i=1; i<=5; i++)); do
echo "计数器: $i"
done
# 指定步长
for ((i=0; i<=10; i+=2)); do
echo "偶数: $i"
done
# 多个变量
for ((i=1, j=10; i<=5; i++, j--)); do
echo "i=$i, j=$j"
done
# 复杂条件
for ((i=1; i<=100 && j>0; i++)); do
# 循环体
((j--))
done
5.3.3 遍历文件和目录
bash
#!/bin/bash
# 遍历当前目录下的所有文件
for file in *; do
echo "文件: $file"
done
# 遍历特定类型的文件
for file in *.md; do
echo "Markdown文件: $file"
done
# 递归遍历(使用find)
for file in $(find . -type f -name "*.sh"); do
echo "Shell脚本: $file"
done
# 遍历目录
for dir in */; do
echo "目录: $dir"
done
5.3.4 for循环高级用法
bash
#!/bin/bash
# 1. 并行处理(使用&和wait)
echo "=== 并行处理示例 ==="
for i in {1..5}; do
sleep 1 &
done
wait
echo "所有任务完成"
# 2. 遍历数组
fruits=("apple" "banana" "cherry")
for fruit in "${fruits[@]}"; do
echo "水果: $fruit"
done
# 3. 遍历关联数组
declare -A capitals=([China]="Beijing" [Japan]="Tokyo")
for country in "${!capitals[@]}"; do
echo "$country 的首都是 ${capitals[$country]}"
done
# 4. 读取文件每行(避免空格问题)
while IFS= read -r line; do
echo "行: $line"
done < "filename.txt"
# 5. 嵌套循环
for i in {1..3}; do
for j in {1..3}; do
echo "i=$i, j=$j"
done
done
5.4 while/until循环
5.4.1 while循环
条件为真时循环:
bash
#!/bin/bash
# 基本while循环
count=1
while [ $count -le 5 ]; do
echo "计数: $count"
count=$((count + 1))
done
# 读取文件行(逐行处理)
while read line; do
echo "行内容: $line"
done < /etc/hostname
# 无限循环(配合break使用)
count=1
while true; do
echo "无限循环计数: $count"
if [ $count -ge 3 ]; then
break
fi
count=$((count + 1))
done
# 逐行读取文件并处理
while IFS= read -r line || [ -n "$line" ]; do
echo "$line"
done < "file.txt"
5.4.2 until循环
条件为假时循环(直到条件为真):
bash
#!/bin/bash
# 基本until循环
# 直到count大于5才停止
count=1
until [ $count -gt 5 ]; do
echo "计数: $count"
count=$((count + 1))
done
# while和until对比
echo "=== while循环 ==="
i=1
while [ $i -le 3 ]; do
echo "i = $i"
i=$((i + 1))
done
echo "=== until循环 ==="
i=1
until [ $i -gt 3 ]; do
echo "i = $i"
i=$((i + 1))
done
5.4.3 计数器与累加器
bash
#!/bin/bash
# 数字累加计算
sum=0
i=1
while [ $i -le 100 ]; do
sum=$((sum + i))
i=$((i + 1))
done
echo "1+2+...+100 = $sum"
# 读取多个数字并求和
sum=0
while true; do
read -p "请输入数字(0结束): " num
[ "$num" = "0" ] && break
sum=$((sum + num))
echo "当前总和: $sum"
done
echo "最终结果: $sum"
# 计算阶乘
factorial=1
num=5
while [ $num -gt 1 ]; do
factorial=$((factorial * num))
num=$((num - 1))
done
echo "5! = $factorial"
5.5 循环控制:break与continue
5.5.1 break语句
立即退出循环:
bash
#!/bin/bash
# 基本break用法
for i in 1 2 3 4 5; do
if [ $i -eq 3 ]; then
echo "遇到3,退出循环"
break
fi
echo "当前数字: $i"
done
echo "循环结束"
# 嵌套循环中的break
for i in 1 2 3; do
for j in 1 2 3; do
if [ $j -eq 2 ]; then
break # 只退出内层循环
fi
echo "i=$i, j=$j"
done
done
5.5.2 continue语句
跳过当前迭代,继续下一次循环:
bash
#!/bin/bash
# 基本continue用法
for i in 1 2 3 4 5; do
if [ $i -eq 3 ]; then
echo "跳过3"
continue
fi
echo "当前数字: $i"
done
# 过滤特定条件
for i in {1..10}; do
if [ $((i % 2)) -eq 0 ]; then
continue # 跳过偶数
fi
echo "奇数: $i"
done
5.5.3 break与continue的高级用法
bash
#!/bin/bash
# break指定退出层级
for i in 1 2 3; do
for j in 1 2 3; do
if [ $i -eq 2 ] && [ $j -eq 2 ]; then
break 2 # 退出两层循环
fi
echo "i=$i, j=$j"
done
done
# continue跳到指定层级
for i in {1..3}; do
for j in {1..3}; do
if [ $j -eq 2 ]; then
continue # 跳到下一次内层循环
fi
echo "i=$i, j=$j"
done
done
5.6 高级循环技巧
5.6.1 循环中的超时控制
bash
#!/bin/bash
# 使用timeout命令
echo "=== 超时控制示例 ==="
timeout 3 bash -c 'while true; do echo "运行中"; sleep 1; done'
# 使用read超时
echo "请在3秒内输入:"
if read -t 3 -r response; then
echo "你输入了: $response"
else
echo "超时未输入"
fi
# 循环中的超时处理
counter=0
while true; do
sleep 1
counter=$((counter + 1))
if [ $counter -ge 5 ]; then
echo "达到最大循环次数"
break
fi
done
5.6.2 循环中的信号处理
bash
#!/bin/bash
# 捕获Ctrl+C信号
trap 'echo "收到中断信号,退出循环"; break' INT
# 或使用EXIT清理
cleanup() {
echo "清理资源..."
}
trap cleanup EXIT
count=0
while true; do
echo "计数: $count"
sleep 1
count=$((count + 1))
done
# 忽略信号
trap '' INT # 忽略Ctrl+C
echo "按Ctrl+C无效"
sleep 10
trap INT # 恢复信号处理
5.6.3 进度显示
bash
#!/bin/bash
# 简单进度条
progress_bar() {
local current=$1
local total=$2
local width=50
percent=$((current * 100 / total))
completed=$((width * current / total))
remaining=$((width - completed))
printf "\r进度: ["
printf "%${completed}s" | tr ' ' '='
printf "%${remaining}s" | tr ' ' '-'
printf "] %3d%%" $percent
[ $current -eq $total ] && echo ""
}
# 使用进度条
total=20
for i in $(seq 1 $total); do
sleep 0.2
progress_bar $i $total
done
# 旋转指针
spinner() {
local pid=$1
local delay=0.1
local spinstr='|/-\'
while ps -p $pid > /dev/null 2>&1; do
local temp=${spinstr#?}
printf "\r[%c] " "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep $delay
done
printf "\r"
}
# 后台任务配合旋转指针
sleep 10 &
spinner $!
echo "任务完成"
5.6.4 循环中的错误处理
bash
#!/bin/bash
# 遇到错误继续执行
for file in file1.txt file2.txt nonexistent.txt; do
if [ -f "$file" ]; then
echo "处理 $file"
else
echo "警告: $file 不存在,跳过" >&2
continue
fi
done
# 遇到错误退出
set -e # 遇到错误立即退出
for i in {1..5}; do
echo "处理 $i"
if [ $i -eq 3 ]; then
exit 1
fi
done
# 错误计数
error_count=0
for i in {1..10}; do
if [ $((i % 3)) -eq 0 ]; then
echo "错误发生在 $i" >&2
error_count=$((error_count + 1))
else
echo "处理 $i 成功"
fi
done
echo "共发生 $error_count 个错误"
5.7 综合示例
5.7.1 菜单驱动的系统管理脚本
bash
#!/bin/bash
show_menu() {
echo "================================"
echo " 系统管理脚本"
echo "================================"
echo "1. 查看系统信息"
echo "2. 查看磁盘使用"
echo "3. 查看内存使用"
echo "4. 查看进程"
echo "5. 查看网络连接"
echo "6. 批量文件操作"
echo "0. 退出"
echo "================================"
}
show_system_info() {
echo "=== 系统信息 ==="
echo "主机名: $(hostname)"
echo "内核版本: $(uname -r)"
echo "运行时间: $(uptime -p)"
echo "负载: $(cat /proc/loadavg | awk '{print $1, $2, $3}')"
}
show_disk_usage() {
echo "=== 磁盘使用情况 ==="
df -h | grep -v tmpfs
}
show_memory() {
echo "=== 内存使用情况 ==="
free -h
}
show_processes() {
echo "=== 运行中的进程 ==="
ps aux --sort=-%mem | head -n 11
}
show_network() {
echo "=== 网络连接 ==="
netstat -tun | tail -n +3
}
batch_file_operation() {
echo "=== 批量文件操作 ==="
echo "请选择操作1. :"
echo "批量重命名"
echo "2. 批量压缩"
echo "3. 批量复制"
read -p "选择: " choice
case $choice in
1)
read -p "输入文件前缀: " prefix
for f in *; do
[ -f "$f" ] && mv "$f" "${prefix}$f"
done
;;
2)
tar -czf backup.tar.gz *
;;
3)
read -p "目标目录: " dest
cp -r * "$dest"
;;
*)
echo "无效选择"
;;
esac
}
# 主循环
while true; do
show_menu
read -p "请选择 [0-6]: " choice
case $choice in
1) show_system_info ;;
2) show_disk_usage ;;
3) show_memory ;;
4) show_processes ;;
5) show_network ;;
6) batch_file_operation ;;
0) echo "感谢使用,再见!"; exit 0 ;;
*) echo "无效选择,请重新输入" ;;
esac
echo ""
read -p "按回车继续..."
done
5.7.2 批量处理工具
bash
#!/bin/bash
# 批量处理图片文件
# 创建测试目录
mkdir -p /tmp/image_process
cd /tmp/image_process
# 创建测试文件
touch photo1.jpg photo2.jpg photo3.png photo4.jpg screenshot.png
echo "=== 原始文件 ==="
ls -la
echo ""
echo "=== 开始处理 ==="
count=0
skipped=0
errors=0
for file in *; do
# 跳过目录
[ -d "$file" ] && continue
# 获取文件扩展名
ext="${file##*.}"
case $ext in
jpg|jpeg)
echo "[处理] $file -> JPEG图片"
# 这里可以添加实际的图片处理命令
count=$((count + 1))
;;
png)
echo "[处理] $file -> PNG图片"
count=$((count + 1))
;;
gif)
echo "[处理] $file -> GIF图片"
count=$((count + 1))
;;
*)
echo "[跳过] $file -> 未知格式"
skipped=$((skipped + 1))
;;
esac
done
echo ""
echo "=== 处理完成 ==="
echo "共处理 $count 个图片文件"
echo "跳过 $skipped 个文件"
5.7.3 猜数字游戏
bash
#!/bin/bash
# 猜数字游戏
# 生成1-100的随机数
target=$((RANDOM % 100 + 1))
attempts=0
max_attempts=7
echo "================================"
echo " 猜数字游戏"
echo "================================"
echo "我已经想好了一个1-100之间的数字"
echo "你最多可以猜 $max_attempts 次"
echo "================================"
while true; do
read -p "请输入你的猜测: " guess
# 验证输入
if ! [[ $guess =~ ^[0-9]+$ ]]; then
echo "请输入有效的数字!"
continue
fi
# 检查范围
if [ $guess -lt 1 ] || [ $guess -gt 100 ]; then
echo "请输入1-100之间的数字!"
continue
fi
attempts=$((attempts + 1))
# 判断猜测结果
diff=$((guess - target))
diff=${diff#-} # 取绝对值
if [ $guess -lt $target ]; then
if [ $diff -le 10 ]; then
echo "很接近了!再大一点"
else
echo "太小了!再试试"
fi
elif [ $guess -gt $target ]; then
if [ $diff -le 10 ]; then
echo "很接近了!再小一点"
else
echo "太大了!再试试"
fi
else
echo "================================"
echo "恭喜你!猜对了!"
echo "正确答案是 $target"
echo "你一共猜了 $attempts 次"
# 评价
if [ $attempts -le 3 ]; then
echo "太厉害了!你是高手!"
elif [ $attempts -le 5 ]; then
echo "很不错!继续加油!"
else
echo "运气不错!"
fi
echo "================================"
break
fi
# 检查剩余次数
remaining=$((max_attempts - attempts))
if [ $remaining -le 0 ]; then
echo "================================"
echo "游戏结束!"
echo "正确答案是 $target"
echo "你已经猜了 $attempts 次"
echo "================================"
break
else
echo "剩余次数: $remaining"
fi
done
5.7.4 数据处理脚本
bash
#!/bin/bash
# 数据处理脚本 - 处理CSV文件
# 假设CSV文件格式: 姓名,年龄,城市,职业
CSV_FILE="data.csv"
# 创建测试数据
mkdir -p /tmp/csv_process
cat > /tmp/csv_process/data.csv << 'EOF'
张三,25,北京,工程师
李四,30,上海,设计师
王五,28,广州,产品经理
赵六,35,深圳,CEO
孙七,22,杭州,实习生
周八,40,北京,总监
EOF
cd /tmp/csv_process
echo "=== 原始数据 ==="
cat data.csv
echo ""
echo "=== 数据统计 ==="
total_lines=$(wc -l < data.csv)
echo "总记录数: $total_lines"
echo ""
echo "=== 按城市统计 ==="
cut -d',' -f3 data.csv | sort | uniq -c | while read count city; do
echo "$city: $count 人"
done
echo ""
echo "=== 年龄分析 ==="
ages=$(cut -d',' -f2 data.csv)
avg_age=$(echo "$ages" | awk '{sum+=$1} END {print int(sum/NR)}')
max_age=$(echo "$ages" | sort -n | tail -1)
min_age=$(echo "$ages" | sort -n | head -1)
echo "平均年龄: $avg_age"
echo "最大年龄: $max_age"
echo "最小年龄: $min_age"
echo ""
echo "=== 年龄段分布 ==="
for i in {20..40..10}; do
next=$((i + 9))
count=$(awk -F',' -v start=$i -v end=$next '$2>=start && $2<end' data.csv | wc -l)
[ $count -gt 0 ] && echo "$i-$next岁: $count 人"
done
echo ""
echo "=== 数据处理完成 ==="
5.8 本章小结
本章详细介绍了Shell脚本中的流程控制语法:
-
if-elif-else条件语句:
- 单分支、双分支、多分支if语句
- 条件表达式的写法(
[ ]、[[ ]]、(( ))) - 嵌套if语句与多层条件
- if语句的退出状态
-
case多分支语句:
- 基本语法和结束标记(
esac) - 模式匹配(通配符、
|分隔多个模式) - 实战应用(服务管理、文件处理、菜单驱动)
- 基本语法和结束标记(
-
for循环:
- 列表for循环(遍历值、序列、文件)
- C风格for循环
- 遍历文件和命令输出
- 并行处理、遍历数组和关联数组
-
while/until循环:
- while循环(条件为真循环)
- until循环(条件为假循环)
- 读取文件内容
- 计数器与累加器
-
循环控制:
break:立即退出循环continue:跳过当前迭代break n:退出多层循环
-
高级循环技巧:
- 超时控制
- 信号处理
- 进度显示
- 错误处理
掌握流程控制语法后,你将能够编写具有复杂逻辑判断和循环处理的Shell脚本,实现各种自动化任务。
更多内容,欢迎访问南徽玉的个人博客