Shell script 的執行參數
來聊聊 shell script 的 `set` 參數們
在寫 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 -k
option 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