强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 后忘记 thenif [[ x ]] echo ... 语法错误if [[ x ]]; then echo ...; fi
for 中 word splittingfor f in $(ls) 文件名有空格时出错使用 for f in *while read
管道中的 while变量在子 Shell 中修改无效使用 < <() 进程替换
case 缺少 ;;导致 fall-through每个分支末尾加 ;;
select 无法退出输入 EOF (Ctrl+D) 才退出加退出选项 + break
空 glob*.txt 无匹配时保留字面值shopt -s nullglob

5.10 扩展阅读