Рекурсивные символы подстановки в GNU make?

93

Прошло много времени с тех пор, как я использовал make, так что терпите меня ...

У меня есть каталог flac, содержащий файлы .FLAC. У меня есть соответствующий каталог, mp3содержащий файлы MP3. Если файл FLAC новее, чем соответствующий файл MP3 (или соответствующий файл MP3 не существует), я хочу запустить несколько команд для преобразования файла FLAC в файл MP3 и скопировать теги.

Кикер: мне нужно flacрекурсивно искать в каталоге и создавать в нем соответствующие подкаталоги mp3. Каталоги и файлы могут иметь пробелы в именах и имеют имена в кодировке UTF-8.

И хочу использовать makeдля этого.

Роджер Липскомб
источник
1
Есть ли причина для выбора производителя для этой цели? Я бы подумал, что написать сценарий на bash будет проще
4
@Neil, концепция make как преобразование файловой системы на основе шаблонов - лучший способ решить исходную проблему. Возможно, у реализации этого подхода есть свои ограничения, но makeон ближе к реализации, чем голый bash.
П Швед
1
@Pavel Ну, shсценарий, который просматривает список файлов flac ( find | while read flacname), создает mp3nameиз него, запускает "mkdir -p" dirname "$mp3name"и затем if [ "$flacfile" -nt "$mp3file"]конвертирует "$flacname"в , на "$mp3name"самом деле не волшебство. Единственная функция, которую вы фактически теряете по сравнению с makeбазовым решением, - это возможность запускать Nпроцессы преобразования файлов параллельно с make -jN.
ndim
4
@ndim Я впервые слышу, как синтаксис make можно описать как "хороший" :-)
1
Использование makeи наличие пробелов в именах файлов - противоречивые требования. Используйте инструмент, соответствующий проблемной области.
Йенс

Ответы:

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)является частью каталога имени целевого файла ( $@)
  • Убедитесь, что строки с командами оболочки в них начинаются с табуляции, а не с пробелов.

Даже если это должно обрабатывать все символы UTF-8 и прочее, он не будет работать с пробелами в именах файлов или каталогов, поскольку makeиспользует пробелы для разделения вещей в make-файлах, и я не знаю, как это обойти. Боюсь, у вас останется только сценарий оболочки: - /

ndim
источник
Вот куда я шел ... быстрые пальцы для победы. Хотя, похоже, вы сможете сделать что-нибудь по-умному vpath. Должен изучить это на днях.
dmckee --- котенок экс-модератора
1
Не работает, если в названиях каталогов есть пробелы.
Роджер Липскомб,
Не понимал, что мне придется выложиться, чтобы findполучить имена рекурсивно ...
Роджер Липскомб,
1
@PaulKonova: Беги make -jN. Для Nиспользования количества преобразований, которые makeдолжны выполняться параллельно. Внимание: запуск make -jбез Nсимвола запускает все процессы преобразования одновременно, что может быть эквивалентно форк-бомбе.
ndim
1
@Adrian: .PHONY: allСтрока сообщает программе make, что рецепт для allцели должен быть выполнен, даже если есть файл с именем allновее, чем все файлы $(MP3_FILES).
ndim
53

Вы можете определить свою собственную рекурсивную функцию с подстановочными знаками следующим образом:

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
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
@TomaszGandor Вам не обязательно понимать повторение. Вы должны понимать рекурсию, и для этого вы должны сначала понять рекурсию.
user1129682
Плохо, я влюбился в ложного друга по языку. Комментарии нельзя редактировать по прошествии такого долгого времени, но я надеюсь, что все поняли суть. И они также понимают рекурсию.
Томаш Гандор
2

FWIW, я использовал что-то вроде этого в Makefile :

RECURSIVE_MANIFEST = `find . -type f -print`

В приведенном выше примере выполняется поиск в текущем каталоге ( '.' ) Всех «простых файлов» ( '-type f' ) и установка RECURSIVE_MANIFESTпеременной make для каждого найденного файла. Затем вы можете использовать подстановки шаблонов, чтобы уменьшить этот список, или, альтернативно, предоставить больше аргументов в find, чтобы сузить то, что он возвращает. См. Справочную страницу для поиска .

Михаил Шебанов
источник
1

Мое решение основано на приведенном выше, 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 создает для вас каталоги!
user3199485
источник
1

Вот сценарий 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

Если вы используете Bash 4.x, вы можете использовать новую опцию подстановки , например:

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

Такой рекурсивный подстановочный знак может найти все интересующие вас файлы ( .flac , .mp3 и т. Д.). О

Kenorb
источник
1
Мне кажется, работает даже просто $ (wildcard flac / ** / *. Flac). OS X, Gnu Make 3.81
akauppi
2
Я попробовал $ (wildcard ./**/*.py), и он вел себя так же, как $ (wildcard ./*/*.py). Я не думаю, что make на самом деле поддерживает **, и он просто не терпит неудач, когда вы используете две * рядом друг с другом.
lahwran
@lahwran Это должно происходить при вызове команд через оболочку Bash и при включенной опции globstar . Возможно, вы не используете GNU make или что-то еще. Вы также можете попробовать этот синтаксис . Проверьте комментарии для некоторых предложений. В противном случае это новый вопрос.
kenorb
@kenorb нет, я даже не пробовал вашу вещь, потому что хотел избежать вызова оболочки для этой конкретной вещи. Я использовал предложенную Акауппи штуку. То, что я выбрал, выглядело как ответ Ларскхолта, хотя я получил его откуда-то еще, потому что в комментариях здесь говорилось, что этот был слегка сломан.
пожимают
@lahwran В этом случае **не сработает, потому что расширенная подстановка - это вещь bash / zsh.
kenorb