Try   HackMD

Bash Script Debug mode

實作目標

將 Bash Function 裡面,命令的正確執行結果 + 錯誤輸出 + 真正執行的命令(變數會變成值) 通通都倒到同一個檔案中,不用手動一行一行在行尾加上 & > /dev/null,而且加上行號、function 名稱,好讓你以後除錯時超清楚!

注意事項

  1. 因 log file 的目錄區是在 /var/log 底下,所以以下範例需要由 root 使用者或 sudo user 執行。
  2. log 的檔名前面有加 . 代表隱藏檔案,所以不能用 ll,大部分的 ll 都是 alias ll="ls -l",要記得用 ls -al

範例程式

#!/bin/bash TRACE_LOG() { Command_log_file="/var/log/.$(basename "${BASH_SOURCE[0]}").log" exec >>"${Command_log_file}" 2>&1 exec 3>> "${Command_log_file}" export BASH_XTRACEFD=3 PS4='($BASH_SOURCE:$LINENO:$FUNCNAME): ' set -x } TEST() { TRACE_LOG for i in {1..10}; do echo ${i} done # failed command fdsffa fdsafasf set +x } TEST

程式重點介紹

TRACE_LOG() {

這是宣告一個 Bash function,叫做 TRACE_LOG,之後你可以在 script 其他地方用 TRACE_LOG 來呼叫它。

Command_log_file="/var/log/.$(basename "${BASH_SOURCE[0]}").log"
  • 設定 log 檔的檔名與路徑。
  • $(basename "${BASH_SOURCE[0]}") 會取得目前這個 script 的檔名,例如 myscript.sh
  • 結果會變成這樣的一個 log 路徑,例如:
    ​​/var/log/.myscript.sh.log
    

這樣做的目的是:自動為每個 script 建立一個獨立的 log 檔,方便紀錄。

exec >>"${Command_log_file}" 2>&1
  • 把所有之後的「正常輸出(stdout)」與「錯誤訊息(stderr)」全部寫進前面設定的 log 檔中。
  • 從這一行開始,像 echols 等指令的輸出都不會出現在螢幕上,而是寫進 log 檔。
exec 3>> "${Command_log_file}"
  • 開啟一個「新的資料管道」,叫做「檔案描述符 3(FD 3)」,也指向同一個 log 檔。
  • 這行本身不會產生輸出,它只是預先準備好「FD 3」,之後會專門用來記錄除錯訊息(debug trace)。
export BASH_XTRACEFD=3 PS4='($BASH_SOURCE:$LINENO:$FUNCNAME): '

這是在啟用 Bash 的「除錯模式」所需的變數。

  • BASH_XTRACEFD=3:告訴 Bash,把除錯訊息(由 set -x 產生的 trace)寫入「檔案描述符 3」,也就是 log 檔。
  • PS4='($BASH_SOURCE:$LINENO:$FUNCNAME): ':設定每一行除錯訊息的開頭格式,包括:
    • $BASH_SOURCE:是哪個 script 檔案
    • $LINENO:是哪一行
    • $FUNCNAME:是在哪個 function 裡

舉例產生的 log 可能會長這樣:

(myscript.sh:15:myfunc): echo "啟動中"

這樣可以清楚知道程式執行到哪裡、哪一行。

  set -x
  • 這行開啟 Bash 的「指令追蹤模式(debug trace mode)」。
  • 開啟後,Bash 每執行一行指令,都會先印出該指令的內容(寫入你上面指定的 log 檔,因為有設定 BASH_XTRACEFD)。

使用方式

你只要在 script 裡面某個地方寫:

TRACE_LOG

從那一行開始,這支 script 的輸出和行為紀錄就會開始寫進 log 檔 /var/log/.xxx.log

查看 Log

# cat /var/log/.test.sh.log

執行結果 :

(./test.sh:17:test): for i in {1..10}
(./test.sh:19:test): echo 1
1
(./test.sh:17:test): for i in {1..10}
(./test.sh:19:test): echo 2
2
(./test.sh:17:test): for i in {1..10}
(./test.sh:19:test): echo 3
3
(./test.sh:17:test): for i in {1..10}
(./test.sh:19:test): echo 4
4
(./test.sh:17:test): for i in {1..10}
(./test.sh:19:test): echo 5
5
(./test.sh:17:test): for i in {1..10}
(./test.sh:19:test): echo 6
6
(./test.sh:17:test): for i in {1..10}
(./test.sh:19:test): echo 7
7
(./test.sh:17:test): for i in {1..10}
(./test.sh:19:test): echo 8
8
(./test.sh:17:test): for i in {1..10}
(./test.sh:19:test): echo 9
9
(./test.sh:17:test): for i in {1..10}
(./test.sh:19:test): echo 10
10
(./test.sh:23:test): fdsffa
./test.sh: line 23: fdsffa: command not found
(./test.sh:24:test): fdsafasf
./test.sh: line 24: fdsafasf: command not found
(./test.sh:25:test): set +x