日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢(xún)
選擇下列產(chǎn)品馬上在線(xiàn)溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷(xiāo)解決方案
Bash編程易犯的錯(cuò)誤

前一段時(shí)間發(fā)現(xiàn)一個(gè)很好的wiki站點(diǎn),上面有很多優(yōu)秀的Bash文章。最近挑了一篇介紹Bash編程容易犯的各種錯(cuò)誤的文章看,收獲很多,不感獨(dú)享,把這篇文章以半翻譯半筆記的形式分享給大家。

為鞏義等地區(qū)用戶(hù)提供了全套網(wǎng)頁(yè)設(shè)計(jì)制作服務(wù),及鞏義網(wǎng)站建設(shè)行業(yè)解決方案。主營(yíng)業(yè)務(wù)為成都做網(wǎng)站、成都網(wǎng)站建設(shè)、鞏義網(wǎng)站設(shè)計(jì),以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務(wù),秉承以專(zhuān)業(yè)、用心的態(tài)度為用戶(hù)提供真誠(chéng)的服務(wù)。我們深信只要達(dá)到每一位用戶(hù)的要求,就會(huì)得到認(rèn)可,從而選擇與我們長(zhǎng)期合作。這樣,我們也可以走得更遠(yuǎn)!

1. for i in $(ls *.mp3)

Bash寫(xiě)循環(huán)代碼的時(shí)候,確實(shí)比較容易犯下面的錯(cuò)誤:

 
 
 
  1. for i in $(ls *.mp3); do    # 錯(cuò)誤! 
  2.     some command $i         # 錯(cuò)誤! 
  3. done 
  4.   
  5. for i in $(ls)              # 錯(cuò)誤! 
  6. for i in `ls`               # 錯(cuò)誤! 
  7.   
  8. for i in $(find . -type f)  # 錯(cuò)誤! 
  9. for i in `find . -type f`   # 錯(cuò)誤! 
  10.   
  11. files=($(find . -type f))   # 錯(cuò)誤! 
  12. for i in ${files[@]}        # 錯(cuò)誤! 

這里主要兩個(gè)問(wèn)題:

  • 使用命令展開(kāi)時(shí)不帶引號(hào),其執(zhí)行結(jié)果會(huì)使用IFS作為分隔符,拆分成參數(shù)傳遞給for循環(huán)處理;
  • 不應(yīng)該讓腳本去解析ls命令的結(jié)果;

我們不能避免某些文件名中包含空格,Shell會(huì)對(duì)$(ls *.mp3)展開(kāi)的結(jié)果會(huì)被做單詞拆分(WordSplitting)的處理。假設(shè)有一個(gè)文件,名字為01 – Don’t Eat the Yellow Snow.mp3,for循環(huán)處理的時(shí)候,會(huì)今次遍歷文件名中的每個(gè)單詞:01, -, Don’t, Eat等等:

 
 
 
  1. $ for i in $(ls *.mp3); do echo $i; done 
  2. 01 
  3. Don't 
  4. Eat 
  5. the 
  6. Yellow 
  7. Snow.mp3 

比這更差的情況是,上面命令展開(kāi)的結(jié)果可能被Shell進(jìn)一步處理,比如文件名展開(kāi)。比如,ls執(zhí)行的結(jié)果中包含*號(hào),按照通配符的規(guī)則, *號(hào)會(huì)被展開(kāi)成當(dāng)前目錄下的所有文件:

 
 
 
  1. $ touch "1*.mp3" "1.mp3" "11.mp3" "12.mp3" 
  2. $ for i in $(ls *.mp3); do echo $i; done 
  3. 1*.mp3 1.mp3 11.mp3 12.mp3 
  4. 1.mp3 
  5. 11.mp3 
  6. 12.mp3 
  7. 1.mp3 
  8. 11.mp3 
  9. 12.mp3 

不過(guò),在這種場(chǎng)景下,你即使加上引號(hào),也是無(wú)濟(jì)于事的:

 
 
 
  1. $ for i in "$(ls *.mp3)"; do echo --$i--; done 
  2. --1*.mp3 1.mp3 11.mp3 12.mp3-- 

加上引號(hào)后,ls執(zhí)行的結(jié)果會(huì)被當(dāng)成一個(gè)整體,所以for循環(huán)只會(huì)執(zhí)行一次,達(dá)不到預(yù)期的效果。

事實(shí)上,這種情況下,根本不需要使用ls命令。ls命令的結(jié)果本身就設(shè)計(jì)成給人讀的,而不是給腳本解析的。正確的處理方法是,直接使用文件名展開(kāi)(通配符)的功能:

 
 
 
  1. $ for i in *.mp3; do 
  2. >     echo "$i" 
  3. > done 
  4. 1*.mp3 
  5. 1.mp3 
  6. 11.mp3 
  7. 12.mp3 

文件名展開(kāi)是位于各種展開(kāi)(花括號(hào)展開(kāi)、變量替換、命令展開(kāi)等)功能中的最后一個(gè)環(huán)節(jié),所以不會(huì)有之前不帶引號(hào)的命令展開(kāi)的副作用。如果你需要遞歸地處理文件,可以考慮使用Find命令。

到這一步,之間的問(wèn)題看樣子已經(jīng)修復(fù)了。但是,如果你進(jìn)一步思考,假設(shè)當(dāng)前目錄上沒(méi)有文件時(shí)會(huì)怎么樣?沒(méi)有文件的時(shí)候,*.mp3不會(huì)被展開(kāi)直接傳 遞給for循環(huán)處理,所以這個(gè)時(shí)候循環(huán)還是會(huì)執(zhí)行一次。這種情況不是我們預(yù)期的行為。保險(xiǎn)起見(jiàn),可以在循環(huán)處理的時(shí)候,檢查下文件是否存在:

 
 
 
  1. # POSIX 
  2. for i in *.mp3; do 
  3.     [ -e "$i" ] || continue 
  4.     some command "$i" 
  5. done 

