分號、&& 與 ||:每天敲一萬次、卻極少人真正理解的命令鏈
分號、&& 與 ||:每天敲一萬次、卻極少人真正理解的命令鏈 每天早上我都會收到「我的 shell 腳本寫崩了」的求助,八成問題不在邏輯,而在這一行: git pull && deploy.sh || echo "部署失敗" 看起來天經地義:拉程式碼成功就部署,…

驗證報告
分號、&& 與 ||:每天敲一萬次、卻極少人真正理解的命令鏈
每天早上我都會收到「我的 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),精細控制還是靠顯式 && 舒服。
什麼時候該用什麼
- 需要「成功才繼續」:用
&&。建構、遷移、部署,每一步的失敗都會汙染下一步。 - 需要「失敗也別停下來,先記錄」:用
;。
或者更優雅地用npm run build; echo "建構結束(成功與否都會列印)"||接失敗分支: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。安全寫法:
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會掛起整段鏈,恢復時容易亂序。除錯時拆成單步執行。
常見誤判
- 「失敗跳過」是
||,不是;。;永遠執行下一條,它沒有「跳過」的概念。 set -e不是萬能的。 它在全域層面對&&、||、if的條件陳述式有豁免(這些是正常控制流,不算錯誤),這在make的 recipe、Ansible 的 inline task 裡經常踩坑。顯式&&/||永遠優先。- 管線退出碼預設只看最後一個指令。 上面的
&& jq ... && echo OK不會在 jq 失敗時停,除非加 pipefail。 $?/$status取上一次指令的退出碼。 注意是「上一條」,寫了 echo 之後$?就變成 echo 的(echo 基本恆為 0)。取退出碼要立刻取,別中間插任何指令。
清單:寫鏈前過一遍
- 這一步失敗,下一步有沒有必要繼續?是 →
;;否 →&& -
||的兜底分支本身會不會出錯?會 → 用函式封裝,函式內再設兜底 - 有管線,是否加了
pipefail? - 關鍵錯誤碼是不是要單獨判斷(別只靠非 0/0 二分)?
- 超過 3 步?考慮寫腳本,別繼續加
&& - 在帶
set -e/set -x的環境裡跑嗎?是否會被靜默忽略(條件陳述式豁免)?
小結
命令鏈本身很輕,但它的靜默路徑也多。把 && 當「守護門」、; 當「無腦接力」、|| 當「兜底通道」,再用 explicit if/else 處理「二選一邊」、pipefail 處理管線,你可以把腳本從「能跑」推到「跑崩時一眼看出崩在哪一步」。
最後一句反直覺但很重要的話:能寫成 if/else 的分支,就不要用 || 的隱含語義。 下一位接手你腳本的人,包括六個月後的你,會感謝你的。
怎麼用
先在隔離環境中按記錄步驟重現,再決定是否採用這項技能。
實測效果
實驗室只記錄可重現結果,不把未經驗證的說法寫成結論。
踩坑
套用前核對權限、輸入、回復步驟和證據是否完整。
適用場景
適合環境條件與本報告證據範圍一致的任務。
不適用場景
缺少必要證據、隔離條件或回復控制時不要使用。