對話 UNIX,第 6 部分: 通過腳本實現操作的自動化
下面是簡介:
如果您曾經在資深 Unix® 用戶工作時站在他的背后注視屏幕,可能會對命令行上不斷滾動的咒語般的奇怪內容感到相當迷惑。如果您閱讀過對話 UNIX 系列中以前的文章(請參見參考資料),那么至少所輸入的某些詩一般的神秘內容——如波形符 (~)、管道 (|)、變量和重定向(< 和 >)——看起來是熟悉的。您也許還會認出某些 UNIX 命令名稱和組合,或者了解何時使用別名來作為某個命令組合的簡寫形式。
盡管如此,還有其他命令組合可能是您無法理解的,因為資深的 UNIX 用戶通常以 Shell 腳本 的形式收集一大堆小的、高度專門化的命令組合,以簡化或自動化經常重復的任務。與輸入或重新輸入(可能)復雜的命令來完成某個繁瑣任務不同,Shell 腳本可以自動化該工作。
在對話 UNIX 系列(請參見參考資料)的第 6 部分中,您將學習如何編寫 Shell 腳本和更多命令行訣竅。
核心就是一個詞:“自動化
有些 Shell 腳本完全就是反復運行同樣的命令,并處理同樣的一組文件。例如,將您的整個主目錄內容傳播到三臺遠程計算機的 Z Shell 腳本可以像清單 1 一樣簡單。
清單 1. 跨多臺計算機同步主目錄的簡單 Shell 腳本
#! /bin/zshfor each Machine (groucho chico harpo)rsync -e ssh --times --perms --recursive --delete $HOME $machine:end
若要將清單 1 用作 Shell 腳本,可以將上述內容保存到某個文件——例如 simpleprop.zsh——并運行 chmod +x simpleprop.zsh 以使該文件成為可執行文件。您可以通過輸入 ./simpleprop.zsh 來運行該腳本。
如果您想查看 Z Shell 如何展開每個命令,可以將 -x 選項添加到腳本的 #!(# 號-感嘆號對通常稱為 shuh-bang)行的結尾,如下所示:
#! /bin/zsh -x
該腳本對 groucho、chico 和 harpo 中的每一臺計算機運行 rsync 命令,并將 $HOME 替換為您的主目錄(例如,/home/joe),將 $Machine 替換為計算機名稱。
如清單 1 所示,變量和諸如循環等腳本控制結構使腳本更容易編寫和維護。如果您想將第四臺計算機(例如 zeppo)包括到計算機池中,只需將其添加到該列表。如果您必須更改 rsync 命令,比如說添加另一個選項,則只需編輯一個實例。與在傳統編程中一樣,您也應該努力避免在 Shell 腳本中進行剪切和粘貼。
使用恰當的參數
其他 Shell 腳本需要參數,或要處理的對象——文件、目錄、計算機名稱——的動態列表。例如,考慮清單 2,這是前一示例的變體,它允許您使用命令行來指定您想要與之同步的計算機。
清單 2. 允許您指定要處理的計算機的清單 1 的變體
#! /bin/zshfor each machinersync -e ssh --times --perms --recursive --delete $HOME $machine:end
假設您將清單 2 保存在名為 synch.zsh 的文件中,您得按照 zsh synch.zsh moe larry curly 的形式調用該腳本,以將主目錄復制到另外的計算機 larry 和 curly。
foreach 行上缺少的列表并不是輸入錯誤:如果您省略某個列表,則 foreach 結構將處理命令行上給出的參數列表。命令行參數也稱為位置參數 (positional parameter),因為某個參數在命令行上的位置通常在語義上非常重要。
例如,如果您未 指定任何參數,則 清單 2 可以利用位置參數的存在性或非存在性來提供有幫助的用法信息。增強的腳本如清單 3 所示。
清單 3. 許多腳本將在未提供參數時提供有幫助的消息
#! /bin/zshif [[ -z $1 || $1 == "--help" ]]thenecho "usage: $0 Machine [machine ...]fiforeach machinersync -e ssh --times --perms --recursive --delete $HOME $machine:end
命令行上的每個空格分隔的字符串變成了位置參數,包括所調用的腳本的名稱。因此,命令 synch.zsh 只有一個位置參數 $0。synch.zsh --help 命令有兩個位置參數:$0 和 $1,其中 $1 是字符串 --help。
所以,清單 3 表示“如果第一個位置參數為空(-z 操作符測試空字符串)或(由 || 表示)如果第一個參數等于‘—help’,則打印用法信息。(如果您剛開始編寫腳本,可以考慮在每個腳本中提供用法信息作為提示。它提醒其他人——甚至您自己,如果您忘了的話——如何使用該腳本。)
短語 [[ -z $1 || $1 == "--help" ]] 是 if 語句的 條件,但您也可以將同樣的條件子句用作命令,并將其與其他命令組合使用以控制通過腳本的流。請查看清單 4。它枚舉您的 $PATH 中的所有可執行命令,并將條件與其他命令組合使用以執行適當的工作。
清單 4. 列出 $PATH 中的命令
#! /bin/zshDirectorIEs=(`echo $PATH | column -s ':' -t`)for directory in $directoriesdo [[ -d $directory ]] || continue pushd "$directory" for file in * do [[ -x $file && ! -d $file ]] || continue echo $file done popddone | sort | uniq
此腳本中執行了相當多的操作,我們將它細分為以下幾部分:
第一個實際腳本行——DirectorIEs=(`echo $PATH | column -s ':' -t`)——創建指定目錄的數組。您在 zsh 中通過將參數放在括號中來創建數據,例如 directories=(...)。在此例中,數組元素是通過在每個冒號(column -s ':')處分拆 $PATH 以產生空格分隔的目錄列表(column 的 -t 參數)來生成的。
對于列表中的每個目錄,該腳本嘗試枚舉該目錄中的可執行文件。步驟 3 至步驟 6 描述了該過程。
[[ -d $directory ]] || continue 行是所謂的 short-circuiting 命令的一個示例。short-circuiting 命令在其邏輯條件產生確定的結果時立即終止。
例如,[[ -d $directory ]] || continue 短語使用邏輯“或(||)——它首先執行第一個命令,并且——當且僅當——第一個命令失敗時才執行第二個命令。因此,如果 $directory 中的條目存在,并且是一個目錄(-d 操作符),則測試成功,求值結束,并且 continue 命令(它跳過當前元素的處理)永遠不會執行。
然而,如果第一個測試失敗,則會執行該邏輯的下一個條件或執行 continue。(continue 始終成功,因此它通常出現在 short-circuiting 命令的最后)。
基于邏輯“與(&&) 的 Short-circuiting 首先執行第一個命令,并且——當且僅當——第一個命令成功時才執行第二個命令。
pushd 和對應的 popd 分別用于在處理前切換到新目錄和在處理后切換到先前的目錄。使用目錄堆棧是一種理想的腳本技術,用于維持您在文件系統中的位置。
內部的 for 循環枚舉當前工作目錄中的所有文件——通配符 *(星號)匹配所有條目——然后測試每個條目是否為文件。[[ -x $file && ! -d $file ]] || continue 行表示“如果 $file 存在并且是可執行文件而且不是目錄,則處理它;否則執行 continue。
最后,如果前面的所有條件都滿足,則使用 echo 來顯示文件名。
您弄明白該腳本的最后一行了嗎?您可以將大多數控制結構的輸出發送給另一個 Unix 命令——畢竟,Shell 將該控制結構視為一個命令。因此,整個腳本的輸出通過 sort、然后通過 uniq 進行管道傳輸,以產生在您的 $PATH 中找到的唯一命令的字母排序列表。
如果將清單 4 保存到一個名為 listcmds.zsh 的可執行文件,則輸出可能類似如下:
$ ./listcmds.zsh[a2pabacacceptacctonaclocal
short-circuiting 命令在腳本中非常有用。它在單個命令中組合了條件和操作。而且由于每個 UNIX 命令都返回一個指示成功或失敗的狀態代碼,因此,您可以使用任何命令作為“條件——而不僅僅是使用測試操作符。根據約定,UNIX 返回零 (0) 表示成功,返回非零表示失敗,其中非零值反映所發生的錯誤類型。
例如,如果將 [[ -d $Directory ]] || continue 行替換為 cd $directory || continue,則可以從清單 4 中消除 pushd 和 popd。如果 cd 命令成功,則它會返回 0,并且邏輯“或的求值可以立即結束。然而,如果 cd 失敗,則它會返回非零,并且會執行 continue。
不要刪除。應存檔!
現代 UNIX Shell——bash、ksh、zsh——提供了許多控制結構和操作以創建復雜的腳本。由于您可以調用所有 UNIX 命令來將數據從一種形式處理為另一種形式,Shell 腳本編程幾乎與諸如 C 或 Perl 等完整語言中的編程一樣豐富。
您可以使用腳本來自動化幾乎所有個人或系統任務。腳本可以監視、存檔、更新、上載、下載和轉換數據。一個腳本可以只有單行或包括無數個子系統。任務無論大小,均可通過腳本來處理。實際上,如果您查看 /etc/init.d 目錄,會看到在每次啟動計算機時運行服務的各種 Shell 腳本。如果您創建了一個非常有用的腳本,您甚至可以將它部署為系統范圍的實用程序。只需將其放到用戶的 $PATH 上的某個目錄中。
讓我們創建一個實用程序,以練習您新發現的訣竅。腳本 myrm 將替換系統自己的 rm 實用程序。與徹底刪除某個文件不同,myrm 把要刪除的文件復制到某個存檔,對其進行唯一命名以便您以后能夠找到它,然后再刪除原始文件。myrm 腳本有效但是非常簡單,并且您還可以添加許多雜項功能。您還可以編寫一個廣泛的 unrm(撤銷刪除)腳本作為配套實用程序。(您可以搜索 Internet 來找到各種各樣的實現。)
myrm 腳本如清單 5 所示。
清單 5. 用于在從文件系統中刪除文件之前備份該文件的簡單實用程序
#! /bin/zshbackupdir=$HOME/.tombsystemrm=/bin/rmif [[ -z $1 || $1 == "--help" ]]then exec $systemrmfiif [[ ! -d $backupdir ]]then mkdir -m 0700 $backupdir || echo "$0: Cannot create $backupdir"exitfiargs$=$( getopt dfiPRrvw $* ) || exec $systemrmcount=0flags = ""foreach argument in $argsdo case $argument in--) break;;; *) flags="$flags $argument";(( count=$count + 1 ));;; esacdoneshift $(( $count ))for filedo [[ -e $file ]] || continue copyfile=$backupdir/$(basename $file).$(date "+%m.%d.%y.%H.%M.%S") /bin/cp -R $file $copyfiledoneexec $systemrm $=flags "$@"
您應該發現該 Shell 腳本很容易理解,盡管其中存在一些之前尚未討論過的新內容。讓我們探討一下那些新內容,然后查看整個腳本。
當 Shell 運行某個命令(如 cp 或 ls)時,它會為該命令產生一個新進程,然后在繼續之前等待該(子)進程完成。exec 命令還啟動另外一個命令,但是與產生新進程不同,exec 使用一個新命令來“替換當前進程——即 Shell 進程——的任務。換句話說,exec 重用同一進程來啟動一個新任務。在該腳本的上下文中,exec 立即“終止該腳本并啟動指定的任務。
Unix 實用程序 getopt 掃描位置參數以獲得您指定的命名參數。這里,dfiPRrvw 列表查找 -d、-f、-i、-P、-R、-r、-v 和 -w。如果出現別的選項,則 getopt 將會失敗。否則,getopt 返回一個以特殊字符串 -- 結尾的選項字符串。
shift 命令從左到右刪除位置參數。例如,如果命令行為 myrm, -r -f -P file1 file2 file3,則 shift 3 將分別刪除 $0、$1 和 $2,或 -r、-f 和 -P。file1、file2 和 file3 將被重新編號為 $0、$1 和 $2。
case 語句的工作方式與傳統編程語言中的對應結構相似。它將其參數與列表中的每個模式比較;當找到匹配項時,則執行對應的代碼。與在 Shell 中非常類似,* 匹配所有條目,并且可用作在未找到其他匹配項時的缺省操作。
特殊符號 $@ 展開為所有(其余)的位置參數。
zsh 操作符 $= 在空白邊界處拆分單詞。當您有一個非常長的字符串,并且希望將該字符串拆分為各個參數時,$= 是非常有用的。例如,如果變量 x 包含字符串 '-r -f'——這是一個具有五個字符的單詞——$=x 將變為兩個單獨的單詞 -r 和 -f。
給出這些解釋之后,您現在應該能夠詳細分析該腳本了。下面讓我們逐塊地研究一下該代碼:
第一個塊設置整個腳本中使用的變量。
下一個塊應該是非常熟悉的:它在未提供參數時打印用法信息。它為什么執行 (exec) 實際的 rm 實用程序呢?如果您將此腳本命名為“rm并將其放在 $PATH 中靠前的位置,則它就可以充當 /bin/rm 的替代者。該腳本的錯誤選項也是 /bin/rm 的錯誤選項,因此該腳本允許 /bin/rm 提供用法信息。
下一個塊在備份目錄不存在時創建該目錄。如果 mkdir 失敗,則該腳本終止并顯示適當的錯誤消息。
下一個塊查找位置參數列表中的 dash 參數。如果 getopt 成功,則 $args 具有一個選項列表。如果 getopt 失敗,例如在它無法識別某個選項的時候,則它會打印錯誤消息,并且該腳本將退出并顯示用法信息。
隨后的塊捕獲一個字符串中旨在提供給 rm 的所有選項。當遇到特殊 getopt 選項 -- 時,選項收集過程停止。shift 從參數列表中刪除所有已處理的參數,保留待處理的文件和目錄列表。
從以 for file 開頭的塊復制每個文件和目錄,以便在您自己的存檔目錄中保存它們。每個文件的目錄被逐字 (-R) 復制到存檔目錄,并附帶當前日期和時間作為后綴,以確保該副本是唯一的,并且不會改寫以前存檔的具有相同名稱的條目。
最后,使用傳遞給該腳本的相同命令行選項來刪除文件和目錄。
然而,如果您碰巧需要剛才刪除(意外刪除?)的文件或目錄,您可以在存檔中查找原始副本。
向自動化進軍
您使用 Unix 的時間越多,就越有可能創建腳本。腳本可以節省重新輸入復雜的較長命令序列所需的時間和精力,并且還可以防止發生錯誤。Web 上充滿了其他人已創建的用于許多目的的有用腳本。很快您也會發布自己的神奇腳本。
相關文章: