
分号、&& 与 ||:每天敲一万次、却极少人真正理解的命令链 Cut
每天早上我都会收到"我的 shell 脚本写崩了"的求助,八成问题不在逻辑,而在这一行:
📋 实验室验证报告
分号、&& 与 ||:每天敲一万次、却极少人真正理解的命令链 Cut
每天早上我都会收到"我的 shell 脚本写崩了"的求助,八成问题不在逻辑,而在这一行:
git pull && deploy.sh || echo "部署失败"
看起来天经地义:拉代码成功就部署,失败就喊一声。但如果你实际测试过,会发现它有两个坑——"部署成功"时那条"部署失败"可能会喊出来,而"部署脚本自身崩溃"时也不一定会走那个分支。这是 CLI 里最常用、也最容易被误解的机制,值得用五分钟讲清楚。
核心规则:先看前一条命令的退出码
三个连接符的差别只用一句话就能说完:
- **`;`(分号)**:上一条跑完就跑下一条,**不管成功失败,都执行**。它是"无条件接力"。
- **`&&`(与)**:上一条**成功**(退出码 0)才执行下一条,失败就整段停下来。
- **`||`(或)**:上一条**失败**才执行下一条,成功就跳过。
退出码(exit code)是一切的基础:**0 代表成功,非 0 代表失败**。你可以随时用 `echo $?` 查看上一条命令的退出码,也可以用 `#` 在注释里记下"为什么要非 0"。
git pull
echo "git 退出码:$?" # 0 = 成功,128 开头通常是致命错误
场景:三个操作串起来
假设你要完成"备份数据库 → 跑迁移 → 发通知",并且**迁移失败必须终止**,否则会把坏代码发出去:
pg_dump mydb > backup.sql && migrate-v2.0 && notify.sh "迁移完成"
`&&` 的好处:`pg_dump` 失败(磁盘满、权限错),后面的就不会跑,你把故障圈在最小半径。`set -e` 可以让整个脚本在任何失败时中止,但它是全局的、容易误伤(比如 `grep` 搜不到东西也是非 0),精细控制还是靠显式 `&&` 舒服。
什么时候该用什么
- **需要"成功才继续":用 `&&`**。构建、迁移、部署,每一步的失败都会污染下一步。
- **需要"失败也别停下来,先记录":用 `;`**。
```bash
npm run build; echo "构建结束(成功与否都会打印)"
```
或者更优雅地用 `||` 接失败分支:
```bash
npm run build || { echo "构建失败" >&2; exit 1; }
```
- **需要"二选一":用 `||` 接兜底**。
- `command1 || command2`:command1 成功就跳过,失败才跑 command2。
- 但上面开头那个 `A && B || C` 的写法要慎用。直觉上"成功走 B,失败走 C"是对的,但有第三个情况:**A 成功、B 失败**时,C 也会跑。如果你的 B 是 `deploy`,你想用 C 做"部署失败告警",这倒是对的;但如果 C 是别的副作用,就很容易出现"部署成功还在执行 C"的诡异 bug。安全写法:
```bash
if git pull; then
deploy.sh
else
echo "拉代码失败,中止" >&2
exit 1
fi
```
语义一目了然,没有歧义。
一个真实的坑:`echo` 掩盖了失败
很多人写:
$TOOL_MIGRATE || echo "二级数据迁移出错"
看起来 `||` 只会在 `$TOOL_MIGRATE` 失败时输出告警。但如果你加过 `set -x` 或 debug 日志,会发现 $TOOL_MIGRATE 真的失败了,而且告警打印了——没错,那是对的;问题是很多团队的告警通道在这台机器上根本不通(网络隔离、log 服务重启),`echo` 只是"看起来兜住了",实际上是**静默失败**。`||` 不保证你的 fallback 也成功,它只保证"成功时不跑、失败时跑"。所以告警要双通道,且用 `echo ... >&2`,再配合 cron/atop/logwatch 做最终兜底。
管道与 `;` 的结合
管道 `|` 传递的是 **stdout**,退出码默认取**最后一个**命令的退出码:
cat data.json | jq '.items' > out.json && echo "OK"
如果 `jq` 出错了(字段不存在),`echo "OK"` 还是会打印,哪怕 `out.json` 是空的或者报错。要"任何一段失败都整段失败",需要 pipefail:
set -o pipefail
cat data.json | jq '.items' > out.json && echo "OK"
现在 jq 失败,管道整体非 0,`&&` 后面的就停了。这是处理数据链(日志采集 → 转换 → 入库)最常被忽略的一行。
何时不该用命令链
- **超过 3~5 步的流程不要写成一行链**。一行代码越长,可读性越差,且退出码一多你就追不清了。超过 3 步,写个 `.sh` 文件,每步一个函数,函数开头 `set -euo pipefail`,每步结束打一条 `[step]` 日志。
- **链在前台跑的状态、错误处理、重试**:链式命令没有重试、没有超时、没有分段状态。如果这是生产脚本,至少加 `timeout` 和手动 `for i in 1 2 3; do ... && break; done` 结构。
- **在交互式 shell 里调试链式命令**:`Ctrl-Z` 会挂起整段链,恢复时容易乱序。调试时拆成单步执行。
常见误判
1. **"失败跳过"是 `||`,不是 `;`。** `;` 永远执行下一条,它没有"跳过"的概念。
2. **`set -e` 不是万能的。** 它在全局层面对 `&&`、`||`、`if` 的条件语句有豁免(这些是正常控制流,不算错误),这在 `make` 的 recipe、Ansible 的 inline task 里经常踩坑。显式 `&&`/`||` 永远优先。
3. **管道退出码默认只看最后一个命令。** 上面的 `&& jq ... && echo OK` 不会在 jq 失败时停,除非加 pipefail。
4. **`$?` / `$status` 取上一次命令的退出码。** 注意是"上一条",写了 echo 之后 `$?` 就变成 echo 的(echo 基本恒为 0)。取退出码要立刻取,别中间插任何命令。
清单:写链前过一遍
- [ ] 这一步失败,下一步有没有必要继续?是 → `;`;否 → `&&`
- [ ] `||` 的兜底分支本身会不会出错?会 → 用函数封装,函数内再设兜底
- [ ] 有管道,是否加了 `pipefail`?
- [ ] 关键错误码是不是要单独判断(别只靠非 0/0 二分)?
- [ ] 超过 3 步?考虑写脚本,别继续加 `&&`
- [ ] 在带 `set -e` / `set -x` 的环境里跑吗?是否会被静默忽略(条件语句豁免)?
小结
命令链本身很轻,但它的静默路径也多。把 `&&` 当"守护门"、`;` 当"无脑接力"、`||` 当"兜底通道",再用 explicit if/else 处理"二选一边"、`pipefail` 处理管道,你可以把脚本从"能跑"推到"跑崩时一眼看出崩在哪一步"。
最后一句反直觉但很重要的话:**能写成 if/else 的分支,就不要用 `||` 的隐式语义。** 下一位接手你脚本的人,包括六个月后的你,会感谢你的。
⚙️ 安装与赋能
clawhub install skill-20260908-command-chains安装后在你的 Agent 配置中启用此技能,重启 Agent 即可生效。