Bash編程易犯的錯誤
前一段時間發(fā)現(xiàn)一個很好的wiki站點(diǎn),上面有很多優(yōu)秀的Bash文章。最近挑了一篇介紹Bash編程容易犯的各種錯誤的文章看,收獲很多,不感獨(dú)享,把這篇文章以半翻譯半筆記的形式分享給大家。
1. for i in $(ls *.mp3)
Bash寫循環(huán)代碼的時候,確實(shí)比較容易犯下面的錯誤:
- 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進(jìn)一步處理,比如文件名展開。比如,ls執(zhí)行的結(jié)果中包含*號,按照通配符的規(guī)則, *號會被展開成當(dāng)前目錄下的所有文件:
- $ 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
不過,在這種場景下,你即使加上引號,也是無濟(jì)于事的:
- $ for i in "$(ls *.mp3)"; do echo --$i--; done
- --1*.mp3 1.mp3 11.mp3 12.mp3--
加上引號后,ls執(zhí)行的結(jié)果會被當(dāng)成一個整體,所以for循環(huán)只會執(zhí)行一次,達(dá)不到預(yù)期的效果。
事實(shí)上,這種情況下,根本不需要使用ls命令。ls命令的結(jié)果本身就設(shè)計(jì)成給人讀的,而不是給腳本解析的。正確的處理方法是,直接使用文件名展開(通配符)的功能:
- $ for i in *.mp3; do
- > echo "$i"
- > done
- 1*.mp3
- 1.mp3
- 11.mp3
- 12.mp3
文件名展開是位于各種展開(花括號展開、變量替換、命令展開等)功能中的最后一個環(huán)節(jié),所以不會有之前不帶引號的命令展開的副作用。如果你需要遞歸地處理文件,可以考慮使用Find命令。
到這一步,之間的問題看樣子已經(jīng)修復(fù)了。但是,如果你進(jìn)一步思考,假設(shè)當(dāng)前目錄上沒有文件時會怎么樣?沒有文件的時候,*.mp3不會被展開直接傳 遞給for循環(huán)處理,所以這個時候循環(huán)還是會執(zhí)行一次。這種情況不是我們預(yù)期的行為。保險起見,可以在循環(huán)處理的時候,檢查下文件是否存在:
- # POSIX
- for i in *.mp3; do
- [ -e "$i" ] || continue
- some command "$i"
- done
如果你有使用引號和避免單詞拆分的習(xí)慣,你完全可以避免很多錯誤。
注意下循環(huán)體內(nèi)部的”$i”,這里會導(dǎo)致下面我們要說的另外一個比較容易犯的錯誤。
2. cp $file $target
上面的命令有什么問題呢?如果你提前知道,$file和$target文件名中不會包含空格或者*號。否則,這行命令執(zhí)行前在經(jīng)過單詞拆分和文件名展開的時候會出現(xiàn)問題。所以,兩次強(qiáng)調(diào),在使用展開的地方切勿忘記使用引號:
- $ cp -- "$file" "$target"
如果不帶引號,當(dāng)你執(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會認(rèn)為你提供的是一個命令行選項(xiàng),這個錯誤下面會介紹。
3. 文件名中包含短橫’-’
文件名以’-'開頭會導(dǎo)致許多問題,*.mp3這種通配符會根據(jù)當(dāng)前的locale展開成一個列表,但在絕大多數(shù)環(huán)境下,’-'排序的時候會排在大多數(shù)字母前。這個展開的列表傳遞給有些命令的時候,會錯誤的將-filename解析成命令行選項(xiàng)。這里有兩種方法來解決這個問題。
第一種方法是在命令和參數(shù)之間加上–,這種語法告訴命令不要繼續(xù)對–之后的內(nèi)容進(jìn)行命令行參數(shù)/選項(xiàng)解析:
- $ cp -- "$file" "$target"
這種方法可以解這個問題,但是你需要在每個命令后面都要加上–,而且依賴具體的命令解析的方式,如果一些命令不兼容這種約定俗成的規(guī)范,這種做法是無效的。
另外一種方法是,確保文件名都使用相對或者絕對的路徑,以目錄開頭:
- for i in ./*.mp3; do
- cp "$i" /target
- ...
- done
這種情況下,即使某個文件以-開頭,展開后文件名依然是./-foo.mp3這種形式,完全不會有問題。
4. [ $foo = "bar" ]
這是一個與第2個問題類似的問題,雖然用到了引號,但是放錯了位置,對于字符串字面值,除非有特殊符號,否則不大需要用引號括起來。但是,你應(yīng)該把變量的值用括號括起來,從而避免它們包含空格或能通配符,這一點(diǎn)我們在前面的問題中都解釋過。
這個例子在以下情況下會出錯:
如果[中的變量不存在,或者為空,這個時候上面的例子最終解析結(jié)果是:
- [ = "bar" ] # 錯誤!
并且執(zhí)行會出錯:unary operator expected,因?yàn)?是二元操作符,它需要左右各一個操作數(shù)。
如果變量值包含空格,它首先在執(zhí)行之前進(jìn)行單詞拆分,因此[命令看到的樣子可能是這樣的:
- [ multiple words here = "bar" ];
正確的做法應(yīng)該是:
- # POSIX
- [ "$foo" = bar ]
這種寫法,在POSIX兼容的實(shí)現(xiàn)中都不會有問題,即使$foo以短橫"-"開頭,因?yàn)镻OSIX實(shí)現(xiàn)的test命令通過傳遞的參數(shù)來確定執(zhí)行的行為。
只有一些非常古老的shell可能會遇到問題,這個時候你可以使用下面的寫法來解決(相信你肯定看到過這種寫法):
- # POSIX / Bourne
- [ x"$foo" = xbar ]
在Bash中,還有另外一種選擇是使用[[關(guān)鍵字:
- # Bash / Ksh
- [[ $foo == bar ]]
這里你不需要使用引號,因?yàn)樵赱[里面參數(shù)不會進(jìn)行展開,當(dāng)然帶上引號也不會有錯。
不過有一點(diǎn)要注意的是,[[里的==不僅僅是文本比較,它會檢查左邊的值是否匹配右側(cè)的表達(dá)式,==右側(cè)的值加上引號,會讓它成為一個普通的字面量,*?等通配符會失去特殊含義。
5. cd $(dirname "$f")
這又是一個引號的問題,命令展開的結(jié)果會進(jìn)一步地進(jìn)行單詞拆分或者文件名展開。因此下面的寫法才是正確的:
- cd "$(dirname "$f")"
但是,上面引號的寫法可能比較怪異,你可能會認(rèn)為第一、二個引號,第三、四個引號是一組的。
但是事實(shí)上,Bash將命令替換里面的引號當(dāng)成一組,外面的當(dāng)成另外一組。如果你是用反引號的寫法,引號的行為就不是這樣的了,所以$()寫法更加推薦。
#p#
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 ]]
原文作者認(rèn)為算術(shù)比較不應(yīng)該用[[,而是用((,我沒弄明白是為什么。
如果有理解的同學(xué),歡迎以評論回復(fù),謝謝。
8. grep foo bar | while read -r; do ((count++)); done
這種寫法初看沒有問題,但是你會發(fā)現(xiàn)當(dāng)執(zhí)行完后,count變量并沒有變化。原因是管道后面的命令是在一個子Shell中執(zhí)行的。
POSIX規(guī)范并沒有說明管道的最后一個命令是不是在子Shell中執(zhí)行的。一些shell,例如ksh93或者Bash>=4.2可以通過shopt -s lastpipe
命令,指明管道中的最后一個命令在當(dāng)前shell中執(zhí)行。由于篇幅限制,在此就不展開,有興趣的可以看Bash FAQ #24。
9. if [grep foo myfile]
初學(xué)者會錯誤地認(rèn)為,[是if語法的一部分,正如C語言中的if ()。但是事實(shí)并非如此,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> # optional
- then <COMMANDS>
- else <COMMANDS> # optional
- fi # required
再次強(qiáng)調(diào),[是一個命令,它同其它常規(guī)的命令一樣接受參數(shù)。if是一個復(fù)合命令,它包含其它命令,[并不是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語句的條件一樣,它是一個命令。
如果你想表達(dá)一個復(fù)合的條件表達(dá)式,可以這樣寫:
- 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)鍵字,因?yàn)樗С?amp;&的用法:
- if [[ a = b && c = d ]]; then ...
12. read $foo
read命令中你不需要在變量名之前使用$。如果你想把讀入的數(shù)據(jù)存放到名為foo的變量中,下面的寫法就夠了:
- read foo
或者,更加安全地方法:
- IFS= read -r foo
read $foo
會把一行的內(nèi)容讀入到變量中,該變量的名稱存儲在$foo中。所以兩者的含義是完全不一樣的。
13. cat file | sed s/foo/bar/ > file
你不應(yīng)該在一個管道中,從一個文件讀的同時,再往相同的文件里面寫,這樣的后果是未知的。
你可以為此創(chuàng)建一個臨時文件,這種做法比較安全可靠:
- # sed 's/foo/bar/g' file > tmpfile && mv tmpfile file
或者,如果你用得是 GNU Sed 4.x 以上的版本,可以使用-i 選項(xiàng)即時修改文件的內(nèi)容:
- # sed -i 's/foo/bar/g' file
14. echo $foo
這種看似無害的命令往往會給初學(xué)者千萬極大的困擾,他們會懷疑是不是因?yàn)?$foo 變量的值是錯誤的。事實(shí)卻是因?yàn)椋?foo 變量在這里沒有使用雙引號,所以在解析的時候會進(jìn)行單詞拆分和文件名展開,最終導(dǎo)致執(zhí)行結(jié)果與預(yù)期大相徑庭:
- msg="Please enter a file name of the form *.zip"
- echo $msg
這里整句話會被拆分成單詞,然后其中的通配符會被展開,例如*.zip。當(dāng)你的用戶看到如下的結(jié)果時,他們會怎樣想:
- Please enter a file name of the form freenfss.zip lw35nfss.zip
再舉一個例子(假設(shè)當(dāng)前目錄下有以 .zip 結(jié)尾的文件):
- var="*.zip" # var 包括一個星號,一個點(diǎn)號和 zip
- echo "$var" # 輸出 *.zip
- echo $var # 輸出所有以 .zip 結(jié)尾的文件
實(shí)際上,這里使用 echo 命令并不是絕對的安全。例如,當(dāng)變量的值包含-n時,echo 會認(rèn)為它是一個合法的選項(xiàng)而不是要輸出的內(nèi)容(當(dāng)然如果你能夠保證不會有-n 這種值,可以放心地使用 echo 命令)。
完全可靠的打印變量值的方法是使用 printf:
- printf "%s\n" "$foo"
15. $foo=bar
略過
16. foo = bar
當(dāng)賦值時,等號兩邊是不允許出現(xiàn)空格的,這同 C 語言不一樣。當(dāng)你寫下 foo = bar 時,shell 會將該命令解析成三個單詞,然后第一個單詞 foo 會被認(rèn)為是一個命令,后面的內(nèi)容會被當(dāng)作命令參數(shù)。
同樣地,下面的寫法也是錯誤的:
- foo= bar # WRONG!
- foo =bar # WRONG!
- $foo = bar; # COMPLETELY WRONG!
- 正確的寫法應(yīng)該是這樣的:
- <pre class="prettyprint lang-sh">
- foo=bar # Right.
- foo="bar" # More Right.
17. echo <<EOF
當(dāng)腳本需要嵌入大段的文本內(nèi)容時,here document往往是一個非常有用的工具,它將其中的文本作為命令的標(biāo)準(zhǔn)輸入。不過,echo 命令并不支持從標(biāo)準(zhǔn)輸入讀取內(nèi)容,所以下面的寫法是錯誤的:
- # This is wrong:
- echo <<EOF
- Hello world
- How's it going?
- EOF
正確的方法是,使用 cat 命令來完成:
- # This is what you were trying to do:
- cat <<EOF
- Hello world
- How's it going?
- EOF
或者可以使用雙引號,它也可以跨越多行,而且因?yàn)?echo 命令是內(nèi)置命令,相同情況下它會更加高效:
- echo "Hello world
- How's it going?"
#p#
18. su -c 'some command'
這種寫法“幾乎”是正確的。問題是,在許多平臺上,su 支持 -c 參數(shù),但是它不一定是你認(rèn)為的。比如,在 OpenBSD 平臺上你這樣執(zhí)行會出錯:
- $ su -c 'echo hello'
- su: only the superuser may specify a login class
在這里,-c是用于指定login-class。如果你想要傳遞 -c 'some command' 給 shell,最好在之前顯示地指定 username:
- $ su root -c 'some command' # Now it's right.
19. cd /foo; bar
如果你不檢查 cd 命令執(zhí)行是否成功,你可以會在錯誤的目錄下執(zhí)行 bar 命令,這有可能會帶來災(zāi)難,比如 bar 命令是 rm -rf *。
你必須經(jīng)常檢查 cd 命令執(zhí)行是否有錯誤,簡單的做法是:
- cd /foo && bar
如果在 cd 命令后有多個命令,你可以選擇這樣寫:
- cd /foo || exit 1
- bar
- baz
- bat ... # Lots of commands
出錯時,cd 命令會報告無法改變當(dāng)前目錄,同時將錯誤消息輸出到標(biāo)準(zhǔn)錯誤,例如"bash: cd: /foo: No such file or directory"。如果你想要在標(biāo)準(zhǔn)輸出同時輸出自定義的錯誤提示,可以使用復(fù)合命令(command grouping):
- cd /net || { echo "Can't read /net. Make sure you've logged in to the Samba network, and try again."; exit 1; }
- do_stuff
- more_stuff
注意,在{號和 echo 之間需要有一個空格,同時}之前要加上分號。
順便提一下,如果你要在腳本里頻繁改變當(dāng)前目錄,可以看看 pushd/popd/dirs 等命令,可能你在代碼里面寫的 cd/pwd 命令都是沒有必要的。
說到這,比較下下面兩種寫法:
- find ... -type d -print0 | while IFS= read -r -d '' subdir; do
- here=$PWD
- cd "$subdir" && whatever && ...
- cd "$here"
- done
- find ... -type d -print0 | while IFS= read -r -d '' subdir; do
- (cd "$subdir" || exit; whatever; ...)
- done
下面的寫法,在循環(huán)中 fork 了一個子 shell 進(jìn)程,子 shell 進(jìn)程中的 cd 命令僅會影響當(dāng)前 shell的環(huán)境變量,所以父進(jìn)程中的環(huán)境命令不會被改變;當(dāng)執(zhí)行到下一次循環(huán)時,無論之前的 cd 命令有沒有執(zhí)行成功,我們會回到相同的當(dāng)前目錄。這種寫法相較前面的用法,代碼更加干凈。
20. [ bar == "$foo" ]
正確的用法:
- [ bar = "$foo" ] && echo yes
- [[ bar == $foo ]] && echo yes
21. for i in {1..10}; do ./something &; done
你不應(yīng)該在&后面添加分號,刪除它:
- for i in {1..10}; do ./something & done
或者改成多行的形式:
- for i in {1..10}; do
- ./something &
- done
&和分號一樣也可以用作命令終止符,所以你不要將兩個混用到一起。一般情況下,分號可以被換行符替換,但是不是所有的換行符都可以用分號替換。
22. cmd1 && cmd2 || cmd3
有些人喜歡把&&和||作為if...then...else...fi 的簡寫語法,在多數(shù)情況下,這種寫法沒有問題。例如:
- [[ -s $errorlog ]] && echo "Uh oh, there were some errors." || echo "Successful."
但是,這種結(jié)構(gòu)并不是在所有情況下都完全等價于 if...fi 語法。這是因?yàn)樵?amp;&后面的命令執(zhí)行結(jié)束時也會生成一個返回碼,如果該返回碼不是真值(0代表 true),||后面的命令也會執(zhí)行,例如:
- i=0
- true && ((i++)) || ((i--))
- echo $i # 輸出 0
看起來上面的結(jié)果應(yīng)該是返回1,但是結(jié)果卻是輸出0,為什么呢?原因是這里 i++ 和 i-- 都執(zhí)行了一遍。
其中,((i++))命令執(zhí)行算術(shù)運(yùn)算,表達(dá)式計(jì)算的結(jié)果為0。這里和 C 語言一樣,表達(dá)式的結(jié)果為0被認(rèn)為是 false。所以當(dāng) i=0 的時候,((i++))命令執(zhí)行的返回碼為1(false),從而會執(zhí)行接下來的((i--))命令。
如果我們在這里使用前綴自增運(yùn)算符的話,返回的結(jié)果恰恰為1,因?yàn)?(++i))執(zhí)行的返回碼是0(true):
- i=0
- true && (( ++i )) || (( --i ))
- echo $i # Prints 1
不過在你無法保證 y 的執(zhí)行結(jié)果是,絕對不要依靠 x && y || z這種寫法。上面這種巧合,在 i 初始化為-1時也會有問題。
如果你喜歡代碼更加安全健壯,建議使用 if...fi 語法:
- i=0
- if true; then
- ((i++))
- else
- ((i--))
- fi
- echo $i # 輸出 1
23. echo "Hello World!"
在交互式的 Shell 環(huán)境下,你執(zhí)行以上命令會遇到下面的錯誤:
- bash: !": event not found
這是因?yàn)?,在默認(rèn)的交互式 Shell 環(huán)境下,Bash 發(fā)現(xiàn)感嘆號時會執(zhí)行歷史命令展開。在 Shell 腳本中,這種行為是被禁止的,所以不會發(fā)生錯誤。
不幸地是,你認(rèn)為明顯正確地修復(fù)方法,也不能工作,你會發(fā)現(xiàn)反斜杠并沒有轉(zhuǎn)義感嘆號:
- # echo "hi\!"
- hi\!
最簡單地方法是禁用 histexpand 選項(xiàng),你可以通過 set +H 或者 set +o histexpand 命令來完成。
下面四種寫法都可以解決:
- # 1. 使用單引號
- echo 'Hello World!'
- # 2. 禁用 histexpand 選項(xiàng)
- set +H
- echo "Hello World!"
- # 3. 重置 histchars
- histchars=
- # 4. 控制 shell 展開的順序,命令行歷史展開是在單詞拆分之前執(zhí)行的
- # 參見:<a href="http://linux.die.net/man/1/bash" target="_blank">Bash man 手冊的History Expansion一節(jié)</a>
- exmark='!'
- echo "Hello, world$exmark"
24. for arg in $*
和大多數(shù) Shell 一樣,Bash 支持依次讀取單個命令行參數(shù)的語法。不過這并是$*或者$@,這兩種寫法都不正確,它們只能得到完整的參數(shù)列表,并非單獨(dú)的一個個參數(shù)。
正確的語法是(沒錯要加上引號):
- for arg in "$@"
- # 或者更簡單的寫法
- for arg
在腳本中遍歷所有參數(shù)是一個再普遍不過的需求,所以 for arg 默認(rèn)等價于 for arg in "$@"。$@使用雙引號后就有特殊的魔力,每個參數(shù)展開后成為一個獨(dú)立的單詞。("$@"等價于"$1" "$2" "$3" ...)
下面是一個錯誤的例子:
- for x in $*; do
- echo "parameter: '$x'"
- done
執(zhí)行的結(jié)果為:
- $ ./myscript 'arg 1' arg2 arg3
- parameter: 'arg'
- parameter: '1'
- parameter: 'arg2'
- parameter: 'arg3'
正確的寫法:
- for x in "$@"; do
- echo "parameter: '$x'"
- done
執(zhí)行的結(jié)果為:
- $ ./myscript 'arg 1' arg2 arg3
- parameter: 'arg 1'
- parameter: 'arg2'
- parameter: 'arg3'
上面正確的例子中,第一個參數(shù)'arg 1'在展開后依然是一個獨(dú)立的單詞,而不會被拆分成兩個。
#p#
25. function foo()
這種寫法不一定能夠兼容所有 shell,兼容的寫法是:
- foo() {
- ...
- }
26. echo "~"
波浪號展開(Tilde expansion)僅當(dāng)~沒有引號的時候發(fā)生,在上面的例子中,只會向標(biāo)準(zhǔn)輸出打印~符號,而不是當(dāng)前用戶的家目錄路徑。
當(dāng)用引號將路徑參數(shù)引起來時,如果要用引號將相對于家目錄的路徑引起來時,推薦使用 $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)
當(dāng)在函數(shù)中聲明局部變量時,local作為一個獨(dú)立的命令,這種奇特的行為有時候可能會導(dǎo)致困擾。比如,當(dāng)你想要捕獲命令替換的返回碼時,你就不能這樣做。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/'
單引號內(nèi)部不會展開 $foo變量,在這里可以換成雙引號:
- foo="hello"; sed "s/$foo/good bye/"
但是要注意,如果你使用了雙引號,就需要考慮更多轉(zhuǎn)義的事情,具體可以看Quotes這一頁。.
30. tr [A-Z] [a-z]
這里至少有三個問題。第一個問題是, [A-Z] 和 [a-z] 會被 shell 認(rèn)為是通配符。如果在當(dāng)前目錄下沒用文件名為單個字母的文件,這個命令似乎能正確執(zhí)行,否則會錯誤地執(zhí)行,也許你會在周末耗費(fèi)許多小時來修復(fù)這個問題。
第二個問題是,這不是 tr 命令正確的寫法,實(shí)際上,上面的命令會把[轉(zhuǎn)換成[,將任意大寫字符轉(zhuǎn)換成對應(yīng)的小寫字符,將]轉(zhuǎn)換成],所以你根本不需要加上括號,這樣第一個問題就可以解決了。
第三個問題是,上面的命令執(zhí)行結(jié)果依賴于當(dāng)前的 locale,A-Z 或者 a-z 不一定會代表26個 ASCII 字母。實(shí)際上,在一些語言環(huán)境下,z 位于字母表的中間位置。這個問題的解法,取決于你希望發(fā)生的行為是哪一種。
如果你僅希望改變26個英文字母的大小寫(強(qiáng)制 locale為 C):
- LC_COLLATE=C tr A-Z a-z
如果你希望根據(jù)實(shí)際的語言環(huán)境來轉(zhuǎn)換:
- tr '[:upper:]' '[:lower:]'
31. ps ax | grep gedit
這里的根本問題是正在運(yùn)行的進(jìn)程名稱,本質(zhì)上是不可靠的??赡軙卸鄠€合法的gedit進(jìn)程,也有可能是別的東西偽裝成gedit進(jìn)程(改變執(zhí)行命令名稱是一件簡單的事情 ),更多細(xì)節(jié)可以看ProcessManagement這一篇文章。
執(zhí)行以上命令,往往會在結(jié)果中包含 grep 進(jìn)程:
- # ps ax | grep gedit
- 10530 ? S 6:23 gedit
- 32118 pts/0 R+ 0:00 grep gedit
這個時候,需要過濾多余的結(jié)果:
- # ps ax | grep -v grep | grep gedit
上面的寫法比較丑陋,另外一種方法是:
- # ps ax | grep [g]edit
32. printf "$foo"
如果$foo 變量的值中包括\或者%符號,上面命令的執(zhí)行結(jié)果可能會出乎你的意料之外。
下面是正確的寫法:
- printf %s "$foo"
- printf '%s\n' "$foo"
33. for i in {1..$n}
Bash的命令解釋器會優(yōu)先展開大括號,所以這時大括號{}表達(dá)式里面看到的是文字上的$n(沒有展開)。$n 不是一個數(shù)值,所以這里的大括號{}并不會展開成數(shù)字列表??梢?,這導(dǎo)致很難使用大括號來展開大小只能在運(yùn)行時才知道的列表。
可以用下面的方法:
- for ((i=1; i<=n; i++)); do
- ...
- done
注:之前我也有寫過一篇文章來介紹這個問題:Shell生成數(shù)字序列。
34. if [[ $foo = $bar ]]
在[[內(nèi)部,當(dāng)=號右邊的值沒有用引號引起來,bash 會將它當(dāng)作模式來匹配,而不是一個簡單的字符串。所以,在上面的例子中 ,如果 bar 的值是一個*號,執(zhí)行的結(jié)果永遠(yuǎn)是 true。
所以,如果你想檢查兩側(cè)的字符串是否相同,等號右側(cè)的值一定要用引號引起來。
- if [[ $foo = "$bar" ]]
如果你確實(shí)要執(zhí)行模式匹配,聰明的做法是取一個更加有意義的變量名(例如$patt),或者加上注釋說明。
35. if [[ $foo =~ 'some RE' ]]
同上,如果=~號右側(cè)的值加上引號,它會散失特殊的正則表達(dá)式含義,而變成一個普通的字符串。
如果你想使用一個長的或者復(fù)雜的正則表達(dá)式,避免大量的反斜杠轉(zhuǎn)義,建議把它放在一個變量中:
- re='some RE'
- if [[ $foo =~ $re ]]
36. [ -n $foo ] or [ -z $foo ]
這個例子中,$foo 沒有用引號引起來,當(dāng)$foo包含空格或者$foo為空時都會出問題:
- $ foo="some word" && [ -n $foo ] && echo yes
- -bash: [: some: binary operator expected
- $ foo="" && [ -n $foo ] && echo yes
- yes
正確的寫法是:
- [ -n "$foo" ]
- [ -z "$foo" ]
- [ -n "$(some command with a "$file" in it)" ]
- [[ -n $foo ]]
- [[ -z $foo ]]
37. [[ -e "$broken_symlink" ]] returns 1 even though $broken_symlink exists
這里-e 選項(xiàng)是看文件是否存在,當(dāng)緊跟的文件是一個軟鏈接時,它不看軟鏈接是否存在,而是看實(shí)際指向的文件是否存在。所以當(dāng)軟鏈接損壞時,即實(shí)際指向的文件被刪除后,-e 的結(jié)果返回1。
所以如果你確實(shí)要判斷后面的文件是否存在,正確的寫法是:
- [[ -e "$broken_symlink" || -L "$broken_symlink" ]]
#p#
38. ed file <<<"g/d\{0,3\}/s//e/g" fails
ed 命令使用的正則語法,不支持0次出現(xiàn)次數(shù),下面的就可以正常工作:
- ed file <<<"g/d\{1,3\}/s//e/g"
略過,現(xiàn)在很少會有人用 ed 命令吧。
39. expr sub-string fails for "match"
下面的例子多數(shù)情況下運(yùn)行不會有問題:
- word=abcde
- expr "$word" : ".\(.*\)"
- bcde
但是當(dāng) $work 不巧剛好是 match 時,就有可能出錯了(MAC OSX 下的 expr 命令不支持 match,所以依然能正常工作):
- word=match
- expr "$word" : ".\(.*\)"
原因是 match 是 expr 命令里面的一個特殊關(guān)鍵字,針對 GNU系統(tǒng),解決方法是在前面加一個'+':
- word=match
- expr + "$word" : ".\(.*\)"
- atch
'+'號可以讓 expr 命令忽略后續(xù) token 的特殊含義。
另外一個建議是,不要再使用 expr 命令了,expr 能做的事情都可以用 Bash 原生支持的參數(shù)展開(Parameter Expansion)或者字符串展開(Substring Expansion)來完成。并且相同情況下,內(nèi)置的功能肯定比外部命令的效率要高。
上面的例子,目的是為了刪除單詞中的首字符,可以這樣做:
- $ word=match
- $ echo "${word#?}" # PE
- atch
- $ echo "${word:1}" # SE
- atch
40. On UTF-8 and Byte-Order Marks (BOM)
多數(shù)情況下,UNIX 下 UTF-8 類型的文本不需要使用 BOM,文本的編碼是根據(jù)當(dāng)前語言環(huán)境,MIME類型或者其它文件元數(shù)據(jù)信息確定的。人為閱讀時,不會因?yàn)樵谖募_始處加 BOM 標(biāo)記而腚影響,但是當(dāng)文件要被腳本解釋執(zhí)行時,BOM 標(biāo)記會像 MS-DOS 下的換行符(^M)一樣奇怪。
41. content=$(<file)
這里沒有什么錯誤,不過你要知道命令替換會刪除結(jié)尾多余的換行符。
略過,原文給的優(yōu)化方法需要 Bash 4.2+ 以上的版本,手頭沒有這樣的環(huán)境。
42. somecmd 2>&1 >>logfile
這是一個很常見的錯誤,顯然你本來是想將標(biāo)準(zhǔn)輸出與標(biāo)準(zhǔn)錯誤輸出都重定向到文件logfile 中,但是你會驚訝地發(fā)現(xiàn),標(biāo)準(zhǔn)錯誤依然輸出到屏幕中。
這種行為的原因是,重定向在命令執(zhí)行之前解析,并且是從左往右解析。上面的命令可以翻譯成,將標(biāo)準(zhǔn)錯誤輸出重定向到標(biāo)準(zhǔn)輸出(此刻是終端),然后將標(biāo)準(zhǔn)輸出重定向到文件 logfile 中。所以,到最后,標(biāo)準(zhǔn)錯誤并沒有重定向到文件中,而是依然輸出到終端:
- somecmd >>logfile 2>&1
更加詳細(xì)的說明見BashFAQ。
43. cmd; (( ! $? )) || die
只有需要捕獲上一個命令的執(zhí)行結(jié)果進(jìn),才需要記錄$?的值,否則如果你只需要檢查上一個命令是否執(zhí)行成功,直接檢測命令:
- if cmd; then
- ...
- fi
或者使用 case 語句來檢測多個或能的返回碼:
- cmd
- status=$?
- case $status in
- 0)
- echo success >&2
- ;;
- 1)
- echo 'Must supply a parameter, exiting.' >&2
- exit 1
- ;;
- *)
- echo 'Unknown error, exiting.' >&2
- exit $status
- esac
原文鏈接:http://kodango.com/bash-pitfalls-part-1