Shell脚本精读 · S06-03 | 条件与控制流综合:读 30 行脚本的判断链

模块 :S06 控制流

篇号 :S06-03 / 42

预计阅读 :55 分钟

主线 :Bash(必读三星 · 读脚本能力核心篇)


文章目录

    • 本篇目标
    • [30 秒速览](#30 秒速览)
    • 正文
      • [1. 读判断链的四步](#1. 读判断链的四步)
      • [2. 样例脚本(全文)](#2. 样例脚本(全文))
      • [3. 分块读法](#3. 分块读法)
        • [3.1 骨架(第 1~4 行)](#3.1 骨架(第 1~4 行))
        • [3.2 选项解析(第 6~18 行)](#3.2 选项解析(第 6~18 行))
        • [3.3 参数个数(第 20~21 行)](#3.3 参数个数(第 20~21 行))
        • [3.4 文件存在(第 23 行)](#3.4 文件存在(第 23 行))
        • [3.5 类型分支(第 25~29 行)--- `case` 模式](#3.5 类型分支(第 25~29 行)— case 模式)
        • [3.6 校验和链(第 31~41 行)--- 嵌套 `if` + `(( FORCE ))`](#3.6 校验和链(第 31~41 行)— 嵌套 if + (( FORCE )))
        • [3.7 收尾(第 43~44 行)](#3.7 收尾(第 43~44 行))
      • [4. 代入三组输入:走读结果](#4. 代入三组输入:走读结果)
        • [场景 A](#场景 A)
        • [场景 B](#场景 B)
        • [场景 C](#场景 C)
      • [5. 判断链「形状」速查](#5. 判断链「形状」速查)
      • [6. 第二段练习:更短的判断链(15 行)](#6. 第二段练习:更短的判断链(15 行))
      • [7. 与 S04、S05 的对照表](#7. 与 S04、S05 的对照表)
      • [8. 读脚本时易漏的点](#8. 读脚本时易漏的点)
    • 读脚本检查清单
    • 练习
      • [练习 1:场景走读(样例脚本)](#练习 1:场景走读(样例脚本))
      • [练习 2:场景走读(第 6 节短脚本)](#练习 2:场景走读(第 6 节短脚本))
      • [练习 3:找问题(改错题)](#练习 3:找问题(改错题))
      • [练习 4:画判断链](#练习 4:画判断链)
      • [练习 5:改写](#练习 5:改写)
      • [练习 6:读脚本选择题](#练习 6:读脚本选择题)
    • [S06 模块小结](#S06 模块小结)
    • 下一篇预告

本篇目标

S04~S06 串起来:面对一段 二三十行 的 Bash 脚本,能自上而下 理清「参数怎么验、选项怎么解析、文件怎么判、失败往哪走」。会用本篇的读法步骤判断链图谱,独立回答「给定输入会执行哪条分支、退出码大概多少」。


30 秒速览

  • 先找 脚本入口三件套set -euousage/参数个数case 选项循环
  • if / case / && || 常叠在同一段里:短路与多分支分工不同。
  • 读分支时标 真/假 → 走 then 还是 else/下一 elif,不要按「看起来像」猜。
  • [[ 模式(( )) 数值[ -f ] 文件 混用时,先分清每一条测的是什么
  • 提前 exit 比深层嵌套更常见;看到 || { ...; exit; } 要当成一整条「失败即停」。

正文

1. 读判断链的四步

步骤 做什么
1. 划块 按空行/注释分成:选项解析、参数校验、业务分支、清理
2. 标入口 谁决定「能不能继续」?(( $# ))[[ -f ]]case *)
3. 追退出 每个 exit、`
4. 代入 用一组具体参数在纸上走一遍(本篇练习核心)

下面用一段 约 35 行、风格接近真实运维/发布脚本的例子,逐步走读。


2. 样例脚本(全文)

bash 复制代码
#!/usr/bin/env bash
set -euo pipefail

usage() { echo "usage: $0 [-fv] <artifact>" >&2; exit 2; }

VERBOSE=0
FORCE=0
while (( $# > 0 )); do
  case "$1" in
    -h|--help) usage ;;
    -v|--verbose) VERBOSE=1; shift ;;
    -f|--force) FORCE=1; shift ;;
    --) shift; break ;;
    -*) echo "unknown option: $1" >&2; exit 1 ;;
    *) break ;;
  esac
done

(( $# == 1 )) || usage
artifact=$1

[[ -f "$artifact" ]] || { echo "not found: $artifact" >&2; exit 1; }

case "$artifact" in
  *.tar.gz) ;;
  *.zip) echo "zip not supported" >&2; exit 1 ;;
  *) echo "unknown artifact type" >&2; exit 1 ;;
esac

if [[ -f "${artifact}.sha256" ]]; then
  if sha256sum -c "${artifact}.sha256" >/dev/null 2>&1; then
    echo "checksum ok"
  elif (( FORCE )); then
    echo "warn: checksum failed (forced)" >&2
  else
    echo "checksum failed" >&2
    exit 1
  fi
else
  (( FORCE )) || { echo "missing ${artifact}.sha256 (use -f)" >&2; exit 1; }
fi

(( VERBOSE )) && set -x
echo "ready: $artifact"

建议先通读一遍,再对照下面分块说明。


3. 分块读法

3.1 骨架(第 1~4 行)
bash 复制代码
set -euo pipefail
  • 任一命令非 0 可能直接退出(少数例外,S01-04)。
  • 后面若写 grep ... || true,是在主动抵消 set -e

usage 以退出码 2 结束(惯例:用法错误)。


3.2 选项解析(第 6~18 行)
bash 复制代码
while (( $# > 0 )); do
  case "$1" in
    ...
  esac
done
分支 行为
`-h --help`
-v / -f 置标志位,shift 吃掉选项
-- shiftbreak,后面参数留给位置参数
`-*) 未知选项,exit 1
*) 非选项,break 出循环

读点casewhile (( $# > 0 )) 配合;shift 只发生在选项分支里,位置参数 release.tar.gz*)不 shift ,留在 $1


3.3 参数个数(第 20~21 行)
bash 复制代码
(( $# == 1 )) || usage
artifact=$1
  • 条件为假 → 执行 usage(退出 2)。
  • 为真 → 继续,恰好一个位置参数。

3.4 文件存在(第 23 行)
bash 复制代码
[[ -f "$artifact" ]] || { echo "not found: ..."; exit 1; }
  • [[ -f ]] 成功 → 短路,{ ... }
  • 失败 → 打印并 exit 1

等价于:

bash 复制代码
if [[ ! -f "$artifact" ]]; then
  echo "not found: $artifact" >&2
  exit 1
fi

3.5 类型分支(第 25~29 行)--- case 模式
bash 复制代码
case "$artifact" in
  *.tar.gz) ;;
  *.zip) ... exit 1 ;;
  *) ... exit 1 ;;
esac
  • 只接受 .tar.gz 后缀(Shell 模式,不是正则)。
  • app.zip → 第二支,exit 1
  • app.bin*)exit 1

3.6 校验和链(第 31~41 行)--- 嵌套 if + (( FORCE ))

外层 : sidecar 文件 ${artifact}.sha256 是否存在。

情况 内层逻辑
.sha256 sha256sum -c 成功 → 打印 ok
有,校验失败 (( FORCE )) 为真 → 警告继续;否则 exit 1
.sha256 (( FORCE )) 为真 → 跳过;否则 exit 1

读点

  • 内层 if sha256sum -cif 后面是命令,看退出码。
  • elif (( FORCE )) :整数标志,不是字符串 "1"
  • else 分支 里的 (( FORCE )) || { ... exit 1; }:FORCE=0 时失败并退出。

3.7 收尾(第 43~44 行)
bash 复制代码
(( VERBOSE )) && set -x
echo "ready: $artifact"
  • 仅当 VERBOSE=1 时打开跟踪。
  • 能执行到这里 → 前面分支都没 exit ,脚本正常结束(退出码 0,来自最后 echo 成功)。

4. 代入三组输入:走读结果

场景 A
bash 复制代码
./check.sh -v release.tar.gz
# 且 release.tar.gz、release.tar.gz.sha256 存在,校验通过
阶段 结果
选项 -v → VERBOSE=1;release.tar.gz 留在 $1
个数 $#==1
文件 -f
case 匹配 *.tar.gz,空分支继续
校验 sha256sum -c 成功 → checksum ok
结尾 set -x 执行 → 打印 ready: release.tar.gz
场景 B
bash 复制代码
./check.sh -f broken.tar.gz
# broken.tar.gz 存在,有 .sha256 但内容不匹配
阶段 结果
FORCE=1 校验失败走 elif (( FORCE )) → 警告,不 exit
结尾 打印 ready: broken.tar.gz(带警告)
场景 C
bash 复制代码
./check.sh app.zip
阶段 结果
case *.zip 支 → exit 1(到不了 checksum)

5. 判断链「形状」速查

读脚本时常见结构,见到可对号入座:

复制代码
┌─────────────────────────────────────┐
│  (( $# )) / : "${1:?}"  参数门禁    │
└─────────────────┬───────────────────┘
                  ▼
┌─────────────────────────────────────┐
│  while + case  选项解析               │
└─────────────────┬───────────────────┘
                  ▼
┌─────────────────────────────────────┐
│  [[ -f ]] / [ -d ]  路径存在         │
└─────────────────┬───────────────────┘
                  ▼
┌─────────────────────────────────────┐
│  case 模式 / [[ == pat ]]  类型分支  │
└─────────────────┬───────────────────┘
                  ▼
┌─────────────────────────────────────┐
│  if 命令 / if [[ ]] / if (( ))      │
│  业务逻辑 + exit                    │
└─────────────────────────────────────┘
形状 示例 含义
门禁 `(( $# >= 1 ))
存在则做 [[ -f f ]] && cmd 成功才继续
失败则停 `cmd
多值分发 case "$cmd" in ... esac 子命令/类型
嵌套 if 外层文件、内层命令退出码 两层条件
标志位 (( FORCE )) 放宽检查 -f 选项对应

6. 第二段练习:更短的判断链(15 行)

bash 复制代码
#!/usr/bin/env bash
mode=${1:-}
file=${2:-}

if [[ -z "$mode" || -z "$file" ]]; then
  echo "usage: $0 <check|run> <file>" >&2
  exit 2
fi

case "$mode" in
  check)
    [[ -r "$file" ]] || exit 1
    grep -q . "$file" || { echo "empty"; exit 1; }
    echo "ok"
    ;;
  run)
    [[ -x "$file" ]] && exec "$file" || exit 1
    ;;
  *)
    echo "bad mode" >&2
    exit 1
    ;;
esac

读点速记

  • 第 4 行:一个 [[|| (S05-03),两个 -z 任一为空则 usage。
  • check[[ -r ]]grep -q 两道关;grep 无匹配 → 退出码 1 → 走 || { echo empty; exit 1 }
  • run[[ -x ]] && exec ,不可执行则 || exit 1
  • *):模式不在列表 → exit 1。

7. 与 S04、S05 的对照表

你看到的代码 模块 问什么
cmd1 && cmd2 S04-02 cmd1 失败时 cmd2 跑不跑?
`cmd exit 1`
[ -f "$f" ] S05-01/02 测的是文件还是字符串?
[[ "$x" == *.log ]] S05-03 右侧有引号吗?是模式还是字面?
(( $# < 1 )) S05-04 是数值比较还是字符串?
if grep -q ... S06-01 测的是 grep 退出码,不是输出
case ... in *.gz) S06-02 默认 * 在哪?

8. 读脚本时易漏的点

现象 说明
set -e + 管道 管道中失败命令未必让脚本退出(S09、S01-04)
if [ -f $f ] 未引号,空格路径会拆词(S02-03)
[[ $x > 10 ]] [[ 里是重定向,应 (( x > 10 ))
case*.log" 引号导致只匹配字面 *.log
grep 无匹配 退出码 1 = 条件假,不是「脚本坏了」
嵌套 fi 数清楚层次,或改扁平 exit

读脚本检查清单

  • 能否在 1 分钟内指出:选项循环参数个数检查默认 *) 分支
  • 给定 ./script -f foo.tar.gz,能否说出 FORCE 影响哪一段?
  • if 后面[/[[/((/grep 中的哪一种?
  • || exit / &&set -e 同时存在时,失败会不会意外退出?
  • 脚本正常结束路径上,最后一道 可能 exit 的条件是什么?

练习

练习 1:场景走读(样例脚本)

第 2 节 全文脚本,判断下列调用能否 执行到 echo "ready: ...",并写出关键退出码(若提前退出)。

  1. ./check.sh(无参数)
  2. ./check.sh -h
  3. ./check.sh release.tar.gz(仅有 tar.gz, .sha256
  4. ./check.sh -f release.tar.gz(同上,无 sidecar)
  5. ./check.sh -v release.tar.gz(有 .sha256 且校验失败,无 -f

参考答案

  1. 否 → usage,退出码 2
  2. 否 → usage,退出码 2
  3. 否 → missing ...sha256,退出码 1
  4. 是 → FORCE 跳过缺失 sidecar,最后 ready: ...,退出码 0
  5. 否 → checksum failed,退出码 1

练习 2:场景走读(第 6 节短脚本)

./short.sh check empty.txtempty.txt 存在且零字节。输出与退出码?
参考答案

grep -q . 失败 → 执行 { echo "empty"; exit 1; }

输出一行 empty,退出码 1 。走不到 echo ok

练习 3:找问题(改错题)

下面脚本意图:至少一个参数;第一个参数为 clean 时删除第二个参数指定的普通文件

bash 复制代码
#!/usr/bin/env bash
set -e
if [ $# -lt 1 ]; then
  exit 1
fi
case $1 in
  clean)
    if [ -f $2 ]; then
      rm $2
    fi
    ;;
esac

列出至少 4 处 问题并给出修改要点。
参考答案

  1. [ $# -lt 1 ][ "$#" -lt 1 ](( $# < 1 )),且应打印 usage。
  2. case $1case "$1" in
  3. clean 分支未检查 $# >= 2$2 可能为空。
  4. [ -f $2 ][ -f "$2" ]rm $2rm -- "$2"
  5. clean 无默认 *) 时其他子命令静默成功(视需求加 *) 报错)。
  6. set -erm 失败会直接退出(若需捕获应处理)。

练习 4:画判断链

用一句话描述 第 2 节脚本while case 结束到 echo ready 之间,必须经过哪三个「关卡」(不要求画正式流程图)。
参考答案

示例答案:① (( $# == 1 )) 参数个数;② [[ -f "$artifact" ]] 文件存在;③ case 类型为 *.tar.gz;④ 校验和逻辑(存在则验、缺失则 FORCE)。任写清三个连续关卡即可,④ 可拆成两条。

练习 5:改写

把 S06-01 练习里的 start|stop|restart if/elif 改成 case (一行一分支 ;; 可接受)。
参考答案

bash 复制代码
case "$cmd" in
  start)   echo "starting" ;;
  stop)    echo "stopping" ;;
  restart) echo "restarting" ;;
  *)
    echo "usage: $0 start|stop|restart" >&2
    exit 2
    ;;
esac

练习 6:读脚本选择题

对第 2 节脚本, 存在 pkg.tar.gz 与正确的 pkg.tar.gz.sha256,执行:

bash 复制代码
./check.sh -v -f pkg.tar.gz

下列哪项正确?

  • A. 一定打印 checksum ok
  • B. 一定执行 set -x
  • C. 若校验失败,仍可能打印 ready
  • D. case 会因 -f 匹配失败

参考答案

B、C 对。

  • A:校验失败且 FORCE=1 时走警告,不一定打印 ok。
  • B:VERBOSE=1 → (( VERBOSE )) && set -x 会执行。
  • C:FORCE 允许校验失败后继续。
  • D:-f 在选项循环已消费,不会进入 case "$artifact" 的模式匹配。

S06 模块小结

篇号 主题 读脚本时干什么
S06-01 if / elif / else 认退出码驱动的分支与嵌套
S06-02 case 认模式、`
S06-03 综合 把 S04~S06 叠在一段里走读

S05 提供「条件怎么写」,S06 提供「条件怎么挂到控制流」;本篇是 S06 的结业练习S14-03 会在更长真实脚本上做全模块复盘。


下一篇预告

S07-01 :《for 循环:列表、in{1..10}》--- 进入循环模块,处理多个文件与列表。