Shell:常見錯誤總結(一)
Blog:部落格園 個人
譯自BashPitfalls
本文總結了編寫Shell腳本中的常見錯誤。
for f in $(ls *.mp3)
最常犯的錯之一就是編寫這樣的循環:
for f in $(ls *.mp3); do # Wrong!
some command $f # Wrong!
done
for f in $(ls) # Wrong!
for f in `ls` # Wrong!
for f in $(find . -type f) # Wrong!
for f in `find . -type f` # Wrong!
files=($(find . -type f)) # Wrong!
for f in ${files[@]} # Wrong!
確實,如果可以將ls
的輸出或者find
作為文件名列表並對其進行迭代,看起來確實沒啥問題。但是,這類方法是有缺陷的。
比如:
- 如果文件名包含空格,
for
循環會將空格也分割(默認IFS為空格、\n
、“\t),例如
01 – Don’t Eat the Yellow Snow.mp3,
for循環會分割成
01、
–、
Don’t、
Eat、
the、
Yellow、
Snow.mp3`。 - 如果文件名包含glob字元(例如
*
),包含它的單詞將被識別為模式並用與其匹配的所有文件名列表替換。 - 如果命令替換返回多個文件名,則無法區分第一個文件名的結束位置和第二個文件名的開始位置。路徑名可以包含除NUL之外的任何字元。是的,這包括換行符。
ls
實用程式可能會損壞文件名。根據您使用的平台、使用(或未使用)的參數以及其標準輸出是否指向終端,ls
可能會隨機決定將文件名中的某些字元替換為「?」,或者乾脆不列印它們。永遠不要嘗試解析ls
的輸出。ls
完全是不必要的。它是一個外部命令,其輸出專門供人讀取,而不是由腳本解析。- 命令替代(Command Substitution)從其輸出中剝離所有尾隨換行符。這看起來可能是可取的,因為
ls
添加了一個換行符,但是如果列表中的最後一個文件名以換行符結束,則命令替代會刪除換行符。
正確做法:
for file in ./*.mp3; do # Better! and…
some command "$file" # …always double-quote expansions!
done
cp $file $target
如果 $file
和$target
中有空格(如果沒有修改$IFS
),cp $file $target
執行會報錯,例如複製文件01 - Don't Eat the Yellow Snow.mp3
到/mn/usb:
cp 01 - Don't Eat the Yellow Snow.mp3 /mnt/usb
會報以下錯誤:
cp: cannot stat 『01』: No such file or directory
cp: cannot stat 『-』: No such file or directory
cp: cannot stat 『Don't』: No such file or directory
cp: cannot stat 『Eat』: No such file or directory
cp: cannot stat 『the』: No such file or directory
cp: cannot stat 『Yellow』: No such file or directory
cp: cannot stat 『Snow.mp3』: No such file or directory
正確做法:
cp -- "$file" "$target"
強烈建議:引用變數的時候,一定要加雙引號。
Filenames with leading dashes
如果文件名帶有-
,命令可能錯誤把它當作參數。
解決的方法之一是,在變數前面加--
,例如:
cp -- "$file" "$target"
加--
是告訴命令,停止掃描參數。
注意:此方法的潛在問題,必須確保每條命令都要插入
--
,這很容易遺漏。
還有一種方法是使用相對路徑或者絕對路徑。例如:
for i in ./*.mp3; do
cp "$i" /target
…
done
在這種情況下,即使開頭包含-
的文件,也可以確保變數始終包含類似./-foo.mp3
的文件,這樣就比較安全。
[ $foo = "bar" ]
如果[
中引用的變數不存在或為空,則[
命令最終將如下所示:
[ = "bar" ] # Wrong!
Tips:
=
是二元一次運算符,不是一元一次運算符。
如果變數包含內部空格,則會在[
命令看到它之前將其拆分成單獨的單詞,例如:
[ multiple words here = "bar" ]
看起來沒啥問題,但在[]
語法中是錯誤的,正確方式是加上雙引號:
# POSIX
[ "$foo" = bar ] # Right!
# Bash / Ksh
[[ $foo == bar ]] # Right!
cd $(dirname "$f")
這也是引用錯誤。正確做法:
cd -P -- "$(dirname -- "$f")"
Tips:
-p
參數是遞歸處理,將指定目錄下的所有文件與子目錄一併處理。