理解 Bash 的 if 語句 (轉(zhuǎn))
寫 bash 腳本的日子也不短了,但是每次用到 if 語句時大腦還是會卡殼一下,要翻教程和看以前的代碼,因?yàn)闂l件部分語法神出鬼沒,捉摸不定,于是我還是花點(diǎn)時間狠狠研究了一下,寫了這篇文章做總結(jié)。
詭異的語法
一般 bash 教程給出的語法示例基本就是:
if condition; then echo yes else echo no fi
看起來很簡單,除了 condition 外(也就是條件部分),其余關(guān)鍵字都沒什么難懂了,頂多注意一下寫在同一行要加分號。
但條件部分代碼說得不明不白,只是列出一堆實(shí)際例子,例如判斷文件存在可以用 [ -f file ],判斷目錄用 [ -d dir ] 等等。這些例子也不算復(fù)雜,但是真寫起來,想構(gòu)造復(fù)雜點(diǎn)條件都能讓你調(diào)試到欲仙欲死。
所以這篇文章說理解 if 語句,就是指理解 if 語句的條件部分。
大概有如下問題:
為毛是用方括號,不是小括號?
為毛見到有的代碼用兩個方括號,有的還是兩個小括號?
為毛有時比較字符串相等可以用
-eq,但又可以用等號?為毛有時還能直接用命令,命令到底要不要加反單引號呢?
寫了句復(fù)合條件,結(jié)果一堆看不懂的語法錯誤?
測試好麻煩啊,每次都要 if/then/else 整句寫全,有沒有簡單點(diǎn)測試方法?
五花八門的語法一直讓我無比糾結(jié),一般編程語言的 if 語句說明都不過半頁,但是 bash 的 if 語句,我看了好幾本書和教程,都沒搞清楚,不能一口氣就能寫出來,而是要翻教程和看以前的代碼,反復(fù)調(diào)試。
最后是靠 Advanced Bash-Scripting Guide 這本書搞清楚的。
條件部分的意義
很多教程都這么說:condition 的代碼執(zhí)行后,如果結(jié)果為 true,就繼續(xù)執(zhí)行 then 部分,否則繼續(xù)執(zhí)行 else部分。跟其它語言的一樣的,沒區(qū)別,例如 [ -f file ] 判斷文件是否存在,文件存在就是 true 了,不存在就是 false 了。
說 true 和 false,也就是執(zhí)行結(jié)果是個布爾值,這么說就是造成條件部分難寫的原因,被這個說法誤導(dǎo)了,所以要換一個角度看。
應(yīng)該說 condition 的代碼正常執(zhí)行。
什么叫「正常執(zhí)行」呢?這里要搞懂一個叫「退出狀態(tài)碼」(exit status)概念,有時候也叫「返回狀態(tài)碼」(return code)。也就是子進(jìn)程退出時向調(diào)用它的父進(jìn)程返回的一個整數(shù)值,一般編程語言都有個 exit() 函數(shù)來直接退出,這個函數(shù)的參數(shù)就是返回給父進(jìn)程的狀態(tài)碼,不傳默認(rèn)就是 0。
對于 bash 來說,調(diào)用過的子進(jìn)程的返回狀態(tài)碼保存在 $? 環(huán)境變量中,每執(zhí)行過一個命令后都會被更新,可以用 echo 來查看。
$ ls; echo $? 0
按 unix 的規(guī)范,返回狀態(tài)碼為 0 就是表示正常執(zhí)行,其它值都表示不正常。
非要當(dāng)成布爾值看待的話,可以這樣想:為 0 就是正常,正常就是 true,不為 0 就是不正常,不正常就是 false。這跟其它語言 0 當(dāng) false 不同,所以特容易搞反。
但是返回什么值是程序自己決定的,一般常見 unix 程序都會仔細(xì)定義狀態(tài)碼。
對于 ls 來說,列得到文件表示正常:
$ ls exists.txt; echo $?; exists.txt 0 $ ls not_exists.txt; echo $?; ls: cannot access not_exists.txt: No such file or directory 2
對于 grep 來說,有匹配表示正常:
$ echo 'abc' | grep 'a'; echo $? abc 0 $ echo 'abc' | grep 'd'; echo $? 1
或者具體點(diǎn)說,condition 的代碼執(zhí)行后,這時候環(huán)境變量 $? 的值是否為 0。
先定義一下用語,ls、grep 這些這里稱為「程序」,而「命令」是指在提示符(即交互 shell)里打的整條字符串,「程序」名稱加上參數(shù)或管道就是一條「命令」了。
下面來逐步解釋一下。
test 程序
如果要檢查文件是否存在,只要找到一個程序,存在文件返回狀態(tài)碼 0,不存在就返回非 0 就行了,上面的 ls 就這樣了。
然后構(gòu)造一個 ls 命令來判斷,不需要反單引號包圍,套到 if 語句里:
$ if ls exists.txt; then echo yes; else echo no; fi exists.txt yes $ if ls not_exists.txt; then echo yes; else echo no; fi ls: cannot access not_exists.txt: No such file or directory no
可以把 if 關(guān)鍵詞的作用當(dāng)成:判斷后面跟著的命令的 $? 是否為 0。
這里 ls 也輸出我們不需要的信息,因?yàn)槲覀冎蛔屗?$? 就夠了,要屏蔽掉這些輸出:
$ if ls exists.txt &> /dev/null; then echo yes; else echo no; fi yes $ if ls not_exists.txt &> /dev/null; then echo yes; else echo no; fi no
加上了 &> /dev/null 略顯丑陋,那有沒有其它程序僅僅更新 $? 但沒有任何輸出呢?
這就是 test 程序了,用 -f 參數(shù)表示判斷是否存在文件,先檢查下:
$ test -f exists.txt; echo $? 0 $ test -f not_exists.txt; echo $? 1
確實(shí)無誤,套上 if 語句:
$ if test -f exists.txt; then echo yes; else echo no; fi yes $ if test -f not_exists.txt; then echo yes; else echo no; fi no
這里的「文件」指普通文件,如果我要判斷其它類型的文件,例如目錄、軟鏈接或管道呢?test 也提供判斷這些文件類型的參數(shù),可以通過 man test 查看手冊。
單方括號語法糖
如果你打開了 test 的 man 后,發(fā)現(xiàn)幾個眼熟的東西:
SYNOPSIS test EXPRESSION test [ EXPRESSION ] [ ] [ OPTION
bash 給 test 程序特殊優(yōu)待,可以用另一種語法來編寫,也就是把 test 的參數(shù)包圍在單個方括號里。
即 test args 也可以寫成 [ args ],注意方括號和里面的參數(shù)要留個空格,不然提示語法錯誤,我曾經(jīng)就被這樣折騰了半天。
也可以在直接在提示符里執(zhí)行,效果跟用普通方法沒差別:
$ [ -f exists.txt ]; echo $? 0 $ [ -f not_exists.txt ]; echo $? 1
套上 if 語句:
$ if [ -f exists.txt ]; then echo yes; else echo no; fi yes $ if [ -f not_exists.txt ]; then echo yes; else echo no; fi no
這就是為什么 if 條件部分用的是單個方括號,bash 會把這個寫法轉(zhuǎn)換回一般寫法,所以說是語法糖。
為什么要提供這個語法糖呢?估計 bash 覺得這樣寫更好看吧,也讓你打少兩個字符。但是這個語法糖,迷惑了我好多年,那么小括號有什么用?
單個小括號的作用
單個小括號在 bash 中不像其它語言那樣表示分隔符和優(yōu)先級調(diào)整,而是啟動一個 subshell 來執(zhí)行里面的代碼,也就是再啟動一個 bash 來運(yùn)行,好處是 subshell 有獨(dú)立的環(huán)境變量。
例如,你在 home 目錄,cd 到 /tmp 目錄,sleep 5 秒,最后 cd 回 home,但是你會在 sleep 的過程中按 <kbd>Ctrl + c</kbd> 中斷。
如果你使用這個命令:
~$ cd /tmp/; sleep 5; cd ~ ^C /tmp$
你會留在 /tmp 目錄中,因?yàn)樽詈蟮?cd ~ 根本沒執(zhí)行。所以如果你希望臨時切換別的目錄執(zhí)行某些命令,但又希望中斷后回到原來的目錄,這個方法就不湊效了。
但是如果你加上小括號:
~$ ( cd /tmp/; sleep 5; ) ^C ~$
這里沒有最后的 cd ~,因?yàn)槎啻艘慌e,subshell 有自己的工作目錄,相當(dāng)于你另外開一個終端而已,這樣避免一些環(huán)境變量被某些代碼弄亂。
取反操作
你會想當(dāng)然認(rèn)為就是加 ! 符號:
$ [ ! -f exists.txt ]; echo $? 1
確實(shí)對了,但是這只是 test 命令里的內(nèi)部取反,而不是 bash 的,換回一般寫法就是:
$ test ! -f exists.txt; echo $? 1
對于 bash 的取反,也就是不正常運(yùn)行 $? 應(yīng)該為 0,也是在命令開頭加 !:
$ ! test -f exists.txt; echo $? 1 $ ! test -f not_exists.txt; echo $? 0
注意 ! 后要有一空格,不然在提示符中會被當(dāng)成「調(diào)用歷史命令」解析了,但以腳本執(zhí)行時不會,反正都加上最好。
于是這樣就是蛋疼的雙重否定了:
$ ! test ! -f exists.txt; echo $? 0
數(shù)字和字符串比較
如果你想比較數(shù)字是否相等,想當(dāng)然寫成:
$ [ 3 == 1 ]; echo $? 1 $ [ 3 != 1 ]; echo $? 0
相等也可以用單個等號,用兩個比較符合習(xí)慣。但是等號左右一定要有空格,否則結(jié)果不如你想,因?yàn)闆]空格就是變量賦值!
如果你想比較兩個數(shù)字,于是這樣寫:
$ [ 3 > 1 ]; echo $? 0
看起來也如你想的一樣,但是如果:
$ [ 3 > 6 ]; echo $? 0
這是搞毛啊?趕緊 ls 一下看看當(dāng)前目錄是不是多了兩個名字為 1 和 6 的空文件。
那是因?yàn)?> 不是表示大于,而是標(biāo)準(zhǔn)輸出重定向,因?yàn)闃?biāo)準(zhǔn)輸出為空,所以只建立了空文件,相當(dāng)于 touch 命令了。
所以要對 > 符號轉(zhuǎn)義,這樣就 OK 了:
$ [ 3 \> 1 ]; echo $? 0 $ [ 3 \> 6 ]; echo $? 1
別高興得太早,這里還有坑:
$ [ 3 \> 10 ]; echo $? 0
因?yàn)檫@不是按數(shù)字比較,而是按字符串,這里 3 和 10 在 bash 眼中就是字符串,傳給 test 后,test 默認(rèn)也是當(dāng)成字符串。
如果顯式加上單引號,就清楚了:
$ [ '3' \> '10' ]; echo $? 0
字符串比較就是按 ASCII 編碼比較,因?yàn)橄缺容^第一個字符,3 比 1 的 ASCII 編碼大。
所以上面的幾個比較其實(shí)全部都是字符串比較,只不過長度一樣的話,看起來就是按數(shù)字比較。
如果想按數(shù)字大小怎么辦?可以用 -gt 參數(shù),這樣 test 就會把兩邊當(dāng)成一個數(shù)字看待:
$ [ 3 -gt 1 ]; echo $? 0 $ [ 3 -gt 6 ]; echo $? 1 $ [ 3 -gt 10 ]; echo $? 1
同樣,-eq 也是按數(shù)字比較:
$ [ 1 == 01 ]; echo $?; 1 $ [ 1 -eq 01 ]; echo $?; 0
復(fù)合條件
假如你要再判斷某個目錄是否存在,又想當(dāng)然寫成:
$ [ -f exists.txt && -d exists_folder ]; echo $? bash: [: missing `]' 2
結(jié)果提示漏了右括號,那是因?yàn)?&& 被 bash 預(yù)先解析了,而不是當(dāng)成 test 的參數(shù)傳遞。
&&表示如果左邊的命令正常執(zhí)行了,那么繼續(xù)執(zhí)行右邊的命令,相當(dāng)于沒有 else 部分的 if 語句簡化版。而
||表示如果左邊的命令不是正常執(zhí)行了,那么繼續(xù)執(zhí)行右邊的命令,相當(dāng)于沒有 then 部分的 if 語句(或者 if not)。
從效果看也可以分別當(dāng)成邏輯與和邏輯或的。
所以上面那條命令以 && 分開看,左邊的 [ -f exists.txt 明顯是個不完整命令,漏了個 ],當(dāng)然右邊的也漏了 [。
修正如下:
$ [ -f exists.txt ] && [ -d exists_folder ]; echo $? 0
換回一般寫法也應(yīng)該是:
$ test -f exists.txt && test -d exists_folder; echo $? 0
使用 || 則是:
$ [ -f not_exists.txt ] || [ -d exists_folder ]; echo $? 0
如果你想先把 && 和 || 轉(zhuǎn)義,但 test 不支持這個參數(shù),表示邏輯與和邏輯或的參數(shù)分別是 -a 和 -o,所以這樣就 OK 了:
$ [ -f exists.txt -a -d exists_folder ]; echo $? 0 $ test -f exists.txt -a -d exists_folder; echo $? 0
這樣好處就是只調(diào)用了一次 test 程序而不是兩次。
雙方括號關(guān)鍵詞
上面我們用 [ -f exists.txt && -d exists_folder ] 來表示復(fù)合條件,結(jié)果發(fā)現(xiàn)這是一個坑,于是 bash 后來從 ksh 抄來一個特性來填這個坑,結(jié)果挖了更大的一個坑。
把單括號換成雙括號就 OK 了:
$ [[ -f exists.txt && -d exists_folder ]]; echo $? 0
震驚之情溢于言表,&& 不是隔開兩個命令么,怎么用兩個方括號又合法了?
前面說說單方括號是語法糖,因?yàn)橹皇?test 命令的另一種寫法,bash 最后會調(diào)用程序 test,一般就是 /usr/bin/test。
用 type 程序看下類型:
$ type [ [ is a shell builtin $ type test test is a shell builtin
又說這是叫 builtin,坑爹,不過常用命令如 cd、echo 都是這樣的。
但是說雙方括號是「關(guān)鍵詞」,關(guān)鍵詞就是 bash 自己內(nèi)建的語法分析:
$ type [[ [[ is a shell keyword
就因?yàn)檫@是關(guān)鍵詞,所以被雙方括號包圍的代碼都有另外一種意義,&&、||、> 和 <</CODE> 這些符號的意義都被改變了,就和其它編程語言的用法一樣了。
例如上面的比較大小,對 > 不再需要轉(zhuǎn)義了:
$ [[ 3 > 1 ]]; echo $? 0
但依然是表示按字符串比較,不是按數(shù)字:
$ [[ 3 > 10 ]]; echo $? 0
可以看作增強(qiáng)版的 test,因?yàn)檫壿嬇c和邏輯或已經(jīng)可以直接用 && 和 ||,所以 -a,-o 就不能用了,其余的參數(shù)和 test 基本一樣,-f 和 -d 也可以用。
還可以用 =~ 來檢查是否匹配正則,簡單的就不用勞煩 grep 了:
$ [[ abc =~ a ]]; echo $? 0
因?yàn)閷?&& 那幾個符號自動轉(zhuǎn)義了,比較直觀,不容易搞錯,相對安全,所以推薦優(yōu)先使用 [[ 而不是 [。
雙小括號的作用
雙小括號的作用就是把里面的代碼作為算術(shù)表達(dá)式來執(zhí)行,像雙方括號一樣,里面的代碼有另外的意義。
例如給變量賦值:
$ a=1+1; echo $a 1+1 $ (( b = 1 + 1 )); echo $b 2
a 的 1+1 只是一個字符串,而 b 就是一個算術(shù)表達(dá)式結(jié)果。
正是因?yàn)槭撬阈g(shù)表達(dá)式,所以比較也是按數(shù)字本身而不是字符串:
$ (( 3 > 1 )); echo $? 0 $ (( 3 > 6 )); echo $? 1 $ (( 3 > 10 )); echo $? 1
所以也可以套上 if 語句來用:
$ if (( 3 > 1 )); then echo yes; else echo no; fi yes
真令人抓狂。
一些技巧
可以組合多個命令:
$ if echo abc; echo def; then echo yes; else echo no; fi abc def yes
也可以用管道:
$ if echo abc | grep -q a; then echo yes; else echo no; fi yes
太長或太復(fù)雜的話可以用函數(shù)封裝:
$ function echo_abc() { echo abc | grep -q a; } $ if echo_abc; then echo yes; else echo no; fi yes
如果需要保留命令的標(biāo)準(zhǔn)輸出到變量以便再使用,可以直接比較 $? 的值,單純賦值不改變 $? 的:
$ text=`echo abc | grep a`; $ if [[ $? == 0 ]]; then echo 'text:' $text; else echo no; fi text: abc $ text=`echo abc | grep d`; $ if [[ $? == 0 ]]; then echo 'text:' $text; else echo no; fi no
總結(jié)
親自動手測試了這么多個例子,總算搞把各種堆在一起的概念一一分解開來理解,至少寫起來都知道該看參考手冊的那一部分了。
感覺依然是:到處都是坑啊!
原文鏈接 <wbr> http://qixinglu.com/post/understand_bash_if_statement.html