天天看點

Bash程式設計易犯的錯誤

根據微網誌轉載:http://blog.jobbole.com/46191/

前一段時間發現一個很好的wiki站點,上面有很多優秀的Bash文章。最近挑了一篇介紹Bash程式設計容易犯的各種錯誤的文章看,收獲很多,不敢獨享,把這篇文章以半翻譯半筆記的形式分享給大家。

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

Bash寫循環代碼的時候,确實比較容易犯下面的錯誤:

1 2 3 4 5 6 7 8 9 10 11 12

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[@]}       

# 錯誤!

這裡主要兩個問題:

  • 使用指令展開時不帶引号,其執行結果會使用IFS作為分隔符,拆分成參數傳遞給for循環處理;
  • 不應該讓腳本去解析ls指令的結果;

我們不能避免某些檔案名中包含空格,Shell會對

$(ls *.mp3)

展開的結果會被做單詞拆分(WordSplitting)的處理。假設有一個檔案,名字為01 – Don’t Eat the Yellow Snow.mp3,for循環處理的時候,會今次周遊檔案名中的每個單詞:01, -, Don’t, Eat等等:

1 2 3 4 5 6 7 8

$

for

i

in

$(

ls

*.mp3);

do

echo

$i;

done

01

-

Don't

Eat

the

Yellow

Snow.mp3

比這更差的情況是,上面指令展開的結果可能被Shell進一步處理,比如檔案名展開。比如,ls執行的結果中包含*号,按照通配符的規則, *号會被展開成目前目錄下的所有檔案:

1 2 3 4 5 6 7 8 9

$

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

不過,在這種場景下,你即使加上引号,也是無濟于事的:

1 2

$

for

i

in

"$(ls *.mp3)"

;

do

echo

--$i--;

done

--1*.mp3 1.mp3 11.mp3 12.mp3--

加上引号後,ls執行的結果會被當成一個整體,是以for循環隻會執行一次,達不到預期的效果。

事實上,這種情況下,根本不需要使用ls指令。ls指令的結果本身就設計成給人讀的,而不是給腳本解析的。正确的處理方法是,直接使用檔案名展開(通配符)的功能:

1 2 3 4 5 6 7

$

for

i

in

*.mp3;

do

>    

echo

"$i"

>

done

1*.mp3

1.mp3

11.mp3

12.mp3

檔案名展開是位于各種展開(花括号展開、變量替換、指令展開等)功能中的最後一個環節,是以不會有之前不帶引号的指令展開的副作用。如果你需要遞歸地處理檔案,可以考慮使用Find指令。

到這一步,之間的問題看樣子已經修複了。但是,如果你進一步思考,假設目前目錄上沒有檔案時會怎麼樣?沒有檔案的時候,*.mp3不會被展開直接傳遞給for循環處理,是以這個時候循環還是會執行一次。這種情況不是我們預期的行為。保險起見,可以在循環處理的時候,檢查下檔案是否存在:

1 2 3 4 5

# POSIX

for

i

in

*.mp3;

do

[ -e

"$i"

] ||

continue

some

command

"$i"

done

如果你有使用引号和避免單詞拆分的習慣,你完全可以避免很多錯誤。

注意下循環體内部的”$i”,這裡會導緻下面我們要說的另外一個比較容易犯的錯誤。

2. cp $file $target

上面的指令有什麼問題呢?如果你提前知道,$file和$target檔案名中不會包含空格或者*号。否則,這行指令執行前在經過單詞拆分和檔案名展開的時候會出現問題。是以,兩次強調,在使用展開的地方切勿忘記使用引号:

1

$

cp

--

"$file"

"$target"

如果不帶引号,當你執行如下指令時就會出錯:

1 2 3 4 5

$

file

=

"01 - Don't Eat the Yellow Snow.mp3"

$ target=

"/tmp"

$

cp

$

file

$target

cp

: cannot stat ‘01’: No such

file

or directory

..

如果帶上引号,就不會有上面的問題,除非檔案名以’-'開頭,在這種情況下,cp會認為你提供的是一個指令行選項,這個錯誤下面會介紹。

3. 檔案名中包含短橫’-’

檔案名以’-'開頭會導緻許多問題,*.mp3這種通配符會根據目前的locale展開成一個清單,但在絕大多數環境下,’-'排序的時候會排在大多數字母前。這個展開的清單傳遞給有些指令的時候,會錯誤的将-filename解析成指令行選項。這裡有兩種方法來解決這個問題。

