第五章﹕Shell 和 Shell Script

732阅读 0评论2006-09-09 jmhyy
分类:LINUX

或許﹐許多人都已經聽過 shell 或 bash 這些名字﹐但不知道您是否知道它們究竟是什麼東東呢﹖

先回到電腦基礎常識上吧﹕所有的電腦都是由硬體和軟體構成的﹐硬體就是大家能摸得著看得見的部份﹐例如﹕鍵盤﹑熒幕﹑CPU﹑記憶體﹑硬碟﹑等等。離開了硬體﹐所謂的電腦是不存在的﹐因為整個系統的輸入和輸出以及運算都離不開硬體。請問﹕如果沒有鍵盤和熒幕您是怎樣使用電腦的﹖但是﹐您透過鍵盤進行的輸入﹐以及從熒幕看到的輸出﹐真正發揮功能的﹐是軟體的功勞。而直接負責和這些硬體進行溝通的軟體﹐就是所謂的核心(kernel)﹐kernel 必須能夠接管鍵盤的輸入﹐然後交由 CPU 進行處理﹐最後將執行結果輸出到熒幕上。當然﹐除了鍵盤和熒幕外﹐所有的硬體都必須獲得 kernel 的支援才能使用。

那麼﹐kernel 又如何知道我們鍵盤輸入的東西是什麼呢﹖那就是我們這裡介紹的 shell 所負責的事情了。因為電腦本身所處理的數據﹐都是二進位的機器碼﹐和我們人類習慣使用的語言很不一樣。比方說﹐輸入 pwd 命令﹐我們知道這是 print working directory 的意思(非常簡單的人類語音)﹐但作為 kernel 來說﹐它並不知道 pwd 是什麼﹐kernel 只會看機器碼﹐這時候﹐shell 就會幫我們將 pwd 翻譯為 kernel 能理解的程式碼。所以﹐我們在使用電腦的時候﹐基本上就是和 shell 打交道﹐而不是直接和 kernel 溝通﹐更不是直接控制硬體。

簡單來看﹐我們就這樣來看待它們的關係﹕光從字面來解析的話﹐shell 就是“殼”﹐kernel 就是“核”。好比一個果實一樣﹐您第一眼看到的就是殼﹐把殼扒開才看的到裡面的核。shell 就是使用者和 kernel 之間的界面﹐將使用者下的命令翻譯給 kernel 處理﹐關係如下圖﹕

我們在 shell 輸入一個命令﹐shell 會嘗試搜索整個命令行﹐並對其中的一些特殊字符做出處理﹐如果遇到 CR 字符( Enter ) 的時候﹐就嘗試重組整行命令﹐並解釋給 kernel 執行。而一般的命令格式(syntax)大致如下﹕

# command parameter1 patrameter2 ...

各命令都有自己的選項(options, 通常用“ - ”符號帶領)﹐可輸入也可以不輸入﹐如果沒有額外指定﹐命令通常都有自己的預設選項﹔而參數(argument)則視各程式要求而定﹐有些很嚴格﹐有些也有預設的參數。例如 "ls -l" 這個命令﹐選項是 -l (long list)﹐而預設的參數則是當前目錄。在命令行中,選項和參數都被稱為參項(parameter)。

我們經常談到的 Linux﹐其實是指 kernel 這部份﹐而在 kernel 之外﹐則是各種各樣的程式和工具﹐整合起來才成為一個完整的 Linux 發行套件。無論如何﹐Linux 的 kernel 只有一個(儘管有許多不同的版本﹐都由 Linus Tovalds 負責維護)﹐但 kernel 之外的 shell 卻有許多種﹐例如 bourne Shell﹑C Shell﹑Korn Shell﹑Zsh Shell﹑等等﹐但我們最常接觸到的名叫 BASH (Bourne Again SHell)﹐為 所加強的一個 burne shell 版本﹐ 也是大多數 Linux 套件的預設 shell 。不同的 shell 都各自有其不同的優缺點﹐有興趣您可以自行找這方面的資料來看﹐我這裡就不一一介紹了。

BASH 這個優秀的 shell﹐之所以會被各大 Linux 套件採用為預設的 shell﹐除了它本身是 open source 程式之外﹐它的強大功能應該是吸引大家目光的重要因素之一。BASH 的功能很多﹐實在很難全部介紹﹐下面只列舉其中一少部份而已﹕

命令補全功能﹕
當您輸入命令的時候﹐您可以輸入目錄或檔案的開首字面﹐然後按‘tab’鍵將您的命令路徑補全。比方說﹐您要 ls 一下 /etc/sysconfig 這個目錄的內容(假設您已經在 /etc 目錄下了)﹐您可以只輸入 ls sy 然後接連按兩下 tab 鍵﹐然後就會將 /etc/ 目錄下所有以 sy 開頭的檔案和目錄顯示出來﹐您或許可以看到 sysconfig﹑sysctl.conf ﹑syslog.conf 這三個結果﹔如果您只輸入 ls sys 再按兩下 tab 的話﹐結果是是一樣的﹐因為在 /etc/ 目錄下面﹐所有以 sy 開頭的檔案﹐第 3 個字面都是 s 而沒有其它字面了﹔如果您輸入 ls sysc 再重複這個動作﹐那麼顯示結果就剩下 sysconfig 和 sysctl.conf 而已﹐因為以 sysc 開頭的只有這兩個檔﹐如果您再按 ls sysco 接一個 tab﹐那就會幫您將 sysconfig 這個唯一以 sysco 開頭的檔案補全。

如果您所輸入的路徑﹐是唯一的﹐那麼只要按一下 tab 就能補全﹐否則﹐會聽到一下 beat 聲﹐這時您再補一下 tab ﹐就會將所有以此路徑開頭的檔案列出來﹔假如符號條件的檔案太多﹐那系統會先將符號條件的檔案數目告訴您﹐例如 242 possibilities﹐然後您按 y 才顯示﹐如果按 n 則讓您增加命令的輸入﹐然後您可以重複這些動作﹐直到您所輸入的路徑只剩唯一的對應﹐才可以用一個 tab 補全。

同樣的﹐這個功能也可以用在輸入命令的時候﹐比方說﹐您要輸入 Xconfigurator 命令﹐那您只需輸入 Xc 然後按一下 tab 就可以了﹗是否很方便呢﹖ ^_^

Tip﹕用 tab 來補全命令﹐不但方便迅速﹐而且也比較保險。因為﹐如果您前面的路徑輸入不正確﹐用 tab 是不能完成補全的﹐這可以避免您輸入錯誤的路徑而執行錯誤的程式。我強烈建議您執行每一個命令都常試用 tab 補全功能﹐以確保其正確性。(多敲這個 tab 鍵沒什麼壞處啦)

命令記錄表﹕
每次您輸入一個命令﹐並按 Enter 執行之後﹐那您這個命令就被存放在命令記錄表(command history)中﹐而每個命令都有一個記錄號碼﹐您可以用 history命令來看看當前的命令歷史表。這樣﹐您只要用向上方向鍵﹐就可以依次呼叫出您最近所輸入的命令﹐按下方向鍵則退回最新的命令﹐找到您想要重新輸入的命令﹐然後再按 Enter 即可。

不過﹐也有一下更便利的辦法﹕您可以輸入 !nnn (其中的 nnn 是 history 命令找到的命令記錄號碼)﹐就能執行指定的舊命令了﹔如果您輸入 !! 再 Enter 的話﹐那就是重複上一個命令(和按向上方向鍵再 Enter 一樣)﹔如果您輸入 !ls 的話﹐則是最後一次的 ls 開頭的命令﹐如果是 !cd 那就是上一個 cd 開頭的命令﹐如此類推﹔如果您按著 Ctrl 和 R 兩個鍵之後﹐然後輸入您以前曾經輸入過的命令﹐那它會和上面介紹的補全功能一樣﹐將您以前輸入過的命令補全起來。呵~~ 太厲害啦﹗

Bash 會將您登錄之後的所有命記錄在記 cache 裡面﹐然後﹐只要您成功退出這個 shell 之後﹐那這些記錄就會存放到家目錄的 ~/.bash_history 這個檔裡面(小心看﹐它是以 . 開頭的檔案哦﹐也就是隱藏檔是也﹐您要用 ls -a 才看得到。) 不過﹐這個檔只保持一定數量的命令記錄而已﹐您可以透過 $HISTFILESIZE 這個變數(我們馬上會介紹變數)﹐來獲得或改變檔案的記錄數量。

alias 功能﹕
在 Linux 裡面﹐您可以透過 alias (別名) 的功能﹐來定義出一個命令的預設參數﹐甚至用另外一個名稱來簡化一個命令(及參數)。如果您輸入 alias 這個命令﹐您就會看到目前的 alias 有哪些。您或許會看到其中有一個﹕ alias rm='rm -i' 這行﹐它的意思是﹕如果您執行 rm 這個命令﹐那麼系統實際執行的命令會帶上 -i 的參數﹐也就是以 interactive 模式進行﹐結果是在您進行刪除檔案的時候﹐會經過您的確認才真正刪除。在某些沒有這個 alias 的系統中﹐那您執行 rm 而不另行指定 -i 的話﹐那就無聲無息的將您能砍的檔案給砍掉。小心哦﹐在 Linux 上面﹐檔案一旦刪除就沒辦法救回了﹗所以﹐用心的系統﹐會幫您做這個 alias。

在另外一種情形之下﹐當您發現某些長命令會經常使用到﹐但打字起來挺麻煩的﹐那您就可以用 alias 來解決。比方說﹐您每次關機要輸入的命令是 shutdown -h now 這麼一串﹐那您先輸入 which shd (目的是確定現有的命令名稱)﹐如果您並沒有發現這個命令出現在您的命令路徑之中的話﹐那您可以輸入 alias shd='shutdown -h now'﹐然後再輸入 shd 就可以關機了﹗不過﹐現在不要執行它﹗﹗因為您這樣真的會把機器關掉哦~~ 請您用 alias 替換其它的長命令看看﹖

如果您要取消一個 alias﹐可以使用 unalias 命令﹐如﹕unalias shd 。

