接受/高票的答案很棒,但缺少一些细节。这篇文章介绍了如何更好地处理 shell 路径名扩展 (glob) 失败、文件名包含嵌入的换行符/破折号以及在将结果写入时将命令输出重定向移出 for 循环的情况文件。
当使用* 运行 shell glob 扩展时,如果目录中存在 no 文件并且未扩展的 glob 字符串将传递给要运行的命令,这可能会产生不良结果。 bash shell 为此使用nullglob 提供了扩展的shell 选项。所以循环基本上在包含您的文件的目录中如下所示
shopt -s nullglob
for file in ./*; do
cmdToRun [option] -- "$file"
done
当表达式./* 不返回任何文件时(如果目录为空),您可以安全地退出 for 循环
或以符合 POSIX 的方式(nullglob 是 bash 特定的)
for file in ./*; do
[ -f "$file" ] || continue
cmdToRun [option] -- "$file"
done
当表达式失败一次并且条件[ -f "$file" ] 检查未扩展的字符串./* 是否是该目录中的有效文件名时,这使您可以进入循环,这不是。因此,在这种情况下失败,使用continue 我们恢复到for 循环,该循环不会随后运行。
还要注意在传递文件名参数之前使用--。这是必需的,因为如前所述,shell 文件名可以在文件名的任何位置包含破折号。当名称被正确引用时,一些 shell 命令会解释它并将它们视为命令选项,并在考虑是否提供标志的情况下执行命令。
-- 在这种情况下表示命令行选项的结束,这意味着该命令不应将超出此点的任何字符串解析为命令标志,而只能解析为文件名。
双引号文件名可以正确解决名称包含全局字符或空格的情况。但是 *nix 文件名中也可以包含换行符。所以我们用唯一不能成为有效文件名一部分的字符来限制文件名 - 空字节(\0)。由于bash 内部使用C 样式字符串,其中空字节用于指示字符串的结尾,因此它是正确的候选对象。
所以使用shell的printf选项,使用read命令的-d选项来分隔带有这个NULL字节的文件,我们可以这样做
( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
cmdToRun [option] -- "$file"
done
nullglob 和 printf 包裹在 (..) 周围,这意味着它们基本上在子 shell(子 shell)中运行,因为为了避免 nullglob 选项反映在父 shell 上,一次命令退出。 read 命令的-d '' 选项不 POSIX 兼容,因此需要bash shell 来完成此操作。使用find 命令可以这样做
while IFS= read -r -d '' file; do
cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0)
对于不支持 -print0 的 find 实现(GNU 和 FreeBSD 实现除外),可以使用 printf 进行模拟
find . -maxdepth 1 -type f -exec printf '%s\0' {} \; | xargs -0 cmdToRun [option] --
另一个重要的修复是将重定向移出 for 循环以减少大量文件 I/O。当在循环内使用时,shell 必须为 for 循环的每次迭代执行两次系统调用,一次用于打开,一次用于关闭与文件关联的文件描述符。这将成为运行大型迭代的性能瓶颈。推荐的建议是将其移到循环之外。
用这个修复扩展上面的代码,你可以这样做
( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
cmdToRun [option] -- "$file"
done > results.out
这基本上会将文件输入的每次迭代的命令内容放入标准输出,当循环结束时,打开目标文件一次以写入标准输出的内容并保存它。等效的find 版本将是
while IFS= read -r -d '' file; do
cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0) > results.out