級別: 初級
Daniel Robbins (drobbins@gentoo.org), 總裁兼 CEO, Gentoo Technologies, Inc.
2000 年 3 月 01 日
通過學(xué)習(xí)如何使用 bash 腳本語言編程,將使 Linux 的日常交互更有趣和有生產(chǎn)力,同時(shí)還可以利用那些已熟悉和喜愛的標(biāo)準(zhǔn) UNIX 概念(如管道和重定向)。在此三部分系列中,Daniel Robbins 將以示例指導(dǎo)您如何用 bash 編程。他將講述非常基本的知識(這使此系列十分適合初學(xué)者),并在后續(xù)系列中逐步引入更高級特性。
您可能要問:為什么要學(xué)習(xí) Bash 編程?好,以下是幾條令人信服的理由:
已經(jīng)在運(yùn)行它
如果查看一下,可能會發(fā)現(xiàn):您現(xiàn)在正在運(yùn)行 bash。因?yàn)?bash 是標(biāo)準(zhǔn) Linux shell,并用于各種目的,所以,即使更改了缺省 shell,bash 可能 仍 在系統(tǒng)中某處運(yùn)行。因?yàn)?bash 已在運(yùn)行,以后運(yùn)行的任何 bash 腳本都天生是有效利用內(nèi)存的,因?yàn)樗鼈兣c任何已運(yùn)行的 bash 進(jìn)程共享內(nèi)存。如果正在運(yùn)行的工具可以勝任工作,并且做得很好,為什么還要裝入一個(gè) 500K 的解釋器?
已經(jīng)在使用它
不僅在運(yùn)行 bash,實(shí)際上,您每天還在與 bash 打交道。它總在那里,因此學(xué)習(xí)如何最大限度使用它是有意義的。這樣做將使您的 bash 經(jīng)驗(yàn)更有趣和有生產(chǎn)力。但是為什么要學(xué)習(xí) bash 編程 ?很簡單,因?yàn)槟言诳紤]如何運(yùn)行命令、CPing 文件以及管道化和重定向輸出。為什么不學(xué)習(xí)一種語言,以便使用和利用那些已熟悉和喜愛的強(qiáng)大省時(shí)的概念?命令 shell 開啟了 UNIX 系統(tǒng)的潛能,而 bash 正是 這個(gè) Linux shell。它是您和機(jī)器之間的高級紐帶。增長 bash 知識吧,這將自動提高您在 Linux 和 UNIX 中的生產(chǎn)力 -- 就那么簡單。
Bash 困惑
以錯(cuò)誤方式學(xué)習(xí) bash 令人十分困惑。許多新手輸入 "man bash" 來查看 bash 幫助頁,但只得到非常簡單和技術(shù)方面的 shell 功能性描述。還有人輸入 "info bash"(來查看 GNU 信息文檔),只能得到重新顯示的幫助頁,或者(如果幸運(yùn))略為友好的信息文檔。
盡管這可能使初學(xué)者有些失望,但標(biāo)準(zhǔn) bash 文檔無法滿足所有人的要求,它只適合那些已大體熟悉 shell 編程的人。幫助頁中確實(shí)有很多極好的技術(shù)信息,但對初學(xué)者的幫助卻有限。
這就是本系列的目的所在。在本系列中,我將講述如何實(shí)際使用 bash 編程概念,以便編寫自己的腳本。與技術(shù)描述不同,我將以簡單的語言為您解釋,使您不僅知道事情做什么,還知道應(yīng)在何時(shí)使用。在此三部分系列末尾,您將可以自己編寫復(fù)雜的 bash 腳本,并可以自如地使用 bash 以及通過閱讀(和理解)標(biāo)準(zhǔn) bash 文檔來補(bǔ)充知識。讓我們開始吧。
環(huán)境變量
在 bash 和幾乎所有其它 shell 中,用戶可以定義環(huán)境變量,這些環(huán)境變量在以 ASCII 字符串存儲。環(huán)境變量的最便利之處在于:它們是 UNIX 進(jìn)程模型的標(biāo)準(zhǔn)部分。這意味著:環(huán)境變量不僅由 shell 腳本獨(dú)用,而且還可以由編譯過的標(biāo)準(zhǔn)程序使用。當(dāng)在 bash 中“導(dǎo)出”環(huán)境變量時(shí),以后運(yùn)行的任何程序,不管是不是 shell 腳本,都可以讀取設(shè)置。一個(gè)很好的例子是 vipw 命令,它通常允許 root 用戶編輯系統(tǒng)口令文件。通過將 EDITOR 環(huán)境變量設(shè)置成喜愛的文本編輯器名稱,可以配置 vipw,使其使用該編輯器,而不使用 vi,如果習(xí)慣于 xemacs 而確實(shí)不喜歡 vi,那么這是很便利的。
在 bash 中定義環(huán)境變量的標(biāo)準(zhǔn)方法是:
$ myvar='This is my environment variable!'
|
以上命令定義了一個(gè)名為 "myvar" 的環(huán)境變量,并包含字符串 "This is my environment variable!"。以上有幾點(diǎn)注意事項(xiàng):第一,在等號 "=" 的兩邊沒有空格,任何空格將導(dǎo)致錯(cuò)誤(試一下看看)。第二個(gè)件要注意的事是:雖然在定義一個(gè)字時(shí)可以省略引號,但是當(dāng)定義的環(huán)境變量值多于一個(gè)字時(shí)(包含空格或制表鍵),引號是必須的。
 |
