Bash 脚本编写教程 / 05 - 流程控制
05 - 流程控制
5.1 条件判断:if / elif / else
基本语法
# 单分支
if [[ condition ]]; then
# 代码块
fi
# 双分支
if [[ condition ]]; then
# 条件为真
else
# 条件为假
fi
# 多分支
if [[ condition1 ]]; then
# 条件1
elif [[ condition2 ]]; then
# 条件2
elif [[ condition3 ]]; then
# 条件3
else
# 兜底
fi
三种条件语法对比
# 1. [ ] —— POSIX 兼容,旧语法
# 注意:必须有空格!变量需要加引号!
if [ "$name" = "hello" ]; then
echo "匹配"
fi
# 2. [[ ]] —— Bash 扩展,推荐
# 支持正则、通配符,不需要对变量加引号(但仍建议加)
if [[ "$name" == "hello" ]]; then
echo "匹配"
fi
# 3. (( )) —— 算术判断
if ((a > 10 && b < 20)); then
echo "在范围内"
fi
实用模式
# 多条件组合
if [[ -f "$file" && -r "$file" && -s "$file" ]]; then
echo "文件存在、可读、非空"
fi
# 命令退出码判断
if ping -c 1 -W 3 "google.com" &>/dev/null; then
echo "网络连通"
else
echo "网络不通"
fi
# 变量是否为空
if [[ -n "${variable:-}" ]]; then
echo "变量非空: $variable"
fi
# 命令存在性检查
if command -v docker &>/dev/null; then
echo "Docker 已安装: $(docker --version)"
fi
# 简写条件(一行)
[[ -f "/etc/hosts" ]] && echo "存在" || echo "不存在"
# ⚠️ 注意:以上简写不等同于 if-else
# 当 && 后面的命令失败时,|| 也会执行
# 安全写法:
result="不存在"
[[ -f "/etc/hosts" ]] && result="存在"
echo "$result"
5.2 多路分支:case
case "$variable" in
pattern1)
# 代码块
;;
pattern2)
# 代码块
;;
pattern3|pattern4) # 多个模式用 | 分隔
# 代码块
;;
*)
# 默认分支(相当于 else)
;;
esac
case 中的模式匹配
# 通配符匹配
read -rp "请输入文件名: " filename
case "$filename" in
*.tar.gz)
echo "Tarball 压缩包"
;;
*.zip)
echo "ZIP 压缩包"
;;
*.jpg|*.jpeg|*.png|*.gif)
echo "图片文件"
;;
*.md|*.txt)
echo "文本文件"
;;
*)
echo "未知文件类型"
;;
esac
# 数字匹配
read -rp "请选择操作 [1-5]: " choice
case "$choice" in
1) echo "查看状态" ;;
2) echo "启动服务" ;;
3) echo "停止服务" ;;
4) echo "重启服务" ;;
5) echo "查看日志" ;;
*) echo "无效选项"; exit 1 ;;
esac
# 字符串匹配(忽略大小写)
shopt -s nocasematch
read -rp "确认执行?(yes/no): " confirm
case "$confirm" in
y|yes) echo "执行中..." ;;
n|no) echo "已取消"; exit 0 ;;
*) echo "无效输入"; exit 1 ;;
esac
shopt -u nocasematch
# 范围匹配(使用 [ ] 字符类)
read -rp "输入字符: " char
case "$char" in
[a-z]) echo "小写字母" ;;
[A-Z]) echo "大写字母" ;;
[0-9]) echo "数字" ;;
*) echo "其他字符" ;;
esac
# 嵌套 case
case "$OS" in
Linux)
case "$DISTRO" in
Ubuntu) echo "安装 apt 包" ;;
CentOS) echo "安装 yum 包" ;;
*) echo "未知发行版" ;;
esac
;;
Darwin)
echo "安装 brew 包"
;;
*)
echo "不支持的操作系统"
exit 1
;;
esac
5.3 数字范围循环:for
列表式 for
# 基本列表
for fruit in apple banana cherry; do
echo "水果: $fruit"
done
# 花括号展开
for i in {1..10}; do
echo "数字: $i"
done
# 带步长的花括号展开
for i in {0..100..5}; do
echo "$i" # 0, 5, 10, ..., 100
done
# 字母范围
for c in {a..z}; do
printf "%s " "$c"
done
echo
# 命令输出作为列表
for user in $(cat /etc/passwd | cut -d: -f1); do
echo "用户: $user"
done
# ⚠️ 文件名遍历(推荐 glob)
for file in *.txt; do
[[ -e "$file" ]] || continue # 没有匹配文件时跳过
echo "处理: $file"
done
C 风格 for
# C 风格 for 循环
for ((i = 0; i < 10; i++)); do
echo "i = $i"
done
# 多变量
for ((i = 0, j = 10; i < j; i++, j--)); do
echo "i=$i, j=$j"
done
# 无限循环
for ((;;)); do
echo "按 Ctrl+C 退出"
sleep 1
done
5.4 条件循环:while / until
while 循环
# 基本 while
count=0
while [[ $count -lt 5 ]]; do
echo "count = $count"
((count++))
done
# 读取文件每一行(推荐方式)
while IFS= read -r line; do
echo "行: $line"
done < /etc/hosts
# 读取文件(去掉行尾换行)
while IFS= read -r line || [[ -n "$line" ]]; do
echo "行: $line"
done < "data.txt"
# 读取命令输出
while IFS=: read -r user _ uid _ _ home shell; do
if [[ $uid -ge 1000 && $uid -lt 65534 ]]; then
echo "$user ($uid): $home -> $shell"
fi
done < /etc/passwd
# 带超时的重试循环
max_retries=5
retry_count=0
until curl -sf "https://api.example.com/health" &>/dev/null; do
((retry_count++))
if [[ $retry_count -ge $max_retries ]]; then
echo "服务未就绪,已重试 $max_retries 次"
exit 1
fi
echo "等待服务就绪... ($retry_count/$max_retries)"
sleep 5
done
echo "服务就绪!"
# 管道读取
cat /etc/passwd | while read -r line; do
echo "$line"
done
# ⚠️ 注意:管道中的 while 在子 Shell 中运行
until 循环
# until:条件为假时循环(与 while 相反)
count=0
until [[ $count -ge 5 ]]; do
echo "count = $count"
((count++))
done
# 等待文件出现
until [[ -f "/tmp/ready.flag" ]]; do
echo "等待就绪信号..."
sleep 1
done
echo "就绪!"
5.5 交互式菜单:select
#!/bin/bash
# select 自动创建编号菜单
PS3="请选择操作: "
options=("查看状态" "启动服务" "停止服务" "重启服务" "退出")
select opt in "${options[@]}"; do
case "$opt" in
"查看状态")
systemctl status nginx
;;
"启动服务")
sudo systemctl start nginx
echo "服务已启动"
;;
"停止服务")
sudo systemctl stop nginx
echo "服务已停止"
;;
"重启服务")
sudo systemctl restart nginx
echo "服务已重启"
;;
"退出")
echo "再见!"
break
;;
*)
echo "无效选项: $REPLY"
;;
esac
done
输出效果:
1) 查看状态
2) 启动服务
3) 停止服务
4) 重启服务
5) 退出
请选择操作: 2
服务已启动
5.6 break 和 continue
# break:跳出当前循环
for i in {1..100}; do
if [[ $i -gt 10 ]]; then
echo "到达 10,退出循环"
break
fi
echo "$i"
done
# break N:跳出 N 层循环
for i in {1..5}; do
for j in {1..5}; do
if [[ $j -eq 3 ]]; then
break 2 # 跳出外层循环
fi
echo "i=$i, j=$j"
done
done
# continue:跳过当前迭代
for i in {1..10}; do
if ((i % 2 == 0)); then
continue # 跳过偶数
fi
echo "$i" # 只输出奇数
done
# continue N:跳到外层循环的下一次迭代
for i in {1..3}; do
for j in {1..5}; do
if [[ $j -eq 3 ]]; then
continue 2 # 跳到外层下一次迭代
fi
echo "i=$i, j=$j"
done
done
5.7 循环与重定向
# while 循环 + 重定向输出到文件
while IFS= read -r line; do
echo "处理: $line"
done < input.txt > output.txt
# for 循环 + 重定向
for file in *.csv; do
[[ -e "$file" ]] || continue
tail -n +2 "$file" # 跳过表头
done > all_data.csv
# 将循环输出重定向到变量
result=$(
for i in {1..5}; do
echo "item$i"
done
)
echo "$result"
5.8 业务场景:交互式部署脚本
#!/bin/bash
# deploy.sh —— 交互式部署脚本
set -euo pipefail
readonly APP_NAME="myapp"
readonly DEPLOY_DIR="/opt/$APP_NAME"
# 日志函数
log() { echo "[$(date '+%H:%M:%S')] $*"; }
warn() { echo "[$(date '+%H:%M:%S')] ⚠️ $*"; }
error() { echo "[$(date '+%H:%M:%S')] ❌ $*" >&2; }
# 选择环境
echo "================================"
echo " $APP_NAME 部署工具"
echo "================================"
echo ""
PS3="请选择目标环境: "
select env in "开发(dev)" "测试(test)" "生产(prod)" "退出"; do
case "$env" in
"开发(dev)") TARGET_ENV="dev"; break ;;
"测试(test)") TARGET_ENV="test"; break ;;
"生产(prod)") TARGET_ENV="prod"; break ;;
"退出") echo "已取消"; exit 0 ;;
*) echo "无效选项" ;;
esac
done
# 生产环境二次确认
if [[ "$TARGET_ENV" == "prod" ]]; then
echo ""
warn "即将部署到生产环境!"
read -rp "请输入环境名称 'prod' 确认: " confirm
if [[ "$confirm" != "prod" ]]; then
error "确认失败,已取消部署"
exit 1
fi
fi
log "目标环境: $TARGET_ENV"
# 构建步骤
steps=("拉取代码" "运行测试" "构建镜像" "部署服务" "健康检查")
for ((i = 0; i < ${#steps[@]}; i++)); do
step="${steps[$i]}"
progress=$(( (i + 1) * 100 / ${#steps[@]} ))
log "[$progress%] $step..."
case "$step" in
"拉取代码")
# git pull --rebase origin main
sleep 1
;;
"运行测试")
# make test
sleep 1
;;
"构建镜像")
# docker build -t "$APP_NAME:$TARGET_ENV" .
sleep 1
;;
"部署服务")
# docker compose -f "docker-compose.$TARGET_ENV.yml" up -d
sleep 1
;;
"健康检查")
# 等待服务就绪
retries=10
while ((retries > 0)); do
if curl -sf "http://localhost:8080/health" &>/dev/null; then
break
fi
((retries--))
sleep 2
done
if ((retries == 0)); then
error "健康检查失败!"
exit 1
fi
;;
esac
log "[$progress%] ✅ $step 完成"
done
echo ""
log "🎉 部署完成!环境: $TARGET_ENV"
5.9 注意事项
| 陷阱 | 说明 | 解决方案 |
|---|---|---|
if 后忘记 then | if [[ x ]] echo ... 语法错误 | if [[ x ]]; then echo ...; fi |
for 中 word splitting | for f in $(ls) 文件名有空格时出错 | 使用 for f in * 或 while read |
| 管道中的 while | 变量在子 Shell 中修改无效 | 使用 < <() 进程替换 |
case 缺少 ;; | 导致 fall-through | 每个分支末尾加 ;; |
select 无法退出 | 输入 EOF (Ctrl+D) 才退出 | 加退出选项 + break |
| 空 glob | *.txt 无匹配时保留字面值 | shopt -s nullglob |