将“for file in”转换为“find”,以便我的脚本可以递归应用

将“for file in”转换为“find”,以便我的脚本可以递归应用

我有这样的想法:运行 bash 脚本来检查某些条件,并使用它ffmpeg来将目录中的所有视频从任何格式转换为任何格式,.mkv效果非常好!

问题是,我不知道for file in循环不能递归地工作(https://stackoverflow.com/questions/4638874/how-to-loop-through-a-directory-recursively

但我几乎不理解“管道”,我期待看到一个例子并消除一些不确定性。

我脑子里有这个场景,我认为这对我理解有很大帮助。

假设我有这个 bash 脚本片段:

for file in *.mkv *avi *mp4 *flv *ogg *mov; do
target="${file%.*}.mkv"
    ffmpeg -i "$file" "$target" && rm -rf "$file"
done

它的作用是,对于当前目录,搜索任何*.mkv *avi *mp4 *flv *ogg *mov然后声明输出的扩展名,然后.mkv删除原始文件,然后输出应保存到原始视频所在的同一文件夹中。

  1. 如何将其转换为递归运行?如果我使用find,在哪里声明变量$file?您应该在哪里申报$target?难道一切find真的都只是一句台词吗?我确实需要将文件传递给变量$file,因为我仍然需要运行条件检查。

  2. 并且,假设(1)成功,如何确保满足“然后输出应保存到原始视频所在的同一文件夹”的要求?

答案1

使用 POSIX 查找:

find . \( -name '*.mkv' -o -name '*avi' -o -name '*mp4' -o -name '*flv' -o \
          -name '*ogg' -o -name '*mov' \) -exec sh -c '
  for file do
    target="${file%.*}.mkv"
    echo ffmpeg -i "$file" "$target"
  done' sh {} +

替换echo为您要使用的任何命令。

如果您有 GNU find 或 BSD find,您可以使用-regex

find . -regex '.*\.\(mkv\|avi\|mp4\|flv\|ogg\|mov\)'

答案2

你有这个代码:

for file in *.mkv *avi *mp4 *flv *ogg *mov; do
target="${file%.*}.mkv"
    ffmpeg -i "$file" "$target" && rm -rf "$file"
done

它在当前目录中运行。要将其变成递归过程,您有几种选择。最简单的(IMO)是按照您的建议使用find。 for 的语法find非常“不像 UNIX”,但这里的原则是每个参数都可以与 AND 或 OR 条件一起应用。在这里,我们要说“如果此文件名匹配或该文件名匹配则打印它"。文件名模式被引用,以便 shell 无法获取它们(请记住,shell 负责扩展所有未加引号的模式,因此,如果您有一个未加引号的模式*.mp4并且janeeyre.mp4在当前目录中,shell 会替换*.mp4为匹配项,并且find会看到-name janeeyre.mp4而不是您想要的;如果匹配多个名称,-name *.mp4情况会变得更糟*.mp4...)。\,如果愿意的话'(':)。

find . \( -name '*.mkv' -o -name '*avi' -o -name '*mp4' -o -name '*flv' -o -name '*ogg' -o -name '*mov' \) -print

其输出需要输入到while依次处理每个文件的循环的输入中:

while IFS= read file    ## IFS= prevents "read" stripping whitespace
do
    target="${file%.*}.mkv"
    ffmpeg -i "$file" "$target" && rm -rf "$file"
done

现在剩下的就是用管道将两个部分连接在一起,|以便输出find成为循环的输入while

当您测试此代码时,我建议您在 和 前面加上前缀,ffmpeg以便rmecho可以看到什么被执行 - 以及使用什么路径。

这是最终结果,包括echo我建议测试的语句:

find . \( -name '*.mkv' -o -name '*avi' -o -name '*mp4' -o -name '*flv' -o -name '*ogg' -o -name '*mov' \) -print |
    while IFS= read file    ## IFS= prevents "read" stripping whitespace
        do
            target="${file%.*}.mkv"
            echo ffmpeg -i "$file" "$target" && echo rm -rf "$file"
        done

答案3

没有管道的示例片段(假设您将路径作为参数给出):

#!/bin/bash

backup_dir=/backup/

OIFS="$IFS"
IFS=$'\n'

files="$(find "$1" -type f -name '*.mkv' -or -name '*.avi' -or -name '*.mp4' -or -name '*.ogg' -or -name '*.mov' -or -name '*.flv')"

for f in $files; do
    # get path
    d="${f%/*}"
    # get filename
    b="$(basename "$f")"
    ttarget="${b%.*}.mkv"

    # this is your final target
    target="$d/$ttarget"
    echo $target
    # mv $f "$backup_dir" 
done

IFS="$OIFS"

shell 读取该变量,默认IFS设置为 ( space, tab, )。newline然后它查看 输出中的每个字符find。因此,如果它发现space它认为它是文件名的结尾(包含空格的文件,例如“Sin City.avi”将被视为两个文件“Sin”和“City.avi”)。因此,使用 IFS=$'\n' 我们告诉将输入拆分为newlines。最后我们恢复IFS保存在$OIFS变量中的旧的(默认)。
或者正如评论中所建议的可能更好的方法可能是:

#!/bin/bash

backup_dir=/backup/

find "$1" -type f \( -name '*.mkv' -or -name '*.avi' -or -name '*.mp4' -or -name '*.ogg' -or -name '*.mov' -or -name '*.flv' \) -print0 | while IFS= read -r -d '' f
do
    # get path
    d="${f%/*}"
    # get filename
    b="$(basename "$f")"
    ttarget="${b%.*}.mkv"

    # this is your final target
    target="$d/$ttarget"
    echo $target
    # mv $f "$backup_dir"
done

答案4

欢迎来到 Unix :)

回答主要问题的答案未涵盖的一些小问题:

Shell 脚本确实有一些粗糙的边缘,因为很多东西都会在带有空格的文件名上出现问题。几乎所有文件名都会因换行符而中断(幸运的是,没有人故意这样做)。包含全局字符(如[]、 和 )的文件名*有时也是一个问题。有时,不值得编写符合以下标准的难以阅读的 shell 代码:伍利奇的 BashGuide,供您自己使用,或者一次性使用,您知道您的文件名并不奇怪。

在哪里声明变量:

Shell 变量不需要声明。在 bash 中,您可以shopt -o nounset将引用和取消设置变量设置为错误,但这与未声明并不完全相同。取消设置变量可能很有用。在 shell 函数中,最好使用 声明所有临时变量local foo bar baz;,这样您就不会在 shell 环境中乱扔变量,或者更糟糕的是,踩到调用者的同名变量。

我几乎不明白“管道”。

使用 shell 时,通过将数据打印到标准输出来进行大量数据传递。管道将该数据发送到另一个程序,该程序在标准输入上读取数据(并且通常在标准输出上打印一些内容)。您可以使用命令替换将输出捕获到 shell 变量中$()。例如for i in $( locate foo | grep bar );do echo "$i"; done。 (如果你不小心的话,这会破坏带有空格的文件名,就像很多 shell 代码一样。read如果你想编写可靠的脚本,请使用它。) locate打印、grep读取和打印,然后 shell 读取grep. (shell 通过启动 grep 来获取输出,grep并将其输出连接到 shell 创建的管道的输入端。shell 读取管道的输出端。)

管道只是程序像写入文件一样工作的一种方式,但实际上它们是写入一个小缓冲区。当有数据可用时,从管道读取的进程将read(2)返回其系统调用,这种情况仅在向管道另一端写入数据时才会发生。

shell 的|$()和其他一些语法元素用于告诉 shell 如何设置程序之间以及程序与 shell 之间的管道连接。

学习 shell 编程的糟糕习惯用法很容易,因为许多显而易见的事情和旧的做事方式都隐藏着一些陷阱,这些陷阱会在奇怪的文件名上崩溃。参见示例http://mywiki.wooledge.org/BashFAQ/001

最好从一开始就学习安全的脚本编写方法,而不是学习破坏奇怪文件名的方法,只要它们不太笨重而无法输入。 :)

许多 GNU utils 有一个 -0 选项,使用 ASCII NUL(0 字节,不能出现在文件名或文本中)作为记录分隔符。例如,这使您可以在find和之间传输数据sort,而不可能将查找输出的一“行”转换为多行排序输入。当您想要将数据放入 shell 变量时,这最终并不是非常有用,因为 bash 没有办法读取\0- 分隔的行。 (我认为这不是 IFS 的有效值。)

无论如何,避免让 shell 将数据视为代码是始终对所有可能的内容进行双引号的原因,除非您真的想要分词。如果您想在查看复杂的 shell 代码时感到头疼,只需查看 bash 完成代码即可。 (它处理可编程完成,可以完成一些聪明的事情,例如完成ls --colo => --color或仅完成 *.zip 文件以进行解压缩。) set -x并点击选项卡:P。 (设置 +x 以关闭执行跟踪。)

回复:您的 for 循环:作为*.mkv您的模式之一,您将为这些输入文件提供 source = dest 。 ffmpeg将提示您覆盖每个文件的输出文件。

另外,您真的需要对音频进行转码吗? -c:a copy可能是个好主意。视频比特率通常更重要。您可能希望使用-preset slow(或slower,甚至veryslow) 来获得更高的每比特率质量,但代价是更多的 CPU 使用率。还有-crf 20(默认 23)。 https://trac.ffmpeg.org/wiki/Encode/H.264。希望您已经知道这一点,并忽略它,因为它与 bash 脚本无关,但以防万一... :P -c:v libx264是输出到 mkv 时的默认值,所以这很好。

相关内容