引用細(xì)節(jié)
有關(guān)如何在 bash 中使用引號的非常詳盡的信息,請參閱 bash 幫助頁面中的“引用”一節(jié)。特殊字符序列由其它值“擴(kuò)展”(替換)確實(shí)使 bash 中字符串的處理變得復(fù)雜。本系列將只講述最常用的引用功能。
|
|
第三,雖然通常可以用雙引號來替代單引號,但在上例中,這樣做會導(dǎo)致錯(cuò)誤。為什么呢?因?yàn)槭褂脝我柦昧朔Q為擴(kuò)展的 bash 特性,其中,特殊字符和字符系列由值替換。例如,"!" 字符是歷史擴(kuò)展字符,bash 通常將其替換為前面輸入的命令。(本系列文章中將不講述歷史擴(kuò)展,因?yàn)樗?bash 編程中不常用。有關(guān)歷史擴(kuò)展的詳細(xì)信息,請參閱 bash 幫助頁中的“歷史擴(kuò)展”一節(jié)。)盡管這個(gè)類似于宏的功能很便利,但我們現(xiàn)在只想在環(huán)境變量后面加上一個(gè)簡單的感嘆號,而不是宏。
現(xiàn)在,讓我們看一下如何實(shí)際使用環(huán)境變量。這有一個(gè)例子:
$ echo $myvar
This is my environment variable!
|
通過在環(huán)境變量的前面加上一個(gè) $,可以使 bash 用 myvar 的值替換它。這在 bash 術(shù)語中叫做“變量擴(kuò)展”。但是,這樣做將怎樣:
我們希望回顯 "fooThis is my environment variable!bar",但卻不是這樣。錯(cuò)在哪里?簡單地說,bash 變量擴(kuò)展設(shè)施陷入了困惑。它無法識別要擴(kuò)展哪一個(gè)變量:$m、$my、$myvar 、$myvarbar 等等。如何更明確清楚地告述 bash 引用哪一個(gè)變量?試一下這個(gè):
$ echo foo${myvar}bar
fooThis is my environment variable!bar
|
如您所見,當(dāng)環(huán)境變量沒有與周圍文本明顯分開時(shí),可以用花括號將它括起。雖然 $myvar 可以更快輸入,并且在大多數(shù)情況下正確工作,但 ${myvar} 卻能在幾乎所有情況下正確通過語法分析。除此之外,二者相同,將在本系列的余下部分看到變量擴(kuò)展的兩種形式。請記住:當(dāng)環(huán)境變量沒有用空白(空格或制表鍵)與周圍文本分開時(shí),請使用更明確的花括號形式。
回想一下,我們還提到過可以“導(dǎo)出”變量。當(dāng)導(dǎo)出環(huán)境變量時(shí),它可以自動地由以后運(yùn)行的任何腳本或可執(zhí)行程序環(huán)境使用。shell 腳本可以使用 shell 的內(nèi)置環(huán)境變量支持“到達(dá)”環(huán)境變量,而 C 程序可以使用 getenv() 函數(shù)調(diào)用。這里有一些 C 代碼示例,輸入并編譯它們 -- 它將幫助我們從 C 的角度理解環(huán)境變量:
myvar.c -- 樣本環(huán)境變量 C 程序
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char *myenvvar=getenv("EDITOR");
printf("The editor environment variable is set to %s\n",myenvvar);
}
|
將上面的代碼保存到文件 myenv.c 中,然后發(fā)出以下命令進(jìn)行編譯:
現(xiàn)在,目錄中將有一個(gè)可執(zhí)行程序,它在運(yùn)行時(shí)將打印 EDITOR 環(huán)境變量的值(如果有值的話)。這是在我機(jī)器上運(yùn)行時(shí)的情況:
$ ./myenv
The editor environment variable is set to (null)
|
啊... 因?yàn)闆]有將 EDITOR 環(huán)境變量設(shè)置成任何值,所以 C 程序得到一個(gè)空字符串。讓我們試著將它設(shè)置成特定值:
$ EDITOR=xemacs
$ ./myenv
The editor environment variable is set to (null)
|
雖然希望 myenv 打印值 "xemacs",但是因?yàn)檫€沒有導(dǎo)出環(huán)境變量,所以它卻沒有很好地工作。這次讓它正確工作:
$ export EDITOR
$ ./myenv
The editor environment variable is set to xemacs
|
現(xiàn)在,如您親眼所見:不導(dǎo)出環(huán)境變量,另一個(gè)進(jìn)程(在本例中是示例 C 程序)就看不到環(huán)境變量。順便提一句,如果愿意,可以在一行定義并導(dǎo)出環(huán)境變量,如下所示:
這與兩行版本的效果相同。現(xiàn)在該演示如何使用 unset 來除去環(huán)境變量:
$ unset EDITOR
$ ./myenv
The editor environment variable is set to (null)
|
截?cái)嘧址攀?/span>
截?cái)嘧址菍⒊跏甲址財(cái)喑奢^小的獨(dú)立塊,它是一般 shell 腳本每天執(zhí)行的任務(wù)之一。很多時(shí)候,shell 腳本需要采用全限定路徑,并找到結(jié)束的文件或目錄。雖然可以用 bash 編碼實(shí)現(xiàn)(而且有趣),但標(biāo)準(zhǔn) basename UNIX 可執(zhí)行程序可以極好地完成此工作:
$ basename /usr/local/share/doc/foo/foo.txt
foo.txt
$ basename /usr/home/drobbins
drobbins
|
Basename 是一個(gè)截?cái)嘧址臉O簡便工具。它的相關(guān)命令 dirname 返回 basename 丟棄的“另”一部分路徑。
$ dirname /usr/local/share/doc/foo/foo.txt
/usr/local/share/doc/foo
$ dirname /usr/home/drobbins/
/usr/home
|
命令替換
需要知道一個(gè)簡便操作:如何創(chuàng)建一個(gè)包含可執(zhí)行命令結(jié)果的環(huán)境變量。這很容易:
$ MYDIR=`dirname /usr/local/share/doc/foo/foo.txt`
$ echo $MYDIR
/usr/local/share/doc/foo
|
上面所做的稱為“命令替換”。此例中有幾點(diǎn)需要指出。在第一行,簡單地將要執(zhí)行的命令以 反引號 括起。那不是標(biāo)準(zhǔn)的單引號,而是鍵盤中通常位于 Tab 鍵之上的單引號。可以用 bash 備用命令替換語法來做同樣的事:
$ MYDIR=$(dirname /usr/local/share/doc/foo/foo.txt)
$ echo $MYDIR
/usr/local/share/doc/foo
|
如您所見,bash 提供多種方法來執(zhí)行完全一樣的操作。使用命令替換可以將任何命令或命令管道放在 ` ` 或 $( ) 之間,并將其分配給環(huán)境變量。真方便!下面是一個(gè)例子,演示如何在命令替換中使用管道:
MYFILES=$(ls /etc | grep pa)
bash-2.03$ echo $MYFILES
pam.d passwd
|
象專業(yè)人員那樣截?cái)嘧址?/span>
盡管 basename 和 dirname 是很好的工具,但有時(shí)可能需要執(zhí)行更高級的字符串“截?cái)?#8221;,而不只是標(biāo)準(zhǔn)的路徑名操作。當(dāng)需要更強(qiáng)的說服力時(shí),可以利用 bash 內(nèi)置的變量擴(kuò)展功能。已經(jīng)使用了類似于 ${MYVAR} 的標(biāo)準(zhǔn)類型的變量擴(kuò)展。但是 bash 自身也可以執(zhí)行一些便利的字符串截?cái)唷?匆幌逻@些例子:
$ MYVAR=foodforthought.jpg
$ echo ${MYVAR##*fo}
rthought.jpg
$ echo ${MYVAR#*fo}
odforthought.jpg
|
在第一個(gè)例子中,輸入了 ${MYVAR##*fo}。它的確切含義是什么?基本上,在 ${ } 中輸入環(huán)境變量名稱,兩個(gè) ##,然后是通配符 ("*fo")。然后,bash 取得 MYVAR,找到從字符串 "foodforthought.jpg" 開始處開始、且匹配通配符 "*fo" 的 最長 子字符串,然后將其從字符串的開始處截去。剛開始理解時(shí)會有些困難,為了感受一下這個(gè)特殊的 "##" 選項(xiàng)如何工作,讓我們一步步地看看 bash 如何完成這個(gè)擴(kuò)展。首先,它從 "foodforthought.jpg" 的開始處搜索與 "*fo" 通配符匹配的子字符串。以下是檢查到的子字符串:
f
fo MATCHES *fo
foo
food
foodf
foodfo MATCHES *fo
foodfor
foodfort
foodforth
foodfortho
foodforthou
foodforthoug
foodforthought
foodforthought.j
foodforthought.jp
foodforthought.jpg
|
在搜索了匹配的字符串之后,可以看到 bash 找到兩個(gè)匹配。它選擇最長的匹配,從初始字符串的開始處除去,然后返回結(jié)果。
上面所示的第二個(gè)變量擴(kuò)展形式看起來與第一個(gè)相同,但是它只使用一個(gè) "#" -- 并且 bash 執(zhí)行 幾乎 同樣的過程。它查看與第一個(gè)例子相同的子字符串系列,但是 bash 從初始字符串除去 最短 的匹配,然后返回結(jié)果。所以,一查到 "fo" 子字符串,它就從字符串中除去 "fo",然后返回 "odforthought.jpg"。
這樣說可能會令人十分困惑,下面以一簡單方式記住這個(gè)功能。當(dāng)搜索最長匹配時(shí),使用 ##(因?yàn)?## 比 # 長)。當(dāng)搜索最短匹配時(shí),使用 #。看,不難記吧!等一下,怎樣記住應(yīng)該使用 '#' 字符來從字符串開始部分除去?很簡單!注意到了嗎:在美國鍵盤上,shift-4 是 "$",它是 bash 變量擴(kuò)展字符。在鍵盤上,緊靠 "$" 左邊的是 "#"。這樣,可以看到:"#" 位于 "$" 的“開始處”,因此(根據(jù)我們的記憶法),"#" 從字符串的開始處除去字符。您可能要問:如何從字符串末尾除去字符。如果猜到我們使用美國鍵盤上緊靠 "$" 右邊 的字符 ("%),那就猜對了。這里有一些簡單的例子,解釋如何截去字符串的末尾部分:
$ MYFOO="chickensoup.tar.gz"
$ echo ${MYFOO%%.*}
chickensoup
$ echo ${MYFOO%.*}
chickensoup.tar
|
正如您所見,除了將匹配通配符從字符串末尾除去之外,% 和 %% 變量擴(kuò)展選項(xiàng)與 # 和 ## 的工作方式相同。請注意:如果要從末尾除去特定子字符串,不必使用 "*" 字符:
MYFOOD="chickensoup"
$ echo ${MYFOOD%%soup}
chicken
|
在此例中,使用 "%%" 或 "%" 并不重要,因?yàn)橹荒苡幸粋€(gè)匹配。還要記住:如果忘記了應(yīng)該使用 "#" 還是 "%",則看一下鍵盤上的 3、4 和 5 鍵,然后猜出來。
可以根據(jù)特定字符偏移和長度,使用另一種形式的變量擴(kuò)展,來選擇特定子字符串。試著在 bash 中輸入以下行:
$ EXCLAIM=cowabunga
$ echo ${EXCLAIM:0:3}
cow
$ echo ${EXCLAIM:3:7}
abunga
|
這種形式的字符串截?cái)喾浅:啽悖恍栌妹疤柗珠_來指定起始字符和子字符串長度。
應(yīng)用字符串截?cái)?/span>
現(xiàn)在我們已經(jīng)學(xué)習(xí)了所有截?cái)嘧址闹R,下面寫一個(gè)簡單短小的 shell 腳本。我們的腳本將接受一個(gè)文件作為自變量,然后打印:該文件是否是一個(gè) tar 文件。要確定它是否是 tar 文件,將在文件末尾查找模式 ".tar"。如下所示:
mytar.sh -- 一個(gè)簡單的腳本
#!/bin/bash
if [ "${1##*.}" = "tar" ]
then
echo This appears to be a tarball.
else
echo At first glance, this does not appear to be a tarball.
fi
|
要運(yùn)行此腳本,將它輸入到文件 mytar.sh 中,然后輸入 "chmod 755 mytar.sh",生成可執(zhí)行文件。然后,如下做一下 tar 文件試驗(yàn):
$ ./mytar.sh thisfile.tar
This appears to be a tarball.
$ ./mytar.sh thatfile.gz
At first glance, this does not appear to be a tarball.
|
好,成功運(yùn)行,但是不太實(shí)用。在使它更實(shí)用之前,先看一下上面使用的 "if" 語句。語句中使用了一個(gè)布爾表達(dá)式。在 bash 中,"=" 比較運(yùn)算符檢查字符串是否相等。在 bash 中,所有布爾表達(dá)式都用方括號括起。但是布爾表達(dá)式實(shí)際上測試什么?讓我們看一下左邊。根據(jù)前面所學(xué)的字符串截?cái)嘀R,"${1##*.}" 將從環(huán)境變量 "1" 包含的字符串開始部分除去最長的 "*." 匹配,并返回結(jié)果。這將返回文件中最后一個(gè) "." 之后的所有部分。顯然,如果文件以 ".tar" 結(jié)束,結(jié)果將是 "tar",條件也為真。
您可能會想:開始處的 "1" 環(huán)境變量是什么。很簡單 -- $1 是傳給腳本的第一個(gè)命令行自變量,$2 是第二個(gè),以此類推。好,已經(jīng)回顧了功能,下面來初探 "if" 語句。
If 語句
與大多數(shù)語言一樣,bash 有自己的條件形式。在使用時(shí),要遵循以上格式;即,將 "if" 和 "then" 放在不同行,并使 "else" 和結(jié)束處必需的 "fi" 與它們水平對齊。這將使代碼易于閱讀和調(diào)試。除了 "if,else" 形式之外,還有其它形式的 "if" 語句:
if [ condition ]
then
action
fi
|
只有當(dāng) condition 為真時(shí),該語句才執(zhí)行操作,否則不執(zhí)行操作,并繼續(xù)執(zhí)行 "fi" 之后的任何行。
if [ condition ]
then
action
elif [ condition2 ]
then
action2
.
.
.
elif [ condition3 ]
then
else
actionx
fi
|
以上 "elif" 形式將連續(xù)測試每個(gè)條件,并執(zhí)行符合第一個(gè) 真 條件的操作。如果沒有條件為真,則將執(zhí)行 "else" 操作,如果有一個(gè)條件為真,則繼續(xù)執(zhí)行整個(gè) "if,elif,else" 語句之后的行
|