[轉(zhuǎn)]寫好shell腳本的13個技巧

作者|Joseph Matthias Goh
譯者丨無明
編輯|張嬋 高效開發(fā)運維公眾號

產(chǎn)品的最終用戶通常不懂技術(shù),所以不管你怎么折騰產(chǎn)品代碼都無所謂盒犹。但腳本代碼不一樣庭敦,它們是開發(fā)人員寫給開發(fā)人員的务蝠。

有多少次定欧,你運行./script.sh,然后輸出一些東西聚请,但卻不知道它剛剛都做了些什么荠雕。這是一種很糟糕的腳本用戶體驗。我將在這篇文章中介紹如何寫出具有良好開發(fā)者體驗的 shell 腳本驶赏。

產(chǎn)品的最終用戶通常不懂技術(shù)炸卑,所以不管你怎么折騰產(chǎn)品代碼都無所謂。但腳本代碼不一樣煤傍,它們是開發(fā)人員寫給開發(fā)人員的盖文。

這樣會導(dǎo)致一些問題:

  1. 混亂的腳本——我知道,我們都是工程師蚯姆,讀得懂代碼五续,但即使這樣洒敏,也請為我們這些對 Shell 腳本不是很熟練的人考慮一下(我們在寫代碼時也會為你們考慮的)。

  2. 滿屏的日志和錯誤輸出——就算我們也是工程師疙驾,并不代表我們了解你所做的一切凶伙。

  3. 弄得一團(tuán)糟卻沒有做好清理工作——是的,我們可以順著你的腳本手動撤銷變更它碎,但你真的會讓那些信任你的腳本的人這么做嗎镊靴?

所以,我們可以通過一些方法來為自己和別人寫出更好的 shell 腳本链韭。這里給出的所有示例都可以使用與 POSIX 標(biāo)準(zhǔn)兼容的 shell 運行(#!/bin/sh),因為它是最常用的煮落。嫌文章太長了可以只看以下總結(jié)部分:

  1. 提供--help標(biāo)記
  2. 檢查所有命令的可用性
  3. 獨立于當(dāng)前工作目錄
  4. 如何讀取輸入:環(huán)境變量 vs. 標(biāo)記
  5. 打印對系統(tǒng)執(zhí)行的所有操作
  6. 如果有必要敞峭,提供--silent選項
  7. 重新開啟顯示
  8. 用動畫的方式顯示進(jìn)度
  9. 用顏色編碼輸出
  10. 出現(xiàn)錯誤立即退出腳本
  11. 自己執(zhí)行清理工作
  12. 在退出時使用不同的錯誤碼
  13. 在結(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)試和使用鼓黔。

原文

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市不见,隨后出現(xiàn)的幾起案子澳化,更是在濱河造成了極大的恐慌,老刑警劉巖稳吮,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缎谷,死亡現(xiàn)場離奇詭異,居然都是意外死亡灶似,警方通過查閱死者的電腦和手機列林,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進(jìn)店門瑞你,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人席纽,你說我怎么就攤上這事捏悬。” “怎么了润梯?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵过牙,是天一觀的道長。 經(jīng)常有香客問我纺铭,道長寇钉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任舶赔,我火速辦了婚禮扫倡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘竟纳。我一直安慰自己撵溃,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布锥累。 她就那樣靜靜地躺著缘挑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪桶略。 梳的紋絲不亂的頭發(fā)上语淘,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天,我揣著相機與錄音际歼,去河邊找鬼惶翻。 笑死,一個胖子當(dāng)著我的面吹牛鹅心,可吹牛的內(nèi)容都是我干的吕粗。 我是一名探鬼主播,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼旭愧,長吁一口氣:“原來是場噩夢啊……” “哼溯泣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起榕茧,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤垃沦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后用押,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肢簿,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了池充。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片桩引。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖收夸,靈堂內(nèi)的尸體忽然破棺而出坑匠,到底是詐尸還是另有隱情,我是刑警寧澤卧惜,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布厘灼,位于F島的核電站,受9級特大地震影響咽瓷,放射性物質(zhì)發(fā)生泄漏设凹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一茅姜、第九天 我趴在偏房一處隱蔽的房頂上張望闪朱。 院中可真熱鬧,春花似錦钻洒、人聲如沸奋姿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽胀蛮。三九已至,卻和暖如春糯钙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背退腥。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工任岸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人狡刘。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓享潜,卻偏偏與公主長得像,于是被迫代替她去往敵國和親嗅蔬。 傳聞我的和親對象是個殘疾皇子剑按,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,728評論 2 351

推薦閱讀更多精彩內(nèi)容