一旦您滿意您的新 alias ﹐那您可以修改您的 ~/.bashrc 這個檔﹐將它加在其它 alias 命令之後﹔假如您想系統上所有使用者都能使獲得這個 alias ﹐那就將它放到 /etc/bashrc 裡面吧。(如果您目前還不會編輯檔案﹐那就回到上一章補習 vi 吧:-)

強大的 script 能力
玩過 DOS 的朋友﹐一定會知道 batch 檔案的功能﹐在 BASH 本身可以幫您執行一系列根據條件判斷的命令﹐其功能比 DOS 的 batch 強大多了。在本章的後面部份﹐會詳細討論 shell script 的基本技巧。
事實上﹐bash 還有許多厲害的功能﹐恐怕很難全部介紹了﹐還是留給您自己去找尋了。

還記得上一章裡面﹐我曾經提到過﹕當我們登入系統的時候﹐首先就獲得一 shell﹐而且它也佔據一個行程﹐然後再輸入的命令都屬於這個 shell 的子程式。如果您學習夠細心﹐不難發現我們的 shell 都在 /etc/passwd 這個檔裡面設定的﹐也就是帳號設定的最後一欄﹐預設是 /bin/bash 。

事實上﹐當我們獲得一個 shell 之後﹐我們才真正能和系統溝通﹐例如輸入您的命令﹑執行您的程式﹑等等。您也可以在獲得一個 shell 之後﹐再進入另外一個 shell (也就是啟動一個子程式)﹐然後還可以再進入更深一層的 shell (再進入子程式的子程式)﹐直到您輸入 exit 才退回到上一個 shell 裡面(退回上一級的父程式)。假如您已經閱讀過上一章所說過的子程式概念﹐應該不難理解。不過﹐您的行為也不是無限制的﹐而且﹐有許多設定都必須事先得到定義。所以﹐當您獲得 shell 的時候﹐同時也獲得一些環境設定﹐或稱為“環境變數( Environment variables)”。

所謂的 變數( variable )﹐就是用特定的名稱(或標籤)保存一定的設定值﹐然後供程式將來使用。例如﹐姓=chen ﹔名=kenny ﹐那麼‘姓’和‘名’就是變數名稱﹐而 chen 和 kenny 就是變數所保存的值。由 shell 所定義和管理的變數﹐我們稱為環境變數﹐因為這些變數可以供 shell 所產生的所有子程式使用。環境變數名稱一般都用大寫字母表示﹐例如﹐我們常用的環境變數有這些﹕

變數名稱 代表意思
HISTCMD 當前命令的記錄號碼。
HISTFILE 命令記錄表之存放檔案。
HISTSIZE 命令記錄表體積。
HOME 預設登錄家目錄。
IFS 預設分隔符號。
LINENO 當前命令在 shell script 中的行數。
MAIL 郵件信箱的路徑。
MAILCHECK 檢查郵件的秒數。
OLDPWD 上次進入的目錄路徑。
OSTYPE 作業系統類型。
PATH 預設命令搜索路徑。
PPID 父程式之 PID。
PWD 當前工作目錄路徑。
SECONDS 當前 shell 之持續啟動時間。
SHELL 當前 shell 之執行路徑。
TMOUT 自動登出之最高閑置時間。
UID 使用者之 UID。
$ 當前 shell 之 PID。
最後一個命令之返回狀態。

假如您想看看這些變數值是什麼﹐只要在變數名稱前面加上一個“$”符號﹐然後用 echo 命令來查看就可以了﹕

# echo $PWD
/root
# echo $$
1206
# echo $?
0

第一個命令就是將當前目錄的路徑顯示出來﹐和您執行 pwd 命令的結果是一樣的﹔第二個命令將當前這個 shell 的 PID 顯示出來﹐也就是 1206。如果您這時候輸入 kill -9 1206 的話﹐會將當前的 shell 砍掉﹐那您就要重新登錄才能獲得另外一個 shell﹐而它的 PID 也是新的﹔第三行命令是上一個命令的返回狀態﹕如果命令順利執行﹐並沒有錯誤﹐那通常是 0﹔如果命令遇到錯誤﹐那返回狀態則是非 0 ﹐其值視程式設計者而定(我們在後面的 shell script 的時候會介紹)。關於最後一個命令﹐不妨比較一下如下結果﹕

# ls mbox
mbox
# echo $?
0
# ls no_mbox
ls: no_mbox: No such file or directory
# echo $?
1

您會發現﹕第一命令成功執行﹐所以其返回狀態是 0 ﹔而第二個命令執行失敗﹐其返回狀態是 1 。假如程式設計者為不同的錯誤設定不同的返回狀態等級﹐那您可以根據返回值推算出問題是哪種錯誤引起的。

Tips﹕如果您日後寫程式或 script﹐要養成一個習慣﹐為每一種命令結果設定返回狀態。這非常重要﹐尤其在進行 debug 的時候。這個我們在後面學習 script 的時候再談。

我們隨時都可以用一個 = (等號) 來定義一個新的變數或改變一個原有變數。例如﹕

# MYNAME=kenny
# echo $MYNAME
kenny

假如您要取消一個定義好的變數﹐那麼﹐您可以使用 unset 命令﹕

# unset MYNAME

不過﹐環境變數的特性之一﹐是單向輸出的。也就是說﹕一個 shell 的特定變數﹐只能在這個 shell 裡面使用。如果您要分享給同一個 shell 裡面的其它程式﹑script﹑命令使用﹐或它們的子程式使用﹐那您必須用 export 命令將這個變數進行輸出。但無論如何﹐如果您在一個子程式中定義了一個變數﹐那麼這個變數的值﹐只影響這個子程式本身以及它自己的子程式﹐而永遠不會影像到父程式或父程式產生的其它子程式。

比方說﹐您在一個程式中定義一個新的變數﹐或改變一個原有變數值﹐在程式結束的時候﹐那它所設定的變數均被取消﹔如果您想將變數值分享給該程式所產生的子程式﹐您必須用 export 命令才能保留這個變數值﹐除非子程式另外重新定義。但無論如何﹐當前程式所定義的變數值﹐是無法傳回父程式那邊的。不妨做做如下的實驗﹕

# MYNAME=kenny
# echo $MYNAME
kenny
# export MYNAME
# 設定一個變數。
#
# 當前的設定值。
# 用 export 輸出變數值。
# /bin/bash # 再開一個 shell﹐也就是進入子程式中。
# echo $MYNAME
kenny

#

# 保留原有設定值。

# export MYNAME=netman
# echo $MYNAME
netman

# 重新定義設定值﹐同時也用 export 輸出。

#
# 變數值被新值取代。

# exit

# 退出子程式﹐返回父程式。

# echo $MYNAME
kenny

#

# 父程式的變數值並沒有改變。

關於變數的另一個特性﹐是的變數值是可以繼承的。也就是說﹐您可以將一個變數值來設定另外一個變數名稱。比方說﹕

# FIRST_NAME="Kenny"
# MYNAME=$FIRST_NAME

# echo $MYNAME
Kenny

# 定義一個變數。

# 再定義另一個變數﹐但它的值是第一個變數。

#
# 第二個變數繼承了第一個變數的值。

另外﹐在定義變數的時候您還要注意一些規則﹕

關於後兩項﹐或許我們再找些例子來體會一下﹕

# TOPIC='Q & A'

# 用單引號保留特殊符號和空白

 

# Q1=What\'s\ your\ \"topic\"\?

# echo $Q1
What's your "topic"?

 

# 用 \ 將特殊符號(含引號)和空白跳脫出來

#

# 跳脫後﹐特殊符號和空白都保留下來。

 

# ANS="It is $TOPIC."

# echo $ANS
It is Q & A.

 

# 用雙引號保留變數值($)

#

# 用雙引號﹐顯示出變數值。

 

# WRONG_ANS='It is "$TOPIC".'

# echo $WRONG_ANS
It is "$TOPIC".

 

 

# 用單引號保留特殊符號和空白(同第一行)

#
# 用單引號﹐全部保留﹔同時﹕

# $ 也當成一般符號保留﹐而非變數值。

 

# ALT_ANS='the $TOPIC'\ is\ "'$TOPIC'"\.

# echo $ALT_ANS
The $TOPIC is 'Q & A'.

 

# 同時混合單引號﹑雙引號﹑和跳脫字符 \

#

# 單引號保留全部﹔雙引號保留變數值﹔
# \ 將特殊符號跳脫出來。

我這裡解釋一下最後面的例子好了﹕'the $TOPIC is '"$TOPIC"\.。首先用單引號將 'the $TOPIC is ' 這段文字括好﹐其中用 3 個空白鍵和一個 $ 符號﹔然後用雙引號保留 $TOPIC 的變數值﹔最後用 \ 跳脫小數點。

在引用 " " 和 ' ' 符號的時候﹐基本上﹐ ' ' 所包括的內容﹐會變成單一的字串﹐任何特殊字符都失去其特殊的功能﹐而變成一般字符而已﹐但其中不能再使用 ' 符號﹐而在 " " 中間﹐則沒有 ' ' 那麼嚴格﹐某些特殊字符﹐例如 $ 號﹐仍然保留著它特殊的功能。您不妨實作一下﹐比較看看 echo ' "$HOME" ' 和 echo " '$HOME' " 的差別。

Tips﹕在 shell 命令行的跳脫字符“ \ ”其實我們會經常用到的。例如﹐您的一個命令太長﹐一直打下去可能超過一行﹐或是想要整潔的輸入命令行﹐您或許想按 Enter 鍵敲到下一行繼續輸入。但是﹐當您敲 Enter 鍵的時候﹐事實上是輸入一個 CR (Carriage-Return) 字符﹐一但 shell 讀到 CR 字符﹐就會嘗試執行這個命令。這時﹐您就可以在輸入 Enter 之前先輸入 \ 符號﹐就能將 CR 字符也跳脫出來﹐這樣 shell 就不會馬上執行命令了。這樣的命令行﹐我們在 script 中經常看到﹐但您必須知道那代表什麼意思。

