編寫可靠 shell 腳本的 8 個建議

這八個建議,來源於鍵者幾年來編寫 shell 腳本的一些經驗和教訓。事實上開始寫的時候還不止這幾條,後來思索再三,去掉幾條無關痛癢的,最後剩下八條。絕不誇張地說,每條都是精挑細選的,雖然有幾點算是老生常談了。python

 

1. 指定bash

 

shell 腳本的第一行,#!以後應該是什麼?若是拿這個問題去問別人,不一樣的人的回答可能各不相同。shell

 

我見過/usr/bin/env bash,也見過/bin/bash,還有/usr/bin/bash,還有/bin/sh,還有/usr/bin/env sh。這算是編程界的「’茴’字四種寫法」了。編程

 

在多數狀況下,以上五種寫法都是等價的。可是,寫過程序的人都知道:「少數狀況」裏每每隱藏着意想不到的坑。ruby

 

若是系統的默認 shell 不是 bash 怎麼辦?好比某 Linux 發行版的某個版本,默認的 sh 就不是 bash。bash

 

若是系統的 bash 不是在 /usr/bin/bash 怎麼辦?數據結構

 

我推薦使用 /usr/bin/env bash 和 /bin/bash。前者經過env添加一箇中間層,讓env在$PATH中搜索bash;後者則是官方背書的,約定俗成的 bash 位置,/usr/bin/bash不過是指向它的一個符號連接。函數

 

2. set -e 和 set -x

 

OK,通過一番討論,如今第一行定下來了。接下來該開始寫第二行了吧?工具

且慢!在你開始構思並寫下具體的代碼邏輯以前,先插入一行set -e和一行set -x。測試

 

set -x會在執行每一行 shell 腳本時,把執行的內容輸出來。它可讓你看到當前執行的狀況,裏面涉及的變量也會被替換成實際的值。lua

 

set -e會在執行出錯時結束程序,就像其餘語言中的「拋出異常」同樣。(準確說,不是全部出錯的時候都會結束程序,見下面的注)

 

注:set -e結束程序的條件比較複雜,在man bash裏面,足足用了一段話描述各類情景。大多數執行都會在出錯時退出,除非 shell 命令位於如下狀況:

 

  1. 一個 pipeline 的非結尾部分,好比 error | ok

  2. 一個組合語句的非結尾部分,好比 ok && error || other

  3. 一連串語句的非結尾部分,好比 error; ok

  4. 位於判斷語句內,包括test、if、while等等。

這兩個組合在一塊兒用,能夠在 debug 的時候替你節省許多時間。出於防護性編程的考慮,有必要在寫第一行具體的代碼以前就插入它們。捫心自問,寫代碼的時候可以一次寫對的次數有多少?大多數代碼,在提交以前,一般都經歷過反覆調試修改的過程。與其在焦頭爛額之際才引入這兩個配置,不如一開始就給 debug 留下餘地。在代碼終於能夠提交以後,再考慮是否保留它們也不遲。

 

3. 帶上shellcheck

 

好了,如今我已經有了三行(樣板)代碼,具體的業務邏輯一行都沒寫呢。是否是該開始寫了?

 

且慢!工欲善其事,必先利其器。此次,我就介紹一個 shell 腳本編寫神器:

shellcheck

 

說來慚愧,雖然寫了幾年 shell 腳本,有些語法我仍是記不清楚。這時候就要依仗 shellcheck 指點一下了。shellcheck 除了能夠提醒語法問題之外,還能檢查出 shell 腳本編寫常見的 bad code。原本個人N條建議裏面,還有幾條是關於這些 bad code 的,不過考慮到 shellcheck 徹底能夠發掘出這些問題,因而忍痛把它們都剔除在外了。毫無疑問,使用 shellcheck 給個人 shell 編寫技能帶來了巨大的飛躍。

 

所謂「站在巨人的肩膀上」,雖然咱們這些新兵蛋子,技能不如老兵們強,可是咱們能夠在裝備上遇上對方啊!動動手安裝一下,就能結識一個循循善誘的「老師」,何樂而不爲?

 

順便一提,shellcheck 竟然是用 haskell 寫的。誰說 haskell 只能用來裝逼?

 

4. 變量展開

 

在 shell 腳本中,偶爾能夠看到這樣的作法:echo $xxx | awk/sed/grep/cut... 。看起來大張形勢的樣子,其實不過是想修改一個變量的值。殺雞何須用牛刀?bash內建的變量展開機制已經足以知足你各類需求!仍是老方法, read the f**k manaul! man bash 而後搜索Parameter Expansion,下面就是你想要的技巧。鍵者也寫過一篇相關的文章,但願能助上一臂之力:玩轉Bash變量

 

5. 注意local

 

隨着代碼越寫越多,你開始把重複的邏輯提煉成函數。有可能你會掉到bash的一個坑裏。在bash,若是不加 local 限定詞,變量默認都是全局的。變量默認全局——這跟 js 和 lua 類似;但相較而言,不多有 bash 教程一開始就告知你這個事實。在頂級做用域裏,是不是全局變量並不重要。可是在函數裏面,聲明一個全局變量可能會污染到其餘做用域(尤爲在你根本沒有注意到這一點的狀況下)。因此,對於在函數內聲明的變量,請務必記得加上 local 限定詞。

 

6. trap信號

 

若是你寫過稍微複雜點的在後臺運行的程序,應該知道 posix 標準裏面「信號」是什麼一回事。若是不知道,直接看下一段。像其餘語言同樣,shell 也支持處理信號。trap sighandler INT能夠在接收到 SIGINT 時調用 sighandler 函數。捕獲其餘信號的方式以此類推。

 

不過 trap 的主要應用場景可不是捕獲哪一個信號。trap 命令支持「捕獲」許多不一樣的流程——準確來講,容許用戶給特定的流程注入函數調用。其中最爲經常使用的是trap func EXIT和trap func ERR。

 

trap func EXIT容許在腳本結束時調用函數。因爲不管正常退出抑或異常退出,所註冊的函數都能得以調用,在須要調用一個清理函數的場景下,我都是用它註冊清理函數,而不是簡單地在腳本結尾調用清理函數。

 

trap func ERR容許在運行出錯時調用函數。一個經常使用的技法是,使用全局變量ERROR存儲錯誤信息,而後在註冊的函數中根據存儲的值完成對應的錯誤報告。把本來四分五裂的錯誤處理邏輯集中到一處,有時候會起奇效。不過要記住,程序異常退出時,既會調用EXIT註冊的函數,也會調用ERR註冊的函數。

 

7. 三思後行

 

以上幾條都是具體的建議,剩下兩條比較務虛。

 

這條建議的名字叫「三思而行」。其實不管寫什麼代碼,哪怕只是一個輔助腳本,都要三思而行,切忌粗枝大葉。不,寫腳本的時候更要記住這點。畢竟許多時候,一個複雜的腳本發端於幾行小小的命令。一開始寫這個腳本的人,也許覺得它只是一次性任務。代碼裏不免對一些外部條件有些假定,在當時也許是正常的,可是隨着外部環境的變化,這些就成了隱藏的暗礁。雪上加霜的是,幾乎沒有人會給腳本作測試。除非你去運行它,不然不知道它是否還能正常使用。

 

要想減緩腳本代碼的腐爛速度,須要在編寫的時候辨清哪些是會變的依賴、哪些是腳本正常運行所不可或缺的。要有適當的抽象,編寫可變動的代碼;同時要有防護性編程的意識,給本身的代碼一道護城河。

 

8. 揚長避短

 

有些時候,使用 shell 寫腳本就意味着難以移植、難以統一地進行錯誤處理、難以利索地處理數據。

 

雖然使用外部的命令能夠方便快捷地實現各類複雜的功能,但做爲硬幣的反面,不得不依靠grep、sed、awk等各類工具把它們粘合在一塊兒。

 

若是有兼容多平臺的需求,還得當心規避諸如BSD和GNU coreutils,bash版本差別之類奇奇怪怪的陷阱。

 

因爲缺少完善的數據結構以及一致的API,shell 腳本在處理複雜的邏輯上力不從心。

 

解決特定的問題要用合適的工具。知道何時用 shell,何時切換到另一門更通用的腳本語言(好比ruby/python/perl),這也是編寫可靠 shell 腳本的訣竅。若是你的任務能夠組合常見的命令來完成,並且只涉及簡單的數據,那麼 shell 腳本就是適合的錘子。若是你的任務包含較爲複雜的邏輯,並且數據結構複雜,那麼你須要用ruby/python之類的語言編寫腳本。

相關文章
相關標籤/搜索