級(jí)別: 初級(jí)
Daniel Robbins (drobbins@gentoo.org), 總裁兼 CEO, Gentoo Technologies, Inc.
2000 年 4 月 01 日
在前一篇 bash 的介紹性文章中,Daniel Robbins 為您講解了腳本語言的一些基本元素和使用 bash 的原因。在本文(即第二部分)中,Daniel 繼續(xù)前一篇的內(nèi)容,并講解條件 (if-then) 語句、循環(huán)和更多的 bash 基本結(jié)構(gòu)。
我們先看一下處理命令行自變量的簡單技巧,然后再看看 bash 基本編程結(jié)構(gòu)。
接收自變量
在 介紹性文章 中的樣本程序中,我們使用環(huán)境變量 "$1" 來引用第一個(gè)命令行自變量。類似地,可以使用 "$2"、"$3" 等來引用傳遞給腳本的第二和第三個(gè)自變量。這里有一個(gè)例子:
#!/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 $#
|
除以下兩個(gè)細(xì)節(jié)之外,此例無需說明。第一,"$0" 將擴(kuò)展成從命令行調(diào)用的腳本名稱,"$#" 將擴(kuò)展成傳遞給腳本的自變量數(shù)目。試驗(yàn)以上腳本,通過傳遞不同類型的命令行自變量來了解其工作原理。
有時(shí)需要一次引用 所有 命令行自變量。針對這種用途,bash 實(shí)現(xiàn)了變量 "$@",它擴(kuò)展成所有用空格分開的命令行參數(shù)。在本文稍后的 "for" 循環(huán)部分中,您將看到使用該變量的例子。
Bash 編程結(jié)構(gòu)
如果您曾用過如 C、Pascal、Python 或 Perl 那樣的過程語言編程,則一定熟悉 "if" 語句和 "for" 循環(huán)那樣的標(biāo)準(zhǔn)編程結(jié)構(gòu)。對于這些標(biāo)準(zhǔn)結(jié)構(gòu)的大多數(shù),Bash 有自己的版本。在下幾節(jié)中,將介紹幾種 bash 結(jié)構(gòu),并演示這些結(jié)構(gòu)和您已經(jīng)熟悉的其它編程語言中結(jié)構(gòu)的差異。如果以前編程不多,也不必?fù)?dān)心。我提供了足夠的信息和示例,使您可以跟上本文的進(jìn)度。
方便的條件語句
如果您曾用 C 編寫過與文件相關(guān)的代碼,則應(yīng)該知道:要比較特定文件是否比另一個(gè)文件新需要大量工作。那是因?yàn)?C 沒有任何內(nèi)置語法來進(jìn)行這種比較,必須使用兩個(gè) stat() 調(diào)用和兩個(gè) stat 結(jié)構(gòu)來進(jìn)行手工比較。相反,bash 內(nèi)置了標(biāo)準(zhǔn)文件比較運(yùn)算符,因此,確定“/tmp/myfile 是否可讀”與查看“$myvar 是否大于 4”一樣容易。
下表列出最常用的 bash 比較運(yùn)算符。同時(shí)還有如何正確使用每一選項(xiàng)的示例。示例要跟在 "if" 之后。例如:
if [ -z "$myvar" ]
then
echo "myvar is not defined"
fi
|
運(yùn)算符 |
描述 |
示例 |
文件比較運(yùn)算符 |
-e filename |
如果 filename存在,則為真 |
[ -e /var/log/syslog ] |
-d filename |
如果 filename為目錄,則為真 |
[ -d /tmp/mydir ] |
-f filename |
如果 filename為常規(guī)文件,則為真 |
[ -f /usr/bin/grep ] |
-L filename |
如果 filename為符號(hào)鏈接,則為真 |
[ -L /usr/bin/grep ] |
-r filename |
如果 filename可讀,則為真 |
[ -r /var/log/syslog ] |
-w filename |
如果 filename可寫,則為真 |
[ -w /var/mytmp.txt ] |
-x filename |
如果 filename可執(zhí)行,則為真 |
[ -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 ] |
字符串比較運(yùn)算符 (請注意引號(hào)的使用,這是防止空格擾亂代碼的好方法) |
-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" ] |
算術(shù)比較運(yùn)算符 |
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 ] |
有時(shí),有幾種不同方法來進(jìn)行特定比較。例如,以下兩個(gè)代碼段的功能相同:
if [ "$myvar" -eq 3 ]
then
echo "myvar equals 3"
fi
if [ "$myvar" = "3" ]
then
echo "myvar equals 3"
fi
|
上面兩個(gè)比較執(zhí)行相同的功能,但是第一個(gè)使用算術(shù)比較運(yùn)算符,而第二個(gè)使用字符串比較運(yùn)算符。
字符串比較說明
大多數(shù)時(shí)候,雖然可以不使用括起字符串和字符串變量的雙引號(hào),但這并不是好主意。為什么呢?因?yàn)槿绻h(huán)境變量中恰巧有一個(gè)空格或制表鍵,bash 將無法分辨,從而無法正常工作。這里有一個(gè)錯(cuò)誤的比較示例:
if [ $myvar = "foo bar oni" ]
then
echo "yes"
fi
|
在上例中,如果 myvar 等于 "foo",則代碼將按預(yù)想工作,不進(jìn)行打印。但是,如果 myvar 等于 "foo bar oni",則代碼將因以下錯(cuò)誤失敗:
在這種情況下,"$myvar"(等于 "foo bar oni")中的空格迷惑了 bash。bash 擴(kuò)展 "$myvar" 之后,代碼如下:
[ foo bar oni = "foo bar oni" ]
|
因?yàn)榄h(huán)境變量沒放在雙引號(hào)中,所以 bash 認(rèn)為方括號(hào)中的自變量過多。可以用雙引號(hào)將字符串自變量括起來消除該問題。請記住,如果養(yǎng)成將所有字符串自變量用雙引號(hào)括起的習(xí)慣,將除去很多類似的編程錯(cuò)誤。"foo bar oni" 比較 應(yīng)該寫成:
if [ "$myvar" = "foo bar oni" ]
then
echo "yes"
fi
|
 |
