原帖:
感謝
fcicq,他的new 30 days系列爲咱們帶來了很多好文章。
今天想分析的是這篇
Bash Pitfalls, 介紹了一些bash編程中的經典錯誤。fcicq說可能不適合初學者,而我認爲,正是bash編程的初學者才應該好好閱讀一下這篇文章。
下面就逐個分析一下這篇文章中提到的錯誤。不是徹底的翻譯,有些沒用的話就略過了,有些地方則加了些註釋。
1. for i in `ls *.mp3`
常見的錯誤寫法:
for i in `ls *.mp3`; do # Wrong!
爲何錯誤呢?由於for...in語句是按照空白來分詞的,包含空格的文件名會被拆成多個詞。如遇到 01 - Don't Eat the Yellow Snow.mp3 時,i的值會依次取 01,-,Don't,等等。
用雙引號也不行,它會將ls *.mp3的所有結果當成一個詞來處理。
for i in "`ls *.mp3`"; do # Wrong!
正確的寫法是
for i in *.mp3; do
2. cp $file $target
這句話基本上正確,但一樣有空格分詞的問題。因此應當用雙引號:
cp "$file" "$target"
可是若是湊巧文件名以 - 開頭,這個文件名會被 cp 看成命令行選項來處理,依舊很頭疼。能夠試試下面這個。
cp -- "$file" "$target"
運氣差點的再碰上一個不支持 -- 選項的系統,那隻能用下面的方法了:使每一個變量都以目錄開頭。
for i in ./*.mp3; do
cp "$i" /target
...
3. [ $foo = "bar" ]
當$foo爲空時,上面的命令就變成了
[ = "bar" ]
相似地,當$foo包含空格時:
[ multiple words here = "bar" ]
二者都會出錯。因此應當用雙引號將變量括起來:
[ "$foo" = bar ] # 幾乎完美了。
可是!當$foo以 - 開頭時依然會有問題。在較新的bash中你能夠用下面的方法來代替,[[ 關鍵字能正確處理空白、空格、帶橫線等問題。
[[ $foo = bar ]] # 正確
舊版本bash中能夠用這個技巧(雖然很差理解):
[ x"$foo" = xbar ] # 正確
或者乾脆把變量放在右邊,由於 [ 命令的等號右邊即便是空白或是橫線開頭,依然能正常工做。(Java編程風格中也有相似的作法,雖然目的不同。)
[ bar = "$foo" ] # 正確
4. cd `dirname "$f"`
一樣也存在空格問題。那麼加上引號吧。
cd "`dirname "$f"`"
問題來了,是否是寫錯了?因爲雙引號的嵌套,你會認爲`dirname 是第一個字符串,`是第二個字符串。錯了,那是C語言。在bash中,命令替換(反引號``中的內容)裏面的雙引號會被正確地匹配到一塊兒,不用特地去轉義。
$()語法也相同,以下面的寫法是正確的。
cd "$(dirname "$f")"
5. [ "$foo" = bar && "$bar" = foo ]
[ 中不能使用 && 符號!由於 [ 的實質是 test 命令,&& 會把這一行分紅兩個命令的。應該用如下的寫法。
[ bar = "$foo" -a foo = "$bar" ] # Right!
[ bar = "$foo" ] && [ foo = "$bar" ] # Also right!
[[ $foo = bar && $bar = foo ]] # Also right!
6. [[ $foo > 7 ]]
很惋惜 [[ 只適用於字符串,不能作數字比較。數字比較應當這樣寫:
(( $foo > 7 ))
或者用經典的寫法:
[ $foo -gt 7 ]
但上述使用 -gt 的寫法有個問題,那就是當 $foo 不是數字時就會出錯。你必須作好類型檢驗。
這樣寫也行。
[[ $foo -gt 7 ]]
7. grep foo bar | while read line; do ((count++) ); done
因爲格式問題,標題中我多加了一個空格。實際的代碼應該是這樣的:
grep foo bar | while read line; do ((count++)); done # 錯誤!
這行代碼數出bar文件中包含foo的行數,雖然很麻煩(等同於grep -c foo bar或者 grep foo bar | wc -l)。乍一看沒有問題,但執行以後count變量卻沒有值。由於管道中的每一個命令都放到一個新的子shell中執行,因此子shell中定義的count變量沒法傳遞出來。
8. if [grep foo myfile]
初學者常犯的錯誤,就是將 if 語句後面的 [ 看成if語法的一部分。實際上它是一個命令,至關於 test 命令,而不是 if 語法。這一點C程序員特別應當注意。
if 會將 if 到 then 之間的全部命令的返回值看成判斷條件。所以上面的語句應當寫成
if grep foo myfile > /dev/null; then
9. if [bar="$foo"]
一樣,[ 是個命令,不是 if 語句的一部分,因此要注意空格。
if [ bar = "$foo" ]
10. if [ [ a = b ] && [ c = d ] ]
一樣的問題,[ 不是 if 語句的一部分,固然也不是改變邏輯判斷的括號。它是一個命令。可能C程序員比較容易犯這個錯誤?
if [ a = b ] && [ c = d ] # 正確
11. cat file | sed s/foo/bar/ > file
你
不能在同一條管道操做中同時讀寫一個文件。根據管道的實現方式,file要麼被截斷成0字節,要麼會無限增加直到填滿整個硬盤。若是想改變原文件的內容,只能先將輸出寫到臨時文件中再用mv命令。
sed 's/foo/bar/g' file > tmpfile && mv tmpfile file
12. echo $foo
這句話還有什麼錯誤碼?通常來講是正確的,但下面的例子就有問題了。
MSG="Please enter a file name of the form *.zip"
echo $MSG # 錯誤!
若是恰巧當前目錄下有zip文件,就會顯示成
Please enter a file name of the form freenfss.zip lw35nfss.zip
因此即便是echo也別忘記給變量加引號。
13. $foo=bar
變量賦值時無需加 $ 符號——這不是Perl或PHP。
14. foo = bar
變量賦值時等號兩側不能加空格——這不是C語言。
15. echo <<EOF
here document是個好東西,它能夠輸出成段的文字而不用加引號也不用考慮換行符的處理問題。不過here document輸出時應當使用cat而不是echo。
# This is wrong:
echo <<EOF
Hello world
EOF
# This is right:
cat <<EOF
Hello world
EOF
16. su -c 'some command'
原文的意思是,這條基本上正確,但使用者的目的是要將 -c 'some command' 傳給shell。而剛好 su 有個 -c 參數,因此su 只會將 'some command' 傳給shell。因此應該這麼寫:
su root -c 'some command'
可是在個人平臺上,man su 的結果中關於 -c 的解釋爲
-c, --commmand=COMMAND
pass a single COMMAND to the shell with -c
也就是說,-c 'some command' 一樣會將 -c 'some command' 這樣一個字符串傳遞給shell,和這條就不符合了。無論怎樣,先將這一條寫在這裏吧。
17. cd /foo; bar
cd有可能會出錯,出錯後 bar 命令就會在你預想不到的目錄裏執行了。因此必定要記得判斷cd的返回值。
cd /foo && bar
若是你要根據cd的返回值執行多條命令,能夠用 ||。
cd /foo || exit 1;
bar
baz
關於目錄的一點題外話,假設你要在shell程序中頻繁變換工做目錄,以下面的代碼:
find ... -type d | while read subdir; do
cd "$subdir" && whatever && ... && cd -
done
不如這樣寫:
find ... -type d | while read subdir; do
(cd "$subdir" && whatever && ...)
done
括號會強制啓動一個子shell,這樣在這個子shell中改變工做目錄不會影響父shell(執行這個腳本的shell),就能夠省掉cd - 的麻煩。
你也能夠靈活運用 pushd、popd、dirs 等命令來控制工做目錄。
18. [ bar == "$foo" ]
[ 命令中不能用 ==,應當寫成
[ bar = "$foo" ] && echo yes
[[ bar == $foo ]] && echo yes
19. for i in {1..10}; do ./something &; done
& 後面不該該再放 ; ,由於 & 已經起到了語句分隔符的做用,無需再用;。
for i in {1..10}; do ./something & done
20. cmd1 && cmd2 || cmd3
有人喜歡用這種格式來代替 if...then...else 結構,但其實並不徹底同樣。若是cmd2返回一個非真值,那麼cmd3則會被執行。因此仍是老老實實地用 if cmd1; then cmd2; else cmd3 爲好。
21. UTF-8的BOM(Byte-Order Marks)問題
UTF-8編碼能夠在文件開頭用幾個字節來表示編碼的字節順序,這幾個字節稱爲BOM。但Unix格式的UTF-8編碼不須要BOM。多餘的BOM會影響shell解析,特別是開頭的 #!/bin/sh 之類的指令將會沒法識別。
MS-DOS格式的換行符(CRLF)也存在一樣的問題。若是你將shell程序保存成DOS格式,腳本就沒法執行了。
$ ./dos
-bash: ./dos: /bin/sh^M: bad interpreter: No such file or directory
22. echo "Hello World!"
交互執行這條命令會產生如下的錯誤:
-bash: !": event not found
由於 !" 會被看成命令行歷史替換的符號來處理。不過在shell腳本中沒有這樣的問題。
不幸的是,你沒法使用轉義符來轉義!:
$ echo "hi\!"
hi\!
解決方案之一,使用單引號,即
$ echo 'Hello, world!'
若是你必須使用雙引號,能夠試試經過 set +H 來取消命令行歷史替換。
set +H
echo "Hello, world!"
23. for arg in $*
$*表示全部命令行參數,因此你可能想這樣寫來逐個處理參數,但參數中包含空格時就會失敗。如:
#!/bin/bash
# Incorrect version
for x in $*; do
echo "parameter: '$x'"
done
$ ./myscript 'arg 1' arg2 arg3
parameter: 'arg'
parameter: '1'
parameter: 'arg2'
parameter: 'arg3'
正確的方法是使用 "$@"。
#!/bin/bash
# Correct version
for x in "$@"; do
echo "parameter: '$x'"
done
$ ./myscript 'arg 1' arg2 arg3
parameter: 'arg 1'
parameter: 'arg2'
parameter: 'arg3'
在 bash 的手冊中對 $* 和 $@ 的說明以下:
* 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" ...
可見,不加引號時 $* 和 $@ 是相同的,但"$*" 會被擴展成一個字符串,而 "$@" 會被擴展成每個參數。
24. function foo()
在bash中沒有問題,但其餘shell中有可能出錯。不要把 function 和括號一塊兒使用。最爲保險的作法是使用括號,即
foo() {
...
}