【问题标题】:bash: best practice to iterate over directory content until condition matchesbash:迭代目录内容直到条件匹配的最佳实践
【发布时间】:2016-11-26 20:18:41
【问题描述】:

我有以下情况:

我想检查$1目录中的文件直到其中一个符合我的条件。

详细说明:我想测试该目录是否包含音频文件。一旦找到第一个音频文件,process_audio_dir 就会发生;如果目录中没有音频文件,则会发生process_noaudio

到目前为止我的解决方案:

if [[ -z $(file -b "$1"/* | grep -i audio) ]]; then
  echo "there are no audio files"; process_noaudio
else
  echo "at least one audio file"; process_audio_dir
fi

file -b 告诉我文件是什么文件类型。

我用set -x 查看它的猜测是,这将在所有文件上运行file -b,将结果放在一行中并用greps 匹配该行。 (也许这是一个错误的假设)。

我宁愿有一个循环,直到找到第一个音频文件(一个足以匹配条件)并在那里停止/中断,或者,如果没有音频文件,继续到process_noaudio

我觉得 while/until 可以实现这一点,但我想不通。

检查目录中每个文件直到第一次匹配的方法是什么(您的首选|最佳实践|最优雅|成本最低|最快)?

【问题讨论】:

  • 以为我做到了,但我删除了(额外的)代码标签。你猜这是一条自动评论?
  • 谢谢。代码更易读。
  • 哪个平台?如果我们只需要支持 GNU file,这可以通过一种既不牺牲效率也不牺牲正确性的方式来完成。

标签: bash loops iteration


【解决方案1】:

可能最安全的方法是直接遍历 glob 结果,这样您就不会受到包含特殊字符的文件名的攻击:

for path in "$1"/*; do
    if file -b -- "$path" | grep -qi audio; then
        printf 'Found an audio file %s\n' "$path"
        process_audio_dir
        exit
   fi
done
# since we didn't exit above, most be no audio files
printf "Didn't find any audio files\n"
process_noaudio

或者,如果您不想在那里退出,您可以设置一个标志来指示您找到它并在循环之后检查它,然后只需在if 内使用break 即可退出循环已经找到了。

您将grep 应用于所有file 结果的输出的原因是glob 首先扩展,因此您然后运行命令,例如

file -b dir/file1 dir/file2 dir/file3 ...

然后该命令的输出将被提供给grep 我的解决方案将 glob 放在命令的“外部”,因此我们将在每个文件上单独运行它。当然,多次启动file 会产生更多开销,所以对我来说并不明显哪个会更有效率。这可能取决于有多少文件,第一个音频文件通常在列表中的位置以及类似的东西。

正如 cmets 中所提到的,迭代来自 findls 的打印文件名结果是很危险的,因为这些结果可能会受到分词的影响,并且可能会根据您的操作方式进行通配。使用上面的for 循环通常是推荐的方法。更多内容见Don't Parse ls

【讨论】:

  • 嗨,埃里克;是的,这就是我的目标。我可以用 for each_file in $(find "$1" -type f); do file -b $each_file | grep -i audio; if [matches audio file] do x; continue; else do y; continue; fi 或其他东西做一个 for 循环。我想知道“标准”或最有效的方式是什么?
  • @badlands,使用for 循环以这种方式迭代find 输出是一种反模式;请参阅BashPitfalls,条目 #1。
  • 顺便说一句——为什么“假设目录中没有太多文件”? for 循环不受 argv 长度限制。
  • @CharlesDuffy 啊,我实际上并不确定那部分,所以感谢您的修复,我现在会更新!
  • 唯一让我感到困惑的是,$1 中命名的目录以破折号开头——不太可能,但可能;在扩展"$path" 之前通过-- 将解决此问题。否则,我能找到的唯一反对意见是与效率相关的(每个文件运行一次file),并且在不牺牲正确性的情况下避免那些需要 GNUisms。
【解决方案2】:

低效,但兼容

find "$1" -maxdepth 1 -type f \
  -exec sh -c 'file -b -- "$1" | grep -qi audio' _ {} \; \
  -print \
  -quit

在这里,我们正在对每个单独的名称执行运行 file 的 shell 管道,然后调用 grep 来检查其结果。这显然是低效的,但是由于 -exec 在它运行的 shell 命令返回非零退出代码时失败,find 仍然会在 grep 返回真值的第一个结果上提前退出(因此允许 @987654327 @ 和 -quit 要运行的操作)。


高效,但仅限 GNU

shopt -s nocaseglob # enable case-insensitive matching
while IFS= read -r -d '' filename && IFS= read -r type; do
  if [[ $type = *audio* ]]; then
    break # exit the loop with the name in "$filename" and the type in "$type"
  fi
done < <(find "$1" -type f -maxdepth 1 -exec file -b -0 -- '{}' +)
echo "Found file $filename of type $type"

在这里,我们运行尽可能少的file 实例(使用-exec ... {} + 将多个文件名传递给每个file 调用),使用GNU 扩展名-0 在它们的每个文件名之后打印一个NUL输出。然后使用一对read 命令解析此输出,并检查子字符串audio 的类型。

【讨论】:

  • -maxdepth 应该在-type 和其他非选项参数之前指定。在第一个示例中也缺少续行。
  • 谢谢;修改。
  • Charles,+1,但为了简洁起见,这里可能适合短路:[[ $type = *audio* ]] &amp;&amp; break
【解决方案3】:

在 POSIX C API 中,我们有 opendir()/readdir(),这是以流的形式增量读取目录内容的最灵活方式。但是,这些函数并没有映射到 Bash。

ls 是您在 Bash 中列出目录内容的主要方式。您可以使用ls 执行以下操作以将列表作为流处理(但我很确定ls 是否会缓冲列表。ls 确实支持需要缓冲的排序):

ls "$1" | while read f; do test -f $f && echo $f | grep -q "audio" && echo $f && break; done

另一个常用工具是find。逐步查找作品。以下将适用于“迭代目录内容直到条件匹配”的用例,并且是比ls 更好的方法。这只是打印第一个找到的文件名。当条件匹配时,调整以适应您想要做的事情:

find -maxdepth 1 -type f -exec "bash" -c "file -b '{}' | grep -qi audio" ";" -print -quit

【讨论】:

  • 这里给出的建议是非常有害的:以编程方式使用ls 输出is an antipattern,因为它的格式没有足够明确地以一种可以可靠地反转为的方式准确地表示所有文件名它们的文字内容(因此,以open()-type 调用可用的方式)。出于类似的原因,find 的自动使用应该使用-print0 以明确的形式发出带有换行文字的偶数名称。
  • 另外,OP的条件需要运行file;因此,-name 匹配是不够的。
  • @CharlesDuffy 我展示了如何将ls 输出作为流处理的选项,因为 OP 在他的帖子中使用了它。我的建议是使用 find。
  • 明白了。不过,我仍然认为这是在测试名称而不是由file -b 确定的类型元数据。
  • @CharlesDuffy 关于file -b:OP 的问题是“迭代目录内容直到条件匹配的最佳实践”。解决方案是通用的,可以很容易地适应套件。
猜你喜欢
  • 1970-01-01
  • 2012-02-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-18
  • 2017-05-17
  • 1970-01-01
  • 2013-04-02
相关资源
最近更新 更多