如果﹐您想對一些變數值進行過濾﹐例如﹕MY_FILE=' ~/tmp/test.sh' ﹐而您想將變數值換成 test.sh (也就是將前面的路徑去掉)﹐那您可以將 $MY_FILE 換成 ${MY_FILE##*/}。這是一個變數值字串過濾﹕## 是用來比對變數前端部份﹐然後 */ 是比對的色樣 (也就是任何字母到 / 之間)﹐然後將最長的部份刪除掉。您可以參考如下範例﹕

當 FNAME="/home/kenny/tmp/test.1.sh" 的時候﹕

變數名稱 代表意思 結果
${FNAME} 顯示變數值的全部。

/home/kenny/tmp/test.1.sh
${FNAME##/*/} 比對變數值開端﹐如果以 /*/ 開頭的話﹐砍掉最長的部份。

test.1.sh
${FNAME#/*/} 比對變數值開端﹐如果以 /*/ 開頭的話﹐砍掉最短的部份。

kenny/tmp/test.1.sh
${FNAME%.*} 比對變數值末端﹐如果以 .* 結尾的話﹐砍掉最短的部份。

/home/kenny/tmp/test.1
${FNAME%%.*} 比對變數值末端﹐如果以 .* 結尾的話﹐砍掉最長的部份。

/home/kenny/tmp/test
${FNAME/sh/bash} 如果在變數值中找到 sh 的話﹐將第一個 sh 換成 bash。

/home/kenny/tmp/test.1.bash
${FNAME//sh/bash} 如果在變數值中找到 sh 的話﹐將全部 sh 換成 bash。

/home/kenny/tmp/test.1.bash

您除了能夠對變數進行過濾之外﹐您也能對變數做出限制﹑和改變其變數值﹕

  字串沒設定 空字串 非空字串
使用預設值
var=${str-expr} var=expr var= var=$str
var=${str:-expr} var=expr var=expr var=$str
使用其它值
var=${str+expr} var=expr var=expr var=expr
var=${str:+expr} var=expr var= var=expr
設定預設值
var=${str=expr} str=expr

var=expr

str 不變

var=

str 不變

var=$str

var=${str:=expr} str=expr

var=expr

str=expr

var=expr

str 不變

var=$str

輸出錯誤
var=${str?expr} expr 輸出至 stderr  var= var=str
var=${str:?expr} expr 輸出至 stderr  expr 輸出至 stderr var=str

一開始或許比較難理解上面的兩個表格說明的意思﹐真的很混亂~~ 但只要多做一些練習﹐那您就知道怎麼使用了。比方說﹕

# expr=EXPR
# unset str
# var=${str=expr}; echo var=$var str=$str expr=$expr
var=expr str=expr expr=EXPR
# var=${str:=expr}; echo var=$var str=$str expr=$expr
var=expr str=expr expr=EXPR
# str=
# var=${str=expr}; echo var=$var str=$str expr=$expr
var= str= expr=EXPR
# var=${str:=expr}; echo var=$var str=$str expr=$expr
var=expr str=expr expr=EXPR
# str=STR
# var=${str=expr}; echo var=$var str=$str expr=$expr
var=STR str=STR expr=EXPR
# var=${str:=expr}; echo var=$var str=$str expr=$expr
var=STR str=STR expr=EXPR

# MYSTRING=test
# echo ${MYSTRING?string not set\!}
test
# MYSTRING=
# echo ${MYSTRING?string not set\!}
 
# unset MYSTRING
# echo ${MYSTRING?string not set\!}
bash: MYSTRING: string not set!

請記住這些變數的習性﹐日後您要寫 shell script 的時候就不會將變數搞混亂了。假如您想看看當前 shell 的環境變數有哪些﹐您可以輸入 set 命令﹔如果只想檢查 export 出來的變數﹐可以輸入 exportenv (前者是 shell 預設的輸出變數)。

到這裡﹐您或許會問﹕shell 的環境變數在哪裡定義呢﹖可以調整嗎﹖

嗯﹐第一個問題我不大了解﹐我猜那是 shell 設計者預設定義好的﹐我們一登錄獲得 shell 之後就有了。不過﹐第二個問題﹐我卻可以肯定答復您﹕您可以隨時調整您的環境變數。您可以在進入 shell 之後用在命令行裡面重新定義﹐也可以透過一些 shell 設定檔來設定。

先讓我們看看﹐當您在進行登錄的時候﹐系統會檢查哪些檔案吧﹕

  1. /etc/profile﹕首先﹐系統會檢查這個檔﹐以定義如下這些變數﹕PATH﹑USER﹑LOGNAME﹑MAIL﹑HOSTNAME﹑HISTSIZE﹑INPUTRC。如果您會 shell script (我們後面再討論)﹐那您應該看得出這些變數是如何定義的。另外﹐還指定了 umask 和 ulimit 的設定﹕umask 大家應該知道了﹐而 ulmimit 呢﹖它是用來限制一個 shell 做能建立的行程數目﹐以避免系統資源被無限制的消耗。最後﹐它還會檢查並執行 /etc/profile.d/*.sh 那些 script﹐有興趣您可以追蹤看看。

  2. ~/.bash_profile﹕這裡會定義好 USERNAME﹑BASH_ENV﹑PATH。其中的 PATH 除了現有的 $PATH 之外﹐還會再加入使用者相關的路徑﹐您會發現 root 和普通帳號的路徑是不一樣的﹔而 BASH_ENV 呢﹐仔細點看﹐是下一個要檢查的檔案﹕

  3. ~/.bashrc﹕在這個檔裡面﹐您可以發現一些 alias 設定(哦~~ 原來在這裡﹗)。然後﹐您會發現有一行﹕. /etc/bashrc 。在 shell script 中﹐用一個小數點然後然後一個空白鍵再指向另外一個 script﹐意思是同時執行那個 script 並採用那裡的變數設定。

  4. /etc/bashrc﹕基本上﹐這裡的設定﹐是所有使用者在獲得 shell 的時候都會採用的。這裡指定了一些 terminal 設定﹐以及 shell 提示字符等等。

  5. ~/.bash_login﹕如果 ~/.bash_profile 不存在﹐則使用這個檔。

  6. ~/.profile﹕如果 ~/.bash_profile 和 ~/.bash_login 都不存在﹐則使用這個檔。

  7. ~/.bash_logout﹕這個檔通常只有一個命令﹕clear﹐也就是把熒幕顯示的內容清掉。如果您想要在登出 shell 的時候﹐會執行一些動作﹐例如﹕清空臨時檔(假如您有使用到臨時檔)﹑還原某些設定﹑或是執行某些備份之類的。

您可以透過修改上面提到的檔案﹐來調整您進入 shell 之後的變數值。一般使用者可以修改其家目錄( ~/ )中的檔案﹐以進行個人化的設定﹔而作為 root﹐您可以修改 /etc/下面的檔案﹐設定大家共用的變數值。至於 bash 的變數值如何設定﹖有哪些變數﹖各變數的功能如何﹖您打可以執行 man bash 參考手冊資料。

Tips﹕一旦您修改了 /etc/profile 或 ~/.bash_profile 檔案﹐其新設定要在下次登錄的時候才生效。如果您不想退出﹐又想使用新設定﹐那可以用 source 命令來抓取﹕
source ~/.bash_profile

好了﹐相信您已經對您的 shell 有一定的了解了。然後﹐讓我們看看 shell 上面的一些命令功能吧﹐這些技巧都是作為一個系統管理員基本要素。其中之一就是﹕命令重導向 (command redirection) 和 命令管線 (command pipe) 。

在深入講解這兩個技巧之前﹐先讓我們了解一下 shell 命令的基本概念﹕

名稱 代號 代表意思 設備
STDIN 0 標準輸入 鍵盤
STDOUT 1 標準輸出 熒幕
STDERR 2 標準錯誤 熒幕

表格中分別是我們在 shell 中一個命令的標準 I/O (輸出與輸入)。當我們執行一個命令的時候﹐先讀入輸入 (STDIN)﹐然後進行處理﹐最後將結果進行輸出 (STDOUT)﹔如果處理過程中遇到錯誤﹐那麼命令也會顯示錯誤 (STDERR)。我們可以很容易發現﹕一般的標準輸入﹐都是從我們的鍵盤讀取﹔而標準輸出和標準錯誤﹐都從我們的銀幕顯示。

同時﹐在系統上﹐我們通常用號碼來代表各不同的 I/O﹕STDIN 是 0﹑STDOUT 是 1﹑STDERR 是 2。

當您了解各個 I/O 的意思和所代表號碼之後﹐讓我們看比較如下命令的結果﹕

# ls mbox
mbox
# ls mbox 1> file.stdout

請小心看第二個命令﹕在命令的後面多了一個 1 ﹐而緊接著(沒有空白﹗)是一個大於符號 (>)﹐然後是另外一個檔案名稱。但是﹐熒幕上卻沒有顯示命令的執行結果﹐也就是說﹕ STDOUT 不見了﹗那到底發生什麼事情了呢﹖

呵﹐相信您不會這麼快忘記了 STDOUT 的代號是 1 吧﹗沒錯了﹐因為我們這裡將 1 用一個 > 符號重導到一個檔案中了。結果過是﹕我們將標準輸出從熒幕改變到檔案中﹐所以我們在銀幕就看不到 STDOUT﹐而原先的 STDOUT 結果則保存在大於符號右邊的檔中了。不信﹐您看看這個檔案的內容就知道了﹕

# cat file.stdout
mbox

當我們用一個 > 將命令的 STDOUT 導向到一個檔案的時候﹐如果檔案不存在﹐則會建立一個新檔﹔如果檔案已經存在﹐那麼﹐這個檔案的內容就換成 STDOUT 的結果。有時候﹐您或許想保留原有檔案的內容﹐而將結果增加在檔案末端而已。那您可以多加一個 >﹐也就是使用 >> 就是了。您可以自己玩玩看哦~~﹐通常﹐我們要將一些命令或錯誤記錄下來﹐都用這個方法。

Tips﹕如果您不希望 > 意外的蓋掉一個原有檔﹐那您可以執行這個命令﹕
set -o noclobber

不過﹐仍可以用 >| 來強迫寫入。

上前面的例子中﹐我們指定了 I/O 1 (STDOUT) 進行重導向﹐這也是預設值﹐如果您沒有指定代號﹐那麼就是進行 STDOUT 的重導向﹐所以 1> 和 > 是一樣的﹔1>> 和 >> 也是一樣的。但如果您使用了數字﹐那麼數字和 > 之間一定不能有空白存在。

好了﹐下面再比較兩個命令﹕

# ls no_mbox
ls: no_mbox: No such file or directory
# ls no_mbox 2>> file.stderr
 

嗯﹐相信不用我多解釋了吧﹖(如果檔案不存在﹐>> 和 > 都會建立新的。)

事實上﹐在我們的日常管理中﹐重導向的應用是非常普遍的。我只舉下面這個例子就好了﹕

當我們進行核心編譯的時候(我們下一章再介紹)﹐熒幕會飛快的顯示出成千上萬行信息﹔其中有大部份是 STDOUT﹐但也有些是 STDERR。除非您的眼睛真的那麼厲害﹐否則您很難分辯出哪些是正常信息﹐哪些是錯誤信息。當您要編譯失敗﹐嘗試找錯誤的時候﹐如果已經將 STDERR 重導出來﹐就非常方便了﹕

# make dep clean bzImage modules 1>/dev/null 2>/tmp/kernel.err &

這裡﹐我一共有三個打算﹕(1) 將標準輸出送到一個叫 null 的設備上﹐如果您記性夠好﹐我在前面的文章中曾比喻它為黑洞﹕所有東西進去之後都會消失掉。憑我個人的習慣﹐我會覺得編譯核心時跑出來的信息﹐如果您不感興趣的話﹐那都是垃圾﹐所以我將 STDOUT 給重導到 null 去﹐眼不見為乾淨﹔ (2) 然後﹐我將 STDERR 重導到 /tmp/kernel.err 這個檔去﹐等命令結束後﹐我就可以到那裡看看究竟有部份有問題。有些問題可能不是很重要﹐有些則可能需要重新再編核心﹐看您經驗啦。(3) 最後﹐我將命令送到 background 中執行 (呵~~ 相信您還沒忘記吧﹗)。因為﹐編譯核心都比較花時間﹐所以我將之送到背景去﹐這樣我可以繼續做其它事情。

Tips﹕這時﹐因為系統太忙了﹐可能反應速度上會比較慢些﹐如果您真的很在意﹐不妨考慮把 make 的 nice level 提高。(忘記怎麼做了﹖那翻看前一章吧)

前面的例子﹐我們是分開將 STDOUT 和 STDERR 重導到不同的檔案去﹐那麼﹐我們能否把兩者都重導到同一個檔呢﹖當然是可以的﹐請比較下面三行﹕

# make dep clean bzImage modules >/tmp/kernel.result 2>/tmp/kernel.result
# make dep clean bzImage modules >/tmp/kernel.result 2>&1
# make dep clean bzImage modules &>/tmp/kernel.resultt

我這裡告訴您﹕第一行的命令不怎麼正確﹐因為這樣會造成這兩個輸出同時在‘搶’一個檔案﹐寫入的順序很難控制。而第 2 行和第 3 行的結果都是一樣的﹐看您喜歡用哪個格式了。不過﹐要小心的是﹕& 符號後面不能有空白鍵﹐否則會當成將命令送到背景執行﹐而不是將 STDOUT 和 STDERR 整合。

好了﹐前面我們都在談 STDOUT 和 STDERR 的重導向﹐那麼﹐我們是否能重導 STDIN 呢﹖

當然可以啦~~~

有些命令﹐當我們執行之後﹐它會停在那裡等待鍵盤的 STDIN 輸入﹐直到遇到 EOF (Ctrl+D) 標籤才會真正結束命令。比方說﹐在同一個系統上﹐如果有多位使用者同時登入的話﹐您可以用 write 命令向特的使用者送出短訊。而短訊的內容就是鍵盤敲入的文字﹐這時候命令會進入輸入模式﹐您每輸入一行並按 Enter 之後﹐那麼訊息就會在另外一端﹐直到您按 Ctrl+D 鍵才離開並結束命令。

# write user1
Hello!
It is me... ^_^
How r u!
(Ctrl+D)

這樣通常都需要花一些時間輸入﹐假如對方在寫什麼東西和查看某些資料的時候﹐就很混亂。這時候﹐您或許可以先將短訊的內容寫在一個檔案裡面﹐例如 greeting.msg﹐然後這樣輸入就可以了﹕

write user1 < greeting.msg

就這樣﹐這裡我們用小於符號 (<) 來重導 STDIN 。簡單吧﹖^_^

不過﹐我們用 cat 命令建立簡單的檔案的時候﹐卻是使用 > 符號的﹕

cat > file.tmp

等您按 Ctrl+D 之後﹐從鍵盤輸入的 STDIN﹐就保存在 file.tmp 中了。請想想看為什麼會如此﹖(我在 LPI 的考試中碰到過這道題目哦~~~)

查字典﹐pipe 這個英文是水管﹑管道﹑管線的意思。那麼﹐它和命令又有什麼牽連呢﹖簡單的說﹐一個命令管線﹐就是將一個命令的 STDOUT 作為另一個命令的 STDIN 。

其實﹐這樣的例子我們前面已經碰到多次了﹐例如上一章介紹 tr 命令的時候﹕

# cat /path/to/old_file | tr -d '\r' > /path/to/new_file

上面這個命令行﹐事實上有兩個命令﹕cat 和 tr ﹐在這兩個命令之間﹐我們用一個 “ | ”符號作為這兩個命令的管線﹐也就是將 cat 命令的 STDOUT 作為 tr 命令的 STDIN ﹔然後﹐tr 命令的 STDOUT 用 > 重導到另外一個檔案去。

上面只是一個非常簡單的例子而已﹐事實上﹐我們可以用多個管線連接多個程式﹐最終獲得我們確切想要的結果。比方說﹕我想知道目前有多少人登錄在系統上面﹕

# w | tail +3 | wc -l

我們不妨解讀一下這個命令行﹕(1) w 命令會顯示出當前登錄者的資源使用情況﹐並且每一個登錄者佔一行﹔(2) 再用 tail 命令抓取第 3 行開始的字行﹔(3) 然後用 wc -l 計算出行數。這樣﹐就可以知道當前的登錄人數了。

許多朋友目前都採用撥接 ADSL 上網﹐每次連線的 IP 都未必一樣﹐只要透過簡單的命令管線﹐您就可以將當前的 IP 抓出來了﹕

  1. 我們不妨觀察 ifconfig ppp0 這個命令的輸出結果﹕
    # ifconfig ppp0
    ppp0      Link encap:Point-to-Point Protocol
              inet addr:211.74.48.254  P-t-P:211.74.48.1  Mask:255.255.255.255
              UP POINTOPOINT RUNNING NOARP MULTICAST  MTU:1492  Metric:1
              RX packets:5 errors:0 dropped:0 overruns:0 frame:0
              TX packets:3 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:3
    

  2. 不難發現 IP 位址所在的句子中有著其它句子所沒有的字眼﹕inet addr 。然後﹐我們就可用 grep 把這行抓出來﹕
    # ifconfig ppp0 | grep "inet addr"
              inet addr:211.74.48.254  P-t-P:211.74.48.1  Mask:255.255.255.255
    

  3. 再來﹐我們先用相同的分隔符號將句子分成數列﹐然後抓出 IP 位址所在的那列。

    嗯﹐這裡﹐我們可以用“ : ”來分出 4 列﹔也可以用空白鍵來分出 5 列(空因為句子開首就是一個空白鍵)。如果用空白鍵來分的話﹐由於有些間隔有多個空白鍵的原因﹐那麼﹐我們可以用 tr 命令﹐將多個空白鍵集合成一個空白鍵﹕

    # ifconfig ppp0 | grep "inet addr" | tr -s ' ' ' '
     inet addr:211.74.48.254 P-t-P:211.74.48.1 Mask:255.255.255.255
    
    (注意﹕在 ' ' 之間是一個空白鍵﹗)

  4. 然後用 cut 命令抓出 IP 所在的列﹐細心數一數﹐應該是第 3 列﹕
    # ifconfig ppp0 | grep "inet addr" | tr -s ' ' ' ' | cut -d ' ' -f3
    addr:211.74.48.254
    

  5. 然後我們用“ : ”再分兩列﹐抓第 2 列就是 IP 了﹕
    # ifconfig ppp0 | grep "inet addr" | tr -s ' ' ' ' \
    	| cut -d ' ' -f3 | cut -d ':' -f2
    211.74.48.254
    

這裡﹐我們一共用 5 個 pipe 將 4 個命令連接起來﹐就抓出機器當前的 IP 位址了。是否很好用呢﹖

在同一個命令行裡面出現多個命令的情形﹐除了 “ | ”之外﹐或許您會看到 " ` ` " 符號﹐也就是和 ~ 鍵同一個鍵的符號(不用按 Shift )。它必須是一對使用的﹐其中可以包括單一命令﹐或命令管線。那它的效果和命令管線又有什麼分別呢﹖

我們使用 pipe 將一個命令的 STDOUT 傳給下一個命令的 STDIN﹐但使用 `` 的時候﹐它所產生的 STDOUT 或 STDERR 僅作為命令行中的一個參數而已。嗯﹐不如看看下面命令好了﹕

# TODAY=`date +%D`
# echo Today is $TODAY.
Today is 08/17/01.

從結果我們可以看出﹐我們用 `` 將 date 這個命令括起來(可含參數)﹐那麼它的執行結果可以作為 TODAY 的變數值。我們甚至還可以將一串命令管線直接用在命令行上面﹕

# echo My IP is `ifconfig ppp0 | grep "inet addr" \
    | tr -s ' ' ' ' | cut -d ' ' -f3 | cut -d ':' -f2`

My IP is 211.74.48.254.

註意﹕第一行的 CR 被 \ 跳脫了﹐所以這個命令行‘看起來’有兩行。我之所以弄這麼複雜﹐是告訴您這對 `` 符號可以適用的範圍。

Tips﹕在變數中使用 `` 可以將命令的執行結果當成變數值的部份。事實上﹐除了用 `` 之外﹐您也可以用這樣的格式﹕VAR_NAME=$(command) ﹐那是和 VAR_NAME=`command` 的結果是一樣的。

除了這對 `` 和 | 之外﹐還有另外一個符號 “ ; ”來分隔命令的。不過﹐這個比較簡單﹕就是當第一命令結束之後﹐再執行第二個命令﹐如此類推﹕

# ./configure; make; make install

呵~~ 如果您對您的安裝程式有絕對信心﹐用上面一行命令就夠了﹗

當我們對 shell 變數和命令行有一定認識之後﹐那麼﹐我們就可以嘗試寫自己的 shell script 囉~~ 這可是非常好玩而又有成就感的事情呢﹗^_^

在 linux 裡面的 shell script 可真是無處不在﹕我們開機執行的 run level 基本上都是一些 script ﹔登錄之後的環境設定﹐也是些 script ﹔甚至工作排程和記錄維護也都是 script 。您不妨隨便到 /etc/rc.d/init.d 裡面抓兩個程式回來看看﹐不難發現它們都有一個共同的之處﹕第一行一定是如下這樣的﹕

#!/bin/sh
或﹕
#!/bin/bash

其實﹐這裡的 #! 後面要定義的就是命令的解釋器(command interpreter)﹐如果是 /bin/bash 的話﹐那下面的句子就都用 bash 來解釋﹔如果是 /usr/bin/perl 的話﹐那就用 perl 來解釋。不同的解釋器所使用的句子語法都不一樣﹐非常嚴格﹐就算同是用 shell 來解釋﹐不同的 shell 之間的格式也不僅相同。所以﹐如果您看到 script 的解釋器是 /bin/sh 的話﹐那就要小心了﹕如果您仔細看這個檔案﹐事實上它僅是一個 link 而已﹐有些系統或許會將它 link 到其它 shell 去。假如您的 script 句子使用的語法是 bash 的話﹐而這個 sh 卻 link 到 csh ﹐那執行起來可能會有問題。所以﹐最好還是直接指定 shell 的路徑比較安全一些﹕在這裡的範例都使用 /bin/bash 來作為 script 的解釋器。

在真正開始寫 script 之前﹐先讓我們認識 script 的一些基本概念﹕

簡單來說﹐shell script 裡面就是一連串命令行而已﹐再加上條件判斷﹑流程控制﹑迴圈﹑等技巧﹐聰明地執行正確的命令和使用正確的參數選項。和我們在 shell 裡面輸入命令一樣﹐shell script 也有這樣的特性﹕

一個良好的 script 作者﹐在程式開頭的時候﹐都會用註解說明 script 的名稱﹑用途﹑作者﹑日期﹑版本﹑等信息。如果您有這個機會寫自己的 script﹐也應該有這個良好習慣。

shell script 檔的命名沒一定規則﹐可以使用任何檔案名稱(參考檔案系統)﹐但如果您喜歡的話﹐可以用 .sh 來做它的副檔名﹐不過這不是硬性規定的。不過﹐要執行一個 shell script﹐使用者必須對它有執行權限( x )﹐用文件編輯器新建立的檔案都是沒有 x permission 的﹐請用 chmod 命令加上。執行的時候﹐除非該 script 已經至於 PATH 環境變數之內的路徑內﹐否則您必須指定路徑。例如﹐您寫了一個叫 test.sh 的 shell script﹐放在家目錄內﹐假設這也是您的當前工作目錄﹐您必須加上路徑才能執行﹕./test.sh 或 ~/test.sh 。所以﹐建議您在 script 測試無誤之後﹐放在 ~/bin 目錄裡面﹐那就可以在任何地方執行自己的 script 了﹐當然﹐您要確定 ~/bin 已經出現在您的 PATH 變數裡面。

script 之所以聰明﹐在於它能夠對一些條件進行測試( test )。您可以直接用 test 命令﹐也可以用 if 敘述﹐例如﹕test -f ~/test.sh 。它的意思是測試一下 ~/test.sh 這個檔案是否存在﹐這個 -f 通常用在檔案上面的測試﹐除了它﹐還有很多﹕

標籤 代表意思
-G 存在﹐並且由 GID 所執行的行程所擁有。
-L 存在﹐並且是 symbolic link 。
-O 存在﹐並且由 UID 所執行的行程所擁有。
-S 存在﹐並且是一個 socke 。
-b 存在﹐並且是 block 檔案﹐例如磁碟等。
-c 存在﹐並且是 character 檔案﹐例如終端或磁帶機。
-d 存在﹐並且是一個目錄。
-e 存在。
-f 存在﹐並且是一個檔案。
-g 存在﹐並且有 SGID 屬性。
-k 存在﹐並且有 sticky bit 屬性。
-p 存在﹐並且是用於行程間傳送資訊的 name pipe 或是 FIFO。
-r 存在﹐並且是可讀的。
-s 存在﹐並且體積大於 0 (非空檔)。
-u 存在﹐並且有 SUID 屬性。
-w 存在﹐並且可寫入。
-x 存在﹐並且可執行。

事實上﹐關於這些測試項目還有很多很多﹐您可以 man bash 然後參考 CONDITIONAL EXPRESSIONS 那部份。另外﹐我們還可以同時對兩個檔案進行測試﹐例如﹕test file1 -nt file2 就是測試 file1 是否比 file2 要新。這種測試使用的標籤是﹕

標籤 代表意思
-nt Newer Than﹕第一個檔案比第二個檔案要新。
-ot Older Than﹕第一個檔案比第二個檔案要舊。
-ef Equal File﹕第一個檔案和第二個檔案其實都是同一個檔案 (如 link)。

我們這裡所說的這些測試﹐不單只用來測試檔案﹐而且還常會用來比對‘字串 (string)’或數字(整數)。那什麼是字串呢﹖字面來介紹就是一串文字嘛。在一個測試中﹐~/test.sh 本身是一個檔案﹔但 '~/test.sh' ﹐則是在引號裡面(單引號或雙引號)﹐那就是字串了。

在數字和字串上面的比對(或測試)﹐所使用的標籤大約有﹕

標籤 代表意思
= 等於
!= 不等於
< 小於
> 大於
-eq 等於
-ne 不等於
-lt 小於
-gt 大於
-le 小於或等於
-ge 大於或等於
-a 雙方都成立
-o 單方成立
-z 空字串
-n 非空字串

在上面提到的比對中﹐雖然有些意思一樣﹐但使用場合卻不盡相同。例如 = 和 -eq 都是‘等於’的意思﹐但 = 只能比對字串﹐而 -eq 則可以用來比對字串﹐也能用來比對表示色樣(我們在 regular expression 會碰到)。

我們之所以要進行測試或比對﹐主要是用來做判斷的﹕假如測試或比對成立﹐那就返回一個‘真實 (true)’否則返回‘虛假 (false)’。也就是說﹕如果條件成立那麼就會如何如何﹔如果條件不成立又會如何如何﹐從而讓 script 有所‘智慧’。基本上﹐我們的程式之所以那麼聰明﹐都是從這些簡單到複雜的判斷開始的。

比方說﹐上面的 -a (AND) 和 -o (OR) 是用來測試兩個條件﹕A 和 B 。如果使用 test A -a B ﹐那麼 A 和 B 都必須成立那條件才成立﹔如果使用 test A -o B ﹐那麼只要 A 或 B 成立那條件就成立。至於其它的比對和測試﹐應該更好理解吧﹖

另外﹐還有一個特殊符號﹕“ !”您可不能不會運用。它是‘否’的意思﹐例如﹕"! -f"是非檔案﹔ "-ne" 和 "! -eq" 都是‘不等於’的意思。

我們在命令行上面已經知道如何定義和改變一個變數﹐那在 shell script 裡面就更是司空見慣了。而且﹐越會利用變數﹐您的 script 能力就越高。在 shell script 中所定義的變數有更嚴格的定義﹕

標籤 代表意思
-a 定義為陣列 (array) 變數
-f 僅定義功 (function) 能名稱。
-i 定義為整數。
-r 定義為唯獨變數。
-x 透過環境輸出變數。

我們除了用 “ = ”來定義變數之外﹐還可以用 declare 命令來明確定義變數。例如﹕

# A=3 B="-2"
# RESULT=$A*$B
# echo $RESULT
3*-2
# declare -i A=3 B="-2"
# declare -i RESULT=$A*$B
# echo $RESULT
-6

您這裡會發現﹕如果沒有使用 declare 命令將變數定義為整數的話﹐那麼 A 和 B 的變數值都只是字串而已。

您現在已經知道什麼是變數﹑如何定義變數﹑什麼是字串﹑如何比對和測試字串和檔案﹐這些都是 script 的基本技巧。寫一些簡單的 script 應該不成問題了﹐例如在家目錄寫一個 test.sh ﹐其內容如下﹕

      1 #!/bin/bash
      2 # Purpose: a simple test shell script.
      3 # Author: netman 
      4 # Date: 2001/08/17
      5 # Version: 0.01
      6
      7 CHK_FILE=~/tmp/test.sh
      8
      9 if [ ! -e $CHK_FILE ]
     10 then
     11         echo "$0: Error: '$CHK_FILE' is not found." ; exit 1
     12
     13 elif [ -d $CHK_FILE ];then
     14         echo -n "$CHK_FILE is a directory, and you can "
     15         test -x $CHK_FILE || echo -n "NOT "
     16         echo "search it."
     17         exit 2
     18
     19 elif [ -f $CHK_FILE ]; then
     20         echo "$CHK_FILE is a regular file."
     21         test -r $CHK_FILE && echo "You can read it."
     22         test -x $CHK_FILE && echo "You can execute it."
     23         test -w $CHK_FILE && echo "You can write to it."
     24         test -s $CHK_FILE || echo "However, it is empty."
     25         exit 0
     26
     27 else
     28         echo "$CHK_FILE is a special file."
     29         exit 3
     30
     31 fi
(注意﹕我目前用 vi 編輯﹐並用 :set nu 將行數顯示出來﹐實際的命令行是沒有行數的。)

先讓我們看第一行﹕#!/bin/bash﹐就是定義出 bash 是這個 script 的 command interpreter 。

然後是一些註解﹐說明了這個 script 的用途﹑作者﹑日期﹑版本﹐等資訊。

在註解之後﹐第 7 行才是 script 的真正開始﹕首先定義出一個變數 CHK_FILE ﹐目前內容是家目錄中 tmp 子目錄的 test.sh 檔案。

Tips﹕事實上﹐這個定義比較有局限﹐如果您想改良這個設計﹐可以將這行(第 7 行)換成下面數行﹕
if [ -z $1 ]
then echo "Syntax Erro! Usage: $0 " ; exit 5
else CHK_FILE=$1
fi

第一行是開始一個 if 的判斷命令﹐它一定要用一個 fi 命令來結束(您可以在最後一行找到它)﹔然後在 if 和 fi 之間必須有一個 then 命令。這是典型的 if-then-fi 邏輯判斷﹕如果某某條件成立﹐然後如何如何﹔還有 if-then-else-fi 判斷﹕如果某某條件成立﹐然後如何如何﹐否則如何如何﹔另外﹐也有 if-then-elif-then-else-fi 判斷﹕如果某某成立﹐然後如何如何﹔否則﹐再如果某某成立﹐然後如何如何﹔如果還是不成立﹐那就如何如何。

上面那幾行﹐主要目的是將 CHK_FILE 這個變數值定義為 $1。嗯﹖您或許會問 $1 是什麼啊﹖那是當您執行這個 script 的時候所輸入的第一個參數﹔而 $0 則是 script 命令行本身。所以﹐這裡是先判斷一下 $1 是否為空的 ( -z )﹐然則(then)﹐告訴您語法錯誤﹐並告訴您正確的格式﹐同時退出﹐並返回一個狀態值(後面再談)﹔否則(else)﹐就將 CHK_FILE 定義為 $1。

接下來第 9 行﹐您可以將 "if [ ! -e $CHK_FILE ]" 換成 "if test ! -e $CHK_FILE " ﹐意思都是一個測試。但如果用 [ ] 的話有一個地方要注意﹕"[ " 的右邊必須保留一個空白﹔" ]" 的左邊必須保留一個空白。

在目前這個 script 中﹐判斷邏輯如下﹕

  1. 先檢查 $CHK_FILE (也就是 ~/tmp/test.sh 這個檔) 是否存在( ! -e )﹐如果( if )條件成立﹐那就參考 then 裡面的命令﹔否則參考下面 elif 或 else。

  2. 如果上一步驟成立﹐也就是 ~/tmp/test.sh 不存在﹐然則用 echo 命令告訴您不能讀取這個檔﹐並同時返回父程式一個返回狀態(還記得我們在前面提到過的 $? 變數嗎﹖)﹐這裡為 1。在 script 中﹐任何時候執行 exit 的話﹐就會離開 script﹐不管後面是否還有其它命令行或判斷。因為我將這裡 echo 和 exit 寫在同一行﹐所以用一個 " ; " 符號分隔開來﹐否則﹐您可以將 exit 寫在下一行。

  3. 接下來( 13 行)是一個 elif ﹐就是 else if 的意思﹐也就是說﹕如果上一個 if 不成立﹐然後在這裡再做另外一個 if 測試。這裡是繼續檢查這個檔是否為一個目錄( -d )﹐然則﹐告您它是一個目錄﹐並同時嘗試告訴您是否能對這個目錄進行搜索。

    然後看看下一行( 15 行)動內容﹐請留意上一個 echo 和這個 echo﹐都帶有一個 -n 的參數﹐意思是在顯示信息的時候不進行斷行( newline )的動作﹐所以﹐和下面那行合在一起(共 3 行 script )才是真實顯示的內容。這裡再進行一個測試﹕看看您對這個目錄是否具有 -x 權限﹐否則會在 "and you can" 和 "search it." 之間加上一個 "NOT"﹐如果有權限就不出現這個 NOT 。

    這裡﹐我們沒有用 if-then 來判斷﹐而是直接用 “ || ” ( OR ) 來做判斷﹕非此即彼。這在一些簡單的判斷中非常好用﹐尤其對懶人來說﹐因為不用打太多的字﹔但功能就比較有限﹕判斷之後只能執行一個命令而已。除了 || 之外﹐您也可以用 “ && ”( AND ) 做判斷﹐套句 Jack 的名言﹕You jump I jump。所以﹐這句也可以換成﹕
    test ! -x $CHK_FILE && echo -n "NOT " (粗體字是修改部份)。

    最後﹐根據目前這個 elif 條件所進行的所有命令都執行完畢﹐並退出這個 script﹐同時設定返回狀態為 2 。

  4. 再下來( 19 行)是另一個 elif ﹐也就是說﹕如果連上一個 elif 也不成立的話﹐那這裡繼續檢查這個檔是否是一個常規檔案( -f )﹐然則﹐告訴您是一個常規檔案﹐然後﹐接連進行三個測試﹐分別測試您是否具有 -r﹑-x﹑-w 的權限﹐有的話﹐分別告訴您相關的可行性。最後還檢查這個檔案的長度是否超過 0 ( -s )﹐否則告訴您它是一個空檔。完成這些判斷之後﹐就退出 script﹐並返回一個為 0 的狀態。

  5. 然後( 27 行)是一個 else﹐意思是如果上面的所有 if 和 elif 都不成立﹐那就看這裡的。也就是說﹕這個檔案是存在的﹐但不是目錄﹐也不是常規檔案﹐那它就是一個特殊檔。然後退出 script﹐並設定返回狀態為 3。

    在這個範例中﹐script 一共有 0﹑1﹑2﹑3 這四個返回狀態﹐根據這個返回值( $? )﹐我們就可以得知檢查的檔案究竟是一個常規檔﹑還是不存在﹑還是目錄﹑還是特殊檔。

  6. 最後﹐再沒有其它動作了﹐就結束這個 if 判斷。

目前這個 script 僅提供一些 script 概念給您而已﹐例如﹕定義和使用變數﹑if-then-else-fi 判斷式﹑條件測試﹑邏輯關係﹑退出狀態﹑等等。同時﹐這個範例也提供了一些基本的 script 書寫慣例﹕用不同的縮排來書寫不同的判斷或迴圈。例如這裡一共有兩個 if-then-else-fi 判斷﹐第一個 if﹑then﹑else﹑fi 都沒有縮排﹐然後﹐緊接這些命令後面的敘述就進行縮排﹔當碰到第二層的 if-then-else-fi 的時候﹐也如此類推。事實上﹐並非一定如此寫法﹐但日後如果您的程式越寫越長﹐您自然會這樣安排的啦~~

剛纔我們認識了一個 if-then-else-fi 的判斷﹐事實上﹐在 script 的應用上﹐還有其它的許多判斷技巧﹐在我們開發更強大和複雜的 script 之前﹐不妨先認識一下﹕

case
格式﹕
case string in
	pattern)
	commands
	;;
esac

它能根據不同的字串來做相應的動作﹐不如用例子來說好了﹕

      1 #!/bin/bash
      2 # Purpose: a simple test shell script.
      3 # Author: netman 
      4 # Date: 2001/08/20
      5 # Version: 0.02
      6
      7 
      8 echo Please pick a number:
      9 echo "	"a, To show the local time.
     10 echo "	"b, To list current directory.
     11 echo "	"c, To see who is on the machine.
     12 echo -n "Your choice: "
     13
     14 read choice
     15
     16 case $choice in
     17                 a | A) echo -n "The local time is "
     18                    date ;;
     19                 b | B) echo "The current directory is $PWD ";;
     20                 c | C) echo "There are following users on the machine:"
     21                    who ;;
     22                 *) echo "Your choice is an invalid option." ;;
     23 esac

這個 script 是先請您選擇 a﹑b﹑c 字母﹐再用 read 命令從鍵盤讀入 choice 的變數值﹐然後將這個變數應用在 case 判斷中﹕

  • 如果是 a 或 A﹕執行 date 命令﹔
  • 如果是 b 或 B﹕用 $PWD 這個環境變數顯示當前目錄﹔
  • 如果是 c 或 C﹕則執行 who 命令﹔
  • 如果是其它 ( * ) ﹕則告訴您 invalid 。

不知道您是否有注意到﹕每一個 case 的選項﹐都用一個 " ) " 作指引﹐然後﹐在這個 case 最後一個命令完成以後﹐一定要用 " ;; " 來結束。最後﹐還必須用 case 的倒寫 esac 來關閉這個判斷。

for
格式﹕
for item in list 
do
	commands
done

當您需要重複處理一些事物的時候﹐for 迴圈就非常好用了。它通常用來重複處理一些列表( list ) 中的事物﹐比方說您有一個變數﹐裡面包含著一串列表﹐那麼迴圈會一個接一個的進行處理﹐直到最後一個處理完畢之後才退出。不如又用一個範例來說明好了﹕

      1 #!/bin/bash
      2 # Purpose: a simple test shell script.
      3 # Author: netman 
      4 # Date: 2001/08/21
      5 # Version: 0.03
      6
      7
      8 if [ -z "$1" ] || [ -z "$2" ] ; then
      9         echo "Syntax Error: Usage ${0##*/}  "
     10         exit 1
     11 fi
     12 if [ ! -d $2 ]; then
     13         echo "${0##*/} : Error: $2 is not a directory."
     14         exit 2
     15 fi
     16 TWORD="$1"
     17 TDIR="$2"
     18 TFILE=`grep -r "$TWORD" "$TDIR" | cut -d ':' -f1 | uniq`
     19
     20 if [ ! -z "$TFILE" ]; then
     21         echo "You can find $TWORD in following file(s):"
     22         for i in $TFILE ;do
     23                 echo $i
     24         done
     25         exit 0
     26 else
     27         echo "Could not find $TWORD in any file under $TDIR."
     28         exit 3
     29 fi

這個 script 是在一個目錄下面搜索檔案﹐如果檔案裡面發現有指定的文字﹐就將檔案的名稱列出來。它必須要抓兩個變數﹕TWORD 和 TDIR ﹐這兩個變數分別為 script 的第 1 個和第 2 個參數。

一開始要檢查命令行是否有兩個變數﹐用 -z $1 和 -z $1 來測試﹐如果它們其一沒有指定﹐就告訴您語法錯誤﹐同時退出(返回值為 1 ) 。然後再檢查 $2 是否為目錄﹐如果不是目錄﹐就也提出警告﹐並退出(返回值為 2 )。如果通過上面兩道檢查﹐然後用命令 grep﹑cut﹑uniq﹐將檔案抓出來。注意﹕這就是 for 迴圈需要檢查的列表。

然後會檢查列表是否有內容﹐如果有的話﹐那就用 for 迴圈來重複顯示列表裡面的所有項目﹔一次一個﹐直到列表最後一個項目也處理完畢。這就是一個 for 迴圈的基本運作方式了。如果列表沒有被建立起來﹐那就告訴您找不到您指定的文字﹐並退出(返回值為 3 )。

while
格式﹕
while condition
do
	commands
done

這個迴圈應該蠻容易理解的﹕當條件成立的時候﹐就一直重複﹐直到條件消失為止。我們不妨改良前面的 case 那個 script 看看﹕

      1 #!/bin/bash
      2 # Purpose: a simple test shell script.
      3 # Author: netman 
      4 # Date: 2001/08/21
      5 # Version: 0.02.1
      6
      7 
      8 while [ "$choice" != "x" ]; do
      9         echo
     10         echo Please pick a number:
     11         echo "	"a, To show the local time.
     12         echo "	"b, To list current directory.
     13         echo "	"c, To see who is on the machine.
     14         echo "	"x, To exit.
     15         echo -n "Your choice: "
     16
     17         read choice
     18         echo
     19
     20         case $choice in
     21                 a) echo -n "The local time is "
     22                    date ;;
     23                 b) echo "The current directory is $PWD ";;
     24                 c) echo "There are following users on the machine:"
     25                    who ;;
     26                 x) echo "Bye bye..."; exit 0 ;;
     27                 *) echo "Your choice is an invalid option." ;;
     28         esac
     29 done

