前一段時間發(fā)現(xiàn)一個很好的wiki站點,上面有很多優(yōu)秀的bash文章。最近挑了一篇介紹Bash編程容易犯的各種錯誤的文章看,收獲很多,不感獨享,把這篇文章以半翻譯半筆記的形式分享給大家。
1. for i in $(ls *.mp3)
Bash寫循環(huán)代碼的時候,確實比較容易犯下面的錯誤:
for i in $(ls *.mp3); do # 錯誤! some command $i # 錯誤! done for i in $(ls) # 錯誤! for i in `ls` # 錯誤! for i in $(find . -type f) # 錯誤! for i in `find . -type f` # 錯誤! files=($(find . -type f)) # 錯誤! for i in ${files[@]} # 錯誤!
這里主要兩個問題:
使用命令展開時不帶引號,其執(zhí)行結(jié)果會使用ifS作為分隔符,拆分成參數(shù)傳遞給for循環(huán)處理;
不應(yīng)該讓腳本去解析ls命令的結(jié)果;
我們不能避免某些文件名中包含空格,Shell會對$(ls *.mp3)展開的結(jié)果會被做單詞拆分(WordSplitting)的處理。假設(shè)有一個文件,名字為01 – Don’t Eat the Yellow Snow.mp3,for循環(huán)處理的時候,會今次遍歷文件名中的每個單詞:01, -, Don’t, Eat等等:
$ for i in $(ls *.mp3); do echo $i; done 01 - Don't Eat the Yellow Snow.mp3
比這更差的情況是,上面命令展開的結(jié)果可能被Shell進一步處理,比如文件名展開。比如,ls執(zhí)行的結(jié)果中包含*號,按照通配符的規(guī)則, *號會被展開成當前目錄下的所有文件:
$ touch "1*.mp3" "1.mp3" "11.mp3" "12.mp3" $ for i in $(ls *.mp3); do echo $i; done 1*.mp3 1.mp3 11.mp3 12.mp3 1.mp3 11.mp3 12.mp3 1.mp3 11.mp3 12.mp3
不過,在這種場景下,你即使加上引號,也是無濟于事的:
$ for i in "$(ls *.mp3)"; do echo --$i--; done --1*.mp3 1.mp3 11.mp3 12.mp3--
加上引號后,ls執(zhí)行的結(jié)果會被當成一個整體,所以for循環(huán)只會執(zhí)行一次,達不到預期的效果。
事實上,這種情況下,根本不需要使用ls命令。ls命令的結(jié)果本身就設(shè)計成給人讀的,而不是給腳本解析的。正確的處理方法是,直接使用文件名展開(通配符)的功能:
$ for i in *.mp3; do > echo "$i" > done 1*.mp3 1.mp3 11.mp3 12.mp3
文件名展開是位于各種展開(花括號展開、變量替換、命令展開等)功能中的最后一個環(huán)節(jié),所以不會有之前不帶引號的命令展開的副作用。如果你需要遞歸地處理文件,可以考慮使用Find命令。
到這一步,之間的問題看樣子已經(jīng)修復了。但是,如果你進一步思考,假設(shè)當前目錄上沒有文件時會怎么樣?沒有文件的時候,*.mp3不會被展開直接傳遞給for循環(huán)處理,所以這個時候循環(huán)還是會執(zhí)行一次。這種情況不是我們預期的行為。
保險起見,可以在循環(huán)處理的時候,檢查下文件是否存在:
# POSIX for i in *.mp3; do [ -e "$i" ] || continue some command "$i" done
如果你有使用引號和避免單詞拆分的習慣,你完全可以避免很多錯誤。
注意下循環(huán)體內(nèi)部的”$i”,這里會導致下面我們要說的另外一個比較容易犯的錯誤。
2. cp $file $target
上面的命令有什么問題呢?如果你提前知道,$file和$target文件名中不會包含空格或者*號。否則,這行命令執(zhí)行前在經(jīng)過單詞拆分和文件名展開的時候會出現(xiàn)問題。所以,兩次強調(diào),在使用展開的地方切勿忘記使用引號:
$ cp -- "$file" "$target" 如果不帶引號,當你執(zhí)行如下命令時就會出錯: $ file="01 - Don't Eat the Yellow Snow.mp3" $ target="/tmp" $ cp $file $target cp: cannot stat ‘01’: No such file or directory ..
如果帶上引號,就不會有上面的問題,除非文件名以’-‘開頭,在這種情況下,cp會認為你提供的是一個命令行選項,這個錯誤下面會介紹。
3. 文件名中包含短橫’-‘
文件名以’-‘開頭會導致許多問題,*.mp3這種通配符會根據(jù)當前的locale展開成一個列表,但在絕大多數(shù)環(huán)境下,’-‘排序的時候會排在大多數(shù)字母前。這個展開的列表傳遞給有些命令的時候,會錯誤的將-filename解析成命令行選項。這里有兩種方法來解決這個問題。
第一種方法是在命令和參數(shù)之間加上–,這種語法告訴命令不要繼續(xù)對–之后的內(nèi)容進行命令行參數(shù)/選項解析:
$ cp -- "$file" "$target"
這種方法可以解這個問題,但是你需要在每個命令后面都要加上–,而且依賴具體的命令解析的方式,如果一些命令不兼容這種約定俗成的規(guī)范,這種做法是無效的。
另外一種方法是,確保文件名都使用相對或者絕對的路徑,以目錄開頭:
for i in ./*.mp3; do cp "$i" /target ... done
這種情況下,即使某個文件以-開頭,展開后文件名依然是./-foo.mp3這種形式,完全不會有問題。
4. [ $foo = “bar” ]
這是一個與第2個問題類似的問題,雖然用到了引號,但是放錯了位置,對于字符串字面值,除非有特殊符號,否則不大需要用引號括起來。但是,你應(yīng)該把變量的值用括號括起來,從而避免它們包含空格或能通配符,這一點我們在前面的問題中都解釋過。
這個例子在以下情況下會出錯:
如果[中的變量不存在,或者為空,這個時候上面的例子最終解析結(jié)果是:
[ = “bar” ] # 錯誤!
并且執(zhí)行會出錯:unary operator expected,因為=是二元操作符,它需要左右各一個操作數(shù)。
如果變量值包含空格,它首先在執(zhí)行之前進行單詞拆分,因此[命令看到的樣子可能是這樣的:
[ multiple words here = "bar" ]; 正確的做法應(yīng)該是: # POSIX [ "$foo" = bar ]
這種寫法,在POSIX兼容的實現(xiàn)中都不會有問題,即使$foo以短橫”-“開頭,因為POSIX實現(xiàn)的test命令通過傳遞的參數(shù)來確定執(zhí)行的行為。
只有一些非常古老的shell可能會遇到問題,這個時候你可以使用下面的寫法來解決(相信你肯定看到過這種寫法):
# POSIX / Bourne [ x"$foo" = xbar ] 在Bash中,還有另外一種選擇是使用[[關(guān)鍵字: # Bash / Ksh [[ $foo == bar ]]
這里你不需要使用引號,因為在[[里面參數(shù)不會進行展開,當然帶上引號也不會有錯。
不過有一點要注意的是,[[里的==不僅僅是文本比較,它會檢查左邊的值是否匹配右側(cè)的表達式,==右側(cè)的值加上引號,會讓它成為一個普通的字面量,*?等通配符會失去特殊含義。
5. cd $(dirname “$f”)
這又是一個引號的問題,命令展開的結(jié)果會進一步地進行單詞拆分或者文件名展開。因此下面的寫法才是正確的:
cd "$(dirname "$f")"
但是,上面引號的寫法可能比較怪異,你可能會認為第一、二個引號,第三、四個引號是一組的。
但是事實上,Bash將命令替換里面的引號當成一組,外面的當成另外一組。如果你是用反引號的寫法,引號的行為就不是這樣的了,所以$()寫法更加推薦。
6. [ “$foo” = bar && “$bar” = foo ]
不要在test命令內(nèi)部使用&&,Bash解析器會把你的命令分隔成兩個命令,在&&之前和之后。你應(yīng)該使用下面的寫法:
[ bar = "$foo" ] && [ foo = "$bar" ] # POSIX [[ $foo = bar && $bar = foo ]] # Bash / Ksh
盡量避免使用下面的寫法,雖然它是正確的,但是這種寫法可移植性不好,并且已經(jīng)在POSIX-2008中被廢棄:
[ bar = "$foo" -a foo = "$bar" ]
7. [[ $foo > 7 ]]
原文作者認為算術(shù)比較不應(yīng)該用[[,而是用((,我沒弄明白是為什么。
如果有理解的同學,歡迎以評論回復,謝謝。
8. grep foo bar | while read -r; do ((count++)); done
這種寫法初看沒有問題,但是你會發(fā)現(xiàn)當執(zhí)行完后,count變量并沒有變化。原因是管道后面的命令是在一個子Shell中執(zhí)行的。
POSIX規(guī)范并沒有說明管道的最后一個命令是不是在子Shell中執(zhí)行的。一些shell,例如ksh93或者Bash>=4.2可以通過shopt -s lastpipe命令,指明管道中的最后一個命令在當前shell中執(zhí)行。由于篇幅限制,在此就不展開,有興趣的可以看Bash FAQ #24。
9. if [grep foo myfile]
初學者會錯誤地認為,[是if語法的一部分,正如c語言中的if ()。但是事實并非如此,if后面跟著的是一個命令,[是一個命令,它是內(nèi)置命令test的簡寫形式,只不過它要求最后一個參數(shù)必須是]。下面兩種寫法是一樣的:
# POSIX if [ false ]; then echo "help"; fi if test false; then echo "HELP"; fi
兩個都是檢查參數(shù)”false”是不是非空的,所以上面兩個語句都會輸出HELP。
if語句的語法是:
if COMMANDS then <commands> elif </commands><commands> # optional then </commands><commands> else </commands><commands> # optional fi # required </commands>
再次強調(diào),[是一個命令,它同其它常規(guī)的命令一樣接受參數(shù)。if是一個復合命令,它包含其它命令,[并不是if語法中的一部分。
如果你想根據(jù)grep命令的結(jié)果來做事情,你不需要把grep放到[里面,只需要在if后面緊跟grep即可:
if grep -q fooregex myfile; then ... fi
如果grep在myfile中找到匹配的行,它的執(zhí)行結(jié)果為0(true),then后面的部分就會執(zhí)行。
10. if [bar=”$foo”]; then …
正如上一個問題中提到的,[是一個命令,它的參數(shù)之間必須用空格分隔。
11. if [ [ a = b ] && [ c = d ] ]; then …
不要用把[命令看成C語言中if語句的條件一樣,它是一個命令。
如果你想表達一個復合的條件表達式,可以這樣寫:
if [ a = b ] && [ c = d ]; then ...
注意,if后面有兩個命令,它們用&&分開。等價于下面的寫法:
if test a = b && test c = d; then ...
如果第一個test(或者[)命令返回false,then后面的語句不會執(zhí)行;如果第一個返回true,第二個test命令會執(zhí)行;只有第二個命令同樣返回true的情況下,then后面的語句才會執(zhí)行。
除此之外,還可以使用[[關(guān)鍵字,因為它支持&&的用法:
if [[ a = b && c = d ]]; then ...
12. read $foo
read命令中你不需要在變量名之前使用$。如果你想把讀入的數(shù)據(jù)存放到名為foo的變量中,下面的寫法就夠了:
read foo
或者,更加安全地方法:
IFS= read -r foo read $foo會把一行的內(nèi)容讀入到變量中,該變量的名稱存儲在$foo中。所以兩者的含義是完全不一樣的。