新聞中心
在我最開(kāi)始管理Linux和Unix服務(wù)器時(shí),經(jīng)常遇到其他管理員編寫(xiě)的一大堆臨時(shí)腳本。時(shí)常會(huì)因?yàn)槠渲心硞€(gè)腳本突然停止工作而進(jìn)行故障排查。有時(shí)這些腳本編寫(xiě)得規(guī)范好理解,其他時(shí)候則是雜亂且令人困惑。

雖然排查編寫(xiě)糟糕的腳本很麻煩,但我從中吸取到了教訓(xùn)。即使你認(rèn)為該腳本只會(huì)在今天使用,最好也抱著兩年后還將有人去排查的態(tài)度編寫(xiě)腳本。因?yàn)榭倳?huì)有人查看,甚至很可能是你自己。
在本篇文章中,我想介紹一些優(yōu)化腳本的建議,不是為了方便你編寫(xiě)腳本,而是方便想要弄清腳本為何不工作的人。
以釋伴shebang行開(kāi)頭
Shell腳本編寫(xiě)的第一條規(guī)則是以釋伴(shebang)行開(kāi)頭。雖然聽(tīng)起來(lái)很好笑,但釋伴shebang行卻很重要,它告訴系統(tǒng)使用哪種二進(jìn)制作為腳本的解釋器。沒(méi)有釋伴shebang行,系統(tǒng)就不知道使用哪種語(yǔ)言解釋執(zhí)行腳本。
一個(gè)典型的bash 以釋伴shebang行如下所示:
-
#!/bin/bash
與本文中其他建議不同,這不僅僅是一條建議,而是一條規(guī)定。shell腳本必須以解釋器行開(kāi)始;沒(méi)有這行,你的腳本最終將不能工作。我發(fā)現(xiàn)很多腳本沒(méi)有這一行,有人認(rèn)為沒(méi)有這行腳本就不能工作,但事實(shí)并非如此。如果沒(méi)有指定腳本解釋器,有些系統(tǒng)會(huì)默認(rèn)使用/bin/sh目錄下的解釋器。如果是bourne shell腳本,默認(rèn)/bin/sh路徑?jīng)]有問(wèn)題,如果是KSH或者使用特定bash腳本而不是bourne,該腳本可能產(chǎn)生無(wú)法預(yù)料的結(jié)果。
添加腳本描述頭
當(dāng)編寫(xiě)腳本或者其他程序時(shí),我總會(huì)在腳本開(kāi)頭描述腳本的用途,同時(shí)添加我的名字。如果這些腳本是在工作中編寫(xiě),我還會(huì)加上工作郵箱以及腳本編寫(xiě)日期。
下面是一個(gè)有腳本頭的例子:
1. `#!/bin/bash`
2. `### Description: Adds users based on provided CSV file`
3. `### CSV file must use : as separator`
4. `### uid:username:comment:group:addgroups:/home/dir:/usr/shell:passwdage:password`
5. `### Written by: Benjamin Cane - [email protected] on 03-2012`
為什么要添加這些內(nèi)容?很簡(jiǎn)單。這里的描述是為了向閱讀該腳本的人解釋腳本用途并提供他們需要了解的其他信息。添加名字和郵箱,閱讀該腳本的人如果有疑問(wèn)就可以聯(lián)系上我并提問(wèn)。添加日期,當(dāng)他們閱讀腳本時(shí),至少知道該腳本是多久之前編寫(xiě)的。日期還能觸動(dòng)你的懷舊之情,當(dāng)發(fā)現(xiàn)自己很久前編寫(xiě)的腳本時(shí),你會(huì)問(wèn)問(wèn)自己“在編寫(xiě)該腳本時(shí),我是怎么想的?”。
腳本中的描述頭可以根據(jù)自己的想法隨意定制,沒(méi)有硬性規(guī)定哪些是必須的,哪些不需要。通常只要保證信息有效并且放置在腳本開(kāi)頭即可。
縮進(jìn)代碼
代碼可讀性非常重要,但很多人都會(huì)忽略這一點(diǎn)。在深入了解縮進(jìn)為何很重要前,我們來(lái)看一個(gè)例子:
1. `NEW_UID=$(echo $x |cut-d:-f1)`
2. `NEW_USER=$(echo $x |cut-d:-f2)`
3. `NEW_COMMENT=$(echo $x |cut-d:-f3)`
4. `NEW_GROUP=$(echo $x |cut-d:-f4)`
5. `NEW_ADDGROUP=$(echo $x |cut-d:-f5)`
6. `NEW_HOMEDIR=$(echo $x |cut-d:-f6)`
7. `NEW_SHELL=$(echo $x |cut-d:-f7)`
8. `NEW_CHAGE=$(echo $x |cut-d:-f8)`
9. `NEW_PASS=$(echo $x |cut-d:-f9)`
10. `PASSCHK=$(grep-c ":$NEW_UID:"/etc/passwd)`
11. `if[ $PASSCHK -ge 1]`
12. `then`
13. `echo"UID: $NEW_UID seems to exist check /etc/passwd"`
14. `else`
15. `useradd-u $NEW_UID -c "$NEW_COMMENT"-md $NEW_HOMEDIR -s $NEW_SHELL -g $NEW_GROUP -G $NEW_ADDGROUP $NEW_USER`
16. `if[!-z $NEW_PASS ]`
17. `then`
18. `echo $NEW_PASS |passwd--stdin $NEW_USER`
19. `chage -M $NEW_CHAGE $NEW_USER`
20. `chage -d 0 $NEW_USER`
21. `fi`
22. `fi`
上述代碼能工作嗎?是的,但這段代碼寫(xiě)的并不好,如果這是一個(gè)500行bash腳本,沒(méi)有任何縮進(jìn),那么理解該腳本的用途將非常困難。下面看一下使用縮進(jìn)后的同一段代碼:
1. `NEW_UID=$(echo $x |cut-d:-f1)`
2. `NEW_USER=$(echo $x |cut-d:-f2)`
3. `NEW_COMMENT=$(echo $x |cut-d:-f3)`
4. `NEW_GROUP=$(echo $x |cut-d:-f4)`
5. `NEW_ADDGROUP=$(echo $x |cut-d:-f5)`
6. `NEW_HOMEDIR=$(echo $x |cut-d:-f6)`
7. `NEW_SHELL=$(echo $x |cut-d:-f7)`
8. `NEW_CHAGE=$(echo $x |cut-d:-f8)`
9. `NEW_PASS=$(echo $x |cut-d:-f9)`
10. `PASSCHK=$(grep-c ":$NEW_UID:"/etc/passwd)`
11. `if[ $PASSCHK -ge 1]`
12. `then`
13. `echo"UID: $NEW_UID seems to exist check /etc/passwd"`
14. `else`
15. `useradd-u $NEW_UID -c "$NEW_COMMENT"-md $NEW_HOMEDIR -s $NEW_SHELL -g $NEW_GROUP -G $NEW_ADDGROUP $NEW_USER`
16. `if[!-z $NEW_PASS ]`
17. `then`
18. `echo $NEW_PASS |passwd--stdin $NEW_USER`
19. `chage -M $NEW_CHAGE $NEW_USER`
20. `chage -d 0 $NEW_USER`
21. `fi`
22. `fi`
縮進(jìn)后,很明顯第二個(gè)if語(yǔ)句內(nèi)嵌在第一個(gè)if語(yǔ)句內(nèi),但如果看未縮進(jìn)的代碼,第一眼肯定發(fā)現(xiàn)不了。
縮進(jìn)方式取決于你自己,是使用兩個(gè)空格、四個(gè)空格,還是就使用一個(gè)制表符,這都不重要。重要的是代碼每次以相同的方式一致縮進(jìn)。
增加間距
縮進(jìn)可以增加代碼的可理解性,而間距可以增加代碼的可讀性。通常,我喜歡根據(jù)代碼的用途來(lái)間隔代碼,這是個(gè)人偏好,其意義在于使代碼更加可讀并易于理解。
下面是上述代碼添加行間距后的例子:
1. `NEW_UID=$(echo $x |cut-d:-f1)`
2. `NEW_USER=$(echo $x |cut-d:-f2)`
3. `NEW_COMMENT=$(echo $x |cut-d:-f3)`
4. `NEW_GROUP=$(echo $x |cut-d:-f4)`
5. `NEW_ADDGROUP=$(echo $x |cut-d:-f5)`
6. `NEW_HOMEDIR=$(echo $x |cut-d:-f6)`
7. `NEW_SHELL=$(echo $x |cut-d:-f7)`
8. `NEW_CHAGE=$(echo $x |cut-d:-f8)`
9. `NEW_PASS=$(echo $x |cut-d:-f9)`
10. `PASSCHK=$(grep-c ":$NEW_UID:"/etc/passwd)`
11. `if[ $PASSCHK -ge 1]`
12. `then`
13. `echo"UID: $NEW_UID seems to exist check /etc/passwd"`
14. `else`
15. `useradd-u $NEW_UID -c "$NEW_COMMENT"-md $NEW_HOMEDIR -s $NEW_SHELL -g $NEW_GROUP -G $NEW_ADDGROUP $NEW_USER`
16. `if[!-z $NEW_PASS ]`
17. `then`
18. `echo $NEW_PASS |passwd--stdin $NEW_USER`
19. `chage -M $NEW_CHAGE $NEW_USER`
20. `chage -d 0 $NEW_USER`
21. `fi`
22. `fi`
如你所見(jiàn),行間距雖不易覺(jué)察,但每一處整潔都讓以后的代碼排錯(cuò)更簡(jiǎn)單。
注釋代碼
描述頭適合于添加腳本函數(shù)描述,而代碼注釋適合于解釋代碼本身的用途。下面仍是上述相同的代碼片段,但這次我將添加代碼注釋?zhuān)忉尨a的用途:
1. `### Parse $x (the csv data) and put the individual fields into variables`
2. `NEW_UID=$(echo $x |cut-d:-f1)`
3. `NEW_USER=$(echo $x |cut-d:-f2)`
4. `NEW_COMMENT=$(echo $x |cut-d:-f3)`
5. `NEW_GROUP=$(echo $x |cut-d:-f4)`
6. `NEW_ADDGROUP=$(echo $x |cut-d:-f5)`
7. `NEW_HOMEDIR=$(echo $x |cut-d:-f6)`
8. `NEW_SHELL=$(echo $x |cut-d:-f7)`
9. `NEW_CHAGE=$(echo $x |cut-d:-f8)`
10. `NEW_PASS=$(echo $x |cut-d:-f9)`
11. `### Check if the new userid already exists in /etc/passwd`
12. `PASSCHK=$(grep-c ":$NEW_UID:"/etc/passwd)`
13. `if[ $PASSCHK -ge 1]`
14. `then`
15. `### If it does, skip`
16. `echo"UID: $NEW_UID seems to exist check /etc/passwd"`
17. `else`
18. `### If not add the user`
19. `useradd-u $NEW_UID -c "$NEW_COMMENT"-md $NEW_HOMEDIR -s $NEW_SHELL -g $NEW_GROUP -G $NEW_ADDGROUP $NEW_USER`
20. `### Check if new_pass is empty or not`
21. `if[!-z $NEW_PASS ]`
22. `then`
23. `### If not empty set the password and pass expiry`
24. `echo $NEW_PASS |passwd--stdin $NEW_USER`
25. `chage -M $NEW_CHAGE $NEW_USER`
26. `chage -d 0 $NEW_USER`
27. `fi`
28. `fi`
如果你恰好要閱讀這段bash代碼,卻又不知道這段代碼的用途,至少可以通過(guò)查看注釋充分掌握代碼的實(shí)現(xiàn)目標(biāo)。在代碼中添加注釋對(duì)其他人非常有幫助,甚至對(duì)你自己也有幫助。我曾發(fā)現(xiàn)在瀏覽自己一個(gè)月前編寫(xiě)的腳本時(shí)不知道腳本的用途。如果注釋添加合理,可以在日后節(jié)省你和他人的很多時(shí)間。
創(chuàng)建描述性的變量名
描述性變量名非常直觀,但我發(fā)現(xiàn)自己一直都使用通用變量名。通常這些都是臨時(shí)變量,從不在該代碼塊之外使用,但即使是臨時(shí)變量,解釋清楚它們的含義也很有用。
下面例子中的變量名大部分是描述性的:
1. `for x in`cat $1``
2. `do`
3. `NEW_UID=$(echo $x |cut-d:-f1)`
4. `NEW_USER=$(echo $x |cut-d:-f2)`
可能賦給和NEW_USER的值不是很明顯,的值代表什么以及x的取值是什么都不夠清楚。更具描述性的修改代碼如下:
1. `INPUT_FILE=$1`
2. `for CSV_LINE in`cat $INPUT_FILE``
3. `do`
4. `NEW_UID=$(echo $CSV_LINE |cut-d:-f1)`
5. `NEW_USER=$(echo $CSV_LINE |cut-d:-f2)`
從這段重寫(xiě)的代碼塊中,很容易看出我們是在讀取一個(gè)輸入文件,該文件名是一個(gè)CSV文件。同時(shí)很容易看出我們從什么地方獲取新的UID和新的USER信息來(lái)存儲(chǔ)在和NEW_USER變量中。
上面的例子看上去有點(diǎn)大材小用,但日后會(huì)有人感謝你花費(fèi)額外時(shí)間讓變量更具描述性。
使用 $(command) 進(jìn)行命令替換
如果你想創(chuàng)建一個(gè)變量,其值是其他指令的輸出,在bash中有兩種方式實(shí)現(xiàn)。第一種是將命令封裝在反引號(hào)中,如下所示:
-
DATE=date +%F“
第二種是使用一個(gè)不同的語(yǔ)法:
-
DATE=$(date+%F)
雖然兩者都正確,但我個(gè)人更喜歡第二種方法。這純粹是個(gè)人偏好,但我通常認(rèn)為$(command)句法比使用反引號(hào)更加明顯。假如你在挖掘上百行的bash代碼;你會(huì)發(fā)現(xiàn)隨著自己不斷閱讀,那些反引號(hào)有時(shí)看起來(lái)像是單引號(hào)。此外,有時(shí)單引號(hào)看起來(lái)像是反引號(hào)。最后,所有的建議都與偏好掛鉤。所以使用最適合你的,確保與你所選擇使用的方法一致。
在出錯(cuò)退出前描述問(wèn)題
上述示例可以讓代碼更加易于閱讀和理解,最后一條建議對(duì)在排錯(cuò)過(guò)程前找到錯(cuò)誤點(diǎn)非常有用。在腳本中添加描述性錯(cuò)誤信息,可以在前期節(jié)省很多排錯(cuò)時(shí)間。瀏覽下面的代碼,看看如何能使它更具描述性:
-
if[-d $FILE_PATH ]
-
then
-
for FILE in $(ls $FILE_PATH/*)
-
do
-
echo "This is a file: $FILE"
-
done
-
else
-
exit 1
-
fi
該腳本首先檢查$FILE_PATH變量的值是否是一個(gè)目錄,如果不是,腳本將退出,并返回一個(gè)錯(cuò)誤代碼1。雖然使用退出代碼能夠告訴其他腳本該腳本未成功執(zhí)行,但卻沒(méi)有給運(yùn)行該腳本的人做出解釋。
我們讓代碼變得更加友好些:
-
if[-d $FILE_PATH ]
-
then
-
for FILE in $(ls $FILE_PATH/*)
-
do
-
echo "This is a file: $FILE"
-
done
-
else
-
echo "exiting... provided file path does not exist or is not a directory"
-
exit 1
-
fi
如果運(yùn)行第一個(gè)代碼片段,你將得到大量輸出。如果你得不到輸出,你將不得不打開(kāi)腳本文件查看哪些地方可能出錯(cuò)。但如果你運(yùn)行第二個(gè)代碼片段,你立刻就能知道是在腳本指定了無(wú)效路徑。僅添加一行代碼就省去了以后大量的排錯(cuò)時(shí)間。
上述例子僅僅是我在編程時(shí)嘗試使用的技巧。我相信編寫(xiě)整潔可讀的bash腳本還有其他很多好建議,如果你有任何建議,隨時(shí)在評(píng)論區(qū)回復(fù)。很高興能看到其他人提出來(lái)的技巧。
當(dāng)前題目:編寫(xiě)B(tài)ash腳本的8個(gè)建議
轉(zhuǎn)載來(lái)源:http://m.fisionsoft.com.cn/article/coghsop.html


咨詢(xún)
建站咨詢(xún)
