Linux Shell 程式設計實戰技巧

developerworks發表於2013-09-26

  目前,越來越多的企業應用會部署在 Linux 系統上的,而 Linux Shell 指令碼可以極大地幫助我們完成這些應用的運維任務。這使得 Linux Shell 開發技能成為開發人員的一項重要的、有競爭力的技能。本文就筆者的實際開發經驗,以 Korn Shell 為例分享了指令碼開發中的常見問題及相關技巧。

 避免定時任務指令碼的常見問題

  很多指令碼在實際使用的時候往往是以定時任務的方式執行,而非手工執行。但是實現同樣功能的指令碼在這兩種執行方式下可能遇到的問題不盡相同。

  以定時任務方式執行的指令碼往往會遇到以下幾個問題。

  • 路徑問題:當前目錄往往不是指令碼檔案所在目錄。因此,指令碼在引用其使用的外部檔案,如配置檔案和其它指令碼檔案時,無法方便得使用相對路徑。
  • 命令找不到問題:指令碼中使用到的一些外部命令,在手工執行指令碼的時候可以正常呼叫。但是在定時任務下執行則可能出現指令碼解析器找不到相關命令的問題。
  • 指令碼重複執行問題:一次指令碼的執行未結束,而下一次指令碼的執行已經開始。導致系統中有多個程式在同時執行同一個指令碼。

  下面分享定時任務指令碼開發中上述幾個常見問題的處理方法。

  路徑問題

 定時任務下當前路徑往往不是指令碼檔案所在目錄。因此我們需要用絕對路徑來引用。即先獲取指令碼所在目錄,然後以該目錄為基礎採用絕對路徑的方式去引用指令碼所需的外部檔案。方法如下面程式碼所示。

  清單 1. 獲取指令碼檔案所在路徑

#!/usr/bin/ksh

echo "Current path is: `pwd`"
scriptPath=`dirname $0` #獲取指令碼所在路徑

echo "The script is located at: $scriptPath"
cat "$scriptPath/readme" #使用絕對路徑引用外部檔案

  將清單 1 中的指令碼置於目錄/opt/demo/scripts/auto-task 下,並在 cron 中新增該指令碼。定時任務執行輸出如下。

  Current path is: /home/viscent

  The script is located at: /opt/demo/scripts/auto-task

  命令找不到問題

  定時任務下執行的指令碼可能出現指令碼解析器找不到相關命令的問題。比如 Oracle 資料庫中的 sqlplus 命令,指令碼在呼叫該命令時若沒有特殊處理則在定時任務下執行會使指令碼解析器無法找到這個命令,出現如下所示的錯誤提示:

  sqlplus: command not found

  這是因為指令碼在定時任務下執行時指令碼是由非登入式 Shell 來執行的,並且執行指令碼的父 Shell 並非 Oracle 使用者的 Shell。因此,此時 Oracle 使用者的.profile 檔案並沒有被呼叫。故解決的方法是在指令碼的開頭新增以下程式碼:

  清單 2. 解決找不到外部命令問題

source /home/oracle/.profile

  也就說,對於外部命令找不到的問題,可以通過在指令碼的開頭加一個 source 使用者的.profile 檔案的語句來解決。

  指令碼重複執行問題

  定時任務指令碼的另外一個常見問題是指令碼重複執行的問題。比如,一個指令碼被設定為每 5 分鐘執行一次。若某一次該指令碼的執行無法在 5 分鐘內結束的話,定時任務服務仍然會新啟一個程式來執行該指令碼。這時就出現了執行同一個指令碼的多個程式。而這可能導致指令碼功能紊亂。並且浪費了系統資源。 避免指令碼重複執行的方法通常有兩種。一是在指令碼執行時先檢查系統是否存在執行該指令碼的其它程式。若存在,則終止當前指令碼的執行。二是,指令碼執行時檢查系統中是否存在其它程式執行該指令碼。若存在,則結束那個程式(此方法有一定風險,慎用!)。這兩種方法均需要在指令碼的開頭檢查系統是否已經存在執行當前指令碼的程式,若存在這樣的程式則獲取該程式的 PID。示例程式碼如下清單 3 所示。

  清單 3. 防止指令碼重複執行方法 1

#!/usr/bin/ksh

main(){
selfPID="$$"
scriptFile="$0"

typeset existingPid
existingPid=`getExistingPIDs $selfPID "$scriptFile"`

if [ ! -z "$existingPid" ]; then
  echo "The script already running, exiting..."
  exit -1
fi

doItsTask

}

#獲取除本身程式以外其它執行當前指令碼的程式的 PID
getExistingPIDs(){
selfPID="$1"
scriptFile="$2"

ps -ef | grep "/usr/bin/ksh ${scriptFile}" | grep -v "grep" | awk "{ if(\$2!=$selfPID) print \$2 }"
}

doItsTask(){
echo "Task is now being executed..."
sleep 20  #睡眠 20s,以模擬指令碼在執行需要長時間完成的任務
}

main $*

  清單 4. 防止指令碼重複執行方法 2

#!/usr/bin/ksh

main(){
selfPID="$$"
scriptFile="$0"

typeset existingPid
existingPid=`getExistingPIDs $selfPID "$scriptFile"`

if [ ! -z "$existingPid" ]; then
  echo "The script already running, killing it..."
  kill -9 "$existingPid" #此方法有一定風險,慎用!
fi

doItsTask

}

#獲取除本身程式以外其它執行當前指令碼的程式的 PID
getExistingPIDs(){
selfPID="$1"
scriptFile="$2"
ps -ef | grep "/usr/bin/ksh ${scriptFile}" | grep -v "grep" | awk "{ if(\$2!=$selfPID) print \$2 }"
}

doItsTask(){
echo "Task is now being executed..."
sleep 20  #睡眠 20s,以模擬指令碼在執行需要長時間完成的任務
}

main $*

 指令碼除錯技巧

  雖然 Shell 開發的一個普遍問題是除錯困難,缺乏有效的除錯工具。但是,我們可以採取一些能夠一定程度上幫助我們規避除錯困難的開發與除錯的方式。 由於是指令碼開發,不少人習慣於從直接地一行行地寫程式碼,一個指令碼里面甚至於一個函式都沒有。雖然這種方式在語法上和功能上並無問題。但這增加了除錯的難度。相反,如果採用模組化的方式去編寫指令碼,則使程式碼結構清晰、便於除錯。這點,可以看這樣一個例子。

  假設下面的指令碼的功能是收集生產環境中的相關日誌檔案,用於定位問題。需要收集的日誌檔案包括作業系統日誌、中介軟體日誌以及應用系統本身的日誌。這些檔案會被壓縮成一個 gz 檔案。

  清單 5. 自動收集日誌檔案

#!/usr/bin/ksh

main(){
collectSyslog #收集系統日誌檔案
collectMiddlewareLog #收集中介軟體日誌檔案
collectAppLog #收集應用系統日誌檔案
tar -zcf logs.tgz syslog.zip mdwlog.zip applog.zip #將三中型別的日誌打包,方便下載
}

  若指令碼執行報如下錯誤:

  tar: applog.zip: Cannot stat: No such file or directory

  我們可以很快鎖定 collectAppLog 這個函式。因為它負責輸出 applog.zip 這個檔案。而沒有必要看程式碼中的其它部分。

  採用模組化的方式的另一個好處是程式碼除錯的結果可以鞏固下來。比如上面的例子中,如果你已經除錯好了操作狀態日誌收集的函式。接下來除錯其它函式的時候,這些被除錯的程式碼儘管可能需要改動。但是這些改動影響到之前已經除錯好的程式碼的可能並不大。相反,若是一個指令碼中通篇都是語句,而不帶函式,則改動其中一行程式碼,收集三種日誌的功能可能都受影響。

  另外一個典型的場景是指令碼編寫過程中,我們可能會因為不太確定一些問題如何處理而寫一些嘗試性的程式碼。然後,通過反覆的除錯去確認正確的處理方式。而事實上這些嘗試性的程式碼可能就是一條語句甚至一個命令。但不少人是在大段的程式碼中反覆去除錯這一小段程式碼。這將非常耗時間。尤其是除錯過程中程式碼中的其它部分除錯時出現錯誤時,作者還得先解決其它錯誤,否則可能會時我們真正要除錯的程式碼無法被執行到。這種情形下,專門寫一個測試性的小指令碼。

  在該指令碼中除錯還我們不太確定該如何寫的程式碼,如何將其”整合”到我們正在開發的指令碼中。這樣可以提高除錯效率,避免消耗本不該消耗的時間。比方說,我們在編寫過程中需要獲取指令碼本身所在程式的程式 ID。而此時我們又不太確定這個獲取當前程式 id 的程式碼該怎麼寫。那麼,我們可以新建一個測試性的指令碼在其中嘗試實現這個獲取程式 ID 的功能。找到正確的方法後,將程式碼“移植”到我們真正要開發的指令碼中。

 處理大段字元輸出

  指令碼開發中經常要處理的一個問題是輸出提示資訊。當然,對於簡短的提示資訊輸出,使用 echo 命令就足夠了。但是,對於大段的提示資訊輸出仍然使用 echo 命令處理則顯得不夠優雅。一種更適合的方法是使用 cat 命令結合輸入重定向。下面通過一個具體例子來說明這點。

  假設下面的指令碼會將某個程式安裝到使用者指定的目錄下。若使用者指定的目錄不存在,則提示

  使用者檢查指定的目錄是否正確,並重新執行指令碼。

  清單 6. 使用 echo 命令輸出大段字元

#!/usr/bin/ksh

path="$1"

if [ ! -d "$path" ]; then
        #這裡還必需處理星號這個特殊字元的顯示
echo '****************************************************' 
echo ERROR
echo "The destination directory not exists,make sure below directory you specified is correct:"
echo ${path}
echo "Then re-run this script."
echo '****************************************************'
fi

  這種方式的程式碼可讀性不是很好,閱讀者需要閱讀多個 echo 命令然後再進行”綜合”才能準確理解提示資訊是什麼。另外,一旦提示資訊需要改動。這種改動可能因為改動其中一個 echo 命令時不小心多了一個雙引號等特殊字元而引起語法錯誤,從而影響了整個指令碼的執行。

  清單 7 的程式碼則展示瞭如何使用 cat 命令和輸入重定向來更好地處理大段文字的輸出。

  清單 7. 使用 cat 命令輸出大段字元

#!/usr/bin/ksh

path="$1"
if [ ! -d "$path" ]; then
cat<<EOF
****************************************************
ERROR
The destination directory not exists,make sure below
directory you specified is correct:
${path}
Then re-run this script.
****************************************************
EOF
fi

  顯然,這種處理方式的程式碼更加簡潔,可讀性更好。閱讀者只需要看一條命令,就知道提示資訊的具體內容。並且,若要修改提示語,我們可以放心地在兩個檔案終止符 EOF 之間的部分改。即便修改錯了,也不會影響到程式碼中的其它部分。

 避免使用非必要的臨時檔案

  新手在編寫 Shell 指令碼時往往在不必要使用臨時檔案的情況下使用了臨時檔案。這不僅增加了而外的程式碼編寫工作量(用於處理建立、讀取、和刪除臨時檔案等),而且可能使指令碼執行速度變慢(檔案 I/O 畢竟不是快的操作)。

  下面的例子中假設有個指令碼的功能是往當前目錄下所有的.txt 檔案中新增如下一行文字:

  –End of file name–

  清單 8.和清單 9.中的程式碼分別顯示了在不必要使用臨時檔案的情況下使用臨時檔案的程式碼和不需要使用臨時檔案的程式碼。

  清單 8. 在不必要使用臨時檔案的情況下使用臨時檔案

#!/usr/bin/ksh

ls -lt *.txt | awk '{print $NF}' > tmp #將命令輸出重定向到臨時檔案 tmp

cat tmp

typeset fileName

typeset lastLine

while read fileName #逐行讀取臨時檔案中的每一行

do

 lastLine=`tail -1 "$fileName"`

 if [ ! "$lastLine" == "--End of $fileName--" ]; then

   echo "--End of $fileName--" >> $fileName

 fi

done <tmp #從臨時檔案進行輸入重定向

rm tmp #刪除臨時檔案

  清單 9. 不使用臨時檔案

#!/usr/bin/ksh

typeset fileName

typeset lastLine

for fileName in $(ls -lt *.txt | awk '{print $NF}')

do

 lastLine=`tail -1 "$fileName"`

 if [ ! "$lastLine" == "--End of $fileName--" ]; then

   echo "--End of $fileName--" >> $fileName

 fi

done

 使用支援 FTP 功能的編輯器

  如果你的開發環境是在 Windows 作業系統下,而測試則是通過終端軟體(如 Putty)在 Linux上進行。這種情形下,不少開發者習慣於在終端軟體上直接編輯指令碼(如使用 vi 命令)。顯然,這種方式編輯效率低下。並且,指令碼開發往往需要邊修改邊測試。即使是一個語法錯誤,由於缺乏工具的支援,我們可能要通過執行指令碼才能發現。因此,提高指令碼編輯效率某種程度上便提高了開發效率。在 Windows 系統上開發指令碼時提高指令碼編輯效率的一個不錯的選擇是使用支援簡單的 FTP 功能的編輯器,如 Editplus 和 UltraEditor。可以使用這些編輯器以 FTP 的方式“開啟”(實際上就是下載)Linux 測試主機上的指令碼檔案。編輯好指令碼後對指令碼進行儲存時,這些編輯器會自動將指令碼上傳到測試主機上。接下來只需通過終端軟體對指令碼進行測試。如果測試後指令碼需要繼續修改,則可以利用編輯器的“重新載入文件”的功能(通常可以為該功能設定快捷鍵)。

相關文章