shell指令碼要注意到的問題

2021-09-05 12:04:35 字數 3415 閱讀 5473

1. 指定bash

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不過是指向它的乙個符號鏈結。

2. set -e 和 set -x

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 留下餘地。在**終於可以提交之後,再考慮是否保留它們也不遲。

3. 帶上shellcheck

好了,現在我已經有了三行(樣板)**,具體的業務邏輯一行都沒寫呢。是不是該開始寫了?且慢!工欲善其事,必先利其器。這次,我就介紹乙個 shell 指令碼編寫神器:shellcheck

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

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

4. 變數展開

在 shell 指令碼中,偶爾可以看到這樣的做法:

echo $*** | 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之類的語言編寫指令碼。

ArrayList中儲存陣列時需要注意到的問題

因為陣列的位址是不會發生變化的,每次在陣列中的內容改變後,將陣列新增到arraylist中時,會導致arraylist中的每個內容都是最後新增進去的資料。案例如下所示 object objs new object count while rs.next al.add objs 假設rs中有20個資料...

使用 Calendar 需要注意到的一點地方

calendar cal calendar.getinstance cal.set integer.parseint 2007 integer.parseint 11 integer.parseint 08 system.out.println cal.get calendar.day of mon...

人們不會注意到軟體的內部變化

有些人會經常向我抱怨 為什麼這麼多年過去了,計算器和記事本這兩個小工具一點變化都沒有?實際上,它們確實改變了,只是你沒有注意到而已。如果你觀察下你的記事本程式,你會發現它多了一些額外的選單。還有計算器這個程式也經歷過很多次 大修 對於下面的使用者反饋,我通常不會感到意外 微軟為什麼要花很多時間使wi...