模块 :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 -euo、usage/参数个数 、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 吃掉选项 |
-- |
shift 后 break,后面参数留给位置参数 |
| `-*) | 未知选项,exit 1 |
*) |
非选项,break 出循环 |
读点 :case 与 while (( $# > 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 -c:if后面是命令,看退出码。 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: ...",并写出关键退出码(若提前退出)。
./check.sh(无参数)./check.sh -h./check.sh release.tar.gz(仅有 tar.gz,无.sha256)./check.sh -f release.tar.gz(同上,无 sidecar)./check.sh -v release.tar.gz(有.sha256且校验失败,无-f)
参考答案
- 否 →
usage,退出码 2。 - 否 →
usage,退出码 2。 - 否 →
missing ...sha256,退出码 1。 - 是 → FORCE 跳过缺失 sidecar,最后
ready: ...,退出码 0。 - 否 →
checksum failed,退出码 1。
练习 2:场景走读(第 6 节短脚本)
./short.sh check empty.txt,empty.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 处 问题并给出修改要点。
参考答案
[ $# -lt 1 ]→[ "$#" -lt 1 ]或(( $# < 1 )),且应打印 usage。case $1→case "$1" in。clean分支未检查$# >= 2,$2可能为空。[ -f $2 ]→[ -f "$2" ];rm $2→rm -- "$2"。- 仅
clean无默认*)时其他子命令静默成功(视需求加*)报错)。 set -e下rm失败会直接退出(若需捕获应处理)。
练习 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}》--- 进入循环模块,处理多个文件与列表。