作者|Joseph Matthias Goh
譯者丨無明
編輯|張嬋 高效開發(fā)運維公眾號
產(chǎn)品的最終用戶通常不懂技術(shù),所以不管你怎么折騰產(chǎn)品代碼都無所謂盒犹。但腳本代碼不一樣庭敦,它們是開發(fā)人員寫給開發(fā)人員的务蝠。
有多少次定欧,你運行./script.sh,然后輸出一些東西聚请,但卻不知道它剛剛都做了些什么荠雕。這是一種很糟糕的腳本用戶體驗。我將在這篇文章中介紹如何寫出具有良好開發(fā)者體驗的 shell 腳本驶赏。
產(chǎn)品的最終用戶通常不懂技術(shù)炸卑,所以不管你怎么折騰產(chǎn)品代碼都無所謂。但腳本代碼不一樣煤傍,它們是開發(fā)人員寫給開發(fā)人員的盖文。
這樣會導(dǎo)致一些問題:
混亂的腳本——我知道,我們都是工程師蚯姆,讀得懂代碼五续,但即使這樣洒敏,也請為我們這些對 Shell 腳本不是很熟練的人考慮一下(我們在寫代碼時也會為你們考慮的)。
滿屏的日志和錯誤輸出——就算我們也是工程師疙驾,并不代表我們了解你所做的一切凶伙。
弄得一團(tuán)糟卻沒有做好清理工作——是的,我們可以順著你的腳本手動撤銷變更它碎,但你真的會讓那些信任你的腳本的人這么做嗎镊靴?
所以,我們可以通過一些方法來為自己和別人寫出更好的 shell 腳本链韭。這里給出的所有示例都可以使用與 POSIX 標(biāo)準(zhǔn)兼容的 shell 運行(#!/bin/sh),因為它是最常用的煮落。嫌文章太長了可以只看以下總結(jié)部分:
- 提供--help標(biāo)記
- 檢查所有命令的可用性
- 獨立于當(dāng)前工作目錄
- 如何讀取輸入:環(huán)境變量 vs. 標(biāo)記
- 打印對系統(tǒng)執(zhí)行的所有操作
- 如果有必要敞峭,提供--silent選項
- 重新開啟顯示
- 用動畫的方式顯示進(jìn)度
- 用顏色編碼輸出
- 出現(xiàn)錯誤立即退出腳本
- 自己執(zhí)行清理工作
- 在退出時使用不同的錯誤碼
- 在結(jié)束時打印一個新行
有時間的話可以接著往下看具體內(nèi)容:
提供 --help 標(biāo)記
安裝在系統(tǒng)上的二進(jìn)制文件通常帶有man幫助文檔,但對于腳本來說就不一定了蝉仇。因此我們通常需要為腳本提供-h或--help標(biāo)記來打印有關(guān)如何使用腳本的信息旋讹。如果其他工程師需要修改腳本,這也可以作為腳本的內(nèi)聯(lián)文檔:
#!/bin/sh
if [ ${#@} -ne 0 ] && [ "${@#"--help"}" = "" ]; then
printf -- '...help...\n';
exit 0;
fi;
這段腳本先計算參數(shù)長度(${#@} -ne 0
)轿衔,只有當(dāng)參數(shù)長度不為零時才會檢查--help標(biāo)記沉迹。下一個條件會檢查參數(shù)中是否存在字符串“--help” 。第一個條件是必需的害驹,如果參數(shù)長度為零則不需要打印幫助信息鞭呕。
檢查所有命令的可用性
腳本通常會調(diào)用其他腳本或二進(jìn)制文件。在調(diào)用可能不存在的命令時宛官,請先檢查它們是否可用葫松。可以使用“command -v 二進(jìn)制文件名稱”來執(zhí)行此操作底洗,看看它的退出代碼是否為零腋么。如果命令不可用,可以告訴用戶應(yīng)該如何獲得這個二進(jìn)制文件:
#!/bin/sh
_=$(command -v docker);
if [ "$?" != "0" ]; then
printf -- 'You don\'t seem to have Docker installed.\n';
printf -- 'Get it: https://www.docker.com/community-edition\n';
printf -- 'Exiting with code 127...\n';
exit 127;
fi;
# ...
獨立于當(dāng)前工作目錄
從不同的目錄執(zhí)行腳本可能會發(fā)生錯誤亥揖,這樣的腳本沒有人會喜歡珊擂。要解決這個問題,請使用絕對路徑(/path/to/something
)和腳本的相對路徑(如下所示)费变。
可以使用dirname $0
引用腳本的當(dāng)前路徑:
#!/bin/sh
CURR_DIR="$(dirname $0);"
printf -- 'moving application to /opt/app.jar';
mv "${CURR_DIR}/application.jar" /opt/app.jar;
如何讀取輸入:環(huán)境變量 vs. 標(biāo)記
腳本通過兩種方式接受輸入:環(huán)境變量和選項標(biāo)記(參數(shù))摧扇。根據(jù)經(jīng)驗,對于不影響腳本行為的值胡控,可以使用環(huán)境變量扳剿,而對于可能觸發(fā)腳本不同流程的值,可以使用腳本參數(shù)昼激。
不影響腳本行為的變量可能是訪問令牌和 ID 之類的東西:
#!/bin/sh
# do this
export AWS_ACCESS_TOKEN='xxxxxxxxxxxx';
./provision-everything
# and not
./provisiong-everything --token 'xxxxxxxxxxx';
影響腳本行為的變量可能是需要運行實例的數(shù)量庇绽、是異步還是同步運行锡搜、是否在后臺運行等參數(shù):
#!/bin/sh
# do this
./provision-everything --async --instance-count 400
# and not
INSTANCE_COUNT=400 ASYNC=true ./provision-everything
打印對系統(tǒng)執(zhí)行的所有操作
腳本通常會對系統(tǒng)執(zhí)行有狀態(tài)的更改。不過瞧掺,由于我們不知道用戶何時會向發(fā)送SIGINT
耕餐,也不知道腳本錯誤何時可能導(dǎo)致腳本意外終止,因此很有必要將正在做的事情打印在終端上辟狈,這樣用戶就可以在不去查看腳本的情況下回溯這些步驟:
#!/bin/sh
printf -- 'Downloading required document to ./downloaded... ';
wget -o ./downloaded https://some.site.com/downloaded;
printf -- 'Moving ./downloaded to /opt/downloaded...';
mv ./downloaded /opt/;
printf -- 'Creating symlink to /opt/downloaded...';
ln -s /opt/downloaded /usr/bin/downloaded;
在必要時提供 --silent 選項
有些腳本是為了將其輸出傳給其他腳本肠缔。雖說腳本都應(yīng)該能夠單獨運行,不過有時候也有必要讓它們把輸出結(jié)果傳給另一個腳本哼转∶魑矗可以利用stty -echo
來實現(xiàn)--silent
標(biāo)記:
#!/bin/sh
if [ ${#@} -ne 0 ] && [ "${@#"--silent"}" = "" ]; then
stty -echo;
fi;
# ...
# before point of intended output:
stty +echo && printf -- 'intended output\n';
# silence it again till end of script
stty -echo;
# ...
stty +echo;
exit 0;
重新開啟顯示
在使用stty -echo
關(guān)閉腳本顯示之后,如果發(fā)生致命錯誤壹蔓,腳本將終止趟妥,而且不會恢復(fù)終端輸出,這樣對用戶來說是沒有意義的佣蓉∨悖可以使用trap
來捕捉SIGINT
和其他操作系統(tǒng)級別的信號,然后使用stty echo
打開終端顯示:
#!/bin/sh
error_handle() {
stty echo;
}
if [ ${#@} -ne 0 ] && [ "${@#"--silent"}" = "" ]; then
stty -echo;
trap error_handle INT;
trap error_handle TERM;
trap error_handle KILL;
trap error_handle EXIT;
fi;
# ...
用動畫的方式顯示進(jìn)度
有些命令需要運行很長時間勇凭,并非所有腳本都提供了進(jìn)度條疚膊。在用戶等待異步任務(wù)完成時,可以通過一些方式告訴他們腳本仍在運行虾标。比如在while
循環(huán)中打印一些信息:
#!/bin/sh
printf -- 'Performing asynchronous action..';
./trigger-action;
DONE=0;
while [ $DONE -eq 0 ]; do
./async-checker;
if [ "$?" = "0" ]; then DONE=1; fi;
printf -- '.';
sleep 1;
done;
printf -- ' DONE!\n';
或者可以做一些更好玩的小玩意兒寓盗,比如:
http://mywiki.wooledge.org/BashFAQ/034。
用顏色編碼輸出
在腳本中調(diào)用其他二進(jìn)制文件或腳本時夺巩,對它們的輸出進(jìn)行顏色編碼贞让,這樣就可以知道哪個輸出來自哪個腳本或二進(jìn)制文件。這樣我們就不需要在滿屏的黑白輸出文本中查找想要的輸出結(jié)果柳譬。
理想情況下喳张,腳本應(yīng)該輸出白色(默認(rèn)的,前臺進(jìn)程)美澳,子進(jìn)程應(yīng)該使用灰色(通常不需要销部,除非出現(xiàn)錯誤),使用綠色表示成功制跟,紅色表示失敗舅桩,黃色表示警告。
#!/bin/sh
printf -- 'doing something... \n';
printf -- '\033[37m someone else's output \033[0m\n';
printf -- '\033[32m SUCCESS: yay \033[0m\n';
printf -- '\033[33m WARNING: hmm \033[0m\n';
printf -- '\033[31m ERROR: fubar \033[0m\n';
可以使用\033[Xm
雨膨,其中X
代表顏色代碼擂涛。有些腳本使用\e
而不是\033
,但要注意\e
不適用于所有的 UNIX 系統(tǒng)聊记。
可在.sh 中使用的所有顏色和修飾符:
https://misc.flogisoft.com/bash/tip_colors_and_formatting撒妈。
出現(xiàn)錯誤立即退出腳本
set -e
表示從當(dāng)前位置開始恢暖,如果出現(xiàn)任何錯誤都將觸發(fā)EXIT
。相反狰右,set +e
表示不管出現(xiàn)任何錯誤繼續(xù)執(zhí)行腳本杰捂。
如果腳本是有狀態(tài)的(每個后續(xù)步驟都依賴前一個步驟),那么請使用set -e
棋蚌,在腳本出現(xiàn)錯誤時立即退出腳本嫁佳。如果要求所有命令都要執(zhí)行完(很少會這樣),那么就使用set +e
谷暮。
#!/bin/sh
set +e;
./script-1;
./script-2; # does not depend on ./script-1
./script-3; # does not depend on ./script-2
set -e;
./script-4;
./script-5; # depends on success of ./script-4
# ...
自己執(zhí)行清理工作
大多數(shù)腳本在出現(xiàn)錯誤時不會執(zhí)行清理工作蒿往,能夠做好這方面工作的腳本實屬罕見,但這樣做其實很有用湿弦,還可以省下不少時間熄浓。前面已經(jīng)給出過示例,讓stty
恢復(fù)正常省撑,并借助trap
命令來執(zhí)行清理工作:
#!/bin/sh
handle_exit_code() {
ERROR_CODE="$?";
printf -- "an error occurred. cleaning up now... ";
# ... cleanup code ...
printf -- "DONE.\nExiting with error code ${ERROR_CODE}.\n";
exit ${ERROR_CODE};
}
trap "handle_exit_code" EXIT;
# ... actual script...
在退出時使用不同的錯誤碼
在絕大多數(shù) shell 腳本中,exit 0 表示執(zhí)行成功俯在,exit 1 表示發(fā)生錯誤竟秫。對錯誤與錯誤碼進(jìn)行一對一的映射,這樣有助于腳本調(diào)試跷乐。
#!/bin/sh
# ...
if [ "$?" != "0" ]; then
printf -- 'X happened. Exiting with status code 1.\n';
exit 1;
fi;
# ...
if [ "$?" != "0" ]; then
printf -- 'Y happened. Exiting with status code 2.\n';
exit 2;
fi;
這樣做有另一個額外的好處肥败,就是其他腳本在調(diào)用你的腳本時,可以根據(jù)錯誤碼來判斷發(fā)生了什么錯誤愕提。
在結(jié)束時打印一個新行
如果你有在遵循腳本的最佳實踐馒稍,那么可能會使用printf
代替echo
(它在不同系統(tǒng)中的行為有所差別)。問題是printf
在命令結(jié)束后不會自動添加一個新行浅侨,導(dǎo)致控制臺看起來是這樣的:
這樣一點也不酷纽谒,可以通過簡單的方式打印一個新行:
#!/bin/sh
# ... your awesome script ...
printf -- '\n';
exit 0;
這樣就可以得到:
別人會感謝你這么做的。
總 結(jié)
這篇文章大致總結(jié)了一些簡單易用的技巧如输,讓 shell 腳本更易于調(diào)試和使用鼓黔。