我有这样的想法:运行 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
删除原始文件,然后输出应保存到原始视频所在的同一文件夹中。
如何将其转换为递归运行?如果我使用
find
,在哪里声明变量$file
?您应该在哪里申报$target
?难道一切find
真的都只是一句台词吗?我确实需要将文件传递给变量$file
,因为我仍然需要运行条件检查。并且,假设(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
以便rm
您echo
可以看到什么会被执行 - 以及使用什么路径。
这是最终结果,包括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 时的默认值,所以这很好。