部署檢查腳本要從紀錄找出ready,找到了就繼續,沒有找到就回報仍在等待。你加了set -e,希望命令出錯時停下來,卻發現紀錄沒有那一行,整支腳本就結束。檔案其實存在,grep也正常讀完;問題是把「沒有匹配」和「讀取失敗」當成了同一件事。
GNU grep通常用三種退出狀態表達結果:0代表選到了行,1代表沒有選到行,2代表發生錯誤。腳本若需要區分等待狀態和故障,就要讀這個狀態,不能只用有沒有輸出判斷。
先分清正常的沒有找到與真正錯誤
建立一個只有ready的檔案,再搜尋兩個詞和一個不存在的檔案。以下是小型示範檔案,不要把測試路徑換成正式紀錄後直接覆寫。
printf 'ready\n' > /tmp/status.log
set +e
grep 'ready' /tmp/status.log
printf 'match=%s\n' "$?"
grep 'missing' /tmp/status.log
printf 'no_match=%s\n' "$?"
grep 'ready' /tmp/missing-file
printf 'file_error=%s\n' "$?"
在隔離Ubuntu 24.04、GNU grep 3.11與Bash 5.2.21中,三次狀態依序是0、1、2。第一個搜尋還印出ready;第二個沒有標準輸出;第三個把檔案不存在的訊息寫到標準錯誤。這裡的set +e只為了讓示範完整跑完,不是建議關掉整支正式腳本的錯誤處理。
退出碼和搜尋內容是兩個不同資料。若你用命令替換收集行,空字串不一定足以說明原因;找不到檔案也可能得到空的標準輸出。先記錄退出碼,再決定內容如何使用,錯誤訊息則應保留給維護者。
把可能沒有匹配的搜尋放進條件式
set -e的行為有上下文限制,不是每個非零結果都會立即退出。放在if測試位置的命令可以由你接手判斷,這正適合grep可能正常找不到的情況。先選一般grep,保留本例清楚的三種結果。
if grep 'ready' /tmp/status.log; then
echo '已找到ready'
else
rc=$?
case "$rc" in
1) echo '尚未找到ready' ;;
*) printf '搜尋失敗:%s\n' "$rc" >&2
exit "$rc" ;;
esac
fi
else第一行立即保存$?,因為它表示剛才grep的退出狀態。如果先echo一段提示,再讀$?,拿到的可能是echo成功的0。這種錯誤很隱蔽:維護者看得到失敗訊息,判斷分支卻以為前一個搜尋成功。
這裡把1視為業務上的「尚未出現」,其他非零值則向上回報。GNU版本的一般錯誤常是2,其他grep實作可能使用大於2的值,因此不要把未知的非零碼也塞進沒有找到。若你的流程規定找不到ready本身就是失敗,仍可以自行exit 1;那是流程規則,並非grep損壞。
不要用反向判斷後的狀態冒充原始結果
if ! grep看起來更短,卻會把命令成功與失敗反轉。如果進入這個分支後才讀$?,讀到的是反轉後的條件狀態,不能再用它分辨原始的1和2。需要保留錯誤分類時,使用上面的正向if與else保存原始狀態比較直接。
另一種常見寫法是grep後面接|| true,目的是讓set -e不要停。它同時吞掉「正常沒有匹配」和「無法讀取檔案」,下游只看到成功。當搜尋只是可有可無的顯示資訊,這可能是刻意選擇;但部署、備份或健康檢查若需要可靠分支,就不應把所有非零狀態都改成0。
本例另跑一支set -e腳本,直接搜尋missing後再echo after。結果退出1,after沒有執行;改成if測試後則印出conditional=1與continued。這個實跑只比較獨立命令與if位置,不推廣為所有函式、子shell或管線的完整規則。
只要布林結果時才考慮quiet模式
grep -q不印出匹配行,適合只問是否存在。但GNU手冊指出,quiet模式如果已找到行,即使還發生錯誤也可能回傳0。當你要確認所有輸入都成功讀取,或需要可靠記錄錯誤時,不能把-q的成功當成每個檔案都正常。
若不想顯示匹配行,又需要一般搜尋的讀取結果,可以把標準輸出導到/dev/null,保留標準錯誤;仍應按實際需求檢查退出碼。不要同時把標準錯誤也丟掉,讓權限、路徑拼錯與空結果都變得沒有任何可追蹤線索。
if grep 'ready' /tmp/status.log >/dev/null; then
echo 'ready存在'
else
rc=$?
printf 'grep status=%s\n' "$rc"
fi
管線要另外確認每一段的責任
grep接在另一個命令後面時,預設管線狀態通常看最後一段,前面的讀取失敗可能被遮住。Bash的pipefail可以改變這個行為,但也會讓沒有匹配的grep非零狀態影響整條管線。不要以為加了pipefail就自動分清業務上的空結果。
若要診斷每段,Bash有PIPESTATUS陣列可保存各段狀態,仍必須在下一個命令改掉它之前讀取。對簡單的紀錄檔搜尋,直接讓grep讀檔能減少一個管線環節;如果資料來自命令,則先設計好誰負責產生、誰負責搜尋,以及兩種失敗如何回報。
正式腳本中還應區分搜尋字串和選項。使用者提供的詞可能以減號開頭,可以用-e明確指定模式;若只需原樣文字而不是正規表達式,可以考慮-F。本文示範固定的ready,所以沒有引入動態模式處理,但實際輸入來源不同時要另行評估。
最後,把找到、沒有找到、檔案錯誤三條路徑都測一次,並檢查後續命令是否真的執行。這比只看一次成功輸出更能證明腳本按你的流程做決定。本文依GNU grep 3.11發行包內手冊與Bash 5.2版參考,實跑版本是Ubuntu套件grep 3.11與Bash 5.2.21;其他系統的grep與shell請先核對版本和手冊。
先寫出每種結果的下一步
若這個搜尋是在等待服務就緒,沒有匹配時可以回報仍未就緒,交由外層重試;檔案錯誤則應停下並說明哪個路徑無法讀取。重試的次數與間隔由你的服務流程決定,不要讓一個找不到字串的搜尋無限等下去。相反地,搜尋敏感資料時,找到匹配可能表示需要人工處理,沒有找到才是符合預期。
退出碼本身不代表網站健康或安全。找到ready只能證明這次讀到的內容含有這個模式,不能證明目前服務仍可用,也不能確認這行來自最新啟動。若紀錄會累積或輪替,還要考慮搜尋的時間範圍、檔案來源與是否已切換到新檔。先把這些條件說清楚,腳本才不會拿過期紀錄當作成功證據。
加入測試時,刻意把檔名改成不存在的路徑,確認錯誤分支保留標準錯誤和非零退出碼。然後用存在但沒有匹配的檔案,確認等待分支沒有被當成讀取失敗。兩種情況都可能沒有標準輸出,只有實際走過分支才能發現把它們混在一起的問題。
若由排程或另一支腳本呼叫,還要約定呼叫者如何理解返回值。你可以讓內部搜尋保留三種狀態,再把它轉換成整個任務需要的成功、等待或故障;這個轉換應在清楚的地方完成,避免多層函式各自用不同方式吞掉狀態,最後只剩下一個無法解釋的結果。
參考資料
GNU grep 3.11官方發行包:doc/grep.texi
GNU Bash 5.2.37官方發行包:doc/bashref.texi
POSIX:grep規範
POSIX:shell command language

評論0