問題
除錯時最花時間的不是「找不到錯誤訊息」,是錯誤訊息把你指向錯誤的方向。 整理部署腳本與 CI 設定時接連撞到三種形態,每一種都讓人先往錯的地方查。
形態一:編碼問題偽裝成語法錯誤
症狀
shellcheck 對一支部署腳本報了兩個 error:
scripts/blue-green-deploy.sh:116:8: error: Couldn't parse this brace group. [SC1073]
scripts/blue-green-deploy.sh:211:18: error: For command expansion, the tick should slant left [SC1078]看到「未關閉的 backtick」第一反應是去找哪裡少打了一個反引號。找不到。 去看第 211 行,內容是一串亂碼中文。
原因
那個檔整份是 Big5 編碼,而 Big5 的某些位元組落在 ASCII 的反引號附近, shellcheck 用 UTF-8 解讀時把中文註解的位元組看成了 shell 語法。(轉碼後 error 消失是實測;具體是哪個位元組被當成反引號,我沒有逐一追,這段機制是推論。)
全 repo 掃了一次,三個檔有問題:兩支腳本整檔 Big5,還有一個 CI 設定檔是 UTF-8 但混進了
一個 Big5 的破折號(單一位元組序列 a158)。
修法
三個檔轉成 UTF-8 之後,那兩個 error 直接消失,只剩一個風格建議。
判別原編碼的方法很直接,試著用各種編碼解一次,看哪個解得出像話的中文:
d = pathlib.Path(f).read_bytes()
for enc in ("big5", "cp950", "gbk", "shift_jis", "latin-1"):
try:
t = d.decode(enc)
cjk = sum(1 for c in t if '一' <= c <= '鿿')
print(enc, cjk) # CJK 字元最多的那個就是原編碼
except Exception:
pass教訓:語法檢查工具報的「語法錯」,前提是它讀對了那個檔。 在有中文註解的老檔案上,先確認編碼再相信它的行號。
形態二:重導向在命令失敗時已經先清空了目標檔
症狀
一個把 Jenkins 外掛清單從跑著的實例回寫的指令:
pin-plugins:
$(COMPOSE) exec -T jenkins sh -c 'curl ...' \
| python3 -c '...' \
> docker/controller/plugins.txtpython 那段因為 3.11 不接受 f-string 內含反斜線而失敗。指令當然是紅的,
但目標檔已經被清空了——plugins.txt 從 10 行變成 0 行。
原因
shell 在執行管線之前就先把 > file 的目標截斷成 0 位元組。
也就是說:一個「失敗的」指令,副作用是刪掉了你原本有的東西。 那次檔案有進版控,從 git 還原回來;如果沒進版控,就回不來了。
修法
先寫暫存再搬:
pin-plugins:
... > docker/controller/plugins.txt.tmp && mv docker/controller/plugins.txt.tmp docker/controller/plugins.txt教訓:任何會覆寫既有檔案的指令,都要假設它中途會失敗。 先寫暫存再原子搬移,成本只有一行。
形態三:外掛的預設值把你最需要的功能關掉
症狀
CI 的測試階段紅了(覆蓋率門檻沒過),照理說這正是最需要看覆蓋率報表的時候。 結果報表頁 404,console 寫著:
[Coverage] Skipping execution of coverage recorder since overall result is 'FAILURE'原因
外掛的預設行為是:build 已經失敗就不記報表。這個預設在「build 因為編譯錯誤而失敗」時合理, 在「build 因為覆蓋率不足而失敗」時剛好相反——開發者最需要報表的那一刻,報表被關掉了。
修法
顯式打開:
recordCoverage(tools: [...], failOnError: false, skipPublishingChecks: true, enabledForFailure: true)打開之後,同樣紅的 build 印出了整體 98.71% 的數字,報表頁也回來了,看得到是哪一個檔案只有 28%。
教訓:工具的預設值是照「最常見的情況」設的,不是照「你的情況」設的。 當某個功能「在正常時有用、在出錯時消失」,多半是踩到這類預設,去翻它有沒有 forFailure 之類的開關。
共同的除錯順序
三個形態的共同點:錯誤訊息描述的是工具看到的現象,不是根因。
所以順序應該是:
- 先問「這個工具讀到的輸入,跟我以為的輸入是同一個嗎」(編碼、路徑、版本)
- 再問「這個工具在失敗路徑上做了什麼」(副作用、跳過、預設關閉)
- 最後才照著錯誤訊息的字面意思去查
倒過來做的話,你會在不存在的反引號上找很久。
怎麼預防再次發生
- repo 裡有中文的話,CI 加一條「所有追蹤的文字檔必須能用 UTF-8 解碼」
- Makefile 裡任何
> 既有檔案一律改成先寫.tmp再mv - 報表類外掛裝好之後,故意讓 build 紅一次,確認報表在失敗時也留得下來
- 除錯卡住超過兩次嘗試就換方向:不要再看錯誤訊息,改看工具的輸入與失敗路徑