關於本系列
典型的 UNIX® 管理員擁有一套經常用於輔助管理過程的關鍵實用工具、訣竅和系統。存在各種用於簡化不同過程的關鍵實用工具、命令行鏈和腳本。其中一些工具來自於操作系統,而大部分的訣竅則來源於長期的經驗積累和減輕系統管理員工作壓力的要求。本系列文章主要專注於最大限度地利用各種 UNIX 環境中可用的工具,包括簡化異構環境中的管理任務的方法。
差異和問題
如果您使用多種 UNIX 主機,特別是每種主機都支持不同的 UNIX 風格(Berkeley Software Distribution (BSD)、UNIX System Release 4 (VSVR4) 等)或版本,您也許發現自己要花大量的時間來檢查和確定自己所在的主機類型,以便能夠適應系統操作方式的變化。
例如,ps 命令在基於 BSD 和基於 SVR4 的 UNIX 主機上,分別需要不同的命令行選項來獲得大致相同的信息(有關更多細節,請參閱 系統管理員工具包: 進程管理技巧)。平台之間還存在更廣泛的差異。有時,這種差異是命令名稱發生了更改;Linux® 提供 adduser 命令,而 Solaris 則提供 useradd 命令。
就標准化而言,有多種方法可供您采用。
您可以選擇對主要平台(例如 Solaris)進行標准化,並在其他平台上提供等效命令的包裝以匹配 Solaris 標准。 也可以選擇對為所使用的任務提供最佳組合的命令集進行標准化,挑選您喜歡的命令並為特定平台上不存在的命令構建包裝。 您可以創建自己的一套執行特定任務的腳本(包括您自己用於 ls、ps 等常用工具的替代腳本),以便它們生成您想要的信息。這樣做有點危險,原因是它意味著您可能從未使用原始命令,從而可能在您的腳本不可用時導致潛在的問題。如何具體實現各個命令的包裝以提供一個兼容或唯一的層,這取決於您是嘗試簡單地為功能相同的替代命令提供一個公認名稱,還是需要構建一個或多個命令的包裝以獲得等效的結果。可能的解決方案有三種:
別名——這種解決方案僅在某些外殼中受支持——別名提供了將給定的字符串展開為特定命令的簡單方法。 外殼函數——大多數現代外殼都支持這種解決方案——外殼函數使您能夠創建更復雜的序列,但是由於它們作為內置函數運行,在差異相當小時可能更為實用。 外殼腳本——當您要構建的包裝特別復雜時,更好的解決方案是使用外殼腳本,您可以代替原始命令調用這些腳本。使用外殼腳本,您可以更創造性地處理替代,甚至為另一個命令提供完全由外殼腳本驅動的替代。讓我們研究一下每種可能的解決方案和一些可通過此方法來進行模擬的示例命令。
使用別名
別名在 Korn (ksh)、Bourne-Again SHell (bash)、TENEX C shell (tcsh) 和 Z shell (zsh) 外殼中受支持,當您希望設置命令的特定選項,同時仍然支持其他選項時,別名提供了也許是最簡單的方法。顧名思義,您可以將一個命令用作另一個命令的別名,或者為帶有附加選項的同一個命令提供別名。別名從您鍵入的內容展開為其展開形式。
例如,一個常用的別名是 ll,它調用等效的 ls -l(ll 通常稱為長清單 (long listing))。每當用戶鍵入 ll,就會直接將其替換為展開形式,因此:$ ll a* 在執行前展開為:$ ls -l a*。
命令行選項也仍然有效,換句話說,$ ll -a 展開為:$ ls -l -a。
還可以為現有命令設置別名;假設將 -F 選項添加到所有 ls 命令,這樣,$ ls 將展開為:$ ls -F。
要設置別名,請使用內置的外殼 alias 語句,並在引號中指定所需的展開形式。例如,要設置前面詳細描述的 ll 的展開形式,可使用:$ alias ll='ls -l'。
別名在以下情況下最為有用:您希望使用 base 命令並容易地指定附加選項,同時仍然允許設置特定於平台的選項。
一個很好的例子就是 ps 命令,它在基於 SVR4 和基於 BSD 的 UNIX 主機上是不同的。在本系列的第一篇文章中,請參閱 系統管理員工具包: 進程管理技巧 ——這篇文章解釋了如何使用 ps 的選項來獲得相似的清單。您可以結合別名使用那些選項,而不會影響您指定附加選項的能力。例如,在 BSD 上,您將如清單 1 所示指定別名。
清單 1. 在 BSD 上指定別名
$ alias ps='ps -o pid,ppid,command'
而在 SVR4 主機上,您將如清單 2 所示創建別名。
清單 2. 在 SVR4 上指定別名
$ alias ps='ps -opid,ppid,cmd
現在,在這兩個系統對 ps 的不同操作方式的限制下,您獲得了 ps 產生的標准輸出。和前面一樣,您可以繼續添加更多選項;例如,在安裝了該別名的任一個平台上請求所有進程,添加 -A 選項就是這樣一種情況。這會在 BSD(在此示例中為 Mac OS X)上產生類似於清單 3 的輸出。
清單 3. 在 BSD 上使用 -A 選項
$ ps -A PID PPID COMMAND 1 0 /sbin/launchd 23 1 /sbin/dynamic_pager -F /private/var/vm/swapfile 27 1 kextd 32 1 /usr/sbin/KernelEventAgent 33 1 /usr/sbin/mDNSResponder -launchdaemon 34 1 /usr/sbin/netinfod -s local 35 1 /usr/sbin/syslogd 36 1 /usr/sbin/cron 37 1 /usr/sbin/configd 38 1 /usr/sbin/coreaudiod 39 1 /usr/sbin/diskarbitrationd ...
SVR4 系統(Gentoo Linux 主機)會顯示同樣的列,如清單 4 所示。
清單 4. 在 SVR4 上使用 -A 選項
$ ps -A PID PPID CMD 1 0 init [3] 2 1 [migration/0] 3 1 [ksoftirqd/0] 4 1 [watchdog/0] 5 1 [migration/1] 6 1 [ksoftirqd/1] 7 1 [watchdog/1] 8 1 [events/0] 9 1 [events/1] 10 1 [khelper] 11 1 [kthread] 14 11 [kblockd/0] 15 11 [kblockd/1] 16 11 [kacpid] ...
另一個選項或多或少地鏡像了本文其他地方給出的腳本和函數解決方案。該選項是為給定命令的特定輸出創建別名,這些別名采用同一方法來提供相同的格式化輸出。同樣以 ps 為例,您可以創建別名 ps-all 來輸出所有進程列表,並根據需要為每種平台設置相應的展開形式。
設置這些別名的最佳位置是在登錄期間執行的外殼初始化腳本中,例如 .ksh、.profile 或 .bashrc。您可以在這些腳本中執行同樣的系統檢查,以驗證要啟用哪些別名。如果希望提供適用於所有用戶的全局解決方案,則應將別名定義放在公開可用的文件中(例如放在 /etc or /usr/local 中),並設置用戶初始化腳本以獲得別名定義來源。
別名機制最適合於您希望設置單個命令的命令行選項的情況,雖然也可以使用它們來將給定的命令展開為一組命令或管道。這樣削弱了為展開形式中除最後一個命令以外的其他任何命令指定附加參數的能力。對於處理此類包裝,外殼中的內聯函數可能更為適合。
使用內聯外殼函數
大多數外殼都支持函數,這些函數本質上是微型腳本,您可以在其中放置命令和其他外殼腳本元素以執行特定的任務。由於它們是主外殼定義中的函數,因此使用起來方便快捷,同時仍然支持許多完整外殼腳本所具有的相同功能,如命令行參數。
對於支持別名無法在其中工作的某些命令和組合,對命令行參數的支持非常關鍵。例如,killall 命令最基本的功能是終止所有與特定字符串匹配的命令。該命令並非在所有平台上都可用,但是一旦您了解了它,就會希望在其他環境中使用它。
在 Solaris 上,killall 命令存在,但是將其用作關閉過程的一部分以終止所有進程。設想在 Solaris 主機上意外調用 killall 命令以關閉所有 Apache 進程,沒想到卻實際上關閉了系統!
提供替代——在所有主機上使用相同的名稱或使用不同的名稱——可以實現按名稱終止進程的預期結果,並消除不希望的和可能代價高昂的錯誤,同時擴展本身並不支持該選項的系統的功能。
該命令的關鍵部分是能夠識別正在運行的進程,提取與給定字符串匹配的進程,並使用 kill 命令將 KILL 信號發送到每個匹配進程。在命令行上,您可以通過一系列管道實現等效的功能(使用 KILL 信號),如清單 5 所示。
清單 5. 提供 killall 命令的替代
$ ps -efgrep gccawk '{ print $2; }'xargs kill -9
該命令的關鍵部分是提供給 grep(在此示例中為 gcc)的字符串和 ps 輸出中包含所需進程 ID 的列。上面的例子對 Solaris 主機和大多數 SVR4 UNIX 變種有效。
別名在此示例中無法工作,因為您希望能夠插入命令中的信息不在結尾;別名所實現的是一種展開方法。然而,內聯外殼函數正好適合這種情況。
在支持 Bourne 語法(bash 和 zsh)的外殼中,您可以使用清單 6 所示的以下語法來定義函數。
清單 6. 定義函數
function NAME() { # do stuff here }
調用函數時,函數參數作為 $1、$2 等形式來提供,就像在典型的外殼腳本中一樣。因此,您可以定義一個函數,使其執行與 killall 相同的基於字符串的信號發送功能(請參見清單 7)。
清單 7. 定義一個執行與 killall 相同的信號發送功能的函數
function killall() { ps -efgrep $1awk '{ print $2; }'xargs kill -9 }
請注意,該函數的 awk 部分中的 $2 不會展開,因為您已經對 awk 腳本定義使用了單引號,這樣阻止了展開,並且在此示例中會挑選第二列。
與別名一樣,指定外殼函數的最佳位置是在外殼的初始化腳本中。函數的局限性在於,它們依賴外殼提供支持能力,而這並不總是可能或可用。
雖然可以隨心所欲地使內聯外殼函數變得任意長,但在許多情況下,外殼函數並不理想。例如,在模擬更復雜的命令或提供命令包裝的超長序列中,您需要分析選項並提供本地化的等效命令,此時內聯函數就沒有多大用處了。在這種情況下,外殼腳本可能更為適合。
使用腳本
構建一致環境的最容易和最兼容的方法,是創建可用作實際命令的包裝的外殼腳本,這樣考慮了您希望支持的各種選項和設置。
例如,useradd 和 adduser 命令在設置參數(如用戶 ID 或組成員資格)時支持同樣的單字母命令行選項,因此 Linux 上的 $ adduser -u 1000 -G sales,marketing mcbrown 等效於 Solaris 上的 $ useradd -u 1000 -G sales,marketing mcbrown。
然而,Linux 版本還支持擴展命令選項,例如,--uid 和 --groups 等效於上面的命令行選項。這些擴展選項在 Solaris 上不受支持,但是,如果創建一個名為 adduser 的外殼腳本,您就可以模擬 Linux 版本,然後用適當的選項運行實際的 Solaris useradd 命令。
清單 8 是用作 adduser 或 useradd 命令的包裝的示例外殼腳本。
清單 8. 用作包裝的示例外殼腳本
#!/bin/bash # -*- shell-script -*- for i in $* do case $i in --uid-u) OPT_UID=$2; shift 2;; --groups-G) OPT_GROUPS=$2; shift 2;; --gid-g) OPT_GROUP=$2; shift 2;; --home-dir-d) OPT_HOMEDIR=$2; shift 2;; --shell-s) OPT_SHELL=$2;shift 2;; --non-unique-o) OPT_NONUNIQUE=1;shift 2;; --comment-c) OPT_COMMENT=$2;shift 2;; esac done OPTS="" if [ -n "$OPT_$HOMEDIR" ] then OPTS="$OPTS -d $OPT_HOMEDIR" fi if [ -n "$GROUP" ] then OPTS="$OPTS -g $OPT_GROUP" fi if [ -n "$OPT_GROUPS" ] then OPTS="$OPTS -G $OPT_GROUPS" fi if [ -n "$OPT_SHELL" ] then OPTS="$OPTS -s $OPT_SHELL" fi if [ -n "$OPT_UID" ] then OPTS="$OPTS -u $OPT_UID" fi if [ -n "$OPT_COMMENT" ] then OPTS="$OPTS -c \"$OPT_COMMENT\"" fi if [ -n "$OPT_NOUNIQUE" ] then OPTS="$OPTS -o" fi CMD=adduser UNAME=`uname` case $UNAME in Solaris) CMD=useradd;break;; esac $CMD $OPTS $*
該腳本的關鍵是 foreach 循環,它遍歷所提供的命令行參數(在 $* 中提供)。對於每個選項,case 語句會嘗試識別該選項——使用短格式或長格式並設置一個變量。命令行開關為 $1。如果該選項後面正常地跟著一個值(例如,用戶 ID),您可以將 $2 當作該值來進行訪問,並使用它將該值賦於某個變量。
識別出某個選項後,shift 語句從 $* 變量列表中移動一個位置(若指定了數字,則移動指定數目的位置),以便已經識別出的命令行參數在循環的下一次迭代中不再在 $* 變量中。
識別並提取出可能的參數以後,您所需做的就是構建新的選項來提供給最終要使用的命令。由於 useradd/adduser 都支持短格式的參數,所以可在此基礎上構建新的命令選項字符串。這是通過檢查對應的變量是否已設置並將該選項添加到命令行來實現的。請注意雙引號的使用,它確保了原始命令中引用的參數被保留並得到正確識別。
將該腳本安裝在支持任一種原始命令的平台上以後,您現在可以添加用戶並指定所要的選項,包括對參數進行混合和匹配(請參見清單 9)。
清單 9. 添加用戶
$ adduser.sh --homedir /etc -g wheel --shell /bin/bash -c "New user" mcbrown
同樣的基本原理也可以用於構建其他命令的包裝,甚至更改參數名稱和選項,或者提供等效的表達式。
如果希望用原始名稱安裝該腳本——例如,adduser——並將其放在某個目錄中(例如,/usr/local/compat),您必須確保該目錄在 PATH 中出現在實際命令的目錄之前。下面是假設將兼容性腳本放在 /usr/local/compat 目錄中的一個例子:$ PATH=/usr/local/compat:$PATH。
使用單個源
無論您是使用多個腳本還是單個配置腳本/別名來支持統一的環境,您也許都希望使用單獨一組腳本來支持系統。因此,設置新系統以使用標准化腳本(無論它們是獨立腳本還是安裝外殼函數和別名)非常簡單,只需將它們復制到新系統即可。
通過使用命令行工具和外殼流控制(如 if 或 case)的組合,您可以使用單個源來選擇各種要使用的選項。有兩個工具在這種情況下很有用:一個工具識別主機(如 hostname 或 uname),另一個工具識別平台 (uname)。
uname 產生的缺省輸出是基本操作系統名稱,如 Linux 或 Solaris。例如,可以按照前一部分中的 ps 示例,將該命令與 case 語句結合使用以選擇正確的別名,如清單 10 所示。
清單 10. uname 的輸出
UNAME='uname' case "$UNAME" in FreeBSDNetBSDDarwin) alias ps='ps -o pid,ppid,command' break ;; SolarisLinux) alias ps='ps -o pid,ppid,cmd' break ;; esac
也可以在腳本中使用同樣的基本過程來選擇特定的序列。
在使用內聯外殼函數時,與在每次使用函數時才做出決定相比,使用類似如此的包裝來選擇正確的函數定義通常更容易,因為這樣做會更加高效。
總結
規范化環境對於簡化管理大有幫助。它減輕了您的負擔,幫助您確定所在的系統類型,以及哪個命令和/或選項集最適合於獲取所需信息或執行所需操作。為每個命令選擇正確的機制完全取決於該命令和您要嘗試達到的目的。
在您希望調用命令行選項的單個命令上,最好使用別名機制。內聯函數最適合於您希望容易地將其嵌入當前腳本環境的更復雜操作和序列,而完整的單獨腳本則最適合於麻煩的多步驟操作,或您希望為命令(或選項)提供支持而不更改外殼環境的場合。
盡管有這些明顯的優點,但是務必要記住,如果將自己過於屏蔽在基礎的系統之外,當發生故障而您無法訪問腳本時,您可能處於無准備的狀態——您應該尋求擴展和擴充,而不是替代。