我們先看一下處理命令列自變數的簡單技巧,然後再看看 bash 基本編程結構。
接收自變數
在 介紹性文章 中的樣本程式中,我們使用環境變數 "$1" 來引用第一個命令列自變數。類似地,可以使用 "$2"、"$3" 等來引用傳遞給指令碼的第二和第三個自變數。這裡有一個例子:
#!/usr/bin/env bash echo name of script is $0 echo first argument is $1 echo second argument is $2 echo seventeenth argument is $17 echo number of arguments is $# |
除以下兩個細節之外,此例無需說明。第一,"$0" 將擴充成從命令列調用的指令碼名稱,"$#" 將擴充成傳遞給指令碼的自變數數目。實驗以上指令碼,通過傳遞不同類型的命令列自變數來瞭解其工作原理。
有時需要一次引用 所有 命令列自變數。針對這種用途,bash 實現了變數 "$@",它擴充成所有用空格分開的命令列參數。在本文稍後的 "for" 迴圈部分中,您將看到使用該變數的例子。
回頁首
Bash 編程結構
如果您曾用過如 C、Pascal、Python 或 Perl 那樣的過程語言編程,則一定熟悉 "if" 語句和 "for" 迴圈那樣的標準編程結構。對於這些標準結構的大多數,Bash 有自己的版本。在下幾節中,將介紹幾種 bash 結構,並示範這些結構和您已經熟悉的其它程式設計語言中結構的差異。如果以前編程不多,也不必擔心。我提供了足夠的資訊和樣本,使您可以跟上本文的進度。
回頁首
方便的條件陳述式
如果您曾用 C 編寫過與檔案相關的代碼,則應該知道:要比較特定檔案是否比另一個檔案新需要大量工作。那是因為 C 沒有任何內建文法來進行這種比較,必須使用兩個 stat() 調用和兩個 stat 結構來進行手工比較。相反,bash 內建了標準檔案比較子,因此,確定“/tmp/myfile 是否可讀”與查看“$myvar 是否大於 4”一樣容易。
下表列出最常用的 bash 比較子。同時還有如何正確使用每一選項的樣本。樣本要跟在 "if" 之後。例如:
if [ -z "$myvar" ]then echo "myvar is not defined"fi |
| 運算子 |
描述 |
樣本 |
| 檔案比較子 |
| -e filename |
如果 filename存在,則為真 |
[ -e /var/log/syslog ] |
| -d filename |
如果 filename為目錄,則為真 |
[ -d /tmp/mydir ] |
| -f filename |
如果 filename為常規檔案,則為真 |
[ -f /usr/bin/grep ] |
| -L filename |
如果 filename為符號連結,則為真 |
[ -L /usr/bin/grep ] |
| -r filename |
如果 filename可讀,則為真 |
[ -r /var/log/syslog ] |
| -w filename |
如果 filename可寫,則為真 |
[ -w /var/mytmp.txt ] |
| -x filename |
如果 filename可執行,則為真 |
[ -L /usr/bin/grep ] |
| filename1-nt filename2 |
如果 filename1比 filename2新,則為真 |
[ /tmp/install/etc/services -nt /etc/services ] |
| filename1-ot filename2 |
如果 filename1比 filename2舊,則為真 |
[ /boot/bzImage -ot arch/i386/boot/bzImage ] |
| 字串比較運算子 (請注意引號的使用,這是防止空格擾亂代碼的好方法) |
| -z string |
如果 string長度為零,則為真 |
[ -z "$myvar" ] |
| -n string |
如果 string長度非零,則為真 |
[ -n "$myvar" ] |
| string1= string2 |
如果 string1與 string2相同,則為真 |
[ "$myvar" = "one two three" ] |
| string1!= string2 |
如果 string1與 string2不同,則為真 |
[ "$myvar" != "one two three" ] |
| 算術比較子 |
| num1-eq num2 |
等於 |
[ 3 -eq $mynum ] |
| num1-ne num2 |
不等於 |
[ 3 -ne $mynum ] |
| num1-lt num2 |
小於 |
[ 3 -lt $mynum ] |
| num1-le num2 |
小於或等於 |
[ 3 -le $mynum ] |
| num1-gt num2 |
大於 |
[ 3 -gt $mynum ] |
| num1-ge num2 |
大於或等於 |
[ 3 -ge $mynum ] |
有時,有幾種不同方法來進行特定比較。例如,以下兩個程式碼片段的功能相同:
if [ "$myvar" -eq 3 ]then echo "myvar equals 3"fi if [ "$myvar" = "3" ]then echo "myvar equals 3"fi |
上面兩個比較執行相同的功能,但是第一個使用算術比較子,而第二個使用字串比較運算子。
回頁首
字串比較說明
大多數時候,雖然可以不使用括起字串和字串變數的雙引號,但這並不是好主意。為什麼呢?因為如果環境變數中恰巧有一個空格或製表鍵,bash 將無法分辨,從而無法正常工作。這裡有一個錯誤的比較樣本:
if [ $myvar = "foo bar oni" ]then echo "yes"fi |
在上例中,如果 myvar 等於 "foo",則代碼將按預想工作,不進行列印。但是,如果 myvar 等於 "foo bar oni",則代碼將因以下錯誤失敗:
在這種情況下,"$myvar"(等於 "foo bar oni")中的空格迷惑了 bash。bash 擴充 "$myvar" 之後,代碼如下:
[ foo bar oni = "foo bar oni" ] |
因為環境變數沒放在雙引號中,所以 bash 認為方括弧中的自變數過多。可以用雙引號將字串自變數括起來消除該問題。請記住,如果養成將所有字串自變數用雙引號括起的習慣,將除去很多類似的編程錯誤。"foo bar oni" 比較 應該寫成:
if [ "$myvar" = "foo bar oni" ]then echo "yes"fi |
更多引用細節
如果要擴充環境變數,則必須將它們用 雙引號、而不是單引號括起。單引號 禁用 變數(和曆史)擴充。
以上代碼將按預想工作,而不會有任何令人不快的意外出現。
回頁首
迴圈結構:"for"
好了,已經講了條件陳述式,下面該探索 bash 迴圈結構了。我們將從標準的 "for" 迴圈開始。這裡有一個簡單的例子:
#!/usr/bin/env bash for x in one two three four do echo number $x done 輸出: number one number two number three number four |
發生了什嗎?"for" 迴圈中的 "for x" 部分定義了一個名為 "$x" 的新環境變數(也稱為迴圈控制變數),它的值被依次設定為 "one"、"two"、"three" 和 "four"。每一次賦值之後,執行一次迴圈體("do" 和 "done" 之間的代碼)。在迴圈體內,象其它環境變數一樣,使用標準的變數擴充文法來引用迴圈控制變數 "$x"。還要注意,"for" 迴圈總是接收 "in" 語句之後的某種類型的字列表。在本例中,指定了四個英語單詞,但是字列表也可以引用磁碟上的檔案,甚至檔案萬用字元。看看下面的例子,該例示範如何使用標準 shell 萬用字元:
#!/usr/bin/env bash for myfile in /etc/r* do if [ -d "$myfile" ] then echo "$myfile (dir)"else echo "$myfile"fi done 輸出: /etc/rc.d (dir) /etc/resolv.conf /etc/resolv.conf~ /etc/rpc |
以上代碼列出在 /etc 中每個以 "r" 開頭的檔案。要做到這點,bash 在執行迴圈之前首先取得萬用字元 /etc/r*,然後擴充它,用字串 /etc/rc.d /etc/resolv.conf /etc/resolv.conf~ /etc/rpc 替換。一旦進入迴圈,根據 myfile 是否為目錄,"-d" 條件運算子用來執行兩個不同操作。如果是目錄,則將 "(dir)" 附加到輸出行。
還可以在字列表中使用多個萬用字元、甚至是環境變數:
for x in /etc/r--? /var/lo* /home/drobbins/mystuff/* /tmp/${MYPATH}/* do cp $x /mnt/mydir done |
Bash 將在所有正確位置上執行萬用字元和環境變數擴充,並可能建立一個非常長的字列表。
雖然所有萬用字元擴充樣本使用了 絕對路徑,但也可以使用相對路徑,如下所示:
for x in ../* mystuff/* do echo $x is a silly file done |
在上例中,bash 相對於當前工作目錄執行萬用字元擴充,就象在命令列中使用相對路徑一樣。研究一下萬用字元擴充。您將注意到,如果在萬用字元中使用絕對路徑,bash 將萬用字元擴充成一個絕對路徑列表。否則,bash 將在後面的字列表中使用相對路徑。如果只引用當前工作目錄中的檔案(例如,如果輸入 "for x in *"),則產生的檔案清單將沒有路徑資訊的首碼。請記住,可以使用 "basename" 可執行程式來除去前面的路徑資訊,如下所示:
for x in /var/log/* do echo `basename $x` is a file living in /var/log done |
當然,在指令碼的命令列自變數上執行迴圈通常很方便。這裡有一個如何使用本文開始提到的 "$@" 變數的例子:
#!/usr/bin/env bash for thing in "$@" do echo you typed ${thing}. done 輸出: $ allargs hello there you silly you typed hello. you typed there. you typed you. you typed silly. |
回頁首
Shell 算術
在學習另一類型的迴圈結構之前,最好先熟悉如何執行 shell 算術。是的,確實如此:可以使用 shell 結構來執行簡單的整數運算。只需將特定的算術運算式用 "$((" 和 "))" 括起,bash 就可以計算運算式。這裡有一些例子:
$ echo $(( 100 / 3 )) 33 $ myvar="56" $ echo $(( $myvar + 12 )) 68 $ echo $(( $myvar - $myvar ))0 $ myvar=$(( $myvar + 1 ))$ echo $myvar 57 |
回頁首
更多的迴圈結構:"while" 和 "until"
只要特定條件為真,"while" 語句就會執行,其格式如下:
while [ condition ] do statements done |
通常使用 "While" 語句來迴圈一定次數,比如,下例將迴圈 10 次:
myvar=0 while [ $myvar -ne 10 ] do echo $myvar myvar=$(( $myvar + 1 )) done |
可以看到,上例使用了算術運算式來使條件最終為假,並導致迴圈終止。
"Until" 語句提供了與 "while" 語句相反的功能:只要特定條件為 假 ,它們就重複。下面是一個與前面的 "while" 迴圈具有同等功能的 "until" 迴圈:
myvar=0 until [ $myvar -eq 10 ] do echo $myvar myvar=$(( $myvar + 1 )) done |
回頁首
Case 語句
Case 語句是另一種便利的條件結構。這裡有一個樣本片段:
case "${x##*.}" in gz) gzunpack ${SROOT}/${x} ;; bz2) bz2unpack ${SROOT}/${x} ;; *) echo "Archive format not recognized." exit ;; esac |
在上例中,bash 首先擴充 "${x##*.}"。在代碼中,"$x" 是檔案的名稱,"${x##.*}" 除去檔案中最後句點後文本之外的所有文本。然後,bash 將產生的字串與 ")" 左邊列出的值做比較。在本例中,"${x##.*}" 先與 "gz" 比較,然後是 "bz2",最後是 "*"。如果 "${x##.*}" 與這些字串或模式中的任何一個匹配,則執行緊接 ")" 之後的行,直到 ";;" 為止,然後 bash 繼續執行結束符 "esac" 之後的行。如果不匹配任何模式或字串,則不執行任何程式碼,在這個特殊的程式碼片段中,至少要執行一個代碼塊,因為任何不與 "gz" 或 "bz2" 匹配的字串都將與 "*" 模式比對。
回頁首
函數與名稱空間
在 bash 中,甚至可以定義與其它過程語言(如 Pascal 和 C)類似的函數。在 bash 中,函數甚至可以使用與指令碼接收命令列自變數類似的方式來接收自變數。讓我們看一下樣本函數定義,然後再從那裡繼續:
tarview() { echo -n "Displaying contents of $1 " if [ ${1##*.} = tar ]then echo "(uncompressed tar)" tar tvf $1 elif [ ${1##*.} = gz ]then echo "(gzip-compressed tar)" tar tzvf $1 elif [ ${1##*.} = bz2 ]then echo "(bzip2-compressed tar)" cat $1 | bzip2 -d | tar tvf -fi } |
另一種情況
可以使用 "case" 語句來編寫上面的代碼。您知道如何編寫嗎?
我們在上面定義了一個名為 "tarview" 的函數,它接收一個自變數,即某種類型的 tar 檔案。在執行該函數時,它確定自變數是哪種 tar 檔案類型(未壓縮的、gzip 壓縮的或 bzip2 壓縮的),列印一行資訊性訊息,然後顯示 tar 檔案的內容。應該如下調用上面的函數(在輸入、粘貼或找到該函數後,從指令碼或命令列調用它):
$ tarview shorten.tar.gz Displaying contents of shorten.tar.gz (gzip-compressed tar) drwxr-xr-x ajr/abbot 0 1999-02-27 16:17 shorten-2.3a/ -rw-r--r-- ajr/abbot 1143 1997-09-04 04:06 shorten-2.3a/Makefile -rw-r--r-- ajr/abbot 1199 1996-02-04 12:24 shorten-2.3a/INSTALL -rw-r--r-- ajr/abbot 839 1996-05-29 00:19 shorten-2.3a/LICENSE .... |
互動地使用它們
別忘了,可以將函數(如上面的函數)放在 ~/.bashrc 或 ~/.bash_profile 中,以便在 bash 中隨時使用它們。
如您所見,可以使用與引用命令列自變數同樣的機制來在函數定義內部引用自變數。另外,將把 "$#" 宏擴充成包含自變數的數目。唯一可能不完全相同的是變數 "$0",它將擴充成字串 "bash"(如果從 shell 互動運行函數)或調用函數的指令碼名稱。
回頁首
名稱空間
經常需要在函數中建立環境變數。雖然有可能,但是還有一個技術細節應該瞭解。在大多數編譯語言(如 C)中,當在函數內部建立變數時,變數被放置在單獨的局部名稱空間中。因此,如果在 C 中定義一個名為 myfunction 的函數,並在該函數中定義一個名為 "x" 的自變數,則任何名為 "x" 的全域變數(函數之外的變數)將不受它的印象,從而消除了負作用。
在 C 中是這樣,但在 bash 中卻不是。在 bash 中,每當在函數內部建立環境變數,就將其添加到 全域名稱空間。這意味著,該變數將重寫函數之外的全域變數,並在函數退出之後繼續存在:
#!/usr/bin/env bash myvar="hello" myfunc() { myvar="one two three" for x in $myvar do echo $x done } myfunc echo $myvar $x |
運行此指令碼時,它將輸出 "one two three three",這顯示了在函數中定義的 "$myvar" 如何影響全域變數 "$myvar",以及迴圈控制變數 "$x" 如何在函數退出之後繼續存在(如果 "$x" 全域變數存在,也將受到影響)。
在這個簡單的例子中,很容易找到該錯誤,並通過使用其它變數名來改正錯誤。但這不是正確的方法,解決此問題的最好方法是通過使用 "local" 命令,在一開始就預防影響全域變數的可能性。當使用 "local" 在函數內部建立變數時,將把它們放在 局部名稱空間中,並且不會影響任何全域變數。這裡示範了如何?上述代碼,以便不重寫全域變數:
#!/usr/bin/env bash myvar="hello" myfunc() { local x local myvar="one two three" for x in $myvar do echo $x done } myfunc echo $myvar $x |
此函數將輸出 "hello" -- 不重寫全域變數 "$myvar","$x" 在 myfunc 之外不繼續存在。在函數的第一行,我們建立了以後要使用的局部變數 x,而在第二個例子 (local myvar="one two three"") 中,我們建立了局部變數 myvar, 同時 為其賦值。在將迴圈控制變數定義為局部變數時,使用第一種形式很方便,因為不允許說:"for local x in $myvar"。此函數不影響任何全域變數,鼓勵您用這種方式設計所有的函數。只有在明確希望要修改全域變數時,才 不應該使用 "local"。
回頁首
結束語
我們已經學習了最基本的 bash 功能,現在要看一下如何基於 bash 開發整個應用程式。下一部分正要講到。再見!
參考資料
- 您可以參閱本文在 developerWorks 全球網站上的 英文原文.
- 閱讀介紹性的 bash 文章, developerWorks上的“ Bash 執行個體,第 1 部分”
- 訪問 GNU's bash 首頁
- 查看 bash online reference manual