Shell:常見錯誤總結(一)

不羈的羅恩發表於2022-03-24

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.mp3for迴圈會分割成01-Don'tEattheYellowSnow.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引數是遞迴處理,將指定目錄下的所有檔案與子目錄一併處理。

相關文章