標籤:設定檔 競爭力 linux 技巧 技能
目前,越來越多的公司專屬應用程式會部署在 Linux 系統上的,而 Linux Shell 指令碼可以極大地協助我們完成這些應用的營運任務。這使得 Linux Shell 開發技能成為開發人員的一項重要的、有競爭力的技能。本文就筆者的實際開發經驗,以 Korn Shell 為例分享了指令碼開發中的常見問題及相關技巧。
避免定時任務指令碼的常見問題
很多指令碼在實際使用的時候往往是以定時任務的方式運行,而非手工運行。但是實現同樣功能的指令碼在這兩種運行方式下可能遇到的問題不盡相同。
以定時任務方式啟動並執行指令碼往往會遇到以下幾個問題。
路徑問題:目前的目錄往往不是指令檔所在目錄。因此,指令碼在引用其使用的外部檔案,如設定檔和其它指令檔時,無法方便得使用相對路徑。
命令找不到問題:指令碼中使用到的一些外部命令,在手工執行指令碼的時候可以正常調用。但是在定時任務下運行則可能出現指令碼解析器找不到相關命令的問題。
指令碼重複運行問題:一次指令碼的執行未結束,而下一次指令碼的運行已經開始。導致系統中有多個進程在同時運行同一個指令碼。
下面分享定時任務指令碼開發中上述幾個常見問題的處理方法。
路徑問題
定時任務下當前路徑往往不是指令檔所在目錄。因此我們需要用絕對路徑來引用。即先擷取指令碼所在目錄,然後以該目錄為基礎採用絕對路徑的方式去引用指令碼所需的外部檔案。方法如下面代碼所示。
清單 1. 擷取指令檔所在路徑
#!/usr/bin/kshecho "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/kshmain(){selfPID="$$"scriptFile="$0"typeset existingPidexistingPid=`getExistingPIDs $selfPID "$scriptFile"`if [ ! -z "$existingPid" ]; then echo "The script already running, exiting..." exit -1fidoItsTask}#擷取除本身進程以外其它運行當前指令碼的進程的 PIDgetExistingPIDs(){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/kshmain(){selfPID="$$"scriptFile="$0"typeset existingPidexistingPid=`getExistingPIDs $selfPID "$scriptFile"`if [ ! -z "$existingPid" ]; then echo "The script already running, killing it..." kill -9 "$existingPid" #此方法有一定風險,慎用!fidoItsTask}#擷取除本身進程以外其它運行當前指令碼的進程的 PIDgetExistingPIDs(){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/kshmain(){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/kshpath="$1"if [ ! -d "$path" ]; then #這裡還必需處理星號這個特殊字元的顯示echo ‘****************************************************‘ echo ERRORecho "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/kshpath="$1"if [ ! -d "$path" ]; thencat<<EOF****************************************************ERRORThe destination directory not exists,make sure belowdirectory you specified is correct:${path}Then re-run this script.****************************************************EOFfi
顯然,這種處理方式的代碼更加簡潔,可讀性更好。閱讀者只需要看一條命令,就知道提示資訊的具體內容。並且,若要修改提示,我們可以放心地在兩個檔案終止符 EOF 之間的部分改。即便修改錯了,也不會影響到代碼中的其它部分。
回頁首
避免使用非必要的臨時檔案
新手在編寫 Shell 指令碼時往往在不必要使用臨時檔案的情況下使用了臨時檔案。這不僅增加了而外的代碼編寫工作量(用於處理建立、讀取、和刪除臨時檔案等),而且可能使指令碼運行速度變慢(檔案 I/O 畢竟不是快的操作)。
下面的例子中假設有個指令碼的功能是往目前的目錄下所有的.txt 檔案中添加如下一行文本:
--End of file name--
清單 8.和清單 9.中的代碼分別顯示了在不必要使用臨時檔案的情況下使用臨時檔案的代碼和不需要使用臨時檔案的代碼。
清單 8. 在不必要使用臨時檔案的情況下使用臨時檔案
#!/usr/bin/kshls -lt *.txt | awk ‘{print $NF}‘ > tmp #將命令輸出重新導向到臨時檔案 tmpcat tmptypeset fileNametypeset lastLinewhile read fileName #逐行讀取臨時檔案中的每一行do lastLine=`tail -1 "$fileName"` if [ ! "$lastLine" == "--End of $fileName--" ]; then echo "--End of $fileName--" >> $fileName fidone <tmp #從臨時檔案進行輸入重新導向rm tmp #刪除臨時檔案
清單 9. 不使用臨時檔案
#!/usr/bin/kshtypeset fileNametypeset lastLinefor fileName in $(ls -lt *.txt | awk ‘{print $NF}‘)do lastLine=`tail -1 "$fileName"` if [ ! "$lastLine" == "--End of $fileName--" ]; then echo "--End of $fileName--" >> $fileName fidone
本文出自 “過客” 部落格,謝絕轉載!