Bash script 說難上手是滿快的, 說簡單卻不容易精通, 尤其是 code 長大之後, 錯誤處理就變成可用性的關鍵指標了 (其實所有語式語言應該都是如此).
我自己的習慣是, 當碰到讓 script 跑不下去的問題時, 就結束 script. 但這會有 2 個問題

  1. 如果對每個指令都加 if/else 處理, 會使得 code 難以閱讀, 且 code 重覆率會很高
  2. 直接 exit 的話, 先前配置的一些資源 (如暫存檔) 怎麼清理?

針對第 1 個問題, 我們可以用

  • set -o errexit

來解決. 執行後, 當某一行指令 (if, for, while... 除外) 執行的結果為非 0, bash 會馬上結束 script 的執行, 就不需要一一的進行判斷.
第 2 個問題, 則可用 trap 處理

  • trap "rm -f /tmp/tmo.ERGHEMNF" EXIT

這樣, 在 script 結束前, 就會執行 handler, 也就是這裡的 "rm /tmp/tmo.ERGHEMNF" 這段命令.
不過, 這樣有個問題, 就是對同樣的 signal (雖然 EXIT 並不是 signal) 執行多次 trap 的話, 後面的 handler 會把前面的 overwrite, 沒法進行多次善後動作.
解法呢, 可以讓 trap 執行一個 function, 由這個 function 去執行其他的清理動作.
嗯, 這樣大體上有個方向了. 不過, 等等, 還有有兩點要注意的

  1. 配置的資源生命週期是有長有短的, 像 C 的 atexit() 這樣 register handler 之後沒法 unregister 是會造成麻煩的 (有的可能是在 script 的開始就配置, script 結束才 release, 有的資源可能在 function 執行結束時就 release, 而不會全部累積到最後)
  2. 清理動作通常是要以配置的相反順序執行, 才不會造成額外的問題

從上面要注意的兩點看來, 存放多個 handler 的結構要以 stack 最適合. 嗯~ 差不多可以來寫 code 了. 首先是 stack 的操作

$1: name# $2: valuestack_push(){ eval local -i top=${#${1}[@]} eval ${1}[${top}]="${2}"}

$1: name# $2: callbackstack_pop(){ eval local -i top=${#${1}[@]}

if [[ "0" == "${top}" ]]; then echo "Pop from empty stack" >&2 exit 1 fi let top--
eval local value="${${1}[${top}]}" unset ${1}[${top}]
[[ "${2}" ]] && ${2} "${value}"}

$1: name# return: 1 not empty, 0 emtpystack_is_empty(){ eval local -i top=${#${1}[@]}

return ${top}}

$1: name# $2: callbackstack_pop_all(){ while ! stack_is_empty "${1}"; do stack_pop "${1}" "${2}" done}

這個 stack 是 generic 的, 在 pop 時如果有傳入第 2 個參數, 會被當成 callback 並將被 pop 的 item 傳入. 接著, 用上面的 stack 來完成我們想要的善後功能
cleanup_stack_push(){ stack_push base_cleanup_stack "${1}"}
cleanup_stack_pop(){ stack_pop base_cleanup_stack eval}
cleanup_stack_pop_no_clean(){ stack_pop base_cleanup_stack}
cleanup_stack_finalize(){ stack_pop_all base_cleanup_stack eval}
push 放入善後指令, pop 時則會移除並執行最後 push 的善後指令, pop_no_clean 則只單純的把 item 從 stack 中 pop 但不執行, finalize 則是把所有的善後動作都做一遍.
使用範例如下:

!/bin/bashif [[ "0" != "${UID}" ]]; then echo "superuser access is required" >&2 exit 1fi

source "cleanup-stack"
set -o errexittrap cleanup_stack_finalize EXIT
prepare_rootfs(){ [[ -d "newroot" ]] && return
cleanup_stack_push "rm -rf newroot"
mkdir -p newroot tar -jxvpf gentoo-stage3.tar.bz2 -C newroot
cleanup_stack_pop_no_clean}
do_mount(){ local src="$1"; shift local dest="$1"; shift cleanup_stack_push "umount -l "${dest}"" mount "${src}" "${dest}" [email protected]}
prepare_rootfs
do_mount /proc newroot/proc -Rdo_mount /dev newroot/dev -Rdo_mount /sys newroot/sys -R
chroot newroot
執行時, 會檢查所需的目錄是否存在, 不存在的話則解壓 gentoo stage3 root file system 的內容, 如果解壓失敗就會將已解的內容清空. 如果解壓成功則保留內容 (pop_no_clean). 接著 mount 上 proc, dev, sys 等目錄, 再 chroot 進新產生的 root 中.
這樣, code 主體就可以非常的簡單清楚, 錯誤發生時也能進行很好的處理.