在寫 shell script 的時候部分預設行為可能會跟想像有差異,如果沒有適當的設置參數就會發生「腳本正確地失敗了」的窘境。

最佳實踐
在 Bash strict mode 一文中有建議到我們可以在所有 bash 腳本中設置:
set -euxo pipefail
而 Shell Script Best Practices 文中則建議了:
set -o errexit
set -o nounset
set -o pipefail
兩者建議的參數十分接近,但直觀上又不好對應兩種寫法的差異,查了下文件、嘗試玩一下各種指令然後來做個紀錄。
參數清單
這裡大多數的選項是 sh 就有的,如果是 bash 特有的功能則會另外說明。另外這個清單裡面不放入其他 shell 的參數,考量為這兩個以外的 shell 在可移植性方面會顯著的變差。
-o allexport / -a
所有的變數皆被賦予 export 屬性,即設為環境變數之一,讓其他處理指令可以利用。
舉例來說,FOO 在這裡只是一個腳本自己的變數,在下面這個例子中輸出會是 None:
#! /bin/sh
FOO="hello"
python3 -c 'import os; print(os.getenv("FOO"))'
hello但如果有設置 -a,則 FOO 就會變成一個環境變數,故輸出會變成 hello。
-o notify / -b
Cause the status of terminated background jobs to be reported immediately, rather than before printing the next primary prompt.
(讓背景工作的狀態被即時回報)
試玩了一下,看起來跟用 & 把某個指令丟去背景執緒這個操作無關(嗎),我還沒搞懂就先不要妖言惑眾了。
-o errexit / -e
在某一指令出錯的時候終止腳本。
sh 的設計不同於 python 、 java script 等語言,沒有 exception 來強制終止執行緒。我們僅能利用 exit code 去解讀是否有正確地執行,但逐指令去檢查 $? 實在是勞民傷財。故可以透過設置這個選項讓腳本在某個指令回傳不為 0 的時候主動終止。
設想今天有一腳本中間有發生過錯誤:
#! /bin/sh
false
echo 'pass'
pass在預設況下它是會成功的,因為最後一個指令的 echo 回傳結果為 0。但如果有設置 -e,則在 false 那裡就會發生終止。
注意到一個情況,如果某個指令中間有做管道(pipe),則該指令的成功與否還是只看最後一個程式。如果需要對管道階段中的其他程式做檢查,則需要 pipefail(後面說明)。
-o noglob / -f
停用 glob 行為;即 * 從此就只是個星星符號。
#! /bin/sh
set -f
echo *
*在上面的例子中這段腳本只會輸出一個 *,如果把 -f 拿掉的話 glob 會發生在 argv 被建立之前,故會輸出當前目錄下的所有檔案、資料夾名稱。
-o hashall / -h
將所使用的工具程式、指令的路徑(path)在腳本啟動時即記錄起來(建進雜湊表中);若未設置,則這些工具程式的路徑是在該工具初次被調度的時候記憶。
如在 bash 中則這個指令預設開啟。
理解上用於解決在腳本開跑後、還沒跑到某個指令的過程中有同名的程式出現在 $PATH 中較優先的順位,然後影響腳本結果。聽起來像是某種在多人共用的伺服器上互相惡搞的過程。
-o keyword / -k
將函數參數設為環境變數。
在下面這個範例中我們呼叫了 foo 兩次,而第一次是在有 set -k 的情況下執行,然後就造成了兩次的輸出內容不相同。
#! /bin/sh
foo() {
echo "BAR=$BAR"
}
foo BAR="hello"
set -k; foo BAR="hello"
BAR=
BAR=hello在 POSIX 文件中也有記錄到這個選項,但文件作者顧慮 set -k 的存在會讓一些函數的行為發生變化,可能進一步地導致不預期的錯誤,故其建議永遠不要使用 -k。
-o monitor / -m
讓分支出去的工作在獨立的行程(process)下執行,而不是成為當前行程的子行程。這個指令在 POSIX 文件端有做記錄,但不是強制要求實作;在 bash 端則是對應成將 job control 功能開啟。
-o noexec / -n
假運行(dry run)。shell 應讀完整支腳本、解析但不運行,可用於檢查有沒有語法錯誤。
-o
-o 若不帶參數使用,則會將所有的設定印出到 stdout 上;若帶有參數,則就是很多選項有看到的、比較長的設定格式。
+o
類似於 -o :會將所有的設定印出到 stdout。不過使用 +o 時輸出格式會是適合直接輸入給其他腳本再利用的格式。如:
$ set +o
set +o allexport
set -o braceexpand
set +o emacs
set +o errexit-o privileged / -p [bash only]
啟用 bash privileged mode。
在這個模式下,環境設置 $BASH_ENV 及 $ENV 會被無視,並且 bash 會以腳本所有者的身份去執行該腳本,這個行爲近似 setuid。
在實例上,這個參數多用於系統啟動階段,讓初始化腳本能使用各自的權限去工作、而不是使用到呼叫他們起床的那個 root。但相對地,考慮到系統的腳本中有不少都是由 root 持有,濫用這個參數反而可能造成權限過大而成為一種漏洞。
-r [bash only]
在這個模式下,腳本能做的事會被限制,包括不能變動當前執行路徑、不能直接使用絕對路徑呼叫指令、不能設置 $PATH 及 $SHELL 等。
-o onecmd / -t
Exit after reading and executing one command.
(在讀取及執行一個指令後就結束)
沒看懂實際應用情境就先原文丟著這樣。
這也是一個在 POSIX 文件後被嘴一波說很沒用的設置;不過它也說到這個設置會導致同一指令在 sh 下跟在 ksh 下可能會有不同的執行結果,故不建議使用。
-o nounset / -u
對於未設置的變數、及特殊變數如(@, *)視為一種錯誤,並立即終止腳本。
shell 腳本中不強制要求我們去將變數是先定義過,當我們想使用某個未被設置的變數時,其預設會讀到空字串。而這個未設置的變數在一些情況下,如打錯變數名稱,便是常見的錯誤來源。透過設置 -u 可以去避免掉這種問題:
#! /bin/sh
set -u
echo "$HELLO"
./demo.sh: line 3: HELLO: unbound variable通常會建議 -u 就永遠開著,用於避免一些錯誤的發生;而在某些情境下,我們會允許使用者用特定環境變數來當作設置、以微調腳本的行為,這時候其實可以透過對變數設置預設值來繞過 -u 帶來的限制:
#! /bin/sh
set -u
echo "${HELLO:-hello}"
hello-o verbose / -v
把輸入的指令複述到 stderr。
#! /bin/sh
set -v
echo foo="${bar:-hello}"
echo foo="${bar:-hello}"
foo=hello-o xtrace / -x
將輸入的指令在參數拓展完成、執行之前輸出到 stderr。
直接用 -v 的範例來跑一次:
#! /bin/sh
set -x
echo foo="${bar:-hello}"
+ echo foo=hello
foo=hello在〈最佳實踐〉段落中有一個文章建議了設置 -x;而筆者認為看使用情境而定,在部分的腳本中你可能已經做了很完善的日誌記錄、或是製作了很漂亮的進度回報,那此時 -x 的輸出就會擾亂使用者閱讀。
反之,如果只是某些「反正就這些指令,去給我跑完」的情境,如 CI 或 docker build,那麼設個 -x 則會有助於偵錯。
-o braceexpand / -B [bash only]
啟用 bash 的 brace expansion 功能。預設開啟。
-o noclobber / -C
禁止輸出重導向行為(如 >, >& 及 <>)去覆寫掉存在的檔案。
-o errtrace / -E [bash only]
讓 trap ERR 的行為可以套用到子函數:
#! /usr/bin/env bash
set -E
foo() {
false
echo "done"
}
trap 'echo Failed on line $LINENO' ERR
foo
Failed on line 3
done另外這個選項十分建議跟 -e 一起搭配服用。
-o histexpand / -H [bash only]
啟用 History Expansion 功能中以 ! 符號進行歷史紀錄代換這個項目。
-o physical / -P [bash only]
對於設置工作路徑的的指令(如 cd),不要去解析符號連結(symbolic link)所對應的路徑,而是使用其實際路徑。
舉例而言,macOS 機器上的 /tmp 路徑其實際路徑為 /private/tmp。我們可以用這個路徑去比較兩者的差異:
$ cd /tmp
$ cd ..; echo $PWD
/
$ cd /tmp
$ set -P; cd ..; echo $PWD
/private參考資料
- set(1p), POSIX Programmer's Manual, https://man7.org/linux/man-pages/man1/set.1p.html
- The Set Builtin, Bash Reference Manual, https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html
- Practical usage of
set -koption in bash, https://unix.stackexchange.com/questions/126223/practical-usage-of-set-k-option-in-bash - How to understand Bash privileged mode?, https://unix.stackexchange.com/questions/439056/how-to-understand-bash-privileged-mode
- Why trap ERR failed in sub function of bash?, https://stackoverflow.com/questions/48740008/why-trap-err-failed-in-sub-function-of-bash