本文总结了编写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!
如果变量包含内部空格,则会在[
命令看到它之前将其拆分成单独的单词,例如:
[ multiple words here = "bar" ]
看起来没啥问题,但在[]
语法中是错误的,正确方式是加上双引号:
# POSIX
[ "$foo" = bar ] # Right!
# Bash / Ksh
[[ $foo == bar ]] # Right!
cd $(dirname "$f")
这也是引用错误。正确做法:
cd -P -- "$(dirname -- "$f")"