我想编写一个/bin/sh
shell 脚本来处理任何与通配符匹配的文件。处理 1 个或多个匹配文件很容易。但是,我发现处理 0 个匹配文件的情况很尴尬。
显而易见的结构是:
#!/bin/sh
for f in *.ext; do
handle "$f"
done
where*.ext
可以是一个或多个表达式,shell 会将其与文件路径进行比较,并且handle
是一个 shell 命令,如果给定现有文件的路径,则该命令会正确运行,但如果给定的路径未映射到文件,则运行失败。(在引发这个问题的情况下,它们是*.flac
和ffmpeg
,但我认为这并不重要。)
如果有匹配的文件foo.ext
,bar.ext
则此脚本执行
handle "foo.ext"
handle "bar.ext"
不出所料。然而,如果有不是任何匹配的文件,此脚本都会给出一条错误消息,例如,
handle: *.ext: No such file or directory
我想我明白为什么会发生这种情况:狂欢手册页(我认为这也适用于/bin/sh
)表示“后面的单词列表in
被扩展,生成一个项目列表……如果后面的项目扩展导致列表为空,则不会执行任何命令,返回状态为 0。”显然,当*.ext
“被扩展”时,结果列表包括*.ext
,而不是空列表。但这并不能解释如何阻止这种情况发生。
(更新:sh (1)
Heirloom 项目的手册页它描述的是 Bourne Shell sh
,而不是后来的 Bourne Again Shell bash
。在文件名生成,它清楚地说,“如果没有找到与模式匹配的文件名,那么单词将保持不变。”它解释了为什么在列表中sh
留下一个模式。)*.ext
编写此循环的一种紧凑而惯用的方法是什么,以便如果没有匹配的文件,它将运行零次且不会出现任何错误,但对于每个匹配的文件将运行一次?更好的是,什么可以与多种模式一起使用,例如:
for f in *.ext1 special.ext1 *.ext2; do ...
为了便于移植,我更喜欢使用与 兼容的语法/bin/sh
。我碰巧使用的是 Mac OS X 10.11.6,但我希望语法可以在任何类 Unix 操作系统上运行。
我想到了一些笨拙且不符合习惯的方法来编写这样的循环。如果我没有立即得到好的答案,我会将这些答案作为答案贡献出来,以作记录。
答案1
最简单的可移植方法是,如果扩展没有产生实际存在的东西,则跳过循环:
for f in *.ext1 *.ext2; do
[ -e "$f" ] || continue
handle "$f"
done
解释:假设有多个 .ext2 文件,但没有 .ext1 文件。在这种情况下,通配符将扩展为*.ext1
filea.ext2
fileb.ext2
filec.ext2
。因此[ -e "*.ext1" ]
失败并运行continue
,这将跳过循环的其余迭代。然后[ -e "filea.ext2" ]
等成功,并且循环的那些迭代正常运行。
顺便说一句,您可以将其修改为例如[ -f "$f" ] || continue
跳过任何非纯文本文件(如果您想跳过目录等)。
当然,如果您在 bash 下运行,您可以先运行,shopt -s nullglob
这样它就不会在列表中留下不匹配的模式。然后shopt -u nullglob
再运行以防止出现意外的副作用(例如,如果设置了,grep pattern *.nonexistent
将尝试从 stdin 读取)。nullglob
答案2
/bin/bash
从 Bourne Shell ( ) 切换到 Bourne Again Shell ( /bin/sh
),一个简单的解决方案就成为可能。
这bash(1)
手册页提到空值选项:
如果设置,bash 允许不匹配任何文件的模式(参见路径名扩展上述代码会将其扩展为空字符串,而不是其自身。
这路径名扩展部分内容如下:
单词拆分后,…bash 扫描每个单词以查找字符*,?, 和[。如果出现其中一个字符,则该单词被视为模式,并替换为与该模式匹配的按字母顺序排序的文件名列表。如果没有找到匹配的文件名,则 shell 选项空值未启用,则单词保持不变。如果空值选项已设置,并且未找到匹配项,则该词被删除。...
因此,设置空值选项为模式提供所需的行为。如果没有文件与模式匹配,则循环不会执行。
#!/bin/bash
shopt -s nullglob
for f in *.ext; do
handle "$f"
done
但空值选项可能会干扰其他命令的预期行为。因此,您可能会发现保存现有设置是明智之举空值,然后恢复它。幸运的是,shopt -p
内置命令以可以重用为输入的形式发出输出。但它会为所有选项发出输出,因此使用来grep
挑选空对象设置。请参阅下面的日志(其中$
表示 bash 命令提示符):
$ shopt -p | grep nullglob
shopt -u nullglob
$ shopt -s nullglob
$ shopt -p | grep nullglob
shopt -s nullglob
因此,处理零个或多个与通配符模式匹配的文件的最终 bash 语法如下所示:
#!/bin/bash
SAVED_NULLGLOB=$(shopt -p | grep nullglob)
shopt -s nullglob
for f in *.ext; do
handle "$f"
done
eval "$SAVED_NULLGLOB"
此外,问题提到了多种模式,例如:
for f in *.ext1 special.ext1 *.ext2; do ...
这空值选项不会影响列表中不是“模式”的单词,因此special.ext1
仍会传递给循环。唯一的解决方案是@Gordon Davisson的continue
表情,[ -e "$f" ] || continue
。
致谢:@Ignacio Vazquez-Abrams和@Gordon Davisson提到bash(1)
及其空值选项。谢谢。由于他们没有立即提供完整示例的答案,所以我自己做了。