GNU make中的递归通配符是什么?

115

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

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

重点是:我需要递归搜索flac目录,并在mp3目录中创建相应的子目录。这些目录和文件名中可能会有空格,并且以UTF-8命名。

我想使用make来驱动这个过程。


1
选择make的原因是什么?我认为编写一个bash脚本会更简单。 - anon
4
@Neil,将“make”概念作为基于模式的文件系统转换是解决原始问题的最佳方法。也许这种方法的实现有其局限性,但“make”比裸的“bash”更接近实现它。 - P Shved
1
@Pavel 嗯,一个 sh 脚本遍历 flac 文件列表 (find | while read flacname),从中制作出一个 mp3name,在 "$mp3name" 上运行 "mkdir -p",然后,如果 [ "$flacfile" -nt "$mp3file"],将 "$flacname" 转换为 "$mp3name" 并不是真正的魔法。与基于 make 的解决方案相比,您实际上失去的唯一功能是无法使用 make -jN 并行运行 N 个文件转换进程的可能性。 - ndim
4
@ndim:这是我第一次听到有人将“make”的语法描述为“不错”。 - anon
1
使用make并且文件名中有空格是矛盾的要求。请使用适合该问题领域的工具。 - Jens
显示剩余4条评论
7个回答

140

我会尝试沿着这些方向进行

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脚本,我很抱歉 :/


2
当目录名称中包含空格时,似乎无法正常工作。 - Roger Lipscombe
1
жІЎжғіеҲ°жҲ‘еҫ—дҪҝз”Ёfindе‘Ҫд»ӨжқҘйҖ’еҪ’иҺ·еҸ–ж–Ү件еҗҚгҖӮ - Roger Lipscombe
1
@PaulKonova:运行make -jN。对于N,请使用make应该并行运行的转换数量。注意:在没有N的情况下运行make -j将同时启动所有转换进程,这可能相当于一个fork bomb。 - ndim
是的,那个可以。此外,在这种情况下,有没有一种方法可以从MP3目录中删除旧的目录和文件,这些文件已经不在FLAC目录中了?让MP3目录与FLAC目录保持一致? - paulkon
1
@Adrian:.PHONY: all 这一行告诉 make,即使存在一个比所有 $(MP3_FILES) 都新的名为 all 的文件,也要执行 all 目标的配方。 - ndim
显示剩余13条评论

80

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

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)
这个函数基于这篇文章的实现,经过了一些改进。

这对我似乎没有用。 我已经复制了完全相同的函数,但它仍然无法递归查看。 - Jeroen
5
我正在使用 GNU Make 3.81 版本,看起来它对我有效。但如果文件名中有空格,它就无法正常工作。请注意,返回的文件名相对于当前目录,即使您只列出子目录中的文件。 - user1896626
3
这确实是一个例子,证明make是图灵陷阱(参见这里:http://yosefk.com/blog/fun-at-the-turing-tar-pit.html)。它并不难懂,但人们需要阅读这个链接:https://www.gnu.org/software/make/manual/html_node/Call-Function.html ,然后“理解循环”。你必须以递归的方式编写代码,按照字面意思来说;这不是“从子目录自动包含文件”的日常理解。这是真正的循环,但请记住——“要理解循环,你必须理解循环”。 - Tomasz Gandor
1
@TomaszGandor 你不必理解“递归”。你必须理解“递归”,为此,首先必须理解“递归”。 - user1129682
这比调用 shell 函数更具可移植性。此外,我能够使用它来获取目录列表 $(sort $(dir $(call rwildcard,src,*)))(其中 src 是我的顶级目录)。 - Rodney
显示剩余3条评论

6
如果你使用的是Bash 4.x,你可以使用一个新的globbing选项,例如: 这里
SHELL:=/bin/bash -O globstar
list:
  @echo Flac: $(shell ls flac/**/*.flac)
  @echo MP3: $(shell ls mp3/**/*.mp3)

这种递归通配符可以找到您感兴趣的所有文件(.flac.mp3或其他格式)。

3
对我来说,即使只有$(wildcard flac/ ** / *. Flac),似乎也能正常工作。OS X,Gnu Make 3.81。 - akauppi
2
我尝试了$(wildcard .//.py),它的行为和$(wildcard .//*.py)一样。我不认为make实际上支持,只是在你使用两个连续的*时不会报错。 - lahwran
@lahwran 当您通过Bash shell调用命令并启用_globstar_选项时,它应该可以工作。也许您没有使用GNU make或其他东西。您还可以尝试使用此语法。请查看评论以获取一些建议。否则,这就是一个新问题。 - kenorb
@kenorb 不不,我甚至没有尝试你的方法,因为我想避免在这个特定的事情中调用 shell。我正在使用 akauppi 建议的方法。我选择的方法看起来像 larskholte 的答案,尽管我从其他地方得到了它,因为这里的评论说这个有些微妙的问题。耸肩 :) - lahwran
1
@lahwran 在这种情况下,** 不起作用,因为扩展的 globbing 是 bash/zsh 特有的功能。 - kenorb
显示剩余4条评论

2

FWIW,我在Makefile中使用了类似这样的东西:

RECURSIVE_MANIFEST = `find . -type f -print`

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


2
我的解决方案基于上面的那个,使用sed而不是patsubst来处理find命令的输出,并转义空格。
从flac/转换到ogg/。
OGGS = $(shell find flac -type f -name "*.flac" | sed 's/ /\\ /g;s/flac\//ogg\//;s/\.flac/\.ogg/' )

注意事项:

  1. 如果文件名中有分号,程序仍然会崩溃,但这种情况比较少见。
  2. $(@D)技巧无法使用(输出乱码),但oggenc会为您创建目录!

2
这是我快速编写的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])

当然,这可以进一步改进以并行执行编码。这留给读者作为练习;-)

0
要递归地查找文件而不依赖外部工具(如find),你可以使用函数。然后像另一个答案中那样使用结果来转换文件。
rwildcard=$(wildcard $1) $(foreach d,$1,$(call rwildcard,$(addsuffix /$(notdir $d),$(wildcard $(dir $d)*))))

FLAC_FILES = $(call rwildcard,flac/*.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 "$@"

请参阅https://github.com/markpiffer/gmtt#call-wildcard-reclist-of-globs,了解递归通配符的增强版本。 - Vroomfondel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接