Linux Shell 腳本編程最佳實(shí)踐

前言
與其它的編碼規(guī)范一樣,這里所討論的不僅僅是編碼格式美不美觀的問題, 同時(shí)也討論一些約定及編碼標(biāo)準(zhǔn)。這份文檔主要側(cè)重于我們所普遍遵循的規(guī)則,對(duì)于那些不是明確強(qiáng)制要求的,我們盡量避免提供意見。
為什么要有編碼規(guī)范
編碼規(guī)范對(duì)于程序員而言尤為重要,有以下幾個(gè)原因:
一個(gè)軟件的生命周期中,80%的花費(fèi)在于維護(hù)
幾乎沒有任何一個(gè)軟件,在其整個(gè)生命周期中,均由最初的開發(fā)人員來維護(hù)
編碼規(guī)范可以改善軟件的可讀性,可以讓程序員盡快而徹底地理解新的代碼
如果你將源碼作為產(chǎn)品發(fā)布,就需要確任它是否被很好的打包并且清晰無誤,一如你已構(gòu)建的其它任何產(chǎn)品
編碼規(guī)范原則
本文檔中的準(zhǔn)則致力于最大限度達(dá)到以下原則:
正確性
可讀性
可維護(hù)性
可調(diào)試性
一致性
美觀
盡管本文檔涵蓋了許多基礎(chǔ)知識(shí),但應(yīng)注意的是,沒有編碼規(guī)范可以為我們回答所有問題,開發(fā)人員始終需要再編寫完代碼后,對(duì)上述原則做出正確的判斷。
代碼規(guī)范等級(jí)定義
可選(Optional):用戶可參考,自行決定是否采用;
推薦(Preferable):用戶理應(yīng)采用,但如有特殊情況,可以不采用;
必須(Mandatory):用戶必須采用(除非是少數(shù)非常特殊的情況,才能不采用);
注:未明確指明的則默認(rèn)為必須(Mandatory)
主要參考如下文檔:
Google Shell Style Guide
Bash Hackers Wiki
源文件
基礎(chǔ)
使用場(chǎng)景
僅建議Shell用作相對(duì)簡(jiǎn)單的實(shí)用工具或者包裝腳本。因此單個(gè)shell腳本內(nèi)容不宜太過復(fù)雜。
在選擇何時(shí)使用shell腳本時(shí)時(shí)應(yīng)遵循以下原則:
如主要用于調(diào)用其他工具且需處理的數(shù)據(jù)量較少,則shell是一個(gè)選擇
如對(duì)性能十分敏感,則更推薦選擇其他語(yǔ)言,而非shell
如需處理相對(duì)復(fù)雜的數(shù)據(jù)結(jié)構(gòu),則更推薦選擇其他語(yǔ)言,而非shell
如腳本內(nèi)容逐漸增長(zhǎng)且有可能出現(xiàn)繼續(xù)增長(zhǎng)的趨勢(shì),請(qǐng)盡早使用其他語(yǔ)言重寫
文件名
可執(zhí)行文件不建議有擴(kuò)展名,庫(kù)文件必須使用.sh
作為擴(kuò)展名,且應(yīng)是不可執(zhí)行的。
執(zhí)行一個(gè)程序時(shí),無需知道其編寫語(yǔ)言,且shell腳本并不要求具有擴(kuò)展名,所以更傾向可執(zhí)行文件沒有擴(kuò)展名。
而庫(kù)文件知道其編寫語(yǔ)言十分重要,使用.sh
作為特定語(yǔ)言后綴的擴(kuò)展名,可以和其他語(yǔ)言編寫的庫(kù)文件加以區(qū)分。
文件名要求全部小寫, 可以包含下劃線_
或連字符-
, 建議可執(zhí)行文件使用連字符,庫(kù)文件使用下劃線。
正例:
反例:
文件編碼
源文件編碼格式為UTF-8。避免不同操作系統(tǒng)對(duì)文件換行處理的方式不同,一律使用LF
。
單行長(zhǎng)度
每行最多不超過120個(gè)字符。每行代碼最大長(zhǎng)度限制的根本原因是過長(zhǎng)的行會(huì)導(dǎo)致閱讀障礙,使得縮進(jìn)失效。
除了以下兩種情況例外:
導(dǎo)入模塊語(yǔ)句
注釋中包含的URL
如出現(xiàn)長(zhǎng)度必須超過120個(gè)字符的字符串,應(yīng)盡量使用here document或者嵌入的換行符等合適的方法使其變短。關(guān)注Linux中文社區(qū)
示例:
除了在行結(jié)束使用換行符,空格是源文件中唯一允許出現(xiàn)的空白字符。
字符串中的非空格空白字符,使用轉(zhuǎn)義字符
不允許行前使用tab縮進(jìn),如果使用tab縮進(jìn),必須設(shè)置1個(gè)tab為4個(gè)空格
不應(yīng)在行尾出現(xiàn)沒有意義的空白字符
垃圾清理(推薦)
對(duì)從來沒有用到的或者被注釋的方法、變量等要堅(jiān)決從代碼中清理出去,避免過多垃圾造成干擾。
結(jié)構(gòu)
使用bash
Bash 是唯一被允許使用的可執(zhí)行腳本shell。
可執(zhí)行文件必須以 #!/bin/bash
開始。請(qǐng)使用set
來設(shè)置shell的選項(xiàng),使得用bash <script_name>
調(diào)用你的腳本時(shí)不會(huì)破壞其功能。
限制所有的可執(zhí)行shell腳本為bash使得我們安裝在所有計(jì)算機(jī)中的shell語(yǔ)言保持一致性。
正例:
反例:
許可證與版權(quán)信息需放在源文件的起始位置。例如:
塊縮進(jìn)
每當(dāng)開始一個(gè)新的塊,縮進(jìn)增加4個(gè)空格(不能使用\t字符來縮進(jìn))。當(dāng)塊結(jié)束時(shí),縮進(jìn)返回先前的縮進(jìn)級(jí)別。縮進(jìn)級(jí)別適用于代碼和注釋。
管道
如果一行容不下整個(gè)管道操作,那么請(qǐng)將整個(gè)管道操作分割成每行一個(gè)管段。
如果一行容得下整個(gè)管道操作,那么請(qǐng)將整個(gè)管道操作寫在同一行,管道左右應(yīng)有空格。
否則,應(yīng)該將整個(gè)管道操作分割成每行一段,管道操作的下一部分應(yīng)該將管道符放在新行并且縮進(jìn)4個(gè)空格。這適用于管道符 |
以及邏輯運(yùn)算 ||
和 &&
。
正例:
反例:
請(qǐng)將 ; do , ; then 和 while , for , if 放在同一行。
shell中的循環(huán)略有不同,但是我們遵循跟聲明函數(shù)時(shí)的大括號(hào)相同的原則。即:; do , ; then 應(yīng)該和 while/for/if 放在同一行。else 應(yīng)該單獨(dú)一行。結(jié)束語(yǔ)句應(yīng)該單獨(dú)一行且跟開始語(yǔ)句縮進(jìn)對(duì)齊。
正例:
反例:
case語(yǔ)句
通過4個(gè)空格縮進(jìn)可選項(xiàng)。可選項(xiàng)中的多個(gè)命令應(yīng)該被拆分成多行,模式表達(dá)式、操作和結(jié)束符 ;; 在不同的行。匹配表達(dá)式比 case 和 esac 縮進(jìn)一級(jí)。多行操作要再縮進(jìn)一級(jí)。模式表達(dá)式前面不應(yīng)該出現(xiàn)左括號(hào)。避免使用 ;& 和 ;;& 符號(hào)。示例:
只要整個(gè)表達(dá)式可讀,簡(jiǎn)單的單行命令可以跟模式和 ;; 寫在同一行。當(dāng)單行容不下操作時(shí),請(qǐng)使用多行的寫法。單行示例:
函數(shù)位置
將文件中所有的函數(shù)統(tǒng)一放在常量下面。不要在函數(shù)之間隱藏可執(zhí)行代碼。
如果你有函數(shù),請(qǐng)將他們統(tǒng)一放在文件頭部。只有includes, set 聲明和常量設(shè)置可能在函數(shù)聲明之前完成。不要在函數(shù)之間隱藏可執(zhí)行代碼。如果那樣做,會(huì)使得代碼在調(diào)試時(shí)難以跟蹤并出現(xiàn)意想不到的結(jié)果。
主函數(shù)main
對(duì)于包含至少了一個(gè)其他函數(shù)的足夠長(zhǎng)的腳本,建議定義一個(gè)名為 main 的函數(shù)。對(duì)于功能簡(jiǎn)單的短腳本, main函數(shù)是沒有必要的。
為了方便查找程序的入口位置,將主程序放入一個(gè)名為 main 的函數(shù)中,作為最底部的函數(shù)。這使其和代碼庫(kù)的其余部分保持一致性,同時(shí)允許你定義更多變量為局部變量(如果主代碼不是一個(gè)函數(shù)就不支持這種做法)。文件中最后的非注釋行應(yīng)該是對(duì) main 函數(shù)的調(diào)用:
注釋
代碼注釋的基本原則:
注釋應(yīng)能使代碼更加明確
避免注釋部分的過度修飾
保持注釋部分簡(jiǎn)單、明確
在編碼以前就應(yīng)開始寫注釋
注釋應(yīng)說明設(shè)計(jì)思路而不是描述代碼的行為
注釋與其周圍的代碼在同一縮進(jìn)級(jí)別,#號(hào)與注釋文本間需保持一個(gè)空格以和注釋代碼進(jìn)行區(qū)分。
文件頭
每個(gè)文件的開頭是其文件內(nèi)容的描述。除版權(quán)聲明外,每個(gè)文件必須包含一個(gè)頂層注釋,對(duì)其功能進(jìn)行簡(jiǎn)要概述。
例如:
功能注釋
主體腳本中除簡(jiǎn)潔明了的函數(shù)外都必須帶有注釋。庫(kù)文件中所有函數(shù)無論其長(zhǎng)短和復(fù)雜性都必須帶有注釋。
這使得其他人通過閱讀注釋即可學(xué)會(huì)如何使用你的程序或庫(kù)函數(shù),而不需要閱讀代碼。
所有的函數(shù)注釋應(yīng)該包含:
函數(shù)的描述
全局變量的使用和修改
使用的參數(shù)說明
返回值,而不是上一條命令運(yùn)行后默認(rèn)的退出狀態(tài)
例如:
TODO注釋
對(duì)那些臨時(shí)的, 短期的解決方案, 或已經(jīng)夠好但仍不完美的代碼使用 TODO 注釋.
TODO 注釋要使用全大寫的字符串 TODO, 在隨后的圓括號(hào)里寫上你的名字,郵件地址, bug ID, 或其它身份標(biāo)識(shí)和與這一 TODO 相關(guān)的 issue。主要目的是讓添加注釋的人 (也是可以請(qǐng)求提供更多細(xì)節(jié)的人) 可根據(jù)規(guī)范的TODO 格式進(jìn)行查找。添加 TODO 注釋并不意味著你要自己來修正,因此當(dāng)你加上帶有姓名的 TODO 時(shí), 一般都是寫上自己的名字。
這與C++ Style Guide中的約定相一致。
例如:
命名
函數(shù)名
使用小寫字母,并用下劃線分隔單詞。使用雙冒號(hào)::
分隔包名。函數(shù)名之后必須有圓括號(hào)。
如果你正在寫單個(gè)函數(shù),請(qǐng)用小寫字母來命名,并用下劃線分隔單詞。如果你正在寫一個(gè)包,使用雙冒號(hào) ::
來分隔包名。函數(shù)名和圓括號(hào)之間沒有空格,大括號(hào)必須和函數(shù)名位于同一行。當(dāng)函數(shù)名后存在 ()
時(shí),關(guān)鍵詞 function 是多余的,建議不帶 function 的寫法,但至少做到同一項(xiàng)目?jī)?nèi)風(fēng)格保持一致。
正例:
反例:
變量名
規(guī)則同函數(shù)名一致。
循環(huán)中的變量名應(yīng)該和正在被循環(huán)的變量名保持相似的名稱。示例:
常量和環(huán)境變量名
全部大寫,用下劃線分隔,聲明在文件的頂部。
常量和任何導(dǎo)出到環(huán)境中的變量都應(yīng)該大寫。示例:
有些情況下首次初始化及常量(例如,通過getopts),因此,在getopts中或基于條件來設(shè)定常量是可以的,但之后應(yīng)該立即設(shè)置其為只讀。值得注意的是,在函數(shù)中使用 declare 對(duì)全局變量無效,所以推薦使用 readonly 和 export 來代替。示例:
只讀變量
使用 readonly 或者 declare -r 來確保變量只讀。
因?yàn)槿肿兞吭趕hell中廣泛使用,所以在使用它們的過程中捕獲錯(cuò)誤是很重要的。當(dāng)你聲明了一個(gè)變量,希望其只讀,那么請(qǐng)明確指出。示例:
局部變量
每次只聲明一個(gè)變量,不要使用組合聲明,比如a=1 b=2;
使用 local 聲明特定功能的變量。聲明和賦值應(yīng)該在不同行。
必須使用 local 來聲明局部變量,以確保其只在函數(shù)內(nèi)部和子函數(shù)中可見。這樣可以避免污染全局名稱空間以及避免無意中設(shè)置可能在函數(shù)外部具有重要意義的變量。
當(dāng)使用命令替換進(jìn)行賦值時(shí),變量聲明和賦值必須分開。因?yàn)閮?nèi)建的 local 不會(huì)從命令替換中傳遞退出碼。正例:
反例:
異常與日志
異常
使用shell返回值來返回異常,并根據(jù)不同的異常情況返回不同的值。
日志
所有的錯(cuò)誤信息都應(yīng)被導(dǎo)向到STDERR,這樣將有利于出現(xiàn)問題時(shí)快速區(qū)分正常輸出和異常輸出。
建議使用與以下函數(shù)類似的方式來打印正常和異常輸出:
編程實(shí)踐(持續(xù)分類并完善)
變量擴(kuò)展(推薦)
通常情況下推薦為變量加上大括號(hào)如 "${var}"
而不是 "$var"
,但具體也要視情況而定。
以下按照優(yōu)先順序列出建議:
與現(xiàn)有代碼保持一致
單字符變量在特定情況下才需要被括起來
使用引號(hào)引用變量,參考下一節(jié):變量引用
詳細(xì)示例如下:
正例:
變量引用(推薦)
變量引用通常情況下應(yīng)遵循以下原則:
默認(rèn)情況下推薦使用引號(hào)引用包含變量、命令替換符、空格或shell元字符的字符串
在有明確要求必須使用無引號(hào)擴(kuò)展的情況下,可不用引號(hào)
字符串為單詞類型時(shí)才推薦用引號(hào),而非命令選項(xiàng)或者路徑名
不要對(duì)整數(shù)使用引號(hào)
特別注意
[[
中模式匹配的引號(hào)規(guī)則在無特殊情況下,推薦使用
$@
而非$*
以下通過示例說明:
命令替換
使用 $(command)
而不是反引號(hào)。
因反引號(hào)如果要嵌套則要求用反斜杠轉(zhuǎn)義內(nèi)部的反引號(hào)。而 $(command) 形式的嵌套無需轉(zhuǎn)義,且可讀性更高。
正例:
反例:
使用 [[ ... ]]
,而不是 [
, test
, 和 /usr/bin/[
。
因?yàn)樵?[[
和 ]]
之間不會(huì)出現(xiàn)路徑擴(kuò)展或單詞切分,所以使用 [[ ... ]]
能夠減少犯錯(cuò)。且 [[ ... ]]
支持正則表達(dá)式匹配,而 [ ... ]
不支持。參考以下示例:
字符串測(cè)試
盡可能使用變量引用,而非字符串過濾。
Bash可以很好的處理空字符串測(cè)試,請(qǐng)使用空/非空字符串測(cè)試方法,而不是過濾字符,讓代碼具有更高的可讀性。
正例:
反例:
正例:
反例:
正例:
反例:
當(dāng)進(jìn)行文件名的通配符擴(kuò)展時(shí),請(qǐng)指定明確的路徑。
當(dāng)目錄中有特殊文件名如以 -
開頭的文件時(shí),使用帶路徑的擴(kuò)展通配符 ./*
比不帶路徑的 *
要安全很多。
慎用eval
應(yīng)該避免使用eval。
Eval在用于分配變量時(shí)會(huì)修改輸入內(nèi)容,但設(shè)置變量的同時(shí)并不能檢查這些變量是什么。反例:
慎用管道連接 while 循環(huán)
請(qǐng)使用進(jìn)程替換或者for循環(huán),而不是通過管道連接while循環(huán)。
這是因?yàn)樵诠艿乐蟮膚hile循環(huán)中,命令是在一個(gè)子shell中運(yùn)行的,因此對(duì)變量的修改是不能傳遞給父shell的。
這種管道連接while循環(huán)中的隱式子shell使得bug定位非常困難。反例:
如果你確定輸入中不包含空格或者其他特殊符號(hào)(通常不是來自用戶輸入),則可以用for循環(huán)代替。例如:
使用進(jìn)程替換可實(shí)現(xiàn)重定向輸出,但是請(qǐng)將命令放入顯式子 shell,而非 while 循環(huán)創(chuàng)建的隱式子 shell。例如:
檢查返回值
總是檢查返回值,且提供有用的返回值。
對(duì)于非管道命令,使用 $? 或直接通過 if 語(yǔ)句來檢查以保持其簡(jiǎn)潔。
例如:
內(nèi)建命令和外部命令
當(dāng)內(nèi)建命令可以完成相同的任務(wù)時(shí),在shell內(nèi)建命令和調(diào)用外部命令之間,應(yīng)盡量選擇內(nèi)建命令。
因內(nèi)建命令相比外部命令而言會(huì)產(chǎn)生更少的依賴,且多數(shù)情況調(diào)用內(nèi)建命令比調(diào)用外部命令可以獲得更好的性能(通常外部命令會(huì)產(chǎn)生額外的進(jìn)程開銷)。
正例:
反例:
文件加載
加載外部庫(kù)文件不建議用使用.,建議使用source,已提升可閱讀性。
正例:
反例:
內(nèi)容過濾與統(tǒng)計(jì)
除非必要情況,盡量使用單個(gè)命令及其參數(shù)組合來完成一項(xiàng)任務(wù),而非多個(gè)命令加上管道的不必要組合。常見的不建議的用法例如:cat和grep連用過濾字符串; cat和wc連用統(tǒng)計(jì)行數(shù); grep和wc連用統(tǒng)計(jì)行數(shù)等。
正例:
反例:
正確使用返回與退出
除特殊情況外,幾乎所有函數(shù)都不應(yīng)該使用exit直接退出腳本,而應(yīng)該使用return進(jìn)行返回,以便后續(xù)邏輯中可以對(duì)錯(cuò)誤進(jìn)行處理。
正例:
反例:
附:常用工具
推薦以下工具幫助我們進(jìn)行代碼的規(guī)范:
ShellCheck