來源:公衆號『很酷的程序員』
ID:RealCoolEngineer
正確安全地編寫shell腳本,避免腳本導致出乎意料的結果,並且在出現的問題的時候及時報錯退出。
本文基於筆者以往的shell腳本編程經驗,給出一些基礎的編程技巧,以便提高shell腳本的安全性和健壯性。
一 一定要使用shell嗎?
shell腳本其實很多地方可能都會導致問題,尤其在執行刪除(rm -rf xxx)操作的時候,更是要仔細。因此如果使用shell腳本不是必要的,那就儘量避免使用,畢竟只要不用就不會犯錯(^_^)。
在實踐的過程中,往往也會有額外的字符串處理、目錄遍歷等等一些操作,所以筆者實際工作中更多會採用Python + Shell的方式開發,使用Python的subprocess
模塊執行一些需要在shell環境中執行的命令。但是在使用的時候要進行避免指定shell=True
參數,否則還是可能遇到純shell編程中的一些問題。比如:
import subprocess as sp
# 推薦使用
sp.run(('ls', '-l'))
# 而不是
sp.run('ls -l', shell=True)
二 shell腳本推薦設置
shell本身是有一些配置選項的,通過內置命令set
進行設置和查看,細節可以查看官方文檔:The Set Builtin。
通過命令set -o
可以查看shell的當前選項開關情況如:
➜ ~ # set -o | grep pipe
pipefail off
通過set -o <option-name>
可以開啓某個選項:
➜ ~ # set -o pipefail
➜ ~ # set -o | grep pipe
pipefail on
使用set +o <option-name>
則是關閉指定選項:
➜ ~ # set +o pipefail
➜ ~ # set -o | grep pipe
pipefail off
下面重點來了,對於安全的shell腳本,建議在腳本開頭添加以下設置:
set -euf -o pipefial
1 -e參數
在執行到某個命令返回非0時立即結束腳本。
這是很有用的,在已經發生錯誤的時候及時結束腳本的執行,否則腳本即使有錯誤信息也還是會繼續執行下去,只要最後一個命令正確執行,那麼整個腳本的返回值將會是正常。如下腳本:
#!/bin/bash
set -e
echo "Begin to run"
some-command-not-exists # Will quit script here
echo "Done"
上面的腳本將會在第4行處出現錯誤,並直接退出腳本。
如果某些命令可能執行錯誤是在預期內的,那麼可以通過在命令後面添加|| true
來避免腳本直接退出:
some-comman-may-fail || true
如果是多行命令,則建議使用set +e
暫時關閉發生錯誤則立即退出的功能:
set +e
some-comman-may-fail-1
some-comman-may-fail-2
...
set -e
2 -u參數
將未設置的參數或者變量視爲錯誤並退出腳本。
比如以下腳本:
#!/bin/bash
#set -u
echo "Begin to run"
some-command "p1" $var "p3" # Will quit script here
echo "Done"
因爲var
變量沒有定義,那麼在第4行就根本不會執行命令,而是直接報錯退出。
這裏還有另外一個問題,如果沒有使用
-u
,var
沒有定義,傳遞給命令的第二個參數就會變成"p3"
,顯然這不是編程者的本意。該問題的解決方法後面會提及。
3 -f參數
禁用文件名通配符。
比如腳本:
#!/bin/bash
set -f
echo "Begin to run"
rm -rf *.md
echo "Done"
此時*.md
並不會匹配所有文件名後綴爲.md
的文件,而是文件名只能是"*.md"。這樣的好處在於避免錯誤操作一些後綴名相同的文件,此時可以通過執行需要操作的文件列表來替代。
此功能也可以通過內置命令
shopt
設置shopt -s failglob
來實現,同樣的是不對通配符*
做任何處理。
當然如果必須要使用通配符,那麼就不能使用-f
參數了。
4 -o pipefail選項
針對管道,在管道中某個命令出錯時立即報錯返回。結合-e
就可以實現在管道報錯時也直接退出腳本。
比如以下log分析命令:
set -e -o pipefail
find . -name "logcat*" | xargs grep -a -h "" | grep -a --color=auto -v "\-* switch to"
如果當前目錄沒有任何log文件,那就沒有必要繼續往下執行了。
類似地,如果多個命令之間存在順序依賴關係,那麼可以使用&&
將多個命令連接起來,這樣在某個命令執行失敗時也可以提前返回。比如:
make && make test && make install
5 -x參數
顯示腳本的執行過程。
此參數並不是安全編寫shell腳本的推薦參數,但是對於新手而言,使用此參數可以看到腳本的執行過程。而且不一定需要寫在腳本內部,也可以通過bash -x xxx.sh
查看執行過程。
比如腳本:
#!/bin/bash
echo "Begin to run"
some-command "p1" $var "p3"
echo "Done"
執行時可以看到其流程:
+ echo 'Begin to run'
Begin to run
+ some-command p1 p3
./demo.sh: line 3: some-command: command not found
+ echo Done
Done
每個+
後面就是執行的命令,緊接着的就是執行命令的輸出。
二 安全使用變量
1 任何時候都應該使用引號
在使用變量時,總應該使用雙引號。因爲shell腳本會執行Word Splitting,可能導致一些意想不到的情況。
和前面示範demo的一樣,對於some-command "p1" $var "p3"
,因爲var
沒有定義,所以在執行時實際執行的是some-command p1 p3
,因此就會發生意料之外的問題;而如果使用了雙引號,第二個參數就會是一個空字符串,這纔是更加符合預期的結果。
2 設置參數默認值
可以給變量設置默認值以避免一些異常的發生。
針對未定義字符串:
- ${var-defaultValue}:如果var未定義,那麼表達式爲默認值;
- ${var=defaultValue}:如果var沒有定義,那麼表達式爲默認值,並且設置var爲默認值。
針對未定義或者空字符串:
- ${var:-defaultValue}:如果未定義或者爲空,那麼表達式爲默認值;
- ${var:=defaultValue}:如果var沒有定義或者爲空,那麼表達式爲默認值,並且設置var爲默認值。
三 藉助工具進行檢查
寫完腳本可以使用外部工具做一次檢查,比如開源的shellcheck工具,可以在本地安裝,也可以使用網頁版:https://www.shellcheck.net/。
其實很多IDE(Clion、vscode)都有相關的插件支持shell腳本的檢查,安裝shellcheck插件即可。