首先﹐我們用 while 進行應該條件判斷﹕如果 $choice 的變數值不等於 x 的話﹐那就重複迴圈﹐直到遇到 x (條件消失)為止。那麼這個 script 會一直提示您鍵入選項﹐然後進行處理﹐直到您按 x 鍵才會結束。

until
格式﹕
until condition
do
	commands
done

這個 until 剛好和 while 相反﹕如果條件不成立就一直重複迴圈﹐直到條件成立為止。如果繼續引用上例﹐只需將 while 的條件設為相反就可以了﹕

修改前﹕
      8 while [ "$choice" != "x" ]; do

修改後﹕
      8 until [ "$choice" = "x" ]; do

沒錯﹕就是這麼簡單﹗

sub function
格式﹕
function function_name
{
	commands
}

或﹕
function_name ()
{
	commands
}

當您在一個 script 中﹐寫好了段可以用來處理特定條件的程式之後﹐或許後面會重複用到。當然﹐您可以重複寫這些句子﹐但更便利的辦法是﹕將這些重複性的句子做成 sub function。如果您有模組的概念﹐那就是將一些能夠共享的程式做成模組﹐然後提供給需要用到此功能的其它程式使用。說實在﹐看一個程式撰寫人的模組化程度﹐也就能看得出這個人的程式功力。

