7C00.ME/houmu 2015-04-24

shell带空格文件名的处理

在 Unix/Linux 系统生态中,文件名用小写英文字母(和 - 、 . 等很少的几种符号)是一种传统。大写字母很少出现在 *nix 的系统文件名中,空格和其他字符就更是少见了。而在用户文件中,各种类型字符都有可能出现,好在 *nix 对 UTF-8 支持良好,使得非英文字符(比如说汉字)在 shell 命令中也能和英文字符几乎一样地使用。但是对于文件名中的空格,还是要特别关照一下。

空格是 shell 命令的分隔符,如 ls -al /bin,这应该是在英语文法习惯的自然延续,包括很多编程语言也是如此(当然也有例外,某种编程语言的空格是没有实际意义的)。如果在文件名中出现空格,命令可能机会出现歧义。比如如果一个文件叫 file 1.txt,那么执行 cat file 1.txt 的时候有可能提示 file 和 1.txt两个问文件不存在。对此有下面几种解决办法:1,使用转义字符:cat file\ 1.txt;2,使用引号:cat "file 1.txt"cat 'file 1.txt';3,使用通配符:cat file*1.txt,这个方法适合在终端里面手动执行,不适合写为脚本。在 shell脚本中,如果文件名存在一个变量中,就必须使用引号了。比如filename="file 1.txt",执行cat $filename会提示 file 和 1.txt 两个文件不存在,执行cat "$filename"则可以正常工作。

上面针对的是单个文件的情况,多个文件情况下还有不同。假设现在有两个文件,’file 1.txt’ 和 ‘file 2.txt’,可以这么来cat:cat "file 1.txt" "file 2.txt";在 shell 脚本中可以用一些技巧绕开空格导致的问题,比如 cat file*.txtls file*.txt | xargs -I{} cat {}find -name file*.txt -exec cat {} \; 等。但是这里的 cat 命令存在一定的特殊性,因为 cat 'file 1.txt' 'file 2.txt'cat 'file 1.txt' ; cat 'file 2.txt'等效。然而,有些命令却没有这种等效替代品,比如对于某个命令 foo 必须这样执行 foo 'file 1.txt' 'file 2.txt',而且(为了程序的灵活性)文件名还要动态获得。比如可能存在类似下面的情形:

inputs=$(find -name *.txt -print0)
foo $inputs

如果文件名不存在空格,上面的代码没问题。但是文件名中是可能存在空格的,而且一旦出现空格就会导致异常。为此,首先考虑转义或加引号,转义似乎不太可行,试试加引号,改成这样inputs=$(find -name *.txt -printf "\'%p\' "),这样 $inputs 从类似这种形式 file 1.txt file 2.txt … 变成了 ‘file 1.txt’ ‘file 2.txt’ … ,但是这样做并没有解决问题,因为执行到 foo $inputs 的时候,引号都成了文件名的一部分,相当于执行这个命令了foo "'file" "1.txt'" "'file" "2.txt'" ...。给 $inputs 加上引号也不对,变成了输入参数是一个文件,相当于foo "'file 1.txt' 'file 2.txt' ...", foo处理的文件是 “‘file 1.txt’ ‘file 2.txt’ …” 。好在天无绝人之路,可以用 bash 或 eval 破解这个难题:

inputs=$(find -name *.txt -printf "\'%p\' ")
eval foo $inputs
# or
# bash -c "foo $inputs"

上面的代码可以认为是比较健壮处理文件名的做法了。

另外,eval是bash内建命令,eval通常和bash -c是效果相同,但有些情况下不能使用eval,必须使用bash。比如ls *.txt | xargs -I{} bash -c "command {}; command {}",这里 bash -c 不能被 eval 代替。有些时候时候,shell 命令中的一些特殊字符处理起来会比较麻烦,需要借助 bash -c 或 eval。除了文件名中的空格,还有可能在引号或变量插值等情形遇到类似问题。比如在 eval sed -i 's/$va/$vb/g' 1.txt 中,使用 eval 可以简化问题。


2015-05-08 更新

最近在用 eval 的那段代码时发现一个问题:如果文件名中有连续空格(如 file 1.txt),还是会导致脚本运行错误。究其原因是 eval 会重写空格,连续空格会替换为一个空格,比如命令 eval echo 'hello worldeval echo 'hello world' 是等效的。这不得不说是 eval 的一个陷阱!这样以来只有用 bash -c 了:

inputs=$(find -name *.txt -printf "\'%p\' ")
bash -c "foo $inputs"