Daniel Robbins
總裁兼 CEO,Gentoo Technologies, Inc.
2000 年 3 月
通過學習如何使用 bash 指令碼語言編程,將使 Linux 的日常互動更有趣和有生產力,同時還可以利用那些已熟悉和喜愛的標準 UNIX 概念(如管道和重新導向)。在此三部分系列中,Daniel Robbins 將以樣本指導您如何用 bash 編程。他將講述非常基本的知識(這使此系列十分適合初學者),並在後續系列中逐步引入更進階特性。
您可能要問:為什麼要學習 Bash 編程?好,以下是幾條令人信服的理由:
已經在運行它
如果查看一下,可能會發現:您現在正在運行 bash。因為 bash 是標準 Linux shell,並用於各種目的,所以,即使更改了預設 shell,bash 可能仍在系統中某處運行。因為 bash 已在運行,以後啟動並執行任何 bash 指令碼都天生是有效利用記憶體的,因為它們與任何已啟動並執行 bash 進程共用記憶體。如果正在啟動並執行工具可以勝任工作,並且做得很好,為什麼還要裝入一個 500K 的解譯器?
已經在使用它
不僅在運行 bash,實際上,您每天還在與 bash 打交道。它總在那裡,因此學習如何最大限度使用它是有意義的。這樣做將使您的 bash 經驗更有趣和有生產力。但是為什麼要學習 bash 編程?很簡單,因為您已在考慮如何運行命令、CPing 檔案以及管道化和重新導向輸出。為什麼不學習一種語言,以便使用和利用那些已熟悉和喜愛的強大省時的概念?命令 shell 開啟了 UNIX 系統的潛能,而 bash 正是這個 Linux shell。它是您和機器之間的進階紐帶。增長 bash 知識吧,這將自動提高您在 Linux 和 UNIX 中的生產力 -- 就那麼簡單。
Bash 困惑
以錯誤方式學習 bash 令人十分困惑。許多新手輸入 "man bash" 來查看 bash 協助頁,但只得到非常簡單和技術方面的 shell 功能性描述。還有人輸入 "info bash"(來查看 GNU 資訊文檔),只能得到重新顯示的協助頁,或者(如果幸運)略為友好的資訊文檔。
儘管這可能使初學者有些失望,但標準 bash 文檔無法滿足所有人的要求,它只適合那些已大體熟悉 shell 編程的人。協助頁中確實有很多極好的技術資訊,但對初學者的協助卻有限。
這就是本系列的目的所在。在本系列中,我將講述如何實際使用 bash 編程概念,以便編寫自己的指令碼。與技術描述不同,我將以簡單的語言為您解釋,使您不僅知道事情做什麼,還知道應在何時使用。在此三部分系列末尾,您將可以自己編寫複雜的 bash 指令碼,並可以自如地使用 bash 以及通過閱讀(和理解)標準 bash 文檔來補充知識。讓我們開始吧。
環境變數
在 bash 和幾乎所有其它 shell 中,使用者可以定義環境變數,這些環境變數在以 ASCII 字串儲存。環境變數的最便利之處在於:它們是 UNIX 進程模型的標準部分。這意味著:環境變數不僅由 shell 指令碼獨用,而且還可以由編譯過的標準程式使用。當在 bash 中“匯出”環境變數時,以後啟動並執行任何程式,不管是不是 shell 指令碼,都可以讀取設定。一個很好的例子是 vipw 命令,它通常允許 root 使用者編輯系統口令檔案。通過將 EDITOR 環境變數設定成喜愛的文字編輯器名稱,可以配置 vipw,使其使用該編輯器,而不使用 vi,如果習慣於 xemacs 而確實不喜歡 vi,那麼這是很便利的。
在 bash 中定義環境變數的標準方法是:
$ myvar='This is my environment variable!' |
以上命令定義了一個名為 "myvar" 的環境變數,並包含字串 "This is my environment variable!"。以上有幾點注意事項:第一,在等號 "=" 的兩邊沒有空格,任何空格將導致錯誤(試一下看看)。第二個件要注意的事是:雖然在定義一個字時可以省略引號,但是當定義的環境變數值多於一個字時(包含空格或製表鍵),引號是必須的。
引用細節 有關如何在 bash 中使用引號的非常詳盡的資訊,請參閱 bash 協助頁面中的“引用”一節。特殊字元序列由其它值“擴充”(替換)確實使 bash 中字串的處理變得複雜。本系列將只講述最常用的引用功能。 |
第三,雖然通常可以用雙引號來替代單引號,但在上例中,這樣做會導致錯誤。為什麼呢?因為使用單引號禁用了稱為擴充的 bash 特性,其中,特殊字元和字元系列由值替換。例如,"!" 字元是曆史擴充字元,bash 通常將其替換為前面輸入的命令。(本系列文章中將不講述曆史擴充,因為它在 bash 編程中不常用。有關曆史擴充的詳細資料,請參閱 bash 協助頁中的“曆史擴充”一節。)儘管這個類似於宏的功能很便利,但我們現在只想在環境變數後面加上一個簡單的驚嘆號,而不是宏。
現在,讓我們看一下如何實際使用環境變數。這有一個例子:
$ echo $myvarThis is my environment variable! |
通過在環境變數的前面加上一個 $,可以使 bash 用 myvar 的值替換它。這在 bash 術語中叫做“變數擴充”。但是,這樣做將怎樣:
我們希望回顯 "fooThis is my environment variable!bar",但卻不是這樣。錯在哪裡?簡單地說,bash 變數擴充設施陷入了困惑。它無法識別要擴充哪一個變數:$m、$my、$myvar 、$myvarbar 等等。如何更明確清楚地告述 bash 引用哪一個變數?試一下這個:
$ echo foo${myvar}barfooThis is my environment variable!bar |
如您所見,當環境變數沒有與周圍文本明顯分開時,可以用花括弧將它括起。雖然 $myvar 可以更快輸入,並且在大多數情況下正確工作,但 ${myvar} 卻能在幾乎所有情況下正確通過文法分析。除此之外,二者相同,將在本系列的餘下部分看到變數擴充的兩種形式。請記住:當環境變數沒有用空白(空格或製表鍵)與周圍文本分開時,請使用更明確的花括弧形式。
回想一下,我們還提到過可以“匯出”變數。當匯出環境變數時,它可以自動地由以後啟動並執行任何指令碼或可執行程式環境使用。shell 指令碼可以使用 shell 的內建環境變數支援“到達”環境變數,而 C 程式可以使用 getenv() 函數調用。這裡有一些 C 程式碼範例,輸入並編譯它們 -- 它將協助我們從 C 的角度理解環境變數:
myvar.c -- 樣本環境變數 C 程式
#include #include int main(void) { char *myenvvar=getenv("EDITOR"); printf("The editor environment variable is set to %s/n",myenvvar);} |
將上面的代碼儲存到檔案 myenv.c 中,然後發出以下命令進行編譯:
現在,目錄中將有一個可執行程式,它在運行時將列印 EDITOR 環境變數的值(如果有值的話)。這是在我機器上運行時的情況:
$ ./myenvThe editor environment variable is set to (null) |
啊... 因為沒有將 EDITOR 環境變數設定成任何值,所以 C 程式得到一個Null 字元串。讓我們試著將它設定成特定值:
$ EDITOR=xemacs$ ./myenvThe editor environment variable is set to (null) |
雖然希望 myenv 列印值 "xemacs",但是因為還沒有匯出環境變數,所以它卻沒有很好地工作。這次讓它正確工作:
$ export EDITOR$ ./myenvThe editor environment variable is set to xemacs |
現在,如您親眼所見:不匯出環境變數,另一個進程(在本例中是樣本 C 程式)就看不到環境變數。順便提一句,如果願意,可以在一行定義並匯出環境變數,如下所示:
這與兩行版本的效果相同。現在該示範如何使用 unset 來除去環境變數:
$ unset EDITOR$ ./myenvThe editor environment variable is set to (null) |
dirname 和 basename 請注意:dirname 和 basename 不是磁碟上的檔案或目錄,它們只是字串操作命令。 |
截斷字串概述
截斷字串是將初始字串截斷成較小的獨立塊,它是一般 shell 指令碼每天執行的任務之一。很多時候,shell 指令碼需要採用全限定路徑,並找到結束的檔案或目錄。雖然可以用 bash 編碼實現(而且有趣),但標準 basename UNIX 可執行程式可以極好地完成此工作:
$ basename /usr/local/share/doc/foo/foo.txtfoo.txt$ basename /usr/home/drobbinsdrobbins |
Basename 是一個截斷字串的極簡便工具。它的相關命令 dirname 返回 basename 丟棄的“另”一部分路徑。
$ dirname /usr/local/share/doc/foo/foo.txt/usr/local/share/doc/foo$ dirname /usr/home/drobbins//usr/home |
命令替換
需要知道一個簡便操作:如何建立一個包含可執行命令結果的環境變數。這很容易:
$ MYDIR=`dirname /usr/local/share/doc/foo/foo.txt`$ echo $MYDIR/usr/local/share/doc/foo |
上面所做的稱為“命令替換”。此例中有幾點需要指出。在第一行,簡單地將要執行的命令以 反引號括起。那不是標準的單引號,而是鍵盤中通常位於 Tab 鍵之上的單引號。可以用 bash 備用命令替換文法來做同樣的事:
$ MYDIR=$(dirname /usr/local/share/doc/foo/foo.txt)$ echo $MYDIR/usr/local/share/doc/foo |
如您所見,bash 提供多種方法來執行完全一樣的操作。使用命令替換可以將任何命令或命令管道放在 ` ` 或 $( ) 之間,並將其分配給環境變數。真方便!下面是一個例子,示範如何在命令替換中使用管道:
MYFILES=$(ls /etc | grep pa)bash-2.03$ echo $MYFILESpam.d passwd |
象專業人員那樣截斷字串
儘管 basename 和 dirname 是很好的工具,但有時可能需要執行更進階的字串“截斷”,而不只是標準的路徑名操作。當需要更強的說服力時,可以利用 bash 內建的變數擴充功能。已經使用了類似於 ${MYVAR} 的標準類型的變數擴充。但是 bash 自身也可以執行一些便利的字串截斷。看一下這些例子:
$ MYVAR=foodforthought.jpg$ echo ${MYVAR##*fo}rthought.jpg$ echo ${MYVAR#*fo}odforthought.jpg |
在第一個例子中,輸入了 ${MYVAR##*fo}。它的確切含義是什嗎?基本上,在 ${ } 中輸入環境變數名稱,兩個 ##,然後是萬用字元 ("*fo")。然後,bash 取得 MYVAR,找到從字串 "foodforthought.jpg" 開始處開始、且匹配萬用字元 "*fo" 的最長子字串,然後將其從字串的開始處截去。剛開始理解時會有些困難,為了感受一下這個特殊的 "##" 選項如何工作,讓我們一步步地看看 bash 如何完成這個擴充。首先,它從 "foodforthought.jpg" 的開始處搜尋與 "*fo" 萬用字元匹配的子字串。以下是檢查到的子字串:
f fo MATCHES *fofoo foodfoodf foodfo MATCHES *fofoodforfoodfort foodforthfoodfortho foodforthoufoodforthougfoodforthoughtfoodforthought.jfoodforthought.jpfoodforthought.jpg |
在搜尋了匹配的字串之後,可以看到 bash 找到兩個匹配。它選擇最長的匹配,從初始字串的開始處除去,然後返回結果。
上面所示的第二個變數擴充形式看起來與第一個相同,但是它只使用一個 "#" -- 並且 bash 執行幾乎同樣的過程。它查看與第一個例子相同的子字串系列,但是 bash 從初始字串除去最短的匹配,然後返回結果。所以,一查到 "fo" 子字串,它就從字串中除去 "fo",然後返回 "odforthought.jpg"。
這樣說可能會令人十分困惑,下面以一簡單方式記住這個功能。當搜尋最長相符時,使用 ##(因為 ## 比 # 長)。當搜尋最短匹配時,使用 #。看,不難記吧!等一下,怎樣記住應該使用 '#' 字元來從字串開始部分除去?很簡單!注意到了嗎:在美國鍵盤上,shift-4 是 "$",它是 bash 變數擴充字元。在鍵盤上,緊靠 "$" 左邊的是 "#"。這樣,可以看到:"#" 位於 "$" 的“開始處”,因此(根據我們的記憶法),"#" 從字串的開始處除去字元。您可能要問:如何從字串末尾除去字元。如果猜到我們使用美國鍵盤上緊靠 "$" 右邊的字元 ("%),那就猜對了。這裡有一些簡單的例子,解釋如何截去字串的末尾部分:
$ MYFOO="chickensoup.tar.gz"$ echo ${MYFOO%%.*}chickensoup$ echo ${MYFOO%.*}chickensoup.tar |
正如您所見,除了將匹配萬用字元從字串末尾除去之外,% 和 %% 變數擴充選項與 # 和 ## 的工作方式相同。請注意:如果要從末尾除去特定子字串,不必使用 "*" 字元:
MYFOOD="chickensoup"$ echo ${MYFOOD%%soup}chicken |
在此例中,使用 "%%" 或 "%" 並不重要,因為只能有一個匹配。還要記住:如果忘記了應該使用 "#" 還是 "%",則看一下鍵盤上的 3、4 和 5 鍵,然後猜出來。
可以根據特定字元位移和長度,使用另一種形式的變數擴充,來選擇特定子字串。試著在 bash 中輸入以下行:
$ EXCLAIM=cowabunga$ echo ${EXCLAIM:0:3}cow$ echo ${EXCLAIM:3:7}abunga |
這種形式的字串截斷非常簡便,只需用冒號分開來指定起始字元和子字串長度。
應用字串截斷
現在我們已經學習了所有截斷字串的知識,下面寫一個簡單短小的 shell 指令碼。我們的指令碼將接受一個檔案作為自變數,然後列印:該檔案是否是一個 tar 檔案。要確定它是否是 tar 檔案,將在檔案末尾尋找模式 ".tar"。如下所示:
mytar.sh -- 一個簡單的指令碼
#!/bin/bashif [ "${1##*.}" = "tar" ]then echo This appears to be a tarball.else echo At first glance, this does not appear to be a tarball.fi |
要運行此指令碼,將它輸入到檔案 mytar.sh 中,然後輸入 "chmod 755 mytar.sh",產生可執行檔。然後,如下做一下 tar 檔案實驗:
$ ./mytar.sh thisfile.tarThis appears to be a tarball.$ ./mytar.sh thatfile.gzAt first glance, this does not appear to be a tarball. |
好,成功運行,但是不太實用。在使它更實用之前,先看一下上面使用的 "if" 語句。語句中使用了一個布林運算式。在 bash 中,"=" 比較子檢查字串是否相等。在 bash 中,所有布林運算式都用方括弧括起。但是布林運算式實際上測試什嗎?讓我們看一下左邊。根據前面所學的字串截斷知識,"${1##*.}" 將從環境變數 "1" 包含的字串開始部分除去最長的 "*." 匹配,並返回結果。這將返迴文件中最後一個 "." 之後的所有部分。顯然,如果檔案以 ".tar" 結束,結果將是 "tar",條件也為真。
您可能會想:開始處的 "1" 環境變數是什麼。很簡單 -- $1 是傳給指令碼的第一個命令列自變數,$2 是第二個,以此類推。好,已經回顧了功能,下面來初探 "if" 語句。
If 語句
與大多數語言一樣,bash 有自己的條件形式。在使用時,要遵循以上格式;即,將 "if" 和 "then" 放在不同行,並使 "else" 和結束處必需的 "fi" 與它們水平對齊。這將使代碼易於閱讀和調試。除了 "if,else" 形式之外,還有其它形式的 "if" 語句:
if [ condition ]then actionfi |
只有當 condition
為真時,該語句才執行操作,否則不執行操作,並繼續執行 "fi" 之後的任何行。
if [ condition ]then actionelif [ condition2 ]then action2...elif [ condition3 ]then else actionxfi |
以上 "elif" 形式將連續測試每個條件,並執行符合第一個真條件的操作。如果沒有條件為真,則將執行 "else" 操作,如果有一個條件為真,則繼續執行整個 "if,elif,else" 語句之後的行。
下一次
我們已經學習了最基本的 bash 功能,現在要加快腳步,準備編寫一些實際指令碼。在下一篇中,將講述迴圈概念、函數、名稱空間和其它重要主題。然後,將準備好編寫一些更複雜的指令碼。在第三篇中,將重點講述一些非常複雜的指令碼和功能,以及幾個 bash 指令碼設計選項。再見!
參考資料
- 訪問 GNU's bash 首頁
- 查看 bash online reference manual
轉自:IBM developerWorks 中國網站