我們不妨寫一個 script 來顯示機器目前所使用的網路卡界面資訊﹐看看裡面的 sub function 是怎麼運用的 ﹕

      1 #!/bin/bash
      2 # Purpose: a simple test shell script.
      3 # Author: netman 
      4 # Date: 2001/08/21
      5 # Version: 0.04
      6
      7 
      8 # function 1: to get interface.
      9 getif () {
     10     until [ "$CHKIFOK" = "1" ] || [ "$GETNONE" = "1" ]; do
     11         echo -n "The interface (ethX) for $CHKNET network [Enter for none]: "
     12         read CHKIF
     13         if [ -z "$CHKIF" ]; then
     14             echo
     15             echo "There is no interface for $CHKNET network."
     16             echo
     17             GETNONE=1
     18         else
     19             chkif 	# invoke the second function
     20         fi
     21     done
     22 }
     23
     24 # function 2: to check interface.
     25 chkif () {
     26     TESTIF=`/sbin/ifconfig $CHKIF | grep "inet add"`
     27     if [ -z "$TESTIF" ]; then
     28         echo ""
     29         echo "ERROR: Could not find interface '$CHKIF' on your machine!"
     30         echo "       Please make sure $CHKIF has been set up properly."
     31         echo ""
     32         return 1
     33     else
     34         CHKIFOK=1
     35         getip	# invoke the third function
     36         return 0
     37     fi
     38 }
     39
     40 # function 3: to get ip.
     41 getip () {
     42     CHKIP=`ifconfig $CHKIF | grep "inet addr" | tr -s ' ' ' ' \
     43         | cut -d ' ' -f3 | cut -d ':' -f2`
     44     CHKMASK=`ifconfig $CHKIF | grep "inet addr" | tr -s ' ' ' ' \
     45         | cut -d ' ' -f5 | cut -d ':' -f2`
     46     echo
     47     echo "The interface of $CHKNET network is $CHKIF using $CHKIP/$CHKMASK."
     48     echo
     49     return 0
     50 }
     51
     52 # start of main body
     53 for CHKNET in EXTERNAL INTERNAL DMZ ; do
     54     getif		# invoke the first function
     55     unset GETNONE
     56     unset CHKIFOK
     57 done

