感謝fcicq,他的new 30 days系列為我們帶來(lái)了不少好文章。
今天想分析的是這篇Bash Pitfalls,介紹了一些bash編程中的經(jīng)典錯(cuò)誤。fcicq說(shuō)可能不適合初學(xué)者,而我認(rèn)為,正是bash編程的初學(xué)者才應(yīng)該好好閱讀一下這篇文章。
下面就逐個(gè)分析一下這篇文章中提到的錯(cuò)誤。不是完全的翻譯,有些沒(méi)用的話就略過(guò)了,有些地方則加了些注釋。
常見(jiàn)的錯(cuò)誤寫(xiě)法:
for i in `ls *.mp3`; do # Wrong!
為什么錯(cuò)誤呢?因?yàn)閒or...in語(yǔ)句是按照空白來(lái)分詞的,包含空格的文件名會(huì)被拆成多個(gè)詞。如遇到 01 - Don't Eat the Yellow Snow.mp3 時(shí),i的值會(huì)依次取 01,-,Don't,等等。
用雙引號(hào)也不行,它會(huì)將ls *.mp3的全部結(jié)果當(dāng)成一個(gè)詞來(lái)處理。
for i in "`ls *.mp3`"; do # Wrong!
正確的寫(xiě)法是
for i in *.mp3; do
這句話基本上正確,但同樣有空格分詞的問(wèn)題。所以應(yīng)當(dāng)用雙引號(hào):
cp "$file" "$target"
但是如果湊巧文件名以 - 開(kāi)頭,這個(gè)文件名會(huì)被 cp 當(dāng)作命令行選項(xiàng)來(lái)處理,依舊很頭疼。可以試試下面這個(gè)。
cp -- "$file" "$target"
運(yùn)氣差點(diǎn)的再碰上一個(gè)不支持 -- 選項(xiàng)的系統(tǒng),那只能用下面的方法了:使每個(gè)變量都以目錄開(kāi)頭。
for i in ./*.mp3; do cp "$i" /target ...
當(dāng)$foo為空時(shí),上面的命令就變成了
[ = "bar" ]
類(lèi)似地,當(dāng)$foo包含空格時(shí):
[ multiple words here = "bar" ]
兩者都會(huì)出錯(cuò)。所以應(yīng)當(dāng)用雙引號(hào)將變量括起來(lái):
[ "$foo" = bar ] # 幾乎完美了。
但是!當(dāng)$foo以 - 開(kāi)頭時(shí)依然會(huì)有問(wèn)題。在較新的bash中你可以用下面的方法來(lái)代替,[[ 關(guān)鍵字能正確處理空白、空格、帶橫線等問(wèn)題。
[[ $foo = bar ]] # 正確
舊版本bash中可以用這個(gè)技巧(雖然不好理解):
[ x"$foo" = xbar ] # 正確
或者干脆把變量放在右邊,因?yàn)?[ 命令的等號(hào)右邊即使是空白或是橫線開(kāi)頭,依然能正常工作。(Java編程風(fēng)格中也有類(lèi)似的做法,雖然目的不一樣。)
[ bar = "$foo" ] # 正確
同樣也存在空格問(wèn)題。那么加上引號(hào)吧。
cd "`dirname "$f"`"
問(wèn)題來(lái)了,是不是寫(xiě)錯(cuò)了?由于雙引號(hào)的嵌套,你會(huì)認(rèn)為`dirname 是第一個(gè)字符串,`是第二個(gè)字符串。錯(cuò)了,那是C語(yǔ)言。在bash中,命令替換(反引號(hào)``中的內(nèi)容)里面的雙引號(hào)會(huì)被正確地匹配到一起,不用特意去轉(zhuǎn)義。
$()語(yǔ)法也相同,如下面的寫(xiě)法是正確的。
cd "$(dirname "$f")"
[ 中不能使用 && 符號(hào)!因?yàn)?[ 的實(shí)質(zhì)是 test 命令,&& 會(huì)把這一行分成兩個(gè)命令的。應(yīng)該用以下的寫(xiě)法。
[ bar = "$foo" -a foo = "$bar" ] # Right![ bar = "$foo" ] && [ foo = "$bar" ] # Also right![[ $foo = bar && $bar = foo ]] # Also right!
很可惜 [[ 只適用于字符串,不能做數(shù)字比較。數(shù)字比較應(yīng)當(dāng)這樣寫(xiě):
(( $foo > 7 ))
或者用經(jīng)典的寫(xiě)法:
[ $foo -gt 7 ]
但上述使用 -gt 的寫(xiě)法有個(gè)問(wèn)題,那就是當(dāng) $foo 不是數(shù)字時(shí)就會(huì)出錯(cuò)。你必須做好類(lèi)型檢驗(yàn)。
這樣寫(xiě)也行。
[[ $foo -gt 7 ]]
由于格式問(wèn)題,標(biāo)題中我多加了一個(gè)空格。實(shí)際的代碼應(yīng)該是這樣的:
grep foo bar | while read line; do ((count++)); done # 錯(cuò)誤!
這行代碼數(shù)出bar文件中包含foo的行數(shù),雖然很麻煩(等同于grep -c foo bar或者 grep foo bar | wc -l)。乍一看沒(méi)有問(wèn)題,但執(zhí)行之后count變量卻沒(méi)有值。因?yàn)楣艿乐械拿總€(gè)命令都放到一個(gè)新的子shell中執(zhí)行,所以子shell中定義的count變量無(wú)法傳遞出來(lái)。
初學(xué)者常犯的錯(cuò)誤,就是將 if 語(yǔ)句后面的 [ 當(dāng)作if語(yǔ)法的一部分。實(shí)際上它是一個(gè)命令,相當(dāng)于 test 命令,而不是 if 語(yǔ)法。這一點(diǎn)C程序員特別應(yīng)當(dāng)注意。
if 會(huì)將 if 到 then 之間的所有命令的返回值當(dāng)作判斷條件。因此上面的語(yǔ)句應(yīng)當(dāng)寫(xiě)成
if grep foo myfile > /dev/null; then
同樣,[ 是個(gè)命令,不是 if 語(yǔ)句的一部分,所以要注意空格。
if [ bar = "$foo" ]
同樣的問(wèn)題,[ 不是 if 語(yǔ)句的一部分,當(dāng)然也不是改變邏輯判斷的括號(hào)。它是一個(gè)命令??赡蹸程序員比較容易犯這個(gè)錯(cuò)誤?
if [ a = b ] && [ c = d ] # 正確
你不能在同一條管道操作中同時(shí)讀寫(xiě)一個(gè)文件。根據(jù)管道的實(shí)現(xiàn)方式,file要么被截?cái)喑?字節(jié),要么會(huì)無(wú)限增長(zhǎng)直到填滿整個(gè)硬盤(pán)。如果想改變?cè)募膬?nèi)容,只能先將輸出寫(xiě)到臨時(shí)文件中再用mv命令。
sed 's/foo/bar/g' file > tmpfile && mv tmpfile file
這句話還有什么錯(cuò)誤碼?一般來(lái)說(shuō)是正確的,但下面的例子就有問(wèn)題了。
MSG="Please enter a file name of the form *.zip"echo $MSG # 錯(cuò)誤!
如果恰巧當(dāng)前目錄下有zip文件,就會(huì)顯示成
Please enter a file name of the form freenfss.zip lw35nfss.zip
所以即使是echo也別忘記給變量加引號(hào)。
變量賦值時(shí)無(wú)需加 $ 符號(hào)——這不是Perl或PHP。
變量賦值時(shí)等號(hào)兩側(cè)不能加空格——這不是C語(yǔ)言。
here document是個(gè)好東西,它可以輸出成段的文字而不用加引號(hào)也不用考慮換行符的處理問(wèn)題。不過(guò)here document輸出時(shí)應(yīng)當(dāng)使用cat而不是echo。
# This is wrong:echo <<EOFHello worldEOF# This is right:cat <<EOFHello worldEOF
原文的意思是,這條基本上正確,但使用者的目的是要將 -c 'some command' 傳給shell。而恰好 su 有個(gè) -c 參數(shù),所以su 只會(huì)將 'some command' 傳給shell。所以應(yīng)該這么寫(xiě):
su root -c 'some command'
但是在我的平臺(tái)上,man su 的結(jié)果中關(guān)于 -c 的解釋為
-c, --commmand=COMMAND pass a single COMMAND to the shell with -c
也就是說(shuō),-c 'some command' 同樣會(huì)將 -c 'some command' 這樣一個(gè)字符串傳遞給shell,和這條就不符合了。不管怎樣,先將這一條寫(xiě)在這里吧。
cd有可能會(huì)出錯(cuò),出錯(cuò)后 bar 命令就會(huì)在你預(yù)想不到的目錄里執(zhí)行了。所以一定要記得判斷cd的返回值。
cd /foo && bar
如果你要根據(jù)cd的返回值執(zhí)行多條命令,可以用 ||。
cd /foo || exit 1;barbaz
關(guān)于目錄的一點(diǎn)題外話,假設(shè)你要在shell程序中頻繁變換工作目錄,如下面的代碼:
find ... -type d | while read subdir; do cd "$subdir" && whatever && ... && cd -done
不如這樣寫(xiě):
find ... -type d | while read subdir; do (cd "$subdir" && whatever && ...)done
括號(hào)會(huì)強(qiáng)制啟動(dòng)一個(gè)子shell,這樣在這個(gè)子shell中改變工作目錄不會(huì)影響父shell(執(zhí)行這個(gè)腳本的shell),就可以省掉cd - 的麻煩。
你也可以靈活運(yùn)用 pushd、popd、dirs 等命令來(lái)控制工作目錄。
[ 命令中不能用 ==,應(yīng)當(dāng)寫(xiě)成
[ bar = "$foo" ] && echo yes[[ bar == $foo ]] && echo yes
& 后面不應(yīng)該再放 ; ,因?yàn)?& 已經(jīng)起到了語(yǔ)句分隔符的作用,無(wú)需再用;。
for i in {1..10}; do ./something & done
有人喜歡用這種格式來(lái)代替 if...then...else 結(jié)構(gòu),但其實(shí)并不完全一樣。如果cmd2返回一個(gè)非真值,那么cmd3則會(huì)被執(zhí)行。所以還是老老實(shí)實(shí)地用 if cmd1; then cmd2; else cmd3 為好。
UTF-8編碼可以在文件開(kāi)頭用幾個(gè)字節(jié)來(lái)表示編碼的字節(jié)順序,這幾個(gè)字節(jié)稱(chēng)為BOM。但Unix格式的UTF-8編碼不需要BOM。多余的BOM會(huì)影響shell解析,特別是開(kāi)頭的 #!/bin/sh 之類(lèi)的指令將會(huì)無(wú)法識(shí)別。
MS-DOS格式的換行符(CRLF)也存在同樣的問(wèn)題。如果你將shell程序保存成DOS格式,腳本就無(wú)法執(zhí)行了。
$ ./dos-bash: ./dos: /bin/sh^M: bad interpreter: No such file or directory
交互執(zhí)行這條命令會(huì)產(chǎn)生以下的錯(cuò)誤:
-bash: !": event not found
因?yàn)?!" 會(huì)被當(dāng)作命令行歷史替換的符號(hào)來(lái)處理。不過(guò)在shell腳本中沒(méi)有這樣的問(wèn)題。
不幸的是,你無(wú)法使用轉(zhuǎn)義符來(lái)轉(zhuǎn)義!:
$ echo "hi\!"hi\!
解決方案之一,使用單引號(hào),即
$ echo 'Hello, world!'
如果你必須使用雙引號(hào),可以試試通過(guò) set +H 來(lái)取消命令行歷史替換。
set +Hecho "Hello, world!"
$*表示所有命令行參數(shù),所以你可能想這樣寫(xiě)來(lái)逐個(gè)處理參數(shù),但參數(shù)中包含空格時(shí)就會(huì)失敗。如:
#!/bin/bash# Incorrect versionfor x in $*; do echo "parameter: '$x'"done$ ./myscript 'arg 1' arg2 arg3parameter: 'arg'parameter: '1'parameter: 'arg2'parameter: 'arg3'
正確的方法是使用 "$@"。
#!/bin/bash# Correct versionfor x in "$@"; do echo "parameter: '$x'"done$ ./myscript 'arg 1' arg2 arg3parameter: 'arg 1'parameter: 'arg2'parameter: 'arg3'
在 bash 的手冊(cè)中對(duì) $* 和 $@ 的說(shuō)明如下:
* Expands to the positional parameters, starting from one. When the expansion occurs within double quotes, it expands to a single word with the value of each parameter separated by the first character of the IFS special variable. That is, "$*" is equivalent to "$1c$2c...",@ Expands to the positional parameters, starting from one. When the expansion occurs within double quotes, each parameter expands to a separate word. That is, "$@" is equivalent to "$1" "$2" ...
可見(jiàn),不加引號(hào)時(shí) $* 和 $@ 是相同的,但"$*" 會(huì)被擴(kuò)展成一個(gè)字符串,而 "$@" 會(huì)被擴(kuò)展成每一個(gè)參數(shù)。
在bash中沒(méi)有問(wèn)題,但其他shell中有可能出錯(cuò)。不要把 function 和括號(hào)一起使用。最為保險(xiǎn)的做法是使用括號(hào),即
foo() { ...}
聯(lián)系客服