在我最開始管理Linux和Unix服務器時,經常遇到其他管理員編寫的一大堆臨時腳本。時常會因為其中某個腳本突然停止工作而進行故障排查。有時這些腳本編寫得規范好理解,其他時候則是雜亂且令人困惑。
雖然排查編寫糟糕的腳本很麻煩,但我從中吸取到了教訓。即使你認為該腳本只會在今天使用,最好也抱著兩年後還將有人去排查的態度編寫腳本。因為總會有人查看,甚至很可能是你自己。
在本篇文章中,我想介紹一些優化腳本的建議,不是為了方便你編寫腳本,而是方便想要弄清腳本為何不工作的人。
Shell腳本編寫的第一條規則是以釋伴(shebang)行開頭。雖然聽起來很好笑,但釋伴shebang行卻很重要,它告訴系統使用哪種二進制作為腳本的解釋器。沒有釋伴shebang行,系統就不知道使用哪種語言解釋執行腳本。
一個典型的bash 以釋伴shebang行如下所示:
#!/bin/bash
與本文中其他建議不同,這不僅僅是一條建議,而是一條規定。shell腳本必須以解釋器行開始;沒有這行,你的腳本最終將不能工作。我發現很多腳本沒有這一行,有人認為沒有這行腳本就不能工作,但事實並非如此。如果沒有指定腳本解釋器,有些系統會默認使用/bin/sh目錄下的解釋器。如果是bourne shell腳本,默認/bin/sh路徑沒有問題,如果是KSH或者使用特定bash腳本而不是bourne,該腳本可能產生無法預料的結果。
當編寫腳本或者其他程序時,我總會在腳本開頭描述腳本的用途,同時添加我的名字。如果這些腳本是在工作中編寫,我還會加上工作郵箱以及腳本編寫日期。
下面是一個有腳本頭的例子:
#!/bin/bash
### Description: Adds users based on provided CSV file
### CSV file must use : as separator
### uid:username:comment:group:addgroups:/home/dir:/usr/shell:passwdage:password
### Written by: Benjamin Cane - [email protected] on 03-2012
為什麼要添加這些內容?很簡單。這裡的描述是為了向閱讀該腳本的人解釋腳本用途並提供他們需要了解的其他信息。添加名字和郵箱,閱讀該腳本的人如果有疑問就可以聯系上我並提問。添加日期,當他們閱讀腳本時,至少知道該腳本是多久之前編寫的。日期還能觸動你的懷舊之情,當發現自己很久前編寫的腳本時,你會問問自己“在編寫該腳本時,我是怎麼想的?”。
腳本中的描述頭可以根據自己的想法隨意定制,沒有硬性規定哪些是必須的,哪些不需要。通常只要保證信息有效並且放置在腳本開頭即可。
代碼可讀性非常重要,但很多人都會忽略這一點。在深入了解縮進為何很重要前,我們來看一個例子:
NEW_UID=$(echo $x |cut-d:-f1)
NEW_USER=$(echo $x |cut-d:-f2)
NEW_COMMENT=$(echo $x |cut-d:-f3)
NEW_GROUP=$(echo $x |cut-d:-f4)
NEW_ADDGROUP=$(echo $x |cut-d:-f5)
NEW_HOMEDIR=$(echo $x |cut-d:-f6)
NEW_SHELL=$(echo $x |cut-d:-f7)
NEW_CHAGE=$(echo $x |cut-d:-f8)
NEW_PASS=$(echo $x |cut-d:-f9)
PASSCHK=$(grep-c ":$NEW_UID:"/etc/passwd)
if[ $PASSCHK -ge 1]
then
echo"UID: $NEW_UID seems to exist check /etc/passwd"
else
useradd-u $NEW_UID -c "$NEW_COMMENT"-md $NEW_HOMEDIR -s $NEW_SHELL -g $NEW_GROUP -G $NEW_ADDGROUP $NEW_USER
if[!-z $NEW_PASS ]
then
echo $NEW_PASS |passwd--stdin $NEW_USER
chage -M $NEW_CHAGE $NEW_USER
chage -d 0 $NEW_USER
fi
fi
上述代碼能工作嗎?是的,但這段代碼寫的並不好,如果這是一個500行bash腳本,沒有任何縮進,那麼理解該腳本的用途將非常困難。下面看一下使用縮進後的同一段代碼:
NEW_UID=$(echo $x |cut-d:-f1)
NEW_USER=$(echo $x |cut-d:-f2)
NEW_COMMENT=$(echo $x |cut-d:-f3)
NEW_GROUP=$(echo $x |cut-d:-f4)
NEW_ADDGROUP=$(echo $x |cut-d:-f5)
NEW_HOMEDIR=$(echo $x |cut-d:-f6)
NEW_SHELL=$(echo $x |cut-d:-f7)
NEW_CHAGE=$(echo $x |cut-d:-f8)
NEW_PASS=$(echo $x |cut-d:-f9)
PASSCHK=$(grep-c ":$NEW_UID:"/etc/passwd)
if[ $PASSCHK -ge 1]
then
echo"UID: $NEW_UID seems to exist check /etc/passwd"
else
useradd-u $NEW_UID -c "$NEW_COMMENT"-md $NEW_HOMEDIR -s $NEW_SHELL -g $NEW_GROUP -G $NEW_ADDGROUP $NEW_USER
if[!-z $NEW_PASS ]
then
echo $NEW_PASS |passwd--stdin $NEW_USER
chage -M $NEW_CHAGE $NEW_USER
chage -d 0 $NEW_USER
fi
fi
縮進後,很明顯第二個if語句內嵌在第一個if語句內,但如果看未縮進的代碼,第一眼肯定發現不了。
縮進方式取決於你自己,是使用兩個空格、四個空格,還是就使用一個制表符,這都不重要。重要的是代碼每次以相同的方式一致縮進。
縮進可以增加代碼的可理解性,而間距可以增加代碼的可讀性。通常,我喜歡根據代碼的用途來間隔代碼,這是個人偏好,其意義在於使代碼更加可讀並易於理解。
下面是上述代碼添加行間距後的例子:
NEW_UID=$(echo $x |cut-d:-f1)
NEW_USER=$(echo $x |cut-d:-f2)
NEW_COMMENT=$(echo $x |cut-d:-f3)
NEW_GROUP=$(echo $x |cut-d:-f4)
NEW_ADDGROUP=$(echo $x |cut-d:-f5)
NEW_HOMEDIR=$(echo $x |cut-d:-f6)
NEW_SHELL=$(echo $x |cut-d:-f7)
NEW_CHAGE=$(echo $x |cut-d:-f8)
NEW_PASS=$(echo $x |cut-d:-f9)
PASSCHK=$(grep-c ":$NEW_UID:"/etc/passwd)
if[ $PASSCHK -ge 1]
then
echo"UID: $NEW_UID seems to exist check /etc/passwd"
else
useradd-u $NEW_UID -c "$NEW_COMMENT"-md $NEW_HOMEDIR -s $NEW_SHELL -g $NEW_GROUP -G $NEW_ADDGROUP $NEW_USER
if[!-z $NEW_PASS ]
then
echo $NEW_PASS |passwd--stdin $NEW_USER
chage -M $NEW_CHAGE $NEW_USER
chage -d 0 $NEW_USER
fi
fi
如你所見,行間距雖不易覺察,但每一處整潔都讓以後的代碼排錯更簡單。
描述頭適合於添加腳本函數描述,而代碼注釋適合於解釋代碼本身的用途。下面仍是上述相同的代碼片段,但這次我將添加代碼注釋,解釋代碼的用途:
### Parse $x (the csv data) and put the individual fields into variables
NEW_UID=$(echo $x |cut-d:-f1)
NEW_USER=$(echo $x |cut-d:-f2)
NEW_COMMENT=$(echo $x |cut-d:-f3)
NEW_GROUP=$(echo $x |cut-d:-f4)
NEW_ADDGROUP=$(echo $x |cut-d:-f5)
NEW_HOMEDIR=$(echo $x |cut-d:-f6)
NEW_SHELL=$(echo $x |cut-d:-f7)
NEW_CHAGE=$(echo $x |cut-d:-f8)
NEW_PASS=$(echo $x |cut-d:-f9)
### Check if the new userid already exists in /etc/passwd
PASSCHK=$(grep-c ":$NEW_UID:"/etc/passwd)
if[ $PASSCHK -ge 1]
then
### If it does, skip
echo"UID: $NEW_UID seems to exist check /etc/passwd"
else
### If not add the user
useradd-u $NEW_UID -c "$NEW_COMMENT"-md $NEW_HOMEDIR -s $NEW_SHELL -g $NEW_GROUP -G $NEW_ADDGROUP $NEW_USER
### Check if new_pass is empty or not
if[!-z $NEW_PASS ]
then
### If not empty set the password and pass expiry
echo $NEW_PASS |passwd--stdin $NEW_USER
chage -M $NEW_CHAGE $NEW_USER
chage -d 0 $NEW_USER
fi
fi
如果你恰好要閱讀這段bash代碼,卻又不知道這段代碼的用途,至少可以通過查看注釋充分掌握代碼的實現目標。在代碼中添加注釋對其他人非常有幫助,甚至對你自己也有幫助。我曾發現在浏覽自己一個月前編寫的腳本時不知道腳本的用途。如果注釋添加合理,可以在日後節省你和他人的很多時間。
描述性變量名非常直觀,但我發現自己一直都使用通用變量名。通常這些都是臨時變量,從不在該代碼塊之外使用,但即使是臨時變量,解釋清楚它們的含義也很有用。
下面例子中的變量名大部分是描述性的:
for x in`cat $1`
do
NEW_UID=$(echo $x |cut-d:-f1)
NEW_USER=$(echo $x |cut-d:-f2)
可能賦給$NEW_UID和$NEW_USER的值不是很明顯,$1的值代表什麼以及$x的取值是什麼都不夠清楚。更具描述性的修改代碼如下:
INPUT_FILE=$1
for CSV_LINE in`cat $INPUT_FILE`
do
NEW_UID=$(echo $CSV_LINE |cut-d:-f1)
NEW_USER=$(echo $CSV_LINE |cut-d:-f2)
從這段重寫的代碼塊中,很容易看出我們是在讀取一個輸入文件,該文件名是一個CSV文件。同時很容易看出我們從什麼地方獲取新的UID和新的USER信息來存儲在$NEW_UID和$NEW_USER變量中。
上面的例子看上去有點大材小用,但日後會有人感謝你花費額外時間讓變量更具描述性。
如果你想創建一個變量,其值是其他指令的輸出,在bash中有兩種方式實現。第一種是將命令封裝在反引號中,如下所示:
DATE=`date +%F`
第二種是使用一個不同的語法:
DATE=$(date+%F)
雖然兩者都正確,但我個人更喜歡第二種方法。這純粹是個人偏好,但我通常認為$(command)句法比使用反引號更加明顯。假如你在挖掘上百行的bash代碼;你會發現隨著自己不斷閱讀,那些反引號有時看起來像是單引號。此外,有時單引號看起來像是反引號。最後,所有的建議都與偏好掛鉤。所以使用最適合你的,確保與你所選擇使用的方法一致。
上述示例可以讓代碼更加易於閱讀和理解,最後一條建議對在排錯過程前找到錯誤點非常有用。在腳本中添加描述性錯誤信息,可以在前期節省很多排錯時間。浏覽下面的代碼,看看如何能使它更具描述性:
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變量的值是否是一個目錄,如果不是,腳本將退出,並返回一個錯誤代碼1。雖然使用退出代碼能夠告訴其他腳本該腳本未成功執行,但卻沒有給運行該腳本的人做出解釋。
我們讓代碼變得更加友好些:
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
如果運行第一個代碼片段,你將得到大量輸出。如果你得不到輸出,你將不得不打開腳本文件查看哪些地方可能出錯。但如果你運行第二個代碼片段,你立刻就能知道是在腳本指定了無效路徑。僅添加一行代碼就省去了以後大量的排錯時間。
上述例子僅僅是我在編程時嘗試使用的技巧。我相信編寫整潔可讀的bash腳本還有其他很多好建議,如果你有任何建議,隨時在評論區回復。很高興能看到其他人提出來的技巧。