第一種方法是在指令和參數之間加上–,這種文法告訴指令不要繼續對–之後的内容進行指令行參數/選項解析:

1

$

cp

--

"$file"

"$target"

這種方法可以解這個問題,但是你需要在每個指令後面都要加上–,而且依賴具體的指令解析的方式,如果一些指令不相容這種約定俗成的規範,這種做法是無效的。

另外一種方法是,確定檔案名都使用相對或者絕對的路徑,以目錄開頭:

1 2 3 4

for

i

in

./*.mp3;

do

cp

"$i"

/target

...

done

這種情況下,即使某個檔案以-開頭,展開後檔案名依然是./-foo.mp3這種形式,完全不會有問題。

4. [ $foo = "bar" ]

這是一個與第2個問題類似的問題,雖然用到了引号,但是放錯了位置,對于字元串字面值,除非有特殊符号,否則不大需要用引号括起來。但是,你應該把變量的值用括号括起來,進而避免它們包含空格或能通配符,這一點我們在前面的問題中都解釋過。

這個例子在以下情況下會出錯:

  • 如果[中的變量不存在,或者為空,這個時候上面的例子最終解析結果是:
    1

    [ =

    "bar"

    ]

    # 錯誤!

    并且執行會出錯:unary operator expected,因為=是二進制操作符,它需要左右各一個操作數。
  • 如果變量值包含空格,它首先在執行之前進行單詞拆分,是以[指令看到的樣子可能是這樣的:
    1

    [ multiple words here =

    "bar"

    ];

正确的做法應該是:

1 2

# POSIX

[

"$foo"

= bar ]

這種寫法,在POSIX相容的實作中都不會有問題,即使$foo以短橫"-"開頭,因為POSIX實作的test指令通過傳遞的參數來确定執行的行為。

隻有一些非常古老的shell可能會遇到問題,這個時候你可以使用下面的寫法來解決(相信你肯定看到過這種寫法):

1 2

# POSIX / Bourne

[ x

"$foo"

= xbar ]

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

1 2

# Bash / Ksh

[[ $foo == bar ]]

這裡你不需要使用引号,因為在[[裡面參數不會進行展開,當然帶上引号也不會有錯。

不過有一點要注意的是,[[裡的==不僅僅是文本比較,它會檢查左邊的值是否比對右側的表達式,==右側的值加上引号,會讓它成為一個普通的字面量,*?等通配符會失去特殊含義。

5. cd $(dirname "$f")

這又是一個引号的問題,指令展開的結果會進一步地進行單詞拆分或者檔案名展開。是以下面的寫法才是正确的:

1

cd

"$(dirname "

$f

")"

但是,上面引号的寫法可能比較怪異,你可能會認為第一、二個引号,第三、四個引号是一組的。

但是事實上,Bash将指令替換裡面的引号當成一組,外面的當成另外一組。如果你是用反引号的寫法,引号的行為就不是這樣的了,是以$()寫法更加推薦。

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

不要在test指令内部使用&&,Bash解析器會把你的指令分隔成兩個指令,在&&之前和之後。你應該使用下面的寫法:

1 2

[ bar =

"$foo"

] && [ foo =

"$bar"

]

# POSIX

[[ $foo = bar && $bar = foo ]]      

# Bash / Ksh

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

1

[ bar =

"$foo"

-a foo =

"$bar"

]

7. [[ $foo > 7 ]]

原文作者認為算術比較不應該用[[,而是用((,我沒弄明白是為什麼。

如果有了解的同學,歡迎以評論回複,謝謝。

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

這種寫法初看沒有問題,但是你會發現當執行完後,count變量并沒有變化。原因是管道後面的指令是在一個子Shell中執行的。

POSIX規範并沒有說明管道的最後一個指令是不是在子Shell中執行的。一些shell,例如ksh93或者Bash>=4.2可以通過

shopt -s lastpipe

指令,指明管道中的最後一個指令在目前shell中執行。由于篇幅限制,在此就不展開,有興趣的可以看Bash FAQ #24。

9. if [grep foo myfile]

初學者會錯誤地認為,[是if文法的一部分,正如C語言中的if ()。但是事實并非如此,if後面跟着的是一個指令,[是一個指令,它是内置指令test的簡寫形式,隻不過它要求最後一個參數必須是]。下面兩種寫法是一樣的:

1 2 3

# POSIX

if

[

false

];

then

echo

"HELP"

;

fi

if

test

false

;

then

echo

"HELP"

;

fi

兩個都是檢查參數"false"是不是非空的,是以上面兩個語句都會輸出HELP。

if語句的文法是:

1 2 3 4 5 6

if

COMMANDS

then

<COMMANDS>

elif

<COMMANDS>

# optional

then

<COMMANDS>

else

<COMMANDS>

# optional

fi

# required

再次強調,[是一個指令,它同其它正常的指令一樣接受參數。if是一個複合指令,它包含其它指令,[并不是if文法中的一部分。

如果你想根據grep指令的結果來做事情,你不需要把grep放到[裡面,隻需要在if後面緊跟grep即可:

1 2 3

if

grep

-q fooregex myfile;

then

...

fi

如果grep在myfile中找到比對的行,它的執行結果為0(true),then後面的部分就會執行。

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

正如上一個問題中提到的,[是一個指令,它的參數之間必須用空格分隔。

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

不要用把[指令看成C語言中if語句的條件一樣,它是一個指令。

如果你想表達一個複合的條件表達式,可以這樣寫:

1

if

[ a = b ] && [ c = d ];

then

...

注意,if後面有兩個指令,它們用&&分開。等價于下面的寫法:

1

if

test

a = b &&

test

c = d;

then

...

如果第一個test(或者[)指令傳回false,then後面的語句不會執行;如果第一個傳回true,第二個test指令會執行;隻有第二個指令同樣傳回true的情況下,then後面的語句才會執行。

除此之外,還可以使用[[關鍵字,因為它支援&&的用法:

1

if

[[ a = b && c = d ]];

then

...

12. read $foo

read指令中你不需要在變量名之前使用$。如果你想把讀入的資料存放到名為foo的變量中,下面的寫法就夠了:

1

read

foo

或者,更加安全地方法:

1

IFS=

read

-r foo

read $foo

會把一行的内容讀入到變量中,該變量的名稱存儲在$foo中。是以兩者的含義是完全不一樣的。

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

你不應該在一個管道中,從一個檔案讀的同時,再往相同的檔案裡面寫,這樣的後果是未知的。

你可以為此建立一個臨時檔案,這種做法比較安全可靠:

1

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

或者,如果你用得是 GNU Sed 4.x 以上的版本,可以使用-i 選項即時修改檔案的内容:

1

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

14. echo $foo

這種看似無害的指令往往會給初學者千萬極大的困擾,他們會懷疑是不是因為 $foo 變量的值是錯誤的。事實卻是因為,$foo 變量在這裡沒有使用雙引号,是以在解析的時候會進行單詞拆分和檔案名展開,最終導緻執行結果與預期大相徑庭:

1 2

msg=

"Please enter a file name of the form *.zip"

echo

$msg

這裡整句話會被拆分成單詞,然後其中的通配符會被展開,例如*.zip。當你的使用者看到如下的結果時,他們會怎樣想:

1

Please enter a

file

name of the form freenfss.zip lw35nfss.zip

再舉一個例子(假設目前目錄下有以 .zip 結尾的檔案):

1 2 3

var=

"*.zip"

# var 包括一個星号,一個點号和 zip

echo

"$var"

# 輸出 *.zip

echo

$var    

# 輸出所有以 .zip 結尾的檔案

實際上,這裡使用 echo 指令并不是絕對的安全。例如,當變量的值包含-n時,echo 會認為它是一個合法的選項而不是要輸出的内容(當然如果你能夠保證不會有-n 這種值,可以放心地使用 echo 指令)。

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

1

printf

"%s\n"

"$foo"

15. $foo=bar

略過

16. foo = bar

當指派時,等号兩邊是不允許出現空格的,這同 C 語言不一樣。當你寫下 foo = bar 時,shell 會将該指令解析成三個單詞,然後第一個單詞 foo 會被認為是一個指令,後面的内容會被當作指令參數。

同樣地,下面的寫法也是錯誤的:

1 2 3 4 5 6 7 8

foo= bar   

# WRONG!

foo =bar   

# WRONG!

$foo = bar;

# COMPLETELY WRONG!

正确的寫法應該是這樣的:

<pre class=

"prettyprint lang-sh"

>

foo=bar    

# Right.

foo=

"bar"

# More Right.

17. echo <<EOF

當腳本需要嵌入大段的文本内容時,here document往往是一個非常有用的工具,它将其中的文本作為指令的标準輸入。不過,echo 指令并不支援從标準輸入讀取内容,是以下面的寫法是錯誤的:

1 2 3 4 5

# This is wrong:

echo

<<EOF

Hello world

How's it going?

EOF

正确的方法是,使用 cat 指令來完成:

1 2 3 4 5

# This is what you were trying to do:

cat

<<EOF

Hello world

How's it going?

EOF

或者可以使用雙引号,它也可以跨越多行,而且因為 echo 指令是内置指令,相同情況下它會更加高效:

1 2

echo

"Hello world

How's it going?"

18. su -c 'some command'

這種寫法“幾乎”是正确的。問題是,在許多平台上,su 支援 -c 參數,但是它不一定是你認為的。比如,在 OpenBSD 平台上你這樣執行會出錯:

1 2

$

su

-c

'echo hello'

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 指令執行是否成功,你可以會在錯誤的目錄下執行 bar 指令,這有可能會帶來災難,比如 bar 指令是 rm -rf *。

你必須經常檢查 cd 指令執行是否有錯誤,簡單的做法是:

1

cd

/foo

&& bar

如果在 cd 指令後有多個指令,你可以選擇這樣寫:

1 2 3 4

cd

/foo

||

exit

1

bar

baz

bat ...

# Lots of commands.

出錯時,cd 指令會報告無法改變目前目錄,同時将錯誤消息輸出到标準錯誤,例如"bash: cd: /foo: No such file or directory"。如果你想要在标準輸出同時輸出自定義的錯誤提示,可以使用複合指令(command grouping):

1 2 3

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 指令都是沒有必要的。

說到這,比較下下面兩種寫法:

1 2 3 4 5

find

... -

type

d -print0 |

while

IFS=

read

-r -d

''

subdir;

do

here=$PWD

cd

"$subdir"

&& whatever && ...

cd

"$here"

done

1 2 3

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" ]

正确的用法:

1 2

[ bar =

"$foo"

] &&

echo

yes

[[ bar == $foo ]] &&

echo

yes

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

你不應該在&後面添加分号,删除它:

1

for

i

in

{1..10};

do

.

/something

&

done

或者改成多行的形式:

1 2 3

for

i

in

{1..10};

do

.

/something

&

done

&和分号一樣也可以用作指令終止符,是以你不要将兩個混用到一起。一般情況下,分号可以被換行符替換,但是不是所有的換行符都可以用分号替換。

22. cmd1 && cmd2 || cmd3

有些人喜歡把&&和||作為if...then...else...fi 的簡寫文法,在多數情況下,這種寫法沒有問題。例如:

1

[[ -s $errorlog ]] &&

echo

"Uh oh, there were some errors."

||

echo

"Successful."

但是,這種結構并不是在所有情況下都完全等價于 if...fi 文法。這是因為在&&後面的指令執行結束時也會生成一個傳回碼,如果該傳回碼不是真值(0代表 true),||後面的指令也會執行,例如:

1 2 3

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):

1 2 3

i=0

true

&& (( ++i )) || (( --i ))

echo

$i

# Prints 1

不過在你無法保證 y 的執行結果是,絕對不要依靠 x && y || z這種寫法。上面這種巧合,在 i 初始化為-1時也會有問題。

如果你喜歡代碼更加安全健壯,建議使用 if...fi 文法:

1 2 3 4 5 6 7 8

i=0

if

true

;

then

((i++))

else

((i--))

fi

echo

$i

# 輸出 1

23. echo "Hello World!"

在互動式的 Shell 環境下,你執行以上指令會遇到下面的錯誤:

1

bash

: !": event not found

這是因為,在預設的互動式 Shell 環境下,Bash 發現感歎号時會執行曆史指令展開。在 Shell 腳本中,這種行為是被禁止的,是以不會發生錯誤。

不幸地是,你認為明顯正确地修複方法,也不能工作,你會發現反斜杠并沒有轉義感歎号:

1 2

# echo "hi\!"

hi\!

最簡單地方法是禁用 histexpand 選項,你可以通過 set +H 或者 set +o histexpand 指令來完成。

下面四種寫法都可以解決:

1 2 3 4 5 6 7 8 9 10 11 12 13 14

# 1. 使用單引号

echo

'Hello World!'

# 2. 禁用 histexpand 選項

set

+H

echo

"Hello World!"

# 3. 重置 histchars

histchars=

# 4. 控制 shell 展開的順序,指令行曆史展開是在單詞拆分之前執行的

# 參見:<a href="http://linux.die.net/man/1/bash" target="_blank" rel="external nofollow" target="_blank">Bash man 手冊的History Expansion一節</a>

exmark=

'!'

echo

"Hello, world$exmark"

24. for arg in $*

和大多數 Shell 一樣,Bash 支援依次讀取單個指令行參數的文法。不過這并是$*或者$@,這兩種寫法都不正确,它們隻能得到完整的參數清單,并非單獨的一個個參數。

正确的文法是(沒錯要加上引号):

1 2 3 4

for

arg

in

"$@"

# 或者更簡單的寫法

for

arg

在腳本中周遊所有參數是一個再普遍不過的需求,是以 for arg 預設等價于 for arg in "$@"。$@使用雙引号後就有特殊的魔力,每個參數展開後成為一個獨立的單詞。("$@"等價于"$1" "$2" "$3" ...)

下面是一個錯誤的例子:

1 2 3

for

x

in

$*;

do

echo

"parameter: '$x'"

done

執行的結果為:

1 2 3 4 5

$ .

/myscript

'arg 1'

arg2 arg3

parameter:

'arg'

parameter:

'1'

parameter:

'arg2'

parameter:

'arg3'

正确的寫法:

1 2 3

for

x

in

"$@"

;

do

echo

"parameter: '$x'"

done

執行的結果為:

1 2 3 4

$ .

/myscript

'arg 1'

arg2 arg3

parameter:

'arg 1'

parameter:

'arg2'

parameter:

'arg3'

上面正确的例子中,第一個參數'arg 1'在展開後依然是一個獨立的單詞,而不會被拆分成兩個。

25. function foo()

這種寫法不一定能夠相容所有 shell,相容的寫法是:

1 2 3

foo() {

...

}

26. echo "~"

波浪号展開(Tilde expansion)僅當~沒有引号的時候發生,在上面的例子中,隻會向标準輸出列印~符号,而不是目前使用者的家目錄路徑。

當用引号将路徑參數引起來時,

如果要用引号将相對于家目錄的路徑引起來時,推薦使用 $HOME 而不是 ~, 假如 $HOME 目錄是"/home/my photos",路徑中包含空格。

下面是幾組例子:

1 2 3 4

"~/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指令的傳回碼會覆寫它。

這種情況下,你隻能分成兩行寫:

1 2 3

local

varname

varname=$(

command

)

rc=$?

28. export foo=~/bar

export 與 local 指令一樣,并不是指派語句的一部分。是以,在有些 Shell 下(比如Bash),export foo=~/bar會展開,但是有些(比如 Dash)卻不行。

下面是兩種比較健壯的寫法:

1 2

foo=~

/bar

;

export

foo   

# Right!

export

foo=

"$HOME/bar"

# Right!

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

單引号内部不會展開 $foo變量,在這裡可以換成雙引号:

1

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):

1

LC_COLLATE=C

tr

A-Z a-z

如果你希望根據實際的語言環境來轉換:

1

tr

'[:upper:]'

'[:lower:]'

31. ps ax | grep gedit

這裡的根本問題是正在運作的程序名稱,本質上是不可靠的。可能會有多個合法的gedit程序,也有可能是别的東西僞裝成gedit程序(改變執行指令名稱是一件簡單的事情 ),更多細節可以看ProcessManagement這一篇文章。

執行以上指令,往往會在結果中包含 grep 程序:

1 2 3

# ps ax | grep gedit

10530 ?        S      6:23 gedit

32118 pts

/0

R+     0:00

grep

gedit

這個時候,需要過濾多餘的結果:

1

# ps ax | grep -v grep | grep gedit

上面的寫法比較醜陋,另外一種方法是:

1

# ps ax | grep [g]edit

32. printf "$foo"

如果$foo 變量的值中包括\或者%符号,上面指令的執行結果可能會出乎你的意料之外。

下面是正确的寫法:

1 2

printf

%s

"$foo"

printf

'%s\n'

"$foo"

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

Bash的指令解釋器會優先展開大括号,是以這時大括号{}表達式裡面看到的是文字上的$n(沒有展開)。$n 不是一個數值,是以這裡的大括号{}并不會展開成數字清單。可見,這導緻很難使用大括号來展開大小隻能在運作時才知道的清單。

可以用下面的方法:

1 2 3

for

((i=1; i<=n; i++));

do

...

done

注:之前我也有寫過一篇文章來介紹這個問題:Shell生成數字序列。

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

在[[内部,當=号右邊的值沒有用引号引起來,bash 會将它當作模式來比對,而不是一個簡單的字元串。是以,在上面的例子中 ,如果 bar 的值是一個*号,執行的結果永遠是 true。

是以,如果你想檢查兩側的字元串是否相同,等号右側的值一定要用引号引起來。

1

if

[[ $foo =

"$bar"

]]

如果你确實要執行模式比對,聰明的做法是取一個更加有意義的變量名(例如$patt),或者加上注釋說明。

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

同上,如果=~号右側的值加上引号,它會散失特殊的正規表達式含義,而變成一個普通的字元串。

如果你想使用一個長的或者複雜的正規表達式,避免大量的反斜杠轉義,建議把它放在一個變量中:

1 2

re=

'some RE'

if

[[ $foo =~ $re ]]

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

這個例子中,$foo 沒有用引号引起來,當$foo包含空格或者$foo為空時都會出問題:

1 2 3 4 5

$ foo=

"some word"

&& [ -n $foo ] &&

echo

yes

-

bash

: [: some: binary operator expected

$ foo=

""

&& [ -n $foo ] &&

echo

yes

yes

正确的寫法是:

1 2 3 4 5 6

[ -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 選項是看檔案是否存在,當緊跟的檔案是一個軟連結時,它不看軟連結是否存在,而是看實際指向的檔案是否存在。是以當軟連結損壞時,即實際指向的檔案被删除後,-e 的結果傳回1。

是以如果你确實要判斷後面的檔案是否存在,正确的寫法是:

1

[[ -e

"$broken_symlink"

|| -L

"$broken_symlink"

]]

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

ed 指令使用的正則文法,不支援0次出現次數,下面的就可以正常工作:

1

ed

file

<<<

"g/d\{1,3\}/s//e/g"

略過,現在很少會有人用 ed 指令吧。

39. expr sub-string fails for "match"

下面的例子多數情況下運作不會有問題:

1 2 3

word=abcde

expr

"$word"

:

".\(.*\)"

bcde

但是當 $work 不巧剛好是 match 時,就有可能出錯了(MAC OSX 下的 expr 指令不支援 match,是以依然能正常工作):

1 2

word=match

expr

"$word"

:

".\(.*\)"

原因是 match 是 expr 指令裡面的一個特殊關鍵字,針對 GNU系統,解決方法是在前面加一個'+':

1 2 3

word=match

expr

+

"$word"

:

".\(.*\)"

atch

'+'号可以讓 expr 指令忽略後續 token 的特殊含義。

另外一個建議是,不要再使用 expr 指令了,expr 能做的事情都可以用 Bash 原生支援的參數展開(Parameter Expansion)或者字元串展開(Substring Expansion)來完成。并且相同情況下,内置的功能肯定比外部指令的效率要高。

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

1 2 3 4 5

$ word=match

$

echo

"${word#?}"

# PE

atch

$

echo

"${word:1}"

# SE

atch

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

多數情況下,UNIX 下 UTF-8 類型的文本不需要使用 BOM,文本的編碼是根據目前語言環境,MIME類型或者其它檔案中繼資料資訊确定的。人為閱讀時,不會因為在檔案開始處加 BOM 标記而腚影響,但是當檔案要被腳本解釋執行時,BOM 标記會像 MS-DOS 下的換行符(^M)一樣奇怪。

41. content=$(<file)

這裡沒有什麼錯誤,不過你要知道指令替換會删除結尾多餘的換行符。

略過,原文給的優化方法需要 Bash 4.2+ 以上的版本,手頭沒有這樣的環境。

42. somecmd 2>&1 >>logfile

這是一個很常見的錯誤,顯然你本來是想将标準輸出與标準錯誤輸出都重定向到檔案logfile 中,但是你會驚訝地發現,标準錯誤依然輸出到螢幕中。

這種行為的原因是,重定向在指令執行之前解析,并且是從左往右解析。上面的指令可以翻譯成,将标準錯誤輸出重定向到标準輸出(此刻是終端),然後将标準輸出重定向到檔案 logfile 中。是以,到最後,标準錯誤并沒有重定向到檔案中,而是依然輸出到終端:

1

somecmd >>logfile 2>&1

更加詳細的說明見BashFAQ。

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

隻有需要捕獲上一個指令的執行結果進,才需要記錄$?的值,否則如果你隻需要檢查上一個指令是否執行成功,直接檢測指令:

1 2 3

if

cmd;

then

...

fi

或者使用 case 語句來檢測多個或能的傳回碼:

1 2 3 4 5 6 7 8 9 10 11 12 13 14

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