sh 语法来处理与通配符匹配的零个文件,以及更多?

sh 语法来处理与通配符匹配的零个文件,以及更多?

我想编写一个/bin/shshell 脚本来处理任何与通配符匹配的文件。处理 1 个或多个匹配文件很容易。但是,我发现处理 0 个匹配文件的情况很尴尬。

显而易见的结构是:

#!/bin/sh
for f in *.ext; do
  handle "$f"
done

where*.ext可以是一个或多个表达式,shell 会将其与文件路径进行比较,并且handle是一个 shell 命令,如果给定现有文件的路径,则该命令会正确运行,但如果给定的路径未映射到文件,则运行失败。(在引发这个问题的情况下,它们是*.flacffmpeg,但我认为这并不重要。)

如果有匹配的文件foo.extbar.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 Davissoncontinue表情,[ -e "$f" ] || continue

致谢:@Ignacio Vazquez-Abrams@Gordon Davisson提到bash(1)及其空值选项。谢谢。由于他们没有立即提供完整示例的答案,所以我自己做了。

相关内容