歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> 關於Linux >> Linux Shell 編程實戰技巧

Linux Shell 編程實戰技巧

日期:2017/3/3 16:13:14   编辑:關於Linux

避免定時任務腳本的常見問題

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

以定時任務方式運行的腳本往往會遇到以下幾個問題。

路徑問題:當前目錄往往不是腳本文件所在目錄。因此,腳本在引用其使用的外部文件,如配置文件和其它腳本文件時,無法方便得使用相對路徑。

命令找不到問題:腳本中使用到的一些外部命令,在手工執行腳本的時候可以正常調用。但是在定時任務下運行則可能出現腳本解析器找不到相關命令的問題。

腳本重復運行問題:一次腳本的執行未結束,而下一次腳本的運行已經開始。導致系統中有多個進程在同時運行同一個腳本。

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

路徑問題

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

清單 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 測試主機上的腳本文件。編輯好腳本後對腳本進行保存時,這些編輯器會自動將腳本上傳到測試主機上。接下來只需通過終端軟件對腳本進行測試。如果測試後腳本需要繼續修改,則可以利用編輯器的“重新載入文檔”的功能(通常可以為該功能設置快捷鍵)。

Copyright © Linux教程網 All Rights Reserved