GNU中的递归通配符会使?


92

自从我使用了一段时间以来make,请忍受...

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

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

我想用make这个来驱动。


1
为此有任何选择品牌的理由吗?我还以为写一个bash脚本会更简单

4
@ Neil,make的概念是基于模式的文件系统转换是解决原始问题的最佳方法。也许这种方法的实现有其局限性,但是make比裸露更接近于实现它bash
P Shved

1
@Pavel好,一个sh遍历flac文件列表(find | while read flacname)的脚本,mp3name从中生成一个,在上运行“ mkdir -p” dirname "$mp3name",然后if [ "$flacfile" -nt "$mp3file"]将其"$flacname"转换"$mp3name"为并不是真正的魔术。与make基于基础的解决方案相比,您实际上失去的唯一功能是可以与N并行运行文件转换过程make -jN
2010年

4
@ndim那是我听过make的语法被描述为“好” :-)第一次

1
make在文件名中使用空格并带有空格是矛盾的要求。使用适合于问题域的工具。
詹斯

Answers:


117

我会沿着这些思路尝试

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。这几天必须学习一下。
dmckee ---前主持人小猫,2010年

1
当目录名称中有空格时,该目录似乎不起作用。
罗杰·利普斯科姆2010年

没意识到我必须find
掏腰包

1
@PaulKonova:快跑make -jN。对于N使用的转换次数make应该并行运行。注意:如果make -j不使用,N则会立即并行启动所有转换过程,这可能相当于叉子炸弹。
ndim

1
@Adrian:该.PHONY: all行告诉makeall即使要存在all比所有文件都更新的文件,也要执行目标的配方$(MP3_FILES)
ndim '18

52

您可以定义自己的递归通配符函数,如下所示:

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

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

例子:

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

$(call rwildcard,.,*.c)

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

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

此功能基于本文的实现,并进行了一些改进。


这似乎对我不起作用。我已经复制了确切的函数,但它仍然不会递归查找。
Jeroen 2013年

3
我正在使用GNU Make 3.81,它似乎对我有用。但是,如果任何文件名中都包含空格,则将不起作用。请注意,即使您仅列出子目录中的文件,它返回的文件名也具有相对于当前目录的路径。
larskholte

2
这确实是一个例子,那make就是图灵焦油坑(请参见此处:yosefk.com/blog/fun-at-the-turing-tar-pit.html)。它甚至没有那么难,但是必须阅读以下内容:gnu.org/software/make/manual/html_node/Call-Function.html,然后“了解重复发生”。您必须逐字逐句地编写此代码;这不是“自动包括子目录中的内容”的日常理解。这是实际的递归。但是请记住-“要了解复发,就必须了解复发”。
Tomasz Gandor 2014年

@TomaszGandor您不必了解复发。您必须了解递归,然后您必须首先了解递归。
user1129682

不好,我迷上了语言错误的朋友。这么长的时间后,评论将无法编辑,但我希望每个人都明白这一点。他们也了解递归。
Tomasz Gandor

2

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

RECURSIVE_MANIFEST = `find . -type f -print`

上面的示例将从当前目录('。')中搜索所有“普通文件”('-type f'),并将RECURSIVE_MANIFESTmake变量设置为其找到的每个文件。然后,您可以使用模式替换来减少此列表,或者可以在find中提供更多参数以缩小返回值。有关查找信息,请参见手册页。


1

我的解决方案基于上面的解决方案,sed而不是patsubst用来处理findAND的输出以转义空格。

从flac /到ogg /

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

注意事项:

  1. 如果文件名中包含分号,仍会发出响声,但是这种情况很少见。
  2. $(@ D)技巧不起作用(输出乱码),但是oggenc会为您创建目录!

1

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

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

我最初也想编写一个Makefile来处理这个问题,但是由于它不能处理文件名中的空格(请参阅接受的答案),并且因为编写bash脚本肯定会让我感到痛苦,所以确实如此。它非常简单明了,因此很容易调整您的需求。

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

如果您使用的是Bash 4.x,则可以使用新的globlob选项,例如:

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

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


1
对我来说,甚至$(通配符flac / ** / *。flac)似乎也有效。OS X,Gnu Make 3.81
akauppi

2
我尝试了$(通配符./**/*.py),它的行为与$(通配符./*/*.py)相同。我不认为make实际上支持**,并且当您彼此相邻使用两个*时,它不会失败。
lahwran

@lahwran当您通过Bash shell调用命令并启用了globstar选项时,应该使用。也许您没有使用GNU make或其他东西。您也可以尝试使用此语法。查看评论以获取一些建议。否则,这是一个新问题。
kenorb '16

@kenorb不,不,我什至没有尝试您的操作,因为我想避免对此特定事件进行shell调用。我正在使用akauppi的建议内容。尽管我是从其他地方得到的,但我所处理的事情看起来像是larskholte的答案,因为这里的评论说这个问题被巧妙地打破了。耸耸肩:)
lahwran

@lahwran在这种情况下**不起作用,因为扩展的globing是bash / zsh的事情。
kenorb
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.