上一篇文章參見 第二節:bash編程易犯的錯誤。
和大多數 Shell 一樣,Bash 支持依次讀取單個命令行參數的語法。不過這并是$*或者$@,這兩種寫法都不正確,它們只能得到完整的參數列表,并非單獨的一個個參數。
正確的語法是(沒錯要加上引號):
for arg in "$@"
# 或者更簡單的寫法
for arg
在腳本中遍歷所有參數是一個再普遍不過的需求,所以 for arg 默認等價于 for arg in “$@”。$@使用雙引號后就有特殊的魔力,每個參數展開后成為一個獨立的單詞。(”$@”等價于”$1” “$2” “$3” …)
下面是一個錯誤的例子:
for x in $*; do echo "parameter: '$x'" done 執行的結果為: $ ./myscript 'arg 1' arg2 arg3 parameter: 'arg' parameter: '1' parameter: 'arg2' parameter: 'arg3'
正確的寫法:
for x in "$@"; do echo "parameter: '$x'" done 執行的結果為: $ ./myscript 'arg 1' arg2 arg3 parameter: 'arg 1' parameter: 'arg2' parameter: 'arg3'
上面正確的例子中,第一個參數’arg 1’在展開后依然是一個獨立的單詞,而不會被拆分成兩個。
25. function foo()
這種寫法不一定能夠兼容所有 shell,兼容的寫法是:
foo() { ... }
26. echo “~”
波浪號展開(Tilde expansion)僅當~沒有引號的時候發生,在上面的例子中,只會向標準輸出打印~符號,而不是當前用戶的家目錄路徑。
當用引號將路徑參數引起來時,如果要用引號將相對于家目錄的路徑引起來時,推薦使用 $HOME 而不是 ~, 假如 $HOME 目錄是”/home/my photos”,路徑中包含空格。
下面是幾組例子:
"~/dir with spaces" # expands to "~/dir with spaces" ~"/dir with spaces" # expands to "~/dir with spaces" ~/"dir with spaces" # expands to "/home/my photos/dir with spaces" "$HOME/dir with spaces" # expands to "/home/my photos/dir with spaces"
27. local varname=$(command)
當在函數中聲明局部變量時,local作為一個獨立的命令,這種奇特的行為有時候可能會導致困擾。比如,當你想要捕獲命令替換的返回碼時,你就不能這樣做。local命令的返回碼會覆蓋它。
這種情況下,你只能分成兩行寫:
local varname varname=$(command) rc=$?
28. export foo=~/bar
export 與 local 命令一樣,并不是賦值語句的一部分。因此,在有些 Shell 下(比如Bash),export foo=~/bar會展開,但是有些(比如 dash)卻不行。
下面是兩種比較健壯的寫法:
foo=~/bar; export foo # Right! export foo="$HOME/bar" # Right!
29. sed ‘s/$foo/good bye/’
單引號內部不會展開 $foo變量,在這里可以換成雙引號:
foo="hello"; sed "s/$foo/good bye/"
但是要注意,如果你使用了雙引號,就需要考慮更多轉義的事情,具體可以看Quotes這一頁。.
30. tr [A-Z] [a-z]
這里至少有三個問題。第一個問題是, [A-Z] 和 [a-z] 會被 shell 認為是通配符。如果在當前目錄下沒用文件名為單個字母的文件,這個命令似乎能正確執行,否則會錯誤地執行,也許你會在周末耗費許多小時來修復這個問題。
第二個問題是,這不是 tr 命令正確的寫法,實際上,上面的命令會把[轉換成[,將任意大寫字符轉換成對應的小寫字符,將]轉換成],所以你根本不需要加上括號,這樣第一個問題就可以解決了。
第三個問題是,上面的命令執行結果依賴于當前的 locale,A-Z 或者 a-z 不一定會代表26個 ASCII 字母。實際上,在一些語言環境下,z 位于字母表的中間位置。這個問題的解法,取決于你希望發生的行為是哪一種。
如果你僅希望改變26個英文字母的大小寫(強制 locale為 C):
LC_COLLATE=C tr A-Z a-z 如果你希望根據實際的語言環境來轉換: tr '[:upper:]' '[:lower:]'
31. ps ax | grep gedit
這里的根本問題是正在運行的進程名稱,本質上是不可靠的。可能會有多個合法的gedit進程,也有可能是別的東西偽裝成gedit進程(改變執行命令名稱是一件簡單的事情 ),更多細節可以看ProcessManagement這一篇文章。
執行以上命令,往往會在結果中包含 grep 進程:
# ps ax | grep gedit 10530 ? S 6:23 gedit 32118 pts/0 R+ 0:00 grep gedit 這個時候,需要過濾多余的結果: # ps ax | grep -v grep | grep gedit 上面的寫法比較丑陋,另外一種方法是: # ps ax | grep [g]edit
32. printf “$foo”
如果$foo 變量的值中包括或者%符號,上面命令的執行結果可能會出乎你的意料之外。
下面是正確的寫法:
printf %s "$foo" printf '%s ' "$foo"
33. for i in {1..$n}
Bash的命令解釋器會優先展開大括號,所以這時大括號{}表達式里面看到的是文字上的$n(沒有展開)。$n 不是一個數值,所以這里的大括號{}并不會展開成數字列表。可見,這導致很難使用大括號來展開大小只能在運行時才知道的列表。
可以用下面的方法:
for ((i=1; i< =n; i++)); do ... done
注:之前我也有寫過一篇文章來介紹這個問題:Shell生成數字序列。
34. if [[ $foo = $bar ]]
在[[內部,當=號右邊的值沒有用引號引起來,bash 會將它當作模式來匹配,而不是一個簡單的字符串。所以,在上面的例子中 ,如果 bar 的值是一個*號,執行的結果永遠是 true。
所以,如果你想檢查兩側的字符串是否相同,等號右側的值一定要用引號引起來。
if [[ $foo = "$bar" ]]
如果你確實要執行模式匹配,聰明的做法是取一個更加有意義的變量名(例如$patt),或者加上注釋說明。
35. if [[ $foo =~ ‘some RE’ ]]
同上,如果=~號右側的值加上引號,它會散失特殊的正則表達式含義,而變成一個普通的字符串。
如果你想使用一個長的或者復雜的正則表達式,避免大量的反斜杠轉義,建議把它放在一個變量中:
re='some RE' if [[ $foo =~ $re ]]
由于篇幅限制,本系列文章會分成多篇文章,最后一篇參見 第四節:Bash編程易犯的錯誤。