在這個 script 中﹐目前有三個 sub function﹕

  • getif ()﹕這裡用 until 迴圈從鍵盤那裡讀入指定的網路卡。如果直接按 Enter 表示沒有界面﹐然則﹐回報一個信息﹐並將 GETNONE 變數設定為 1 ﹐同時退出這個 function﹔否則﹐執行下一個 function 。

  • chkif ()﹕當上一個 function 順利讀入網路卡名稱之後﹐會檢查這個界面是否存在。這裡是用 /sbin/ifconfig 和 grep 來檢查﹐如果命令結果抓不到 IP 位址﹐表示這張卡還沒設定好﹐然則﹐回報一個錯誤信息﹐並退出 function﹐返回狀態為 1 ﹔否則﹐執行下一個 function﹐然後退出 function﹐返回狀態為 0。

    (注意﹕這裡的 function 有使用 return 退出以及設定返回狀態﹔但上一個 function 沒有使用 retuen﹐是因為 getif () 有使用 until 迴圈﹐如果那裡用 return 的話﹐就會打斷 until 迴圈。)

  • getip ()﹕當上一個 function 通過界面檢測之後﹐就將界面的 IP 和 netmask 抓出來﹐同時告訴您相關的網路資訊﹐最後退出 function﹐返回狀態為 0。

