Bash的陷阱
下面就逐個分析一下這篇文章中提到的錯誤。不是完全的翻譯,有些沒用的話就略過了, 有些地方則加了些註釋。
- 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
cp $file $target
這句話基本上正確,但同樣有空格分詞的問題。所以應當用雙引號:
cp "$file" "$target"
但是如果湊巧文件名以 - 開頭,這個文件名會被 cp 當作命令行選項來處理,依舊很頭疼。可以試試下面這個。
cp -- "$file" "$target"
運氣差點的再碰上一個不支持 -- 選項的系統,那隻能用下面的方法了:使每個變量都以目錄開頭。
for i in ./*.mp3; do
cp "$i" /target
...
[ $foo = "bar" ]
當$foo為空時,上面的命令就變成了
[ = "bar" ]
類似地,當$foo包含空格時:
[ multiple words here = "bar" ]
兩者都會出錯。所以應當用雙引號將變量括起來:
[ "$foo" = bar ] # 幾乎完美了。
但是!當$foo以 - 開頭時依然會有問題。 在較新的bash中你可以用下面的方法來代替,[[ 關鍵字能正確處理空白、空格、帶橫線等問題。
[[ $foo = bar ]] # 正確
舊版本bash中可以用這個技巧(雖然不好理解):
[ x"$foo" = xbar ] # 正確
或者乾脆把變量放在右邊,因為 [ 命令的等號右邊即使是空白或是橫線開頭,依然能正常工作。 (Java編程風格中也有類似的做法,雖然目的不一樣。)
[ bar = "$foo" ] # 正確
- cd `dirname "$f"`
同樣也存在空格問題。那麼加上引號吧。
cd "`dirname "$f"`"
問題來了,是不是寫錯了?由於雙引號的嵌套,你會認為`dirname 是第一個字符串,`是第二個字符串。 錯了,那是C語言。在bash中,命令替換(反引號``中的內容)裡面的雙引號會被正確地匹配到一起, 不用特意去轉義。
$()
語法也相同,如下面的寫法是正確的。
cd "$(dirname "$f")"
[ "$foo" = bar && "$bar" = foo ]
[ 中不能使用 && 符號!因為 [ 的實質是 test 命令,&& 會把這一行分成兩個命令的。應該用以下的寫法。
[ bar = "$foo" -a foo = "$bar" ] # Right!
[ bar = "$foo" ] && [ foo = "$bar" ] # Also right!
[[ $foo = bar && $bar = foo ]] # Also right!
[ $foo > 7 ]
很可惜 [[ 只適用於字符串,不能做數字比較。數字比較應當這樣寫:
(( $foo > 7 ))
或者用經典的寫法:
[ $foo -gt 7 ]
但上述使用 -gt 的寫法有個問題,那就是當 $foo 不是數字時就會出錯。你必須做好類型檢驗。
這樣寫也行。
[[ $foo -gt 7 ]]
grep foo bar | while read line; do ((count++)); done
這行代碼數出bar文件中包含foo的行數,雖然很麻煩(等同於grep -c foo bar
或者 grep foo bar | wc -l
)。 乍一看沒有問題,但執行之後count變量卻沒有值。因為管道中的每個命令都放到一個新的子shell中執行, 所以子shell中定義的count變量無法傳遞出來。
if [grep foo myfile]
初學者常犯的錯誤,就是將 if 語句後面的 [ 當作if語法的一部分。實際上它是一個命令,相當於 test 命令, 而不是 if 語法。這一點C程序員特別應當注意。
if 會將 if 到 then 之間的所有命令的返回值當作判斷條件。因此上面的語句應當寫成
if grep foo myfile > /dev/null; then
if [bar="$foo"]
同樣,[ 是個命令,不是 if 語句的一部分,所以要注意空格。
if [ bar = "$foo" ]
if [ [ a = b ] && [ c = d ] ]
同樣的問題,[ 不是 if 語句的一部分,當然也不是改變邏輯判斷的括號。它是一個命令。可能C程序員比較容易犯這個錯誤?
if [ a = b ] && [ c = d ] # 正確
cat file | sed s/foo/bar/ > file
你不能在同一條管道操作中同時讀寫一個文件。根據管道的實現方式,file要麼被截斷成0字節,要麼會無限增長直到填滿整個硬盤。 如果想改變原文件的內容,只能先將輸出寫到臨時文件中再用mv命令。
sed 's/foo/bar/g' file > tmpfile && mv tmpfile file
- 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也別忘記給變量加引號。
$foo=bar
變量賦值時無需加 $ 符號——這不是Perl或PHP。
foo = bar
變量賦值時等號兩側不能加空格——這不是C語言。
echo <<EOF
here document是個好東西,它可以輸出成段的文字而不用加引號也不用考慮換行符的處理問題。 不過here document輸出時應當使用cat而不是echo。
# This is wrong:
echo <<EOF
Hello world
EOF
# This is right:
cat <<EOF
Hello world
EOF
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, 和這條就不符合了。不管怎樣,先將這一條寫在這裡吧。
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 等命令來控制工作目錄。
[ bar == "$foo" ]
[ 命令中不能用 ==,應當寫成
[ bar = "$foo" ] && echo yes
[[ bar == $foo ]] && echo yes
for i in {1..10}; do ./something &; done
& 後面不應該再放 ; ,因為 & 已經起到了語句分隔符的作用,無需再用;。
for i in {1..10}; do ./something & done
cmd1 && cmd2 || cmd3
有人喜歡用這種格式來代替 if...then...else 結構,但其實並不完全一樣。如果cmd2返回一個非真值,那麼cmd3則會被執行。 所以還是老老實實地用 if cmd1; then cmd2; else cmd3 為好。
- 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
echo "Hello World!"
交互執行這條命令會產生以下的錯誤:
-bash: !": event not found
因為 !" 會被當作命令行歷史替換的符號來處理。不過在shell腳本中沒有這樣的問題。
不幸的是,你無法使用轉義符來轉義!:
$ echo "hi\!"
hi\!
解決方案之一,使用單引號,即
$ echo 'Hello, world!'
如果你必須使用雙引號,可以試試通過 set +H 來取消命令行歷史替換。
set +H
echo "Hello, world!"
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" ...
可見,不加引號時 $*
和 $@
是相同的,但$*
會被擴展成一個字符串,而 $@
會 被擴展成每一個參數。
function foo()
在bash中沒有問題,但其他shell中有可能出錯。不要把 function 和括號一起使用。 最為保險的做法是使用括號,即
foo() {
...
}