如果你有使用引號(hào)和避免單詞拆分的習(xí)慣,你完全可以避免很多錯(cuò)誤。

注意下循環(huán)體內(nèi)部的”$i”,這里會(huì)導(dǎo)致下面我們要說(shuō)的另外一個(gè)比較容易犯的錯(cuò)誤。

2. cp $file $target

上面的命令有什么問(wèn)題呢?如果你提前知道,$file和$target文件名中不會(huì)包含空格或者*號(hào)。否則,這行命令執(zhí)行前在經(jīng)過(guò)單詞拆分和文件名展開(kāi)的時(shí)候會(huì)出現(xiàn)問(wèn)題。所以,兩次強(qiáng)調(diào),在使用展開(kāi)的地方切勿忘記使用引號(hào):

 
 
 
  1. $ cp -- "$file" "$target" 

如果不帶引號(hào),當(dāng)你執(zhí)行如下命令時(shí)就會(huì)出錯(cuò):

 
 
 
  1. $ file="01 - Don't Eat the Yellow Snow.mp3" 
  2. $ target="/tmp" 
  3. $ cp $file $target 
  4. cp: cannot stat ‘01’: No such file or directory 
  5. .. 

如果帶上引號(hào),就不會(huì)有上面的問(wèn)題,除非文件名以’-'開(kāi)頭,在這種情況下,cp會(huì)認(rèn)為你提供的是一個(gè)命令行選項(xiàng),這個(gè)錯(cuò)誤下面會(huì)介紹。

3. 文件名中包含短橫’-’

文件名以’-'開(kāi)頭會(huì)導(dǎo)致許多問(wèn)題,*.mp3這種通配符會(huì)根據(jù)當(dāng)前的locale展開(kāi)成一個(gè)列表,但在絕大多數(shù)環(huán)境下,’-'排序的時(shí)候會(huì)排在大多數(shù)字母前。這個(gè)展開(kāi)的列表傳遞給有些命令的時(shí)候,會(huì)錯(cuò)誤的將-filename解析成命令行選項(xiàng)。這里有兩種方法來(lái)解決這個(gè)問(wèn)題。

第一種方法是在命令和參數(shù)之間加上–,這種語(yǔ)法告訴命令不要繼續(xù)對(duì)–之后的內(nèi)容進(jìn)行命令行參數(shù)/選項(xiàng)解析:

 
 
 
  1. $ cp -- "$file" "$target" 

這種方法可以解這個(gè)問(wèn)題,但是你需要在每個(gè)命令后面都要加上–,而且依賴(lài)具體的命令解析的方式,如果一些命令不兼容這種約定俗成的規(guī)范,這種做法是無(wú)效的。

另外一種方法是,確保文件名都使用相對(duì)或者絕對(duì)的路徑,以目錄開(kāi)頭:

 
 
 
  1. for i in ./*.mp3; do 
  2.     cp "$i" /target 
  3.     ... 
  4. done 

這種情況下,即使某個(gè)文件以-開(kāi)頭,展開(kāi)后文件名依然是./-foo.mp3這種形式,完全不會(huì)有問(wèn)題。

4. [ $foo = "bar" ]

這是一個(gè)與第2個(gè)問(wèn)題類(lèi)似的問(wèn)題,雖然用到了引號(hào),但是放錯(cuò)了位置,對(duì)于字符串字面值,除非有特殊符號(hào),否則不大需要用引號(hào)括起來(lái)。但是,你應(yīng)該把變量的值用括號(hào)括起來(lái),從而避免它們包含空格或能通配符,這一點(diǎn)我們?cè)谇懊娴膯?wèn)題中都解釋過(guò)。

這個(gè)例子在以下情況下會(huì)出錯(cuò):

如果[中的變量不存在,或者為空,這個(gè)時(shí)候上面的例子最終解析結(jié)果是:

 
 
 
  1. [ = "bar" ] # 錯(cuò)誤! 

并且執(zhí)行會(huì)出錯(cuò):unary operator expected,因?yàn)?是二元操作符,它需要左右各一個(gè)操作數(shù)。

如果變量值包含空格,它首先在執(zhí)行之前進(jìn)行單詞拆分,因此[命令看到的樣子可能是這樣的:

 
 
 
  1. [ multiple words here = "bar" ]; 

正確的做法應(yīng)該是:

 
 
 
  1. # POSIX 
  2. [ "$foo" = bar ] 

這種寫(xiě)法,在POSIX兼容的實(shí)現(xiàn)中都不會(huì)有問(wèn)題,即使$foo以短橫"-"開(kāi)頭,因?yàn)镻OSIX實(shí)現(xiàn)的test命令通過(guò)傳遞的參數(shù)來(lái)確定執(zhí)行的行為。

只有一些非常古老的shell可能會(huì)遇到問(wèn)題,這個(gè)時(shí)候你可以使用下面的寫(xiě)法來(lái)解決(相信你肯定看到過(guò)這種寫(xiě)法):

 
 
 
  1. # POSIX / Bourne 
  2. [ x"$foo" = xbar ] 

在Bash中,還有另外一種選擇是使用[[關(guān)鍵字:

 
 
 
  1. # Bash / Ksh 
  2. [[ $foo == bar ]] 

這里你不需要使用引號(hào),因?yàn)樵赱[里面參數(shù)不會(huì)進(jìn)行展開(kāi),當(dāng)然帶上引號(hào)也不會(huì)有錯(cuò)。

不過(guò)有一點(diǎn)要注意的是,[[里的==不僅僅是文本比較,它會(huì)檢查左邊的值是否匹配右側(cè)的表達(dá)式,==右側(cè)的值加上引號(hào),會(huì)讓它成為一個(gè)普通的字面量,*?等通配符會(huì)失去特殊含義。

5. cd $(dirname "$f")

這又是一個(gè)引號(hào)的問(wèn)題,命令展開(kāi)的結(jié)果會(huì)進(jìn)一步地進(jìn)行單詞拆分或者文件名展開(kāi)。因此下面的寫(xiě)法才是正確的:

 
 
 
  1. cd "$(dirname "$f")" 

但是,上面引號(hào)的寫(xiě)法可能比較怪異,你可能會(huì)認(rèn)為第一、二個(gè)引號(hào),第三、四個(gè)引號(hào)是一組的。

但是事實(shí)上,Bash將命令替換里面的引號(hào)當(dāng)成一組,外面的當(dāng)成另外一組。如果你是用反引號(hào)的寫(xiě)法,引號(hào)的行為就不是這樣的了,所以$()寫(xiě)法更加推薦。

#p#

6. [ "$foo" = bar && "$bar" = foo ]

不要在test命令內(nèi)部使用&&,Bash解析器會(huì)把你的命令分隔成兩個(gè)命令,在&&之前和之后。你應(yīng)該使用下面的寫(xiě)法:

 
 
 
  1. [ bar = "$foo" ] && [ foo = "$bar" ] # POSIX 
  2. [[ $foo = bar && $bar = foo ]]       # Bash / Ksh 

盡量避免使用下面的寫(xiě)法,雖然它是正確的,但是這種寫(xiě)法可移植性不好,并且已經(jīng)在POSIX-2008中被廢棄:

 
 
 
  1. [ bar = "$foo" -a foo = "$bar" ] 

7. [[ $foo > 7 ]]

原文作者認(rèn)為算術(shù)比較不應(yīng)該用[[,而是用((,我沒(méi)弄明白是為什么。

如果有理解的同學(xué),歡迎以評(píng)論回復(fù),謝謝。

8. grep foo bar | while read -r; do ((count++)); done

這種寫(xiě)法初看沒(méi)有問(wèn)題,但是你會(huì)發(fā)現(xiàn)當(dāng)執(zhí)行完后,count變量并沒(méi)有變化。原因是管道后面的命令是在一個(gè)子Shell中執(zhí)行的。

POSIX規(guī)范并沒(méi)有說(shuō)明管道的最后一個(gè)命令是不是在子Shell中執(zhí)行的。一些shell,例如ksh93或者Bash>=4.2可以通過(guò)shopt -s lastpipe命令,指明管道中的最后一個(gè)命令在當(dāng)前shell中執(zhí)行。由于篇幅限制,在此就不展開(kāi),有興趣的可以看Bash FAQ #24。

9. if [grep foo myfile]

初學(xué)者會(huì)錯(cuò)誤地認(rèn)為,[是if語(yǔ)法的一部分,正如C語(yǔ)言中的if ()。但是事實(shí)并非如此,if后面跟著的是一個(gè)命令,[是一個(gè)命令,它是內(nèi)置命令test的簡(jiǎn)寫(xiě)形式,只不過(guò)它要求最后一個(gè)參數(shù)必須是]。下面兩種寫(xiě)法是一樣的:

 
 
 
  1. # POSIX 
  2. if [ false ]; then echo "HELP"; fi 
  3. if test false; then echo "HELP"; fi 

兩個(gè)都是檢查參數(shù)"false"是不是非空的,所以上面兩個(gè)語(yǔ)句都會(huì)輸出HELP。

if語(yǔ)句的語(yǔ)法是:

 
 
 
  1. if COMMANDS 
  2. then  
  3. elif  # optional 
  4. then  
  5. else  # optional 
  6. fi # required 

再次強(qiáng)調(diào),[是一個(gè)命令,它同其它常規(guī)的命令一樣接受參數(shù)。if是一個(gè)復(fù)合命令,它包含其它命令,[并不是if語(yǔ)法中的一部分。

如果你想根據(jù)grep命令的結(jié)果來(lái)做事情,你不需要把grep放到[里面,只需要在if后面緊跟grep即可:

 
 
 
  1. if grep -q fooregex myfile; then 
  2. ... 
  3. fi 

如果grep在myfile中找到匹配的行,它的執(zhí)行結(jié)果為0(true),then后面的部分就會(huì)執(zhí)行。

10. if [bar="$foo"]; then ...

正如上一個(gè)問(wèn)題中提到的,[是一個(gè)命令,它的參數(shù)之間必須用空格分隔。

11. if [ [ a = b ] && [ c = d ] ]; then ...

不要用把[命令看成C語(yǔ)言中if語(yǔ)句的條件一樣,它是一個(gè)命令。

如果你想表達(dá)一個(gè)復(fù)合的條件表達(dá)式,可以這樣寫(xiě):

 
 
 
  1. if [ a = b ] && [ c = d ]; then ... 

注意,if后面有兩個(gè)命令,它們用&&分開(kāi)。等價(jià)于下面的寫(xiě)法:

 
 
 
  1. if test a = b && test c = d; then ... 

如果第一個(gè)test(或者[)命令返回false,then后面的語(yǔ)句不會(huì)執(zhí)行;如果第一個(gè)返回true,第二個(gè)test命令會(huì)執(zhí)行;只有第二個(gè)命令同樣返回true的情況下,then后面的語(yǔ)句才會(huì)執(zhí)行。

除此之外,還可以使用[[關(guān)鍵字,因?yàn)樗С?&的用法:

 
 
 
  1. if [[ a = b && c = d ]]; then ... 

12. read $foo

read命令中你不需要在變量名之前使用$。如果你想把讀入的數(shù)據(jù)存放到名為foo的變量中,下面的寫(xiě)法就夠了:

 
 
 
  1. read foo 

或者,更加安全地方法:

 
 
 
  1. IFS= read -r foo 

read $foo會(huì)把一行的內(nèi)容讀入到變量中,該變量的名稱(chēng)存儲(chǔ)在$foo中。所以?xún)烧叩暮x是完全不一樣的。

13. cat file | sed s/foo/bar/ > file

你不應(yīng)該在一個(gè)管道中,從一個(gè)文件讀的同時(shí),再往相同的文件里面寫(xiě),這樣的后果是未知的。

你可以為此創(chuàng)建一個(gè)臨時(shí)文件,這種做法比較安全可靠:

 
 
 
  1. # sed 's/foo/bar/g' file > tmpfile && mv tmpfile file 

或者,如果你用得是 GNU Sed 4.x 以上的版本,可以使用-i 選項(xiàng)即時(shí)修改文件的內(nèi)容:

 
 
 
  1. # sed -i 's/foo/bar/g' file 

14. echo $foo

這種看似無(wú)害的命令往往會(huì)給初學(xué)者千萬(wàn)極大的困擾,他們會(huì)懷疑是不是因?yàn)?$foo 變量的值是錯(cuò)誤的。事實(shí)卻是因?yàn)椋?foo 變量在這里沒(méi)有使用雙引號(hào),所以在解析的時(shí)候會(huì)進(jìn)行單詞拆分和文件名展開(kāi),最終導(dǎo)致執(zhí)行結(jié)果與預(yù)期大相徑庭:

 
 
 
  1. msg="Please enter a file name of the form *.zip" 
  2. echo $msg 

這里整句話(huà)會(huì)被拆分成單詞,然后其中的通配符會(huì)被展開(kāi),例如*.zip。當(dāng)你的用戶(hù)看到如下的結(jié)果時(shí),他們會(huì)怎樣想:

 
 
 
  1. Please enter a file name of the form freenfss.zip lw35nfss.zip 

再舉一個(gè)例子(假設(shè)當(dāng)前目錄下有以 .zip 結(jié)尾的文件):

 
 
 
  1. var="*.zip"   # var 包括一個(gè)星號(hào),一個(gè)點(diǎn)號(hào)和 zip 
  2. echo "$var"   # 輸出 *.zip 
  3. echo $var     # 輸出所有以 .zip 結(jié)尾的文件 

實(shí)際上,這里使用 echo 命令并不是絕對(duì)的安全。例如,當(dāng)變量的值包含-n時(shí),echo 會(huì)認(rèn)為它是一個(gè)合法的選項(xiàng)而不是要輸出的內(nèi)容(當(dāng)然如果你能夠保證不會(huì)有-n 這種值,可以放心地使用 echo 命令)。

完全可靠的打印變量值的方法是使用 printf:

 
 
 
  1. printf "%s\n" "$foo" 

15. $foo=bar

略過(guò)

16. foo = bar

當(dāng)賦值時(shí),等號(hào)兩邊是不允許出現(xiàn)空格的,這同 C 語(yǔ)言不一樣。當(dāng)你寫(xiě)下 foo = bar 時(shí),shell 會(huì)將該命令解析成三個(gè)單詞,然后第一個(gè)單詞 foo 會(huì)被認(rèn)為是一個(gè)命令,后面的內(nèi)容會(huì)被當(dāng)作命令參數(shù)。

同樣地,下面的寫(xiě)法也是錯(cuò)誤的:

 
 
 
  1. foo= bar    # WRONG! 
  2. foo =bar    # WRONG! 
  3. $foo = bar; # COMPLETELY WRONG! 
  4.   
  5. 正確的寫(xiě)法應(yīng)該是這樣的: 
  6.  
  7. foo=bar     # Right. 
  8. foo="bar"   # More Right. 

17. echo <

當(dāng)腳本需要嵌入大段的文本內(nèi)容時(shí),here document往往是一個(gè)非常有用的工具,它將其中的文本作為命令的標(biāo)準(zhǔn)輸入。不過(guò),echo 命令并不支持從標(biāo)準(zhǔn)輸入讀取內(nèi)容,所以下面的寫(xiě)法是錯(cuò)誤的:

 
 
 
  1. # This is wrong: 
  2. echo <
  3. Hello world 
  4. How's it going? 
  5. EOF 

正確的方法是,使用 cat 命令來(lái)完成:

 
 
 
  1. # This is what you were trying to do: 
  2. cat <
  3. Hello world 
  4. How's it going? 
  5. EOF 

或者可以使用雙引號(hào),它也可以跨越多行,而且因?yàn)?echo 命令是內(nèi)置命令,相同情況下它會(huì)更加高效:

 
 
 
  1. echo "Hello world 
  2. How's it going?" 

#p#

18. su -c 'some command'

這種寫(xiě)法“幾乎”是正確的。問(wèn)題是,在許多平臺(tái)上,su 支持 -c 參數(shù),但是它不一定是你認(rèn)為的。比如,在 OpenBSD 平臺(tái)上你這樣執(zhí)行會(huì)出錯(cuò):

 
 
 
  1. $ su -c 'echo hello' 
  2. su: only the superuser may specify a login class 

在這里,-c是用于指定login-class。如果你想要傳遞 -c 'some command' 給 shell,最好在之前顯示地指定 username:

 
 
 
  1. $ su root -c 'some command' # Now it's right. 

19. cd /foo; bar

如果你不檢查 cd 命令執(zhí)行是否成功,你可以會(huì)在錯(cuò)誤的目錄下執(zhí)行 bar 命令,這有可能會(huì)帶來(lái)災(zāi)難,比如 bar 命令是 rm -rf *。

你必須經(jīng)常檢查 cd 命令執(zhí)行是否有錯(cuò)誤,簡(jiǎn)單的做法是:

 
 
 
  1. cd /foo && bar 

如果在 cd 命令后有多個(gè)命令,你可以選擇這樣寫(xiě):

 
 
 
  1. cd /foo || exit 1 
  2. bar 
  3. baz 
  4. bat ... # Lots of commands 

出錯(cuò)時(shí),cd 命令會(huì)報(bào)告無(wú)法改變當(dāng)前目錄,同時(shí)將錯(cuò)誤消息輸出到標(biāo)準(zhǔn)錯(cuò)誤,例如"bash: cd: /foo: No such file or directory"。如果你想要在標(biāo)準(zhǔn)輸出同時(shí)輸出自定義的錯(cuò)誤提示,可以使用復(fù)合命令(command grouping):

 
 
 
  1. cd /net || { echo "Can't read /net. Make sure you've logged in to the Samba network, and try again."; exit 1; } 
  2. do_stuff 
  3. more_stuff 

注意,在{號(hào)和 echo 之間需要有一個(gè)空格,同時(shí)}之前要加上分號(hào)。

順便提一下,如果你要在腳本里頻繁改變當(dāng)前目錄,可以看看 pushd/popd/dirs 等命令,可能你在代碼里面寫(xiě)的 cd/pwd 命令都是沒(méi)有必要的。

說(shuō)到這,比較下下面兩種寫(xiě)法:

 
 
 
  1. find ... -type d -print0 | while IFS= read -r -d '' subdir; do 
  2.    here=$PWD 
  3.    cd "$subdir" && whatever && ... 
  4.    cd "$here" 
  5. done 
 
 
 
  1. find ... -type d -print0 | while IFS= read -r -d '' subdir; do 
  2.    (cd "$subdir" || exit; whatever; ...) 
  3. done 

下面的寫(xiě)法,在循環(huán)中 fork 了一個(gè)子 shell 進(jìn)程,子 shell 進(jìn)程中的 cd 命令僅會(huì)影響當(dāng)前 shell的環(huán)境變量,所以父進(jìn)程中的環(huán)境命令不會(huì)被改變;當(dāng)執(zhí)行到下一次循環(huán)時(shí),無(wú)論之前的 cd 命令有沒(méi)有執(zhí)行成功,我們會(huì)回到相同的當(dāng)前目錄。這種寫(xiě)法相較前面的用法,代碼更加干凈。

20. [ bar == "$foo" ]

正確的用法:

 
 
 
  1. [ bar = "$foo" ] && echo yes 
  2. [[ bar == $foo ]] && echo yes 

21. for i in {1..10}; do ./something &; done

你不應(yīng)該在&后面添加分號(hào),刪除它:

 
 
 
  1. for i in {1..10}; do ./something & done 

或者改成多行的形式:

 
 
 
  1. for i in {1..10}; do 
  2.     ./something & 
  3. done 

&和分號(hào)一樣也可以用作命令終止符,所以你不要將兩個(gè)混用到一起。一般情況下,分號(hào)可以被換行符替換,但是不是所有的換行符都可以用分號(hào)替換。

22. cmd1 && cmd2 || cmd3

有些人喜歡把&&和||作為if...then...else...fi 的簡(jiǎn)寫(xiě)語(yǔ)法,在多數(shù)情況下,這種寫(xiě)法沒(méi)有問(wèn)題。例如:

 
 
 
  1. [[ -s $errorlog ]] && echo "Uh oh, there were some errors." || echo "Successful." 

但是,這種結(jié)構(gòu)并不是在所有情況下都完全等價(jià)于 if...fi 語(yǔ)法。這是因?yàn)樵?&后面的命令執(zhí)行結(jié)束時(shí)也會(huì)生成一個(gè)返回碼,如果該返回碼不是真值(0代表 true),||后面的命令也會(huì)執(zhí)行,例如:

 
 
 
  1. i=0 
  2. true && ((i++)) || ((i--)) 
  3. echo $i # 輸出 0 

看起來(lái)上面的結(jié)果應(yīng)該是返回1,但是結(jié)果卻是輸出0,為什么呢?原因是這里 i++ 和 i-- 都執(zhí)行了一遍。

其中,((i++))命令執(zhí)行算術(shù)運(yùn)算,表達(dá)式計(jì)算的結(jié)果為0。這里和 C 語(yǔ)言一樣,表達(dá)式的結(jié)果為0被認(rèn)為是 false。所以當(dāng) i=0 的時(shí)候,((i++))命令執(zhí)行的返回碼為1(false),從而會(huì)執(zhí)行接下來(lái)的((i--))命令。

如果我們?cè)谶@里使用前綴自增運(yùn)算符的話(huà),返回的結(jié)果恰恰為1,因?yàn)?(++i))執(zhí)行的返回碼是0(true):

 
 
 
  1. i=0 
  2. true && (( ++i )) || (( --i )) 
  3. echo $i # Prints 1 

不過(guò)在你無(wú)法保證 y 的執(zhí)行結(jié)果是,絕對(duì)不要依靠 x && y || z這種寫(xiě)法。上面這種巧合,在 i 初始化為-1時(shí)也會(huì)有問(wèn)題。

如果你喜歡代碼更加安全健壯,建議使用 if...fi 語(yǔ)法:

 
 
 
  1. i=0 
  2. if true; then 
  3.    ((i++)) 
  4. else 
  5.    ((i--)) 
  6. fi 
  7.   
  8. echo $i # 輸出 1 

23. echo "Hello World!"

在交互式的 Shell 環(huán)境下,你執(zhí)行以上命令會(huì)遇到下面的錯(cuò)誤:

 
 
 
  1. bash: !": event not found 

這是因?yàn)椋谀J(rèn)的交互式 Shell 環(huán)境下,Bash 發(fā)現(xiàn)感嘆號(hào)時(shí)會(huì)執(zhí)行歷史命令展開(kāi)。在 Shell 腳本中,這種行為是被禁止的,所以不會(huì)發(fā)生錯(cuò)誤。

不幸地是,你認(rèn)為明顯正確地修復(fù)方法,也不能工作,你會(huì)發(fā)現(xiàn)反斜杠并沒(méi)有轉(zhuǎn)義感嘆號(hào):

 
 
 
  1. # echo "hi\!" 
  2. hi\! 

最簡(jiǎn)單地方法是禁用 histexpand 選項(xiàng),你可以通過(guò) set +H 或者 set +o histexpand 命令來(lái)完成。

下面四種寫(xiě)法都可以解決:

 
 
 
  1. # 1. 使用單引號(hào) 
  2. echo 'Hello World!' 
  3.   
  4. # 2. 禁用 histexpand 選項(xiàng) 
  5. set +H 
  6. echo "Hello World!" 
  7.   
  8. # 3. 重置 histchars 
  9. histchars= 
  10.   
  11. # 4. 控制 shell 展開(kāi)的順序,命令行歷史展開(kāi)是在單詞拆分之前執(zhí)行的 
  12. # 參見(jiàn):Bash man 手冊(cè)的History Expansion一節(jié) 
  13. exmark='!' 
  14. echo "Hello, world$exmark" 

24. for arg in $*

和大多數(shù) Shell 一樣,Bash 支持依次讀取單個(gè)命令行參數(shù)的語(yǔ)法。不過(guò)這并是$*或者$@,這兩種寫(xiě)法都不正確,它們只能得到完整的參數(shù)列表,并非單獨(dú)的一個(gè)個(gè)參數(shù)。

正確的語(yǔ)法是(沒(méi)錯(cuò)要加上引號(hào)):

 
 
 
  1. for arg in "$@" 
  2.   
  3. # 或者更簡(jiǎn)單的寫(xiě)法 
  4. for arg 

在腳本中遍歷所有參數(shù)是一個(gè)再普遍不過(guò)的需求,所以 for arg 默認(rèn)等價(jià)于 for arg in "$@"。$@使用雙引號(hào)后就有特殊的魔力,每個(gè)參數(shù)展開(kāi)后成為一個(gè)獨(dú)立的單詞。("$@"等價(jià)于"$1" "$2" "$3" ...)

下面是一個(gè)錯(cuò)誤的例子:

 
 
 
  1. for x in $*; do 
  2.    echo "parameter: '$x'" 
  3. done 

執(zhí)行的結(jié)果為:

 
 
 
  1. $ ./myscript 'arg 1' arg2 arg3 
  2. parameter: 'arg' 
  3. parameter: '1' 
  4. parameter: 'arg2' 
  5. parameter: 'arg3' 

正確的寫(xiě)法:

 
 
 
  1. for x in "$@"; do 
  2.    echo "parameter: '$x'" 
  3. done 

執(zhí)行的結(jié)果為:

 
 
 
  1. $ ./myscript 'arg 1' arg2 arg3 
  2. parameter: 'arg 1' 
  3. parameter: 'arg2' 
  4. parameter: 'arg3' 

上面正確的例子中,第一個(gè)參數(shù)'arg 1'在展開(kāi)后依然是一個(gè)獨(dú)立的單詞,而不會(huì)被拆分成兩個(gè)。

#p#

25. function foo()

這種寫(xiě)法不一定能夠兼容所有 shell,兼容的寫(xiě)法是:

 
 
 
  1. foo() { 
  2.   ... 

26. echo "~"

波浪號(hào)展開(kāi)(Tilde expansion)僅當(dāng)~沒(méi)有引號(hào)的時(shí)候發(fā)生,在上面的例子中,只會(huì)向標(biāo)準(zhǔn)輸出打印~符號(hào),而不是當(dāng)前用戶(hù)的家目錄路徑。

當(dāng)用引號(hào)將路徑參數(shù)引起來(lái)時(shí),如果要用引號(hào)將相對(duì)于家目錄的路徑引起來(lái)時(shí),推薦使用 $HOME 而不是 ~, 假如 $HOME 目錄是"/home/my photos",路徑中包含空格。

下面是幾組例子:

 
 
 
  1. "~/dir with spaces" # expands to "~/dir with spaces" 
  2. ~"/dir with spaces" # expands to "~/dir with spaces" 
  3. ~/"dir with spaces" # expands to "/home/my photos/dir with spaces" 
  4. "$HOME/dir with spaces" # expands to "/home/my photos/dir with spaces" 

27. local varname=$(command)

當(dāng)在函數(shù)中聲明局部變量時(shí),local作為一個(gè)獨(dú)立的命令,這種奇特的行為有時(shí)候可能會(huì)導(dǎo)致困擾。比如,當(dāng)你想要捕獲命令替換的返回碼時(shí),你就不能這樣做。local命令的返回碼會(huì)覆蓋它。

這種情況下,你只能分成兩行寫(xiě):

 
 
 
  1. local varname 
  2. varname=$(command) 
  3. rc=$? 

28. export foo=~/bar

export 與 local 命令一樣,并不是賦值語(yǔ)句的一部分。因此,在有些 Shell 下(比如Bash),export foo=~/bar會(huì)展開(kāi),但是有些(比如 Dash)卻不行。

下面是兩種比較健壯的寫(xiě)法:

 
 
 
  1. foo=~/bar; export foo    # Right! 
  2. export foo="$HOME/bar"   # Right! 

29. sed 's/$foo/good bye/'

單引號(hào)內(nèi)部不會(huì)展開(kāi) $foo變量,在這里可以換成雙引號(hào):

 
 
 
  1. foo="hello"; sed "s/$foo/good bye/" 

但是要注意,如果你使用了雙引號(hào),就需要考慮更多轉(zhuǎn)義的事情,具體可以看Quotes這一頁(yè)。.

30. tr [A-Z] [a-z]

這里至少有三個(gè)問(wèn)題。第一個(gè)問(wèn)題是, [A-Z] 和 [a-z] 會(huì)被 shell 認(rèn)為是通配符。如果在當(dāng)前目錄下沒(méi)用文件名為單個(gè)字母的文件,這個(gè)命令似乎能正確執(zhí)行,否則會(huì)錯(cuò)誤地執(zhí)行,也許你會(huì)在周末耗費(fèi)許多小時(shí)來(lái)修復(fù)這個(gè)問(wèn)題。

第二個(gè)問(wèn)題是,這不是 tr 命令正確的寫(xiě)法,實(shí)際上,上面的命令會(huì)把[轉(zhuǎn)換成[,將任意大寫(xiě)字符轉(zhuǎn)換成對(duì)應(yīng)的小寫(xiě)字符,將]轉(zhuǎn)換成],所以你根本不需要加上括號(hào),這樣第一個(gè)問(wèn)題就可以解決了。

第三個(gè)問(wèn)題是,上面的命令執(zhí)行結(jié)果依賴(lài)于當(dāng)前的 locale,A-Z 或者 a-z 不一定會(huì)代表26個(gè) ASCII 字母。實(shí)際上,在一些語(yǔ)言環(huán)境下,z 位于字母表的中間位置。這個(gè)問(wèn)題的解法,取決于你希望發(fā)生的行為是哪一種。

如果你僅希望改變26個(gè)英文字母的大小寫(xiě)(強(qiáng)制 locale為 C):

 
 
 
  1. LC_COLLATE=C tr A-Z a-z 

如果你希望根據(jù)實(shí)際的語(yǔ)言環(huán)境來(lái)轉(zhuǎn)換:

 
 
 
  1. tr '[:upper:]' '[:lower:]' 

31. ps ax | grep gedit

這里的根本問(wèn)題是正在運(yùn)行的進(jìn)程名稱(chēng),本質(zhì)上是不可靠的??赡軙?huì)有多個(gè)合法的gedit進(jìn)程,也有可能是別的東西偽裝成gedit進(jìn)程(改變執(zhí)行命令名稱(chēng)是一件簡(jiǎn)單的事情 ),更多細(xì)節(jié)可以看ProcessManagement這一篇文章。

執(zhí)行以上命令,往往會(huì)在結(jié)果中包含 grep 進(jìn)程:

 
 
 
  1. # ps ax | grep gedit 
  2. 10530 ?        S      6:23 gedit 
  3. 32118 pts/0    R+     0:00 grep gedit 

這個(gè)時(shí)候,需要過(guò)濾多余的結(jié)果:

 
 
 
  1. # ps ax | grep -v grep | grep gedit 

上面的寫(xiě)法比較丑陋,另外一種方法是:

 
 
 
  1. # ps ax | grep [g]edit 

32. printf "$foo"

如果$foo 變量的值中包括\或者%符號(hào),上面命令的執(zhí)行結(jié)果可能會(huì)出乎你的意料之外。

下面是正確的寫(xiě)法:

 
 
 
  1. printf %s "$foo" 
  2. printf '%s\n' "$foo" 

33. for i in {1..$n}

Bash的命令解釋器會(huì)優(yōu)先展開(kāi)大括號(hào),所以這時(shí)大括號(hào){}表達(dá)式里面看到的是文字上的$n(沒(méi)有展開(kāi))。$n 不是一個(gè)數(shù)值,所以這里的大括號(hào){}并不會(huì)展開(kāi)成數(shù)字列表??梢?jiàn),這導(dǎo)致很難使用大括號(hào)來(lái)展開(kāi)大小只能在運(yùn)行時(shí)才知道的列表。

可以用下面的方法:

 
 
 
  1. for ((i=1; i<=n; i++)); do 
  2. ... 
  3. done 

注:之前我也有寫(xiě)過(guò)一篇文章來(lái)介紹這個(gè)問(wèn)題:Shell生成數(shù)字序列。

34. if [[ $foo = $bar ]]

在[[內(nèi)部,當(dāng)=號(hào)右邊的值沒(méi)有用引號(hào)引起來(lái),bash 會(huì)將它當(dāng)作模式來(lái)匹配,而不是一個(gè)簡(jiǎn)單的字符串。所以,在上面的例子中 ,如果 bar 的值是一個(gè)*號(hào),執(zhí)行的結(jié)果永遠(yuǎn)是 true。

所以,如果你想檢查兩側(cè)的字符串是否相同,等號(hào)右側(cè)的值一定要用引號(hào)引起來(lái)。

 
 
 
  1. if [[ $foo = "$bar" ]] 

如果你確實(shí)要執(zhí)行模式匹配,聰明的做法是取一個(gè)更加有意義的變量名(例如$patt),或者加上注釋說(shuō)明。

35. if [[ $foo =~ 'some RE' ]]

同上,如果=~號(hào)右側(cè)的值加上引號(hào),它會(huì)散失特殊的正則表達(dá)式含義,而變成一個(gè)普通的字符串。

如果你想使用一個(gè)長(zhǎng)的或者復(fù)雜的正則表達(dá)式,避免大量的反斜杠轉(zhuǎn)義,建議把它放在一個(gè)變量中:

 
 
 
  1. re='some RE' 
  2. if [[ $foo =~ $re ]] 

36. [ -n $foo ] or [ -z $foo ]

這個(gè)例子中,$foo 沒(méi)有用引號(hào)引起來(lái),當(dāng)$foo包含空格或者$foo為空時(shí)都會(huì)出問(wèn)題:

 
 
 
  1. $ foo="some word" && [ -n $foo ] && echo yes 
  2. -bash: [: some: binary operator expected 
  3.   
  4. $ foo="" && [ -n $foo ] && echo yes 
  5. yes 

正確的寫(xiě)法是:

 
 
 
  1. [ -n "$foo" ] 
  2. [ -z "$foo" ] 
  3. [ -n "$(some command with a "$file" in it)" ] 
  4.   
  5. [[ -n $foo ]] 
  6. [[ -z $foo ]] 

37. [[ -e "$broken_symlink" ]] returns 1 even though $broken_symlink exists

這里-e 選項(xiàng)是看文件是否存在,當(dāng)緊跟的文件是一個(gè)軟鏈接時(shí),它不看軟鏈接是否存在,而是看實(shí)際指向的文件是否存在。所以當(dāng)軟鏈接損壞時(shí),即實(shí)際指向的文件被刪除后,-e 的結(jié)果返回1。

所以如果你確實(shí)要判斷后面的文件是否存在,正確的寫(xiě)法是:

 
 
 
  1. [[ -e "$broken_symlink" || -L "$broken_symlink" ]] 

#p#

38. ed file <<<"g/d\{0,3\}/s//e/g" fails

ed 命令使用的正則語(yǔ)法,不支持0次出現(xiàn)次數(shù),下面的就可以正常工作:

 
 
 
  1. ed file <<<"g/d\{1,3\}/s//e/g" 

略過(guò),現(xiàn)在很少會(huì)有人用 ed 命令吧。

39. expr sub-string fails for "match"

下面的例子多數(shù)情況下運(yùn)行不會(huì)有問(wèn)題:

 
 
 
  1. word=abcde 
  2. expr "$word" : ".\(.*\)" 
  3. bcde 

但是當(dāng) $work 不巧剛好是 match 時(shí),就有可能出錯(cuò)了(MAC OSX 下的 expr 命令不支持 match,所以依然能正常工作):

 
 
 
  1. word=match 
  2. expr "$word" : ".\(.*\)" 

原因是 match 是 expr 命令里面的一個(gè)特殊關(guān)鍵字,針對(duì) GNU系統(tǒng),解決方法是在前面加一個(gè)'+':

 
 
 
  1. word=match 
  2. expr + "$word" : ".\(.*\)" 
  3. atch 

'+'號(hào)可以讓 expr 命令忽略后續(xù) token 的特殊含義。

另外一個(gè)建議是,不要再使用 expr 命令了,expr 能做的事情都可以用 Bash 原生支持的參數(shù)展開(kāi)(Parameter Expansion)或者字符串展開(kāi)(Substring Expansion)來(lái)完成。并且相同情況下,內(nèi)置的功能肯定比外部命令的效率要高。

上面的例子,目的是為了刪除單詞中的首字符,可以這樣做:

 
 
 
  1. $ word=match 
  2. $ echo "${word#?}"    # PE 
  3. atch 
  4. $ echo "${word:1}"    # SE 
  5. atch 

40. On UTF-8 and Byte-Order Marks (BOM)

多數(shù)情況下,UNIX 下 UTF-8 類(lèi)型的文本不需要使用 BOM,文本的編碼是根據(jù)當(dāng)前語(yǔ)言環(huán)境,MIME類(lèi)型或者其它文件元數(shù)據(jù)信息確定的。人為閱讀時(shí),不會(huì)因?yàn)樵谖募_(kāi)始處加 BOM 標(biāo)記而腚影響,但是當(dāng)文件要被腳本解釋執(zhí)行時(shí),BOM 標(biāo)記會(huì)像 MS-DOS 下的換行符(^M)一樣奇怪。

41. content=$(

這里沒(méi)有什么錯(cuò)誤,不過(guò)你要知道命令替換會(huì)刪除結(jié)尾多余的換行符。

略過(guò),原文給的優(yōu)化方法需要 Bash 4.2+ 以上的版本,手頭沒(méi)有這樣的環(huán)境。

42. somecmd 2>&1 >>logfile

這是一個(gè)很常見(jiàn)的錯(cuò)誤,顯然你本來(lái)是想將標(biāo)準(zhǔn)輸出與標(biāo)準(zhǔn)錯(cuò)誤輸出都重定向到文件logfile 中,但是你會(huì)驚訝地發(fā)現(xiàn),標(biāo)準(zhǔn)錯(cuò)誤依然輸出到屏幕中。

這種行為的原因是,重定向在命令執(zhí)行之前解析,并且是從左往右解析。上面的命令可以翻譯成,將標(biāo)準(zhǔn)錯(cuò)誤輸出重定向到標(biāo)準(zhǔn)輸出(此刻是終端),然后將標(biāo)準(zhǔn)輸出重定向到文件 logfile 中。所以,到最后,標(biāo)準(zhǔn)錯(cuò)誤并沒(méi)有重定向到文件中,而是依然輸出到終端:

 
 
 
  1. somecmd >>logfile 2>&1 

更加詳細(xì)的說(shuō)明見(jiàn)BashFAQ。

43. cmd; (( ! $? )) || die

只有需要捕獲上一個(gè)命令的執(zhí)行結(jié)果進(jìn),才需要記錄$?的值,否則如果你只需要檢查上一個(gè)命令是否執(zhí)行成功,直接檢測(cè)命令:

 
 
 
  1. if cmd; then 
  2.     ... 
  3. fi 

或者使用 case 語(yǔ)句來(lái)檢測(cè)多個(gè)或能的返回碼:

 
 
 
  1. cmd 
  2. status=$? 
  3. case $status in 
  4.     0) 
  5.         echo success >&2 
  6.         ;; 
  7.     1) 
  8.         echo 'Must supply a parameter, exiting.' >&2 
  9.         exit 1 
  10.         ;; 
  11.     *) 
  12.         echo 'Unknown error, exiting.' >&2 
  13.         exit $status 
  14. esac 

原文鏈接:http://kodango.com/bash-pitfalls-part-1


標(biāo)題名稱(chēng):Bash編程易犯的錯(cuò)誤
網(wǎng)頁(yè)鏈接:http://m.5511xx.com/article/cogojij.html