當所有 sub function 都定義完畢之後﹐接下來就是 main body 的開始。這裡用一個 for 迴圈﹐分別對 EXTERNAL﹑INTERNAL﹑和 DMZ 網路進行檢查﹐執行第一 function 就開始一連串的動作了。因為 sub function 裡面的變數會重複使用﹐所以﹐在每次使用過其中的功能之後﹐要將某些影響下一個判斷的變數清空﹐用 unset 命令即可。

事實上﹐用在 script 上面的迴圈有非常多的變化﹐恐怕我也沒此功力為大家一一介紹。還是留待您自己去慢慢摸索了。

常規表示式 (RE -- Regular Expression) 應該是所有學習程式的人員必須具備的基本功夫。雖然﹐我的程式能力很差﹐而且這裡的文章也不是以程式為主﹐不過﹐在日後的管理生涯當中﹐如果會運用 RE 的話﹐將令許多事情都時半功倍﹐同時也讓您在管理過程中如虎添翼。下面﹐我們只接觸最基本的 RE 常識﹐至於進階的技巧﹐將留給有興趣的朋友自己發揮。

首先﹐不妨讓我們認識最基本的 RE 符號﹕

符號 代表意思 範例
^ 句子前端 "^dear" ﹕句子必須以 dear 開頭。
$ 句子末端 "dear$"﹕句子必須以 dear 結尾﹔"^$" ﹕空白行。
\ 跳脫字符 "\\" ﹕\ 符號本身﹔"\." ﹕小數點﹔"\ " ﹕空白鍵。 
. 任何單元字符 ".ear" : 可以是 dear, bear, tear﹐但不能是 ear 。
? 前一個 RE 出現 0 次或 1 次

"^[0-9]?$" ﹕ 空白行或只含 1 個數字的字行。

* 前一個 RE 可出現 0 次或多次

"^.*" ﹕所有字行﹔

"^[0-9][0-9]*$" ﹕ 含一或多個數字的字行。

+ 前一個 RE 可出現 1 次或多次

"^[0-9][0-9]+$" ﹕ 含兩個或多個數字的字行。

\{n\} 接在前一字符的 n 個相同範圍字符 "^[0-9]\{3\}[^0-9]" ﹕句子開頭連續 3 個數字﹐然後是一個非數字。
\{n,\} 接在前一字符的最少  n 個相同範圍的字符 "^[0-9]\{3,\}" ﹕句子開頭最少要有連續 3 個數字。
\{n,m\} 接在前一字符的 n 到 m 個相同範圍的字符 "^[0-9]\{3,5\}" ﹕句子開頭連續 3 或 5 個數字。
[list] 列表中任何單元字符 "t[ear]." ﹕可以是 tea, tar, try ﹐但不能是 tim 。
[range] 範圍中任何單元字符 "t[e-r]." ﹕可以是 tea, tim, try ﹐但不能是 tar 。
[^range] 任何不在範圍中的單一字符 "t[^e-r]." ﹕可以是 tar﹐但不能是 tea, tim, try。

通常﹐我們用來處理 RE 的程式有 grep﹑egrep﹑sed﹑awk﹑vi﹑等等﹐各程式的語法和功能都相差很多﹐需要詳細研究過才能摸熟。在某些程式中﹐例如 egrep 和 awk﹐還可以處理某些延伸字符﹐例如﹕" | " 是兩個 RE 的或一關係﹔" ( )" 可用來組合多個 RE ﹔等等。有興趣的話﹐網路上都有許多資料可以找得到﹐例如網站 的 「正規表示式的入門與應用」等系列文章。

許多人提到 RE 的時候﹐都少不了介紹一下 sedawk 這對寶貝﹐它們都可以用來處理字串﹐但處理手法上卻有所不同。有人說用 sed 對‘字行’為單位的處理比較方便﹔而 awk 則在列表處理上面有獨到的神通。是否如此﹐大家不妨自己玩玩看囉。

讓我們先看看 sed 這個程式﹐它的命令語法有點類型 vi 裡面的編輯功能﹕

關於 sed 的常用命令﹐請參考下表﹕

命令 語法 說明
a a\ string 在字行後面增加特定字串(新行)。
c c\ string 將字行換成特定字串。
d d 刪除字行。
i i\ string 在字行前面插入特定字串(新行)。
p p 顯示字行。除非用 -n 指明﹐預設會在處理完畢之後顯示子行。
s s/oldstring/newstring/flag