更多引用細(xì)節(jié)
如果要擴(kuò)展環(huán)境變量,則必須將它們用 雙引號(hào)、而不是單引號(hào)括起。單引號(hào) 禁用 變量(和歷史)擴(kuò)展。
|
|
以上代碼將按預(yù)想工作,而不會(huì)有任何令人不快的意外出現(xiàn)。
循環(huán)結(jié)構(gòu):"for"
好了,已經(jīng)講了條件語句,下面該探索 bash 循環(huán)結(jié)構(gòu)了。我們將從標(biāo)準(zhǔn)的 "for" 循環(huán)開始。這里有一個(gè)簡單的例子:
#!/usr/bin/env bash
for x in one two three four
do
echo number $x
done
輸出:
number one
number two
number three
number four
|
發(fā)生了什么?"for" 循環(huán)中的 "for x" 部分定義了一個(gè)名為 "$x" 的新環(huán)境變量(也稱為循環(huán)控制變量),它的值被依次設(shè)置為 "one"、"two"、"three" 和 "four"。每一次賦值之后,執(zhí)行一次循環(huán)體("do" 和 "done" 之間的代碼)。在循環(huán)體內(nèi),象其它環(huán)境變量一樣,使用標(biāo)準(zhǔn)的變量擴(kuò)展語法來引用循環(huán)控制變量 "$x"。還要注意,"for" 循環(huán)總是接收 "in" 語句之后的某種類型的字列表。在本例中,指定了四個(gè)英語單詞,但是字列表也可以引用磁盤上的文件,甚至文件通配符。看看下面的例子,該例演示如何使用標(biāo)準(zhǔn) 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 中每個(gè)以 "r" 開頭的文件。要做到這點(diǎn),bash 在執(zhí)行循環(huán)之前首先取得通配符 /etc/r*,然后擴(kuò)展它,用字符串 /etc/rc.d /etc/resolv.conf /etc/resolv.conf~ /etc/rpc 替換。一旦進(jìn)入循環(huán),根據(jù) myfile 是否為目錄,"-d" 條件運(yùn)算符用來執(zhí)行兩個(gè)不同操作。如果是目錄,則將 "(dir)" 附加到輸出行。
還可以在字列表中使用多個(gè)通配符、甚至是環(huán)境變量:
for x in /etc/r--? /var/lo* /home/drobbins/mystuff/* /tmp/${MYPATH}/*
do
cp $x /mnt/mydir
done
|
Bash 將在所有正確位置上執(zhí)行通配符和環(huán)境變量擴(kuò)展,并可能創(chuàng)建一個(gè)非常長的字列表。
雖然所有通配符擴(kuò)展示例使用了 絕對路徑,但也可以使用相對路徑,如下所示:
for x in ../* mystuff/*
do
echo $x is a silly file
done
|
在上例中,bash 相對于當(dāng)前工作目錄執(zhí)行通配符擴(kuò)展,就象在命令行中使用相對路徑一樣。研究一下通配符擴(kuò)展。您將注意到,如果在通配符中使用絕對路徑,bash 將通配符擴(kuò)展成一個(gè)絕對路徑列表。否則,bash 將在后面的字列表中使用相對路徑。如果只引用當(dāng)前工作目錄中的文件(例如,如果輸入 "for x in *"),則產(chǎn)生的文件列表將沒有路徑信息的前綴。請記住,可以使用 "basename" 可執(zhí)行程序來除去前面的路徑信息,如下所示:
for x in /var/log/*
do
echo `basename $x` is a file living in /var/log
done
|
當(dāng)然,在腳本的命令行自變量上執(zhí)行循環(huán)通常很方便。這里有一個(gè)如何使用本文開始提到的 "$@" 變量的例子:
#!/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 算術(shù)
在學(xué)習(xí)另一類型的循環(huán)結(jié)構(gòu)之前,最好先熟悉如何執(zhí)行 shell 算術(shù)。是的,確實(shí)如此:可以使用 shell 結(jié)構(gòu)來執(zhí)行簡單的整數(shù)運(yùn)算。只需將特定的算術(shù)表達(dá)式用 "$((" 和 "))" 括起,bash 就可以計(jì)算表達(dá)式。這里有一些例子:
$ echo $(( 100 / 3 ))
33
$ myvar="56"
$ echo $(( $myvar + 12 ))
68
$ echo $(( $myvar - $myvar ))
0 $ myvar=$(( $myvar + 1 ))
$ echo $myvar
57
|
更多的循環(huán)結(jié)構(gòu):"while" 和 "until"
只要特定條件為真,"while" 語句就會(huì)執(zhí)行,其格式如下:
while [ condition ]
do
statements
done
|
通常使用 "While" 語句來循環(huán)一定次數(shù),比如,下例將循環(huán) 10 次:
myvar=0
while [ $myvar -ne 10 ]
do
echo $myvar
myvar=$(( $myvar + 1 ))
done
|
可以看到,上例使用了算術(shù)表達(dá)式來使條件最終為假,并導(dǎo)致循環(huán)終止。
"Until" 語句提供了與 "while" 語句相反的功能:只要特定條件為 假 ,它們就重復(fù)。下面是一個(gè)與前面的 "while" 循環(huán)具有同等功能的 "until" 循環(huán):
myvar=0
until [ $myvar -eq 10 ]
do
echo $myvar
myvar=$(( $myvar + 1 ))
done
|
Case 語句
Case 語句是另一種便利的條件結(jié)構(gòu)。這里有一個(gè)示例片段:
case "${x##*.}" in
gz)
gzunpack ${SROOT}/${x}
;;
bz2)
bz2unpack ${SROOT}/${x}
;;
*)
echo "Archive format not recognized."
exit
;;
esac
|
在上例中,bash 首先擴(kuò)展 "${x##*.}"。在代碼中,"$x" 是文件的名稱,"${x##.*}" 除去文件中最后句點(diǎn)后文本之外的所有文本。然后,bash 將產(chǎn)生的字符串與 ")" 左邊列出的值做比較。在本例中,"${x##.*}" 先與 "gz" 比較,然后是 "bz2",最后是 "*"。如果 "${x##.*}" 與這些字符串或模式中的任何一個(gè)匹配,則執(zhí)行緊接 ")" 之后的行,直到 ";;" 為止,然后 bash 繼續(xù)執(zhí)行結(jié)束符 "esac" 之后的行。如果不匹配任何模式或字符串,則不執(zhí)行任何代碼行,在這個(gè)特殊的代碼片段中,至少要執(zhí)行一個(gè)代碼塊,因?yàn)槿魏尾慌c "gz" 或 "bz2" 匹配的字符串都將與 "*" 模式匹配。
函數(shù)與名稱空間
在 bash 中,甚至可以定義與其它過程語言(如 Pascal 和 C)類似的函數(shù)。在 bash 中,函數(shù)甚至可以使用與腳本接收命令行自變量類似的方式來接收自變量。讓我們看一下樣本函數(shù)定義,然后再從那里繼續(xù):
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" 語句來編寫上面的代碼。您知道如何編寫嗎?
|
|
我們在上面定義了一個(gè)名為 "tarview" 的函數(shù),它接收一個(gè)自變量,即某種類型的 tar 文件。在執(zhí)行該函數(shù)時(shí),它確定自變量是哪種 tar 文件類型(未壓縮的、gzip 壓縮的或 bzip2 壓縮的),打印一行信息性消息,然后顯示 tar 文件的內(nèi)容。應(yīng)該如下調(diào)用上面的函數(shù)(在輸入、粘貼或找到該函數(shù)后,從腳本或命令行調(diào)用它):
$ 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
....
|
 |
交互地使用它們
別忘了,可以將函數(shù)(如上面的函數(shù))放在 ~/.bashrc 或 ~/.bash_profile 中,以便在 bash 中隨時(shí)使用它們。
|
|
如您所見,可以使用與引用命令行自變量同樣的機(jī)制來在函數(shù)定義內(nèi)部引用自變量。另外,將把 "$#" 宏擴(kuò)展成包含自變量的數(shù)目。唯一可能不完全相同的是變量 "$0",它將擴(kuò)展成字符串 "bash"(如果從 shell 交互運(yùn)行函數(shù))或調(diào)用函數(shù)的腳本名稱。
名稱空間
經(jīng)常需要在函數(shù)中創(chuàng)建環(huán)境變量。雖然有可能,但是還有一個(gè)技術(shù)細(xì)節(jié)應(yīng)該了解。在大多數(shù)編譯語言(如 C)中,當(dāng)在函數(shù)內(nèi)部創(chuàng)建變量時(shí),變量被放置在單獨(dú)的局部名稱空間中。因此,如果在 C 中定義一個(gè)名為 myfunction 的函數(shù),并在該函數(shù)中定義一個(gè)名為 "x" 的自變量,則任何名為 "x" 的全局變量(函數(shù)之外的變量)將不受它的印象,從而消除了負(fù)作用。
在 C 中是這樣,但在 bash 中卻不是。在 bash 中,每當(dāng)在函數(shù)內(nèi)部創(chuàng)建環(huán)境變量,就將其添加到 全局名稱空間。這意味著,該變量將重寫函數(shù)之外的全局變量,并在函數(shù)退出之后繼續(xù)存在:
#!/usr/bin/env bash
myvar="hello"
myfunc() {
myvar="one two three"
for x in $myvar
do
echo $x
done
}
myfunc
echo $myvar $x
|
運(yùn)行此腳本時(shí),它將輸出 "one two three three",這顯示了在函數(shù)中定義的 "$myvar" 如何影響全局變量 "$myvar",以及循環(huán)控制變量 "$x" 如何在函數(shù)退出之后繼續(xù)存在(如果 "$x" 全局變量存在,也將受到影響)。
在這個(gè)簡單的例子中,很容易找到該錯(cuò)誤,并通過使用其它變量名來改正錯(cuò)誤。但這不是正確的方法,解決此問題的最好方法是通過使用 "local" 命令,在一開始就預(yù)防影響全局變量的可能性。當(dāng)使用 "local" 在函數(shù)內(nèi)部創(chuàng)建變量時(shí),將把它們放在 局部名稱空間中,并且不會(huì)影響任何全局變量。這里演示了如何實(shí)現(xiàn)上述代碼,以便不重寫全局變量:
#!/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
|
此函數(shù)將輸出 "hello" -- 不重寫全局變量 "$myvar","$x" 在 myfunc 之外不繼續(xù)存在。在函數(shù)的第一行,我們創(chuàng)建了以后要使用的局部變量 x,而在第二個(gè)例子 (local myvar="one two three"") 中,我們創(chuàng)建了局部變量 myvar, 同時(shí) 為其賦值。在將循環(huán)控制變量定義為局部變量時(shí),使用第一種形式很方便,因?yàn)椴辉试S說:"for local x in $myvar"。此函數(shù)不影響任何全局變量,鼓勵(lì)您用這種方式設(shè)計(jì)所有的函數(shù)。只有在明確希望要修改全局變量時(shí),才 不應(yīng)該使用 "local"。
|