上一篇文章參見 第一節:bash編程易犯的錯誤。
13. cat file | sed s/foo/bar/ > file
你不應該在一個管道中,從一個文件讀的同時,再往相同的文件里面寫,這樣的后果是未知的。
你可以為此創建一個臨時文件,這種做法比較安全可靠:
# sed 's/foo/bar/g' file > tmpfile && mv tmpfile file
或者,如果你用得是 gnu Sed 4.x 以上的版本,可以使用-i 選項即時修改文件的內容:
# sed -i 's/foo/bar/g' file
14. echo $foo
這種看似無害的命令往往會給初學者千萬極大的困擾,他們會懷疑是不是因為 $foo 變量的值是錯誤的。事實卻是因為,$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 再舉一個例子(假設當前目錄下有以 .zip 結尾的文件): var="*.zip" # var 包括一個星號,一個點號和 zip echo "$var" # 輸出 *.zip echo $var # 輸出所有以 .zip 結尾的文件
實際上,這里使用 echo 命令并不是絕對的安全。例如,當變量的值包含-n時,echo 會認為它是一個合法的選項而不是要輸出的內容(當然如果你能夠保證不會有-n 這種值,可以放心地使用 echo 命令)。
完全可靠的打印變量值的方法是使用 printf:
printf "%s " "$foo"
15. $foo=bar
略過
16. foo = bar
當賦值時,等號兩邊是不允許出現空格的,這同 C 語言不一樣。當你寫下 foo = bar 時,Shell 會將該命令解析成三個單詞,然后第一個單詞 foo 會被認為是一個命令,后面的內容會被當作命令參數。
同樣地,下面的寫法也是錯誤的:
foo= bar # WRONG! foo =bar # WRONG! $foo = bar; # COMPLETELY WRONG!
正確的寫法應該是這樣的:
foo=bar # Right. foo="bar" # more Right.
17. echo 或者可以使用雙引號,它也可以跨越多行,而且因為 echo 命令是內置命令,相同情況下它會更加高效:
echo "Hello world How's it going?"
18. su -c ‘some command’
這種寫法“幾乎”是正確的。問題是,在許多平臺上,su 支持 -c 參數,但是它不一定是你認為的。比如,在 OpenBSD 平臺上你這樣執行會出錯:
$ 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 命令執行是否成功,你可以會在錯誤的目錄下執行 bar 命令,這有可能會帶來災難,比如 bar 命令是 rm -rf *。
你必須經常檢查 cd 命令執行是否有錯誤,簡單的做法是:
cd /foo && bar 如果在 cd 命令后有多個命令,你可以選擇這樣寫: cd /foo || exit 1 bar baz bat ... # Lots of commands.
出錯時,cd 命令會報告無法改變當前目錄,同時將錯誤消息輸出到標準錯誤,例如”bash: cd: /foo: No such file or directory”。如果你想要在標準輸出同時輸出自定義的錯誤提示,可以使用復合命令(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 之間需要有一個空格,同時}之前要加上分號。
順便提一下,如果你要在腳本里頻繁改變當前目錄,可以看看 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
下面的寫法,在循環中 fork 了一個子 shell 進程,子 shell 進程中的 cd 命令僅會影響當前 shell的環境變量,所以父進程中的環境命令不會被改變;當執行到下一次循環時,無論之前的 cd 命令有沒有執行成功,我們會回到相同的當前目錄。這種寫法相較前面的用法,代碼更加干凈。
20. [ bar == “$foo” ]
正確的用法:
[ bar = "$foo" ] && echo yes [[ bar == $foo ]] && echo yes
21. for i in {1..10}; do ./something &; done
你不應該在&后面添加分號,刪除它:
for i in {1..10}; do ./something & done 或者改成多行的形式: for i in {1..10}; do ./something & done
&和分號一樣也可以用作命令終止符,所以你不要將兩個混用到一起。一般情況下,分號可以被換行符替換,但是不是所有的換行符都可以用分號替換。
22. cmd1 && cmd2 || cmd3
有些人喜歡把&&和||作為if…then…else…fi 的簡寫語法,在多數情況下,這種寫法沒有問題。例如:
[[ -s $errorlog ]] && echo "Uh oh, there were some errors." || echo "Successful."
但是,這種結構并不是在所有情況下都完全等價于 if…fi 語法。這是因為在&&后面的命令執行結束時也會生成一個返回碼,如果該返回碼不是真值(0代表 true),||后面的命令也會執行,例如:
i=0 true && ((i++)) || ((i--)) echo $i # 輸出 0
看起來上面的結果應該是返回1,但是結果卻是輸出0,為什么呢?原因是這里 i++ 和 i– 都執行了一遍。
其中,((i++))命令執行算術運算,表達式計算的結果為0。這里和 C 語言一樣,表達式的結果為0被認為是 false。所以當 i=0 的時候,((i++))命令執行的返回碼為1(false),從而會執行接下來的((i–))命令。
如果我們在這里使用前綴自增運算符的話,返回的結果恰恰為1,因為((++i))執行的返回碼是0(true):
i=0 true && (( ++i )) || (( --i )) echo $i # Prints 1
不過在你無法保證 y 的執行結果是,絕對不要依靠 x && y || z這種寫法。上面這種巧合,在 i 初始化為-1時也會有問題。
如果你喜歡代碼更加安全健壯,建議使用 if…fi 語法:
i=0 if true; then ((i++)) else ((i--)) fi echo $i # 輸出 1
23. echo “Hello World!”
在交互式的 Shell 環境下,你執行以上命令會遇到下面的錯誤:
bash: !": event not found 這是因為,在默認的交互式 Shell 環境下,Bash 發現感嘆號時會執行歷史命令展開。在 Shell 腳本中,這種行為是被禁止的,所以不會發生錯誤。 不幸地是,你認為明顯正確地修復方法,也不能工作,你會發現反斜杠并沒有轉義感嘆號: # echo "hi!" hi! 最簡單地方法是禁用 histexpand 選項,你可以通過 set +H 或者 set +o histexpand 命令來完成。 下面四種寫法都可以解決: # 1. 使用單引號 echo 'Hello World!' # 2. 禁用 histexpand 選項 set +H echo "Hello World!" # 3. 重置 histchars histchars= # 4. 控制 shell 展開的順序,命令行歷史展開是在單詞拆分之前執行的 # 參見:Bash man 手冊的History Expansion一節 exmark='!' echo "Hello, world$exmark"
由于篇幅限制,本系列文章會分成多篇文章,下一篇參見第節:Bash編程易犯的錯誤。