用新的字串替換舊的字串。其中可用的旗標有﹕

g﹕替換行中的所有舊字串(預設只換第一個)﹔

p﹕顯示﹔

wfile﹕寫入特定檔案。

例如﹐您要輸入﹕

sed 1,3d src.file

所顯示的結果﹐就會將 src.file 的前面三行砍掉。如果您輸入﹕

sed '3,$d' src.file

這樣﹐所顯示的結果﹐就會從第 3 行到最後一行都砍掉﹐只剩下第 1 和第 2 行而已。 上面的命令別忘了加引號﹐否則要 \$ 來跳脫。不過﹐我強烈建議您用單引號將 sed 的命令括起來。如果您要將空白行拿掉﹐用 RE 來做非常簡單﹕

sed '/^$/d' src.file

在 sed 裡面引用 RE 的時候﹐ 通常都會用兩個 / / 符號將 RE 括起來﹐然後才是命令。 如果您想要更換字串﹐那就要用 s 命令了﹕

sed 's/red/blue/g' src.file

這樣﹐所有的 red 字串都會被換成 blue ﹔如果沒有加上 g 旗標﹐那麼只有每一行的第一個 red 被替換而已。

除了 d 和 s 命令之外﹐我們還可以用 a 命令在句子後面新增一行﹐內容為字串部份﹔或用 i 命令在句子前面插入一行﹐內容為字串部份﹔也可以用 c 命令將整行換成字串部份。不過﹐您在執行這幾個命令的時候﹐必須要用 ' ' 將命令和參數括起來﹐然後用 \ 符號在命令後面跳脫 Enter 鍵﹐然後才能完成。嗯﹐說起來蠻難理解的﹐不如實作一下吧﹕

sed '$a \
New line appened at the end.' src.file

這樣﹐就會在檔案最後面增加一行句子了。再比方說﹐您要將第 3 行換成另外的文字﹐可以這樣玩﹕

sed '3c \
The third line is replace with this line.' src.file

再比方說﹐您想將您存儲郵件的檔案 ~/mbox 用虛線分開每一封郵件﹐可以這樣試試﹕

sed '/^From /i \
\
-------------------------\
\
' ~/mbox

我想﹐您應該不會忘記我們在前面的文章中﹐用 ifconfig | grep | tr | cut 這些命令管線來抓出網路卡的界面吧。事實上﹐我們用 sed 命令也一樣可以得到同樣的結果﹕

ifconfig eth0 | grep "inet addr" | sed -e 's/^.*addr://' | sed 's/ *Bcast.*$//'

第一個 sed 是將 addr: 到句子前面的字串用 s 命令替換為無字串(也就是在最後的 // 中間沒任何字符)﹔然後第二個 sed 將 Bcast 連同前面的空白﹐到句子末端也用 s 替換為無字串(注意﹕/ *Bcast 的 / 和 * 之間是空白鍵)﹔這樣﹐剩下來的就是 IP 位址了。

目前﹐我們所進行的命令輸出﹐都是在熒幕上﹐既然您已經學會命令的重導向了﹐要將結果保存到其它檔案去﹐應是易如反掌了吧。 ^_^

至於 sed 的應用技巧﹐您可以到如下網站好好研究一下﹕

學習過 sed 之後﹐讓我們再看看 awk 這個命令究竟有什麼神通。就拿剛纔所舉的抓 IP 的例子來說好了﹐換成 awk 也行哦﹕

ifconfig eth0 | grep "inet addr" | awk -F ' ' '{print $2}' | awk -F ':' '{print $2}'

這裡的 awk 和 cut 命令很相似﹕首先﹐用 -F 定義出分隔符號(注意﹕第一個命令用空白做分隔符﹐所以 -F 後面的兩個 ' ' 之間是空白鍵)﹐然後再用 print 命令將相應的列抓出來。對 awk 而言﹐變數 $0 代表每一行被處理的句子 ﹐然後第一個欄位是 $1﹑第二個欄位是 $2﹑.... ﹐如此類推。

如果您以為 awk 只能做這些事情﹐就實在是太小看它了﹗例如您有這樣一個文字檔(dog.txt)﹐裡面只有這麼一行文字﹕

The brown fox jumped on the lazy dog quickly.

然後我們用 awk 來進行處理﹕

# awk '{ $2="black"; $3="dog"; $8="fox"; print}' dog.txt
The black dog jumped on the lazy fox quickly.

從上面的例子中﹐我們發現 awk 具有處理變數的能力﹐事實上﹐它也有自己內建的變數﹕

變數名稱 代表意思
FS 欄位分隔符號(預設是空白鍵)。
NF 當前句子中的欄位數目。
NR 當前句子的行數。
FILENAME 當前處理的檔案名稱。

甚至﹐awk 還能進行數值上的比對﹕

變數名稱 代表意思
> 大於。
< 小於。
>= 大於或等於。
<= 小於或等於。
== 等於。
!= 不等於。

另外﹐如果嚴格來執行的話﹐awk 命令一共分成三個部份﹕BEGINmain﹑和 END。在 awk 命令中﹐BEGIN 的部份﹐是讓程式開始時執行一些一次性的命令﹔而 END 部份則在程式退出的時候執行一些一次性的命令﹔而 main 呢﹐則以迴圈的形式逐行處理輸入。一般來說﹐我們無須定義 BEGIN 和 END﹐直接定義 main 的部份就可以執行 awk 命令了。例如﹕

# echo a b c d | awk 'BEGIN { x=1;y=2;z=x+y } {print $x $y $z}'
abc

這個例子有點多餘﹐僅作示範而已。因為﹐我們在 BEGIN 定義了 x﹑y﹑z 的值﹕( 1﹑2﹑3 )﹐然後我們再將 $x﹑$y﹑$z (也就是 $1﹑$2﹑$3 ) 的欄位列引出來。所以﹐執行結果是第四欄的 d 就沒有顯示了。

再例如﹐您有一個檔案 (result.txt)﹐其內容如下﹕

FName   LName   English Chinese Math
Kenny   Chen    80      80      50
Eward   Lee     70      90      90
Amigo   Chu     50      80      80
John    Smith   90      50      75

您可以用下面的命令﹐找出 Chinese 及格的名單﹐而只顯示其名(忽略其姓)﹕


# awk '{ if ($4 >= 60) print $1" : "$4}' result.txt | tail +2
Kenny : 80
Eward : 90
Amigo : 80

如果您不想顯示作為標頭的第一行句子﹐可以 pipe 到 tail 命令進行過濾。不如﹐讓我們再玩些更複雜的﹐比方說計算所有名單的平均成績算﹐並且以最後一列顯示出來﹐可以這樣設計﹕


# awk '{ 
total = $3 + $4 + $5
number = NF - 2
average = total/number
if (NR < 2) printf("%s\t%s\n", $0, "AVERAGE");
if (NR >= 2) printf("%s\t%3.2f\n", $0, average)
}' result.txt
FName   LName   English Chinese Math    AVERAGE
Kenny   Chen    80      80      50      70.00
Eward   Lee     70      90      90      83.33
Amigo   Chu     50      80      80      70.00
John    Smith   90      50      75      71.67

這個命令看起來有點複雜﹐需要說明一下﹕

  1. 首先﹐我們用一對 { } 將 awk 的命令括起來﹐然後在其外面再加一對 ' ' ﹐這樣您可以在單引號之間敲 Enter 將長命令分成多行輸入。

  2. 然後定義了 total 的變數為第 3﹑4﹑5 欄的總和 (也就是 English + Chinese + Math)﹐以及變數 number 為欄位數目減掉 2 (也就是 NF - FName - LName )。

  3. 然後﹐平均值就是 total 除以 number 。

  4. 因為檔案中的第一行是不能用來運算的﹐而且還必須再加上一個叫 AVERAGE 的欄位標頭﹐所以這裡首先用一個 if 來判斷行號是否少於 2 (不過﹐我在測試的時候﹐發現不能用 = 1 來設定﹐我也不知道為什麼﹖)﹐然則﹐用 printf 命令(注意﹕在 print 後面有一個 f 字母) ﹐以指定格式進行顯示。這裡的格式是﹕首先是一個字串 ( %s )﹐也就是後面所對應的 $0 (整行句子) 以字串格式顯示﹔然後是一個 tab 鍵 ( \t )﹔再下來又是一個字串﹐也就後面的 "AVERAGE" (非變數值必須用 " " 括起來)﹔最後輸入一個斷行符號 ( \n ﹐newline 的意思)。這裡﹐您會發現﹐凡是用 % 表示的格式﹐必須依順序對應到後面的顯示欄位﹔而用 \ 開頭的﹐則是可以從鍵盤輸入的符號。(或許﹐剛開始可能比較難看出個所以然﹐多比較一下﹐就不難發現它的規則啦。後面還有一個範例。)

  5. 接下來的﹐會先用 if 判斷行號是否大於或等於 2 (您也可以用 > 1 ﹐也就是從第二行開始)﹐然則﹐再用 printf 命令﹐按 %s\t%3.2f\n 的格式來顯示。其中的 %s﹑\t﹑\n 相信您都知道了﹐只有 %3.2f 沒見過而已。它定義出浮點數字( floating point )的顯示格式是﹕小數點左邊 3 位數和小數點右邊兩位數。所以這行的格式是﹕先用字串顯示整行﹑然後一個 tab 鍵﹑然後以 3.2 小數點格式顯示前面定義好的 average 變數﹑最後是一個斷行符號﹕

                  %s                     \t   %3.2f  \n
    |                                  |    |      |
     Kenny   Chen    80      80      50       70.00
    |                                  |    |      |
                  $0                         average
    

  6. 然後是 '{ }' 這些括號及引號的關閉﹐最後是要處理的檔案名稱。

而每一行的輸出結果﹐就會在字行後面按指定的格式加上 tab 鍵和平均值了。是否很神奇呢﹖﹗呵呵﹐這只是 awk 的牛刀少試而已﹐若要完全發揮 awk 的強大火力﹐恐怕已經不是我所能介紹的了。

上一篇:第四章﹕基本命令操作
下一篇:第六章﹕編譯核心