目前,愈來愈多的企業應用會部署在 Linux 系統上的,而 Linux Shell 腳本能夠極大地幫助咱們完成這些應用的運維任務。這使得 Linux Shell 開發技能成爲開發人員的一項重要的、有競爭力的技能。本文就筆者的實際開發經驗,以 Korn Shell 爲例分享了腳本開發中的常見問題及相關技巧。html
不少腳本在實際使用的時候每每是以定時任務的方式運行,而非手工運行。可是實現一樣功能的腳本在這兩種運行方式下可能遇到的問題不盡相同。linux
以定時任務方式運行的腳本每每會遇到如下幾個問題。sql
路徑問題:當前目錄每每不是腳本文件所在目錄。所以,腳本在引用其使用的外部文件,如配置文件和其它腳本文件時,沒法方便得使用相對路徑。shell
命令找不到問題:腳本中使用到的一些外部命令,在手工執行腳本的時候能夠正常調用。可是在定時任務下運行則可能出現腳本解析器找不到相關命令的問題。數據庫
腳本重複運行問題:一次腳本的執行未結束,而下一次腳本的運行已經開始。致使系統中有多個進程在同時運行同一個腳本。oracle
下面分享定時任務腳本開發中上述幾個常見問題的處理方法。app
定時任務下當前路徑每每不是腳本文件所在目錄。所以咱們須要用絕對路徑來引用。即先獲取腳本所在目錄,而後以該目錄爲基礎採用絕對路徑的方式去引用腳本所需的外部文件。方法以下面代碼所示。運維
#!/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 中添加該腳本。定時任務運行輸出以下。ide
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 文件並無被調用。故解決的方法是在腳本的開頭添加如下代碼:
source /home/oracle/.profile
也就說,對於外部命令找不到的問題,能夠經過在腳本的開頭加一個 source 用戶的.profile 文件的語句來解決。
定時任務腳本的另一個常見問題是腳本重複運行的問題。好比,一個腳本被設置爲每 5 分鐘運行一次。若某一次該腳本的運行沒法在 5 分鐘內結束的話,定時任務服務仍然會新啓一個進程來執行該腳本。這時就出現了運行同一個腳本的多個進程。而這可能致使腳本功能紊亂。而且浪費了系統資源。 避免腳本重複運行的方法一般有兩種。一是在腳本執行時先檢查系統是否存在運行該腳本的其它進程。若存在,則終止當前腳本的運行。二是,腳本運行時檢查系統中是否存在其它進程運行該腳本。若存在,則結束那個進程(此方法有必定風險,慎用!)。這兩種方法均須要在腳本的開頭檢查系統是否已經存在運行當前腳本的進程,若存在這樣的進程則獲取該進程的 PID。示例代碼以下清單 3 所示。
#!/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 $*
#!/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 文件。
#!/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 命令結合輸入重定向。下面經過一個具體例子來講明這點。
假設下面的腳本會將某個程序安裝到用戶指定的目錄下。若用戶指定的目錄不存在,則提示
用戶檢查指定的目錄是否正確,並從新執行腳本。
#!/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 命令和輸入重定向來更好地處理大段文本的輸出。
#!/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.中的代碼分別顯示了在沒必要要使用臨時文件的狀況下使用臨時文件的代碼和不須要使用臨時文件的代碼。
#!/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 #刪除臨時文件
#!/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