shell指令碼編寫注意

2021-08-08 23:02:51 字數 3561 閱讀 3926

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

shell 指令碼的第一行,#!之後應該是什麼?如果拿這個問題去問別人,不同的人的回答可能各不相同。 

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

在多數情況下,以上五種寫法都是等價的。但是,寫過程式的人都知道:「少數情況」裡往往隱藏著意想不到的坑。

如果系統的預設 shell 不是 bash 怎麼辦?比如某 linux 發行版的某個版本,預設的 sh 就不是 bash。 

如果系統的 bash 不是在 /usr/bin/bash 怎麼辦?

我推薦使用 /usr/bin/env bash 和 /bin/bash。前者通過env新增乙個中間層,讓env在$path中搜尋bash;後者則是官方背書的,約定俗成的 bash 位置,/usr/bin/bash不過是指向它的乙個符號鏈結。

ok,經過一番討論,現在第一行定下來了。接下來該開始寫第二行了吧? 

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

set -x會在執行每一行 shell 指令碼時,把執行的內容輸出來。它可以讓你看到當前執行的情況,裡面涉及的變數也會被替換成實際的值。 

set -e會在執行出錯時結束程式,就像其他語言中的「丟擲異常」一樣。(準確說,不是所有出錯的時候都會結束程式,見下面的注)

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

乙個 pipeline 的非結尾部分,比如 error | ok 

乙個組合語句的非結尾部分,比如 ok && error || other 

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

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

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

好了,現在我已經有了三行(樣板)**,具體的業務邏輯一行都沒寫呢。是不是該開始寫了? 

且慢!工欲善其事,必先利其器。這次,我就介紹乙個 shell 指令碼編寫神器:shellcheck

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

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

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

在 shell 指令碼中,偶爾可以看到這樣的做法:echo $*** | awk/sed/grep/cut… 。看起來大張形勢的樣子,其實不過是想修改乙個變數的值。殺雞何必用牛刀?bash內建的變數展開機制已經足以滿足你各種需求!還是老方法, read the f**k manaul! man bash 然後搜尋parameter expansion,下面就是你想要的技巧。鍵者也寫過一篇相關的文章,希望能助上一臂之力:玩轉bash變數

隨著**越寫越多,你開始把重複的邏輯提煉成函式。有可能你會掉到bash的乙個坑里。在bash,如果不加 local 限定詞,變數預設都是全域性的。變數預設全域性——這跟 js 和 lua 相似;但相較而言,很少有 bash 教程一開始就告知你這個事實。在頂級作用域裡,是否是全域性變數並不重要。但是在函式裡面,宣告乙個全域性變數可能會汙染到其他作用域(尤其在你根本沒有注意到這一點的情況下)。所以,對於在函式內宣告的變數,請務必記得加上 local 限定詞。

如果你寫過稍微複雜點的在後台執行的程式,應該知道 posix 標準裡面「訊號」是什麼一回事。如果不知道,直接看下一段。像其他語言一樣,shell 也支援處理訊號。trap sighandler int可以在接收到 sigint 時呼叫 sighandler 函式。捕獲其他訊號的方式以此類推。

不過 trap 的主要應用場景可不是捕獲哪個訊號。trap 命令支援「捕獲」許多不同的流程——準確來說,允許使用者給特定的流程注入函式呼叫。其中最為常用的是trap func exit和trap func err。

trap func exit允許在指令碼結束時呼叫函式。由於無論正常退出抑或異常退出,所註冊的函式都能得以呼叫,在需要呼叫乙個清理函式的場景下,我都是用它註冊清理函式,而不是簡單地在指令碼結尾呼叫清理函式。

trap func err允許在執行出錯時呼叫函式。乙個常用的技法是,使用全域性變數error儲存錯誤資訊,然後在註冊的函式中根據儲存的值完成對應的錯誤報告。把原本四分五裂的錯誤處理邏輯集中到一處,有時候會起奇效。不過要記住,程式異常退出時,既會呼叫exit註冊的函式,也會呼叫err註冊的函式。

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

這條建議的名字叫「三思而行」。其實無論寫什麼**,哪怕只是乙個輔助指令碼,都要三思而行,切忌粗心大意。不,寫指令碼的時候更要記住這點。畢竟許多時候,乙個複雜的指令碼發端於幾行小小的命令。一開始寫這個指令碼的人,也許以為它只是一次性任務。**裡難免對一些外部條件有些假定,在當時也許是正常的,但是隨著外部環境的變化,這些就成了隱藏的暗礁。雪上加霜的是,幾乎沒有人會給指令碼做測試。除非你去執行它,否則不知道它是否還能正常使用。

要想減緩指令碼**的腐爛速度,需要在編寫的時候辨清哪些是會變的依賴、哪些是指令碼正常執行所不可或缺的。要有適當的抽象,編寫可變更的**;同時要有防禦性程式設計的意識,給自己的**一道護城河。

有些時候,使用 shell 寫指令碼就意味著難以移植、難以統一地進行錯誤處理、難以利索地處理資料。 

雖然使用外部的命令可以方便快捷地實現各種複雜的功能,但作為硬幣的反面,不得不依靠grep、sed、awk等各種工具把它們粘合在一起。 

如果有相容多平台的需求,還得小心規避諸如bsd和gnu coreutils,bash版本差異之類奇奇怪怪的陷阱。 

由於缺乏完善的資料結構以及一致的api,shell 指令碼在處理複雜的邏輯上力不從心。

解決特定的問題要用合適的工具。知道什麼時候用 shell,什麼時候切換到另外一門更通用的指令碼語言(比如ruby/python/perl),這也是編寫可靠 shell 指令碼的訣竅。如果你的任務可以組合常見的命令來完成,而且只涉及簡單的資料,那麼 shell 指令碼就是適合的錘子。如果你的任務包含較為複雜的邏輯,而且資料結構複雜,那麼你需要用ruby/python之類的語言編寫指令碼。

ps:文中提到的幾個點,博主在平時實踐中也有所體會。沒有找到原文的原始出處,所以沒有給出原始鏈結

編寫Shell指令碼

獲取變數的方式 1 echo path 2 echo 3 echo path 引數的提取 引數的個數 n 第n個引數 0 當前指令碼名稱 取出所有引數 shift 引數左移 執行過程 2 編寫指令碼內容 單獨講解 3 新增執行許可權 chmod a x abc.sh 4 當前目錄執行 abc.sh ...

Shell 指令碼編寫

shell 指令碼與 windows dos 下的批處理相似,也就是用各類命令預先放入到乙個檔案中,方便一次性執行的乙個程式檔案,主要是方便管理員進行設定或者管理用的。但是它比 windows 下的批處理更強大,比用其他程式設計程式編輯的程式效率更高,它使用了 linux unix 下的命令。方法一...

shell指令碼編寫

echo echo n不換行輸出,echo e會處理特殊字元,比如有 n則會換行 printf 不自動換行輸出 print 自動換行輸出 傳遞到指令碼的引數個數 以乙個單字串顯示所有向指令碼傳遞的引數。指令碼執行的當前程序id號 後台執行的最後乙個程序的id號 與 相同,但是使用時加引號,並在引號中...