Bash和文件名中的空格

25
以下是一个简单的Bash命令行:
grep -li 'regex' "filename with spaces" "filename"

没有问题。另外,以下内容也完全可以正常工作:

grep -li 'regex' $(<listOfFiles.txt)

listOfFiles.txt 包含要进行 grep 的文件名列表时,每行一个文件名。

问题出现在 listOfFiles.txt 中包含带有空格的文件名。在我尝试过的所有情况中(见下文),Bash 都会将文件名在空格处分割,因此,例如包含像 ./this is a file.xml 这样的名称的 listOfFiles.txt 行最终会尝试对每个部分(./thisisafile.xml)运行 grep。

我认为自己是一个相对高级的 Bash 用户,但我找不到一个简单的魔法咒语来解决这个问题。以下是我尝试过的方法。

grep -li 'regex' `cat listOfFiles.txt`
与上述描述一样失败了(我并没有真正期望这会成功),因此我想给每个文件名加引号:
grep -li 'regex' `sed -e 's/.*/"&"/' listOfFiles.txt`

Bash把引号当作文件名的一部分, 对于每个文件都会报"没有这样的文件或目录"错误(而且仍然使用空格分隔文件名)。

for i in $(<listOfFiles.txt); do grep -li 'regex' "$i"; done

原始尝试失败了(即,它的行为就像引号被忽略一样),而且速度很慢,因为它必须针对每个文件启动一个“grep”进程,而不是在一个调用中处理所有文件。

以下方法可以运行,但如果正则表达式包含shell元字符,则需要注意仔细的双重转义:

eval grep -li 'regex' `sed -e 's/.*/"&"/' listOfFiles.txt`

这是构建命令行以正确处理带空格文件名的唯一方法吗?

6个回答

45

试试这个:

(IFS=$'\n'; grep -li 'regex' $(<listOfFiles.txt))

IFS 是内部字段分隔符。将其设置为 $'\n' 可告诉 Bash 使用换行符来分隔文件名。它的默认值是 $' \t\n',可以使用 cat -etv <<<"$IFS" 命令打印。

将脚本括在括号中会启动一个子shell,因此只有括号内的命令受自定义的 IFS 值影响。


1
即使您不导出它,如果这不是在子shell中运行的话,该值将会持续存在。只需在一行上尝试运行 FOO=bar; echo $FOO,然后在另一行上运行 echo $FOO。管道命令会自动启动子shell,但是 IFS=$'\n' 当然不是管道的一部分。最好的解决方案是用括号将整个语句括起来,这样可以手动告诉bash在子shell中运行命令。 - Cascabel
1
@Dennis:我也这么想,但不知道为什么在我的机器上不起作用。也许IFS=$'\n'的范围在这种情况下非常有限,甚至不适用于$(<listOfFiles.txt) - Stephan202
1
@Dennis:那样做不起作用,因为环境变量仅在处理参数列表后设置 - 因此“grep”看到了正确的IFS值,但处理参数列表的shell没有看到。 - Jonathan Leffler
最后一次编辑是由“sweatybridge”完成的吗?也就是说,命令行(带或不带“$”)是否正确。 - Peter Mortensen
@PeterMortensen 是的,那个更改看起来是正确的。你对最新更改有什么问题,它没有影响到倒数第二个版本吗?(我用这种方式测试了表达式:seq 0 9 | xargs -I{} sh -c 'echo {} > "{} {}.txt"; echo "{} {}.txt" >> listOfFiles.txt' && (IFS=$'\n'; grep -li '[0-5]' $(<listOfFiles.txt)))。 - Stephan202
显示剩余2条评论

9
cat listOfFiles.txt |tr '\n' '\0' |xargs -0 grep -li 'regex'

在xargs上使用-0选项会告诉xargs使用null字符而不是空格作为文件名终止符。 tr命令将输入的换行符转换为null字符。
这满足了OP的要求,使grep不需要被多次调用。我的经验表明,对于大量文件来说,避免多次调用grep可以显著提高性能。
此方案还避免了OP原始方法中的一个错误,因为他的方法会在listOfFiles.txt包含超过命令缓冲区大小的文件时出错。 xargs知道最大命令大小,并将多次调用grep以避免该问题。
使用xargs和grep的相关问题是,当使用多个文件调用grep时,grep将在输出中添加文件名前缀。因为xargs使用多个文件调用grep,所以将接收到带有文件名前缀的输出,但对于只有一个文件在listOfFiles.txt中的情况或最后一个调用包含一个文件的多次调用的情况,则不会出现该问题。为了实现一致的输出,请在grep命令中添加/dev/null:
cat listOfFiles.txt |tr '\n' '\0' |xargs -0 grep -i 'regex' /dev/null

请注意,对于原帖作者来说这不是一个问题,因为他在grep命令中使用了“-l”选项;然而对于其他人来说,这可能是一个问题。

6

这是有效的:

while read file; do grep -li dtw "$file"; done < listOfFiles.txt

这并没有解决楼主的担忧,即为了保持性能而将grep调用最少次数的问题。也就是说,在一个命令行上处理多个文件。 - Be Kind To New Users

1

在Bash 4中,您还可以使用内置的mapfile函数设置一个包含每行内容的数组,并迭代该数组:

$ tree
.
├── a
│   ├── a 1
│   └── a 2
├── b
│   ├── b 1
│   └── b 2
└── c
    ├── c 1
    └── c 2

3 directories, 6 files
$ mapfile -t files < <(find -type f)
$ for file in "${files[@]}"; do
> echo "file: $file"
> done
file: ./a/a 2
file: ./a/a 1
file: ./b/b 2
file: ./b/b 1
file: ./c/c 2
file: ./c/c 1

0
请注意,如果您不小心在文件中使用了Windows行尾符\r\n,那么上面关于输入文件分隔符$IFS(和引用参数)的所有注释都将无效;因此,请确保行尾正确为\n(我使用scite显示行尾,并轻松地从一个更改为另一个)。
此外,cat管道传输到while file read ...似乎可以工作(显然不需要设置分隔符):
cat <(echo -e "AA AA\nBB BB") | while read file; do echo $file; done

...虽然对我来说,更相关的是在文件名中包含空格的目录中进行“grep”:

grep -rlI 'search' "My Dir"/ | while read file; do echo $file; grep 'search\|else' "$ix"; done

0

虽然它可能过于强大,但这是我最喜欢的解决方案:

grep -i 'regex' $(cat listOfFiles.txt | sed -e "s/ /?/g")

2
这太糟糕了!而且,嗯,发现了无用的“cat”使用! - gniourf_gniourf

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接