模块:S06 控制流
篇号:S06-03 / 42
预计阅读:55 分钟
主线:Bash(必读三星· 读脚本能力核心篇)
文章目录
- 本篇目标
- 30 秒速览
- 正文
- 1. 读判断链的四步
- 2. 样例脚本(全文)
- 3. 分块读法
- 3.1 骨架(第 1~4 行)
- 3.2 选项解析(第 6~18 行)
- 3.3 参数个数(第 20~21 行)
- 3.4 文件存在(第 23 行)
- 3.5 类型分支(第 25~29 行)— `case` 模式
- 3.6 校验和链(第 31~41 行)— 嵌套 `if` + `(( FORCE ))`
- 3.7 收尾(第 43~44 行)
- 4. 代入三组输入:走读结果
- 场景 A
- 场景 B
- 场景 C
- 5. 判断链「形状」速查
- 6. 第二段练习:更短的判断链(15 行)
- 7. 与 S04、S05 的对照表
- 8. 读脚本时易漏的点
- 读脚本检查清单
- 练习
- 练习 1:场景走读(样例脚本)
- 练习 2:场景走读(第 6 节短脚本)
- 练习 3:找问题(改错题)
- 练习 4:画判断链
- 练习 5:改写
- 练习 6:读脚本选择题
- 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. 样例脚本(全文)
#!/usr/bin/env bashset-euopipefailusage(){echo"usage:$0[-fv] <artifact>">&2;exit2;}VERBOSE=0FORCE=0while(($#>0));docase"$1"in-h|--help)usage;;-v|--verbose)VERBOSE=1;shift;;-f|--force)FORCE=1;shift;;--)shift;break;;-*)echo"unknown option:$1">&2;exit1;;*)break;;esacdone(($#==1))||usageartifact=$1[[-f"$artifact"]]||{echo"not found:$artifact">&2;exit1;}case"$artifact"in*.tar.gz);;*.zip)echo"zip not supported">&2;exit1;;*)echo"unknown artifact type">&2;exit1;;esacif[[-f"${artifact}.sha256"]];thenifsha256sum-c"${artifact}.sha256">/dev/null2>&1;thenecho"checksum ok"elif((FORCE));thenecho"warn: checksum failed (forced)">&2elseecho"checksum failed">&2exit1fielse((FORCE))||{echo"missing${artifact}.sha256 (use -f)">&2;exit1;}fi((VERBOSE))&&set-xecho"ready:$artifact"建议先通读一遍,再对照下面分块说明。
3. 分块读法
3.1 骨架(第 1~4 行)
set-euopipefail- 任一命令非 0 可能直接退出(少数例外,S01-04)。
- 后面若写
grep ... || true,是在主动抵消set -e。
usage以退出码2结束(惯例:用法错误)。
3.2 选项解析(第 6~18 行)
while(($#>0));docase"$1"in...esacdone| 分支 | 行为 |
|---|---|
-h|--help | 调usage,exit 2 |
-v/-f | 置标志位,shift吃掉选项 |
-- | shift后break,后面参数留给位置参数 |
| `-*) | 未知选项,exit 1 |
*) | 非选项,break出循环 |
读点:case与while (( $# > 0 ))配合;shift只发生在选项分支里,位置参数release.tar.gz遇*)时不 shift,留在$1。
3.3 参数个数(第 20~21 行)
(($#==1))||usageartifact=$1- 条件为假 → 执行
usage(退出 2)。 - 为真 → 继续,恰好一个位置参数。
3.4 文件存在(第 23 行)
[[-f"$artifact"]]||{echo"not found: ...";exit1;}[[ -f ]]成功 → 短路,不进{ ... }。- 失败 → 打印并exit 1。
等价于:
if[[!-f"$artifact"]];thenecho"not found:$artifact">&2exit1fi3.5 类型分支(第 25~29 行)—case模式
case"$artifact"in*.tar.gz);;*.zip)...exit1;;*)...exit1;;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 行)
((VERBOSE))&&set-xecho"ready:$artifact"- 仅当VERBOSE=1时打开跟踪。
- 能执行到这里 → 前面分支都没 exit,脚本正常结束(退出码 0,来自最后
echo成功)。
4. 代入三组输入:走读结果
场景 A
./check.sh-vrelease.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
./check.sh-fbroken.tar.gz# broken.tar.gz 存在,有 .sha256 但内容不匹配| 阶段 | 结果 |
|---|---|
| FORCE=1 | 校验失败走elif (( FORCE ))→ 警告,不 exit |
| 结尾 | 打印ready: broken.tar.gz(带警告) |
场景 C
./check.sh app.zip| 阶段 | 结果 |
|---|---|
| case | *.zip支 →exit 1(到不了 checksum) |
5. 判断链「形状」速查
读脚本时常见结构,见到可对号入座:
┌─────────────────────────────────────┐ │ (( $# )) / : "${1:?}" 参数门禁 │ └─────────────────┬───────────────────┘ ▼ ┌─────────────────────────────────────┐ │ while + case 选项解析 │ └─────────────────┬───────────────────┘ ▼ ┌─────────────────────────────────────┐ │ [[ -f ]] / [ -d ] 路径存在 │ └─────────────────┬───────────────────┘ ▼ ┌─────────────────────────────────────┐ │ case 模式 / [[ == pat ]] 类型分支 │ └─────────────────┬───────────────────┘ ▼ ┌─────────────────────────────────────┐ │ if 命令 / if [[ ]] / if (( )) │ │ 业务逻辑 + exit │ └─────────────────────────────────────┘| 形状 | 示例 | 含义 |
|---|---|---|
| 门禁 | (( $# >= 1 )) || usage | 不满足就停 |
| 存在则做 | [[ -f f ]] && cmd | 成功才继续 |
| 失败则停 | cmd || exit 1 | 一条线收尾 |
| 多值分发 | case "$cmd" in ... esac | 子命令/类型 |
| 嵌套 if | 外层文件、内层命令退出码 | 两层条件 |
| 标志位 | (( FORCE ))放宽检查 | 与-f选项对应 |
6. 第二段练习:更短的判断链(15 行)
#!/usr/bin/env bashmode=${1:-}file=${2:-}if[[-z"$mode"||-z"$file"]];thenecho"usage:$0<check|run> <file>">&2exit2ficase"$mode"incheck)[[-r"$file"]]||exit1grep-q."$file"||{echo"empty";exit1;}echo"ok";;run)[[-x"$file"]]&&exec"$file"||exit1;;*)echo"bad mode">&2exit1;;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 | S04-02 | 失败路径是否终止脚本? |
[ -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时删除第二个参数指定的普通文件。
#!/usr/bin/env bashset-eif[$#-lt1];thenexit1ficase$1inclean)if[-f$2];thenrm$2fi;;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|restartif/elif改成case(一行一分支;;可接受)。
case"$cmd"instart)echo"starting";;stop)echo"stopping";;restart)echo"restarting";;*)echo"usage:$0start|stop|restart">&2exit2;;esac练习 6:读脚本选择题
对第 2 节脚本,已存在pkg.tar.gz与正确的pkg.tar.gz.sha256,执行:
./check.sh-v-fpkg.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}》— 进入循环模块,处理多个文件与列表。