調試 bash 腳本就像大海撈針,特別是當現有程式碼庫中出現新的添加內容而沒有及時考慮結構、日誌記錄和可靠性問題時。 您可能會發現自己陷入這種情況,要么是由於自己的錯誤,要么是在管理複雜的腳本堆時。
團隊
在文章中,作者分享了他在過去幾年中所學到的知識,以及一些讓他措手不及的常見錯誤。 這很重要,因為每個軟體開發人員在其職業生涯的某個階段都會使用腳本來自動化日常工作任務。
陷阱處理程序
當腳本執行期間發生意外情況時,我遇到的大多數 bash 腳本都不會使用有效的清理機制。
驚喜可能來自外部,例如接收來自核心的訊號。 處理此類情況對於確保腳本足夠可靠以在生產系統上運行非常重要。 我經常使用退出處理程序來回應這樣的場景:
function handle_exit() {
// Add cleanup code here
// for eg. rm -f "/tmp/${lock_file}.lock"
// exit with an appropriate status code
}
// trap <HANDLER_FXN> <LIST OF SIGNALS TO TRAP>
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM
trap
是一個 shell 內建指令,可協助您註冊一個清理函數,在出現任何訊號時呼叫該函數。 然而,對於處理者應特別小心,例如 SIGINT
,這會導致腳本中止。
此外,在大多數情況下,您應該只捕獲 EXIT
,但想法是您實際上可以為每個單獨的訊號自訂腳本的行為。
內建設定功能 - 錯誤時快速終止
錯誤發生後立即回應並迅速停止執行非常重要。 沒有什麼比繼續運行這樣的指令更糟糕的了:
rm -rf ${directory_name}/*
請注意變數 directory_name
還沒決定。
使用內建函數來處理此類場景很重要 set
,例如 set -o errexit
, set -o pipefail
或 set -o nounset
在腳本的開頭。 這些函數確保您的腳本在遇到任何非零退出程式碼、使用未定義的變數、透過管道傳遞的無效命令等時立即退出:
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
function print_var() {
echo "${var_value}"
}
print_var
$ ./sample.sh
./sample.sh: line 8: var_value: unbound variable
注: 內建函數,例如 set -o errexit
,一旦出現「原始」回傳程式碼(零除外),就會退出腳本。 因此最好引入自訂錯誤處理,例如:
#!/bin/bash
error_exit() {
line=$1
shift 1
echo "ERROR: non zero return code from line: $line -- $@"
exit 1
}
a=0
let a++ || error_exit "$LINENO" "let operation returned non 0 code"
echo "you will never see me"
# run it, now we have useful debugging output
$ bash foo.sh
ERROR: non zero return code from line: 9 -- let operation returned non 0 code
以這種方式編寫腳本迫使您更加小心腳本中所有命令的行為,並在錯誤出乎您意料之前預見錯誤的可能性。
ShellCheck 用於偵測開發過程中的錯誤
值得整合類似的東西
我在本地開發環境中使用它來獲取有關語法、語義以及我在開發時可能錯過的程式碼中的一些錯誤的報告。 這是一個針對 bash 腳本的靜態分析工具,我強烈建議使用它。
使用您自己的退出代碼
POSIX 中的回傳碼不僅是零或一,而且是零或非零值。 使用這些功能可為各種錯誤情況傳回自訂錯誤代碼(201-254 之間)。
然後,包裝您的腳本的其他腳本可以使用此資訊來準確了解發生的錯誤類型並做出相應的反應:
#!/usr/bin/env bash
SUCCESS=0
FILE_NOT_FOUND=240
DOWNLOAD_FAILED=241
function read_file() {
if ${file_not_found}; then
return ${FILE_NOT_FOUND}
fi
}
注: 請特別小心您定義的變數名稱,以避免意外覆蓋環境變數。
記錄功能
漂亮且結構化的日誌記錄對於輕鬆理解腳本的結果非常重要。 與其他高階程式語言一樣,我總是在 bash 腳本中使用本機日誌記錄功能,例如 __msg_info
, __msg_error
等。
這有助於透過僅在一處進行更改來提供標準化的日誌記錄結構:
#!/usr/bin/env bash
function __msg_error() {
[[ "${ERROR}" == "1" ]] && echo -e "[ERROR]: $*"
}
function __msg_debug() {
[[ "${DEBUG}" == "1" ]] && echo -e "[DEBUG]: $*"
}
function __msg_info() {
[[ "${INFO}" == "1" ]] && echo -e "[INFO]: $*"
}
__msg_error "File could not be found. Cannot proceed"
__msg_debug "Starting script execution with 276MB of available RAM"
我通常會嘗試在我的腳本中使用某種機制 __init
,其中此類記錄器變數和其他系統變數被初始化或設定為預設值。 這些變數也可以在腳本呼叫期間從命令列選項設定。
例如,類似:
$ ./run-script.sh --debug
當執行這樣的腳本時,它確保將系統範圍的設定設為預設值(如果需要),或至少在必要時初始化為適當的值。
我通常根據使用者介面和使用者可以/應該深入研究的配置細節之間的權衡來選擇初始化什麼和不做什麼。
重複使用和清理系統狀態的架構
模組化/可重複使用程式碼
├── framework
│ ├── common
│ │ ├── loggers.sh
│ │ ├── mail_reports.sh
│ │ └── slack_reports.sh
│ └── daily_database_operation.sh
我保留了一個單獨的儲存庫,可以用它來初始化我想要開發的新專案/bash 腳本。 任何可以重複使用的內容都可以儲存在儲存庫中,並由想要使用該功能的其他項目檢索。 以這種方式組織專案可以顯著減少其他腳本的大小,並確保程式碼庫較小且易於測試。
如上例所示,所有日誌記錄功能,例如 __msg_info
, __msg_error
其他的,例如 Slack 報告,單獨包含在 common/*
並在其他場景中動態連接,例如 daily_database_operation.sh
.
留下一個乾淨的系統
如果您在腳本運行時加載任何資源,建議將所有此類資料儲存在具有隨機名稱的共用目錄中,例如 /tmp/AlRhYbD97/*
。 您可以使用隨機文字產生器來選擇目錄名稱:
rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"
工作完成後,可以在上面討論的掛鉤處理程序中提供此類目錄的清理。 如果不處理臨時目錄,它們會累積並在某個階段導致主機出現意外問題,例如磁碟已滿。
使用鎖定文件
通常,您需要確保在任何給定時間一台主機上僅執行一個腳本實例。 這可以使用鎖定檔案來完成。
我通常會建立鎖定文件 /tmp/project_name/*.lock
並檢查它們是否出現在腳本的開頭。 這有助於腳本正常終止,並避免並行運行的另一個腳本對系統狀態進行意外更改。 如果您需要在給定主機上並行執行相同的腳本,則不需要鎖定檔案。
衡量和改進
我們經常需要使用長時間運行的腳本,例如日常資料庫操作。 此類操作通常涉及一系列步驟:載入資料、檢查異常、匯入資料、發送狀態報告等。
在這種情況下,我總是嘗試將腳本分解為單獨的小腳本,並使用以下方法報告它們的狀態和執行時間:
time source "${filepath}" "${args}">> "${LOG_DIR}/RUN_LOG" 2>&1
稍後我可以透過以下方式查看執行時間:
tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"
這有助於我識別腳本中需要優化的問題/緩慢區域。
祝你好運!
還有什麼要讀的:
來源: www.habr.com