GNU make 中的递归通配符?

Posted

技术标签:

【中文标题】GNU make 中的递归通配符?【英文标题】:Recursive wildcards in GNU make? 【发布时间】:2011-01-29 19:17:16 【问题描述】:

我已经有一段时间没有使用make了,所以请耐心等待......

我有一个目录 flac,其中包含 .FLAC 文件。我有一个相应的目录,mp3 包含 MP3 文件。如果 FLAC 文件比对应的 MP3 文件更新(或对应的 MP3 文件不存在),那么我想运行一堆命令将 FLAC 文件转换为 MP3 文件,并复制标签。

踢球者:我需要递归搜索flac目录,并在mp3目录下创建相应的子目录。目录和文件的名称中可以有空格,并以 UTF-8 命名。

我想使用make 来驱动它。

【问题讨论】:

有什么理由为此选择make?我原以为写一个 bash 脚本会更简单 @Neil,make 的基于模式的文件系统转换概念是解决原始问题的最佳方法。也许这种方法的实现有其局限性,但make 比裸bash 更接近实现它。 @Pavel 好吧,一个遍历 flac 文件列表 (find | while read flacname) 的 sh 脚本,从中生成一个 mp3name,在 dirname "$mp3name" 上运行“mkdir -p” ,然后,if [ "$flacfile" -nt "$mp3file"]"$flacname" 转换为"$mp3name" 并不是很神奇。与基于make 的解决方案相比,您实际上失去的唯一功能是可以与make -jN 并行运行N 文件转换过程。 @ndim 这是我第一次听说 make 的语法被描述为“不错” :-) 使用make 和在文件名中有空格是矛盾的要求。使用适合问题域的工具。 【参考方案1】:

我会尝试这些方面的东西

FLAC_FILES = $(shell find flac/ -type f -name '*.flac')
MP3_FILES = $(patsubst flac/%.flac, mp3/%.mp3, $(FLAC_FILES))

.PHONY: all
all: $(MP3_FILES)

mp3/%.mp3: flac/%.flac
    @mkdir -p "$(@D)"
    @echo convert "$<" to "$@"

make初学者的一些快速说明:

命令前面的@ 可防止make 在实际运行之前打印命令。 $(@D)是目标文件名的目录部分($@) 确保其中包含 shell 命令的行以制表符开头,而不是空格。

即使这应该处理所有 UTF-8 字符和内容,它也会在文件或目录名称中的空格处失败,因为 make 使用空格来分隔 makefile 中的内容,我不知道有一种解决方法那。所以恐怕你只剩下一个shell脚本了:-/

【讨论】:

这就是我要去的地方...快速的手指为胜利。虽然看起来你可以用vpath 做一些聪明的事情。这些天必须研究那个。 当目录名称中有空格时似乎不起作用。 没有意识到我必须掏出find 才能递归地获取名称... @PaulKonova:运行make -jN。对于N,使用make 应该并行运行的转换次数。注意:在没有N 的情况下运行make -j 将立即并行启动所有转换过程,这可能相当于分叉炸弹。 @Adrian:.PHONY: all 行告诉 make 即使有一个名为 all 的文件比所有 $(MP3_FILES) 都更新,也要执行 all 目标的配方。 【参考方案2】:

您可以像这样定义自己的递归通配符函数:

rwildcard=$(foreach d,$(wildcard $(1:=/*)),$(call rwildcard,$d,$2) $(filter $(subst *,%,$2),$d))

第一个参数 ($1) 是目录列表,第二个参数 ($2) 是您要匹配的模式列表。

示例:

查找当前目录下的所有C文件:

$(call rwildcard,.,*.c)

要查找src 中的所有.c.h 文件:

$(call rwildcard,src,*.c *.h)

此函数基于this article 的实现,并进行了一些改进。

【讨论】:

这似乎对我不起作用。我已经复制了确切的函数,但它仍然不会递归。 我正在使用 GNU Make 3.81,它似乎对我有用。但是,如果任何文件名中包含空格,它将不起作用。请注意,它返回的文件名具有相对于当前目录的路径,即使您只列出子目录中的文件。 这确实是一个例子,make 是一个图灵焦油坑(见这里:yosefk.com/blog/fun-at-the-turing-tar-pit.html)。这甚至没有那么难,但是必须阅读以下内容:gnu.org/software/make/manual/html_node/Call-Function.html,然后“了解复发”。从逐字的意义上讲,您必须递归地编写它;这不是“自动包含子目录中的内容”的日常理解。这是实际的复发。但请记住——“要了解复发,你必须了解复发”。 @TomaszGandor 您不必了解重复。您必须了解递归,而要做到这一点,您必须首先了解递归。 我的错,我爱上了一个语言假朋友。这么久了评论也不能编辑了,希望大家明白。他们也理解递归。【参考方案3】:

如果您使用的是 Bash 4.x,则可以使用 a new globbing option,例如:

SHELL:=/bin/bash -O globstar
list:
  @echo Flac: $(shell ls flac/**/*.flac)
  @echo MP3: $(shell ls mp3/**/*.mp3)

这种递归通配符可以找到您感兴趣的所有文件(.flac.mp3 等)。哦

【讨论】:

对我来说,即使只是 $(wildcard flac/**/*.flac) 似乎也有效。 OS X, Gnu Make 3.81 我尝试了 $(wildcard ./**/*.py),它的行为与 $(wildcard ./*/*.py) 相同。我不认为 make 实际上支持 **,并且当您使用两个 * 彼此相邻时它不会失败。 @lahwran 当您通过 Bash shell 调用命令并且您启用了 globstar 选项时应该这样做。也许您没有使用 GNU make 或其他东西。你也可以试试this syntax。检查 cmets 以获得一些建议。否则,这是新问题的问题。 @kenorb 不不,我什至没有尝试你的东西,因为我想避免对这个特定的东西调用 shell。我正在使用 akauppi 建议的东西。我去的东西看起来像 larskholte 的答案,虽然我从其他地方得到它,因为这里的 cmets 说这个被巧妙地破坏了。耸耸肩:) @lahwran 在这种情况下,** 将不起作用,因为扩展的 globbing 是 bash/zsh 的事情。【参考方案4】:

FWIW,我在 Makefile 中使用过类似的东西:

RECURSIVE_MANIFEST = `find . -type f -print`

上面的例子将从当前目录('.')搜索所有“普通文件”('-type f')并设置RECURSIVE_MANIFEST为它找到的每个文件制作变量。然后,您可以使用模式替换来减少此列表,或者将更多参数提供给 find 以缩小它返回的范围。请参阅 find 的手册页。

【讨论】:

【参考方案5】:

我的解决方案基于上述解决方案,使用sed 而不是patsubst 来破坏find 的输出并转义空格。

从 flac/ 到 ogg/

OGGS = $(shell find flac -type f -name "*.flac" | sed 's/ /\\ /g;s/flac\//ogg\//;s/\.flac/\.ogg/' )

注意事项:

    如果文件名中有分号,仍然会出错,但这种情况很少见。 $(@D) 技巧不起作用(输出乱码),但 oggenc 会为您创建目录!

【讨论】:

【参考方案6】:

这是我快速编写的一个 Python 脚本来解决最初的问题:保留音乐库的压缩副本。该脚本会将 .m4a 文件(假定为 ALAC)转换为 AAC 格式,除非 AAC 文件已经存在并且比 ALAC 文件更新。库中的 MP3 文件将被链接,因为它们已经被压缩。

请注意,中止脚本 (ctrl-c) 会留下一个转换一半的文件。

我最初也想编写一个 Makefile 来处理这个问题,但由于它无法处理文件名中的空格(请参阅已接受的答案),并且因为编写 bash 脚本可以保证让我陷入痛苦的世界,Python 它是.它相当简单明了,因此应该很容易根据您的需要进行调整。

from __future__ import print_function


import glob
import os
import subprocess


UNCOMPRESSED_DIR = 'Music'
COMPRESSED = 'compressed_'

UNCOMPRESSED_EXTS = ('m4a', )   # files to convert to lossy format
LINK_EXTS = ('mp3', )           # files to link instead of convert


for root, dirs, files in os.walk(UNCOMPRESSED_DIR):
    out_root = COMPRESSED + root
    if not os.path.exists(out_root):
        os.mkdir(out_root)
    for file in files:
        file_path = os.path.join(root, file)
        file_root, ext = os.path.splitext(file_path)
        if ext[1:] in LINK_EXTS:
            if not os.path.exists(COMPRESSED + file_path):
                print('Linking '.format(file_path))
                link_source = os.path.relpath(file_path, out_root)
                os.symlink(link_source, COMPRESSED + file_path)
            continue
        if ext[1:] not in UNCOMPRESSED_EXTS:
            print('Skipping '.format(file_path))
            continue
        out_file_path = COMPRESSED + file_path
        if (os.path.exists(out_file_path)
            and os.path.getctime(out_file_path) > os.path.getctime(file_path)):
            print('Up to date: '.format(file_path))
            continue
        print('Converting '.format(file_path))
        subprocess.call(['ffmpeg', '-y', '-i', file_path,
                         '-c:a', 'libfdk_aac', '-vbr', '4',
                         out_file_path])

当然,这可以增强以并行执行编码。这留给读者作为练习;-)

【讨论】:

以上是关于GNU make 中的递归通配符?的主要内容,如果未能解决你的问题,请参考以下文章

Makefile入门: 用最美味的例子

您可以使用 NMAKE 为变量分配通配符吗? [复制]

允许在 make 文件中使用通配符

将字符串与通配符模式匹配的递归函数

make的特殊之处

来自特定文档 Firestore 安全性的通配符子集合