Les jokers récursifs dans GNU make?

91

Cela fait un moment que je n'ai pas utilisé make, alors supportez-moi ...

J'ai un répertoire flac, contenant des fichiers .FLAC. J'ai un répertoire correspondant, mp3contenant des fichiers MP3. Si un fichier FLAC est plus récent que le fichier MP3 correspondant (ou si le fichier MP3 correspondant n'existe pas), je souhaite exécuter un tas de commandes pour convertir le fichier FLAC en fichier MP3 et copier les balises.

Le kicker: je dois rechercher le flacrépertoire de manière récursive et créer les sous-répertoires correspondants dans le mp3répertoire. Les répertoires et fichiers peuvent avoir des espaces dans les noms et sont nommés en UTF-8.

Et je veux utiliser makepour conduire cela.

Roger Lipscombe
la source
1
Une raison de choisir une marque à cet effet? Je l' aurais pensé écrire un script bash serait plus simple
4
@Neil, le concept de make en tant que transformation de système de fichiers basée sur des modèles est la meilleure façon d'aborder le problème d'origine. Peut-être que les implémentations de cette approche ont ses limites, mais elles makesont plus proches de sa mise en œuvre que nues bash.
P Shved
1
@Pavel Eh bien, un shscript qui parcourt la liste des fichiers flac ( find | while read flacname), crée un à mp3namepartir de là, exécute "mkdir -p" sur le dirname "$mp3name", puis se if [ "$flacfile" -nt "$mp3file"]convertit "$flacname"en "$mp3name"n'est pas vraiment magique. La seule fonctionnalité que vous perdez réellement par rapport à une makesolution basée est la possibilité d'exécuter Ndes processus de conversion de fichiers en parallèle avec make -jN.
ndim
4
Ce @ndim est la première fois que je l' ai jamais entendu la syntaxe de make être décrit comme « agréable » :-)
1
L'utilisation makeet la présence d'espaces dans les noms de fichiers sont des exigences contradictoires. Utilisez un outil adapté au domaine du problème.
Jens

Réponses:

115

J'essaierais quelque chose de ce genre

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 "$@"

Quelques notes rapides pour les makedébutants:

  • Le @devant des commandes empêche maked'imprimer la commande avant de l'exécuter.
  • $(@D)est la partie répertoire du nom du fichier cible ( $@)
  • Assurez-vous que les lignes contenant des commandes shell commencent par une tabulation et non par des espaces.

Même si cela doit gérer tous les caractères UTF-8 et autres choses, il échouera aux espaces dans les noms de fichiers ou de répertoires, car il makeutilise des espaces pour séparer les éléments dans les fichiers makefiles et je ne suis pas au courant d'un moyen de contourner cela. Donc, cela ne vous laisse qu'un script shell, j'ai peur: - /

ndim
la source
C'est là que j'allais ... doigts rapides pour la victoire. Bien qu'il semble que vous puissiez faire quelque chose d'intelligent avec vpath. Doit étudier ça un de ces jours.
dmckee --- ex-moderator chaton
1
Ne semble pas fonctionner lorsque les répertoires ont des espaces dans les noms.
Roger Lipscombe
Je ne savais pas que je devais débourser findpour obtenir les noms de manière récursive ...
Roger Lipscombe
1
@PaulKonova: Courez make -jN. Pour Nutiliser le nombre de conversions qui makedoivent s'exécuter en parallèle. Attention: L'exécution make -jsans an Nlancera tous les processus de conversion en même temps en parallèle, ce qui pourrait être équivalent à une bombe à fourche.
ndim
1
@Adrian: La .PHONY: allligne indique à make que la recette de la allcible doit être exécutée même s'il existe un fichier appelé allplus récent que tous les $(MP3_FILES).
ndim
52

Vous pouvez définir votre propre fonction générique récursive comme ceci:

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

Le premier paramètre ( $1) est une liste de répertoires, et le second ($2 ) est une liste de modèles que vous souhaitez faire correspondre.

Exemples:

Pour trouver tous les fichiers C dans le répertoire courant:

$(call rwildcard,.,*.c)

Pour trouver tous les fichiers .cet .hdans src:

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

Cette fonction est basée sur l'implémentation de cet article , avec quelques améliorations.

Larskholte
la source
Cela ne semble pas fonctionner pour moi. J'ai copié la fonction exacte et elle ne sera toujours pas récursive.
Jeroen
3
J'utilise GNU Make 3.81, et cela semble fonctionner pour moi. Cela ne fonctionnera pas si l'un des noms de fichiers contient des espaces. Notez que les noms de fichiers qu'il renvoie ont des chemins relatifs au répertoire courant, même si vous ne listez que les fichiers dans un sous-répertoire.
larskholte
2
C'est vraiment un exemple, c'est makeun goudron de Turing (voir ici: yosefk.com/blog/fun-at-the-turing-tar-pit.html ). Ce n'est même pas si difficile, mais il faut lire ceci: gnu.org/software/make/manual/html_node/Call-Function.html et ensuite "comprendre la récurrence". VOUS deviez écrire ceci de manière récursive, au sens littéral du terme; ce n'est pas la compréhension quotidienne de "inclure automatiquement des éléments des sous-répertoires". C'est la RÉCURRENCE réelle. Mais rappelez-vous - "Pour comprendre la récurrence, vous devez comprendre la récurrence".
Tomasz Gandor
@TomaszGandor Vous n'avez pas à comprendre la récurrence. Vous devez comprendre la récursivité et pour ce faire, vous devez d'abord comprendre la récursivité.
user1129682
Mon mauvais, je suis tombé amoureux d'un faux-ami linguistique. Les commentaires ne peuvent pas être modifiés après si longtemps, mais j'espère que tout le monde a compris. Et ils comprennent également la récursivité.
Tomasz Gandor
2

FWIW, j'ai utilisé quelque chose comme ça dans un Makefile :

RECURSIVE_MANIFEST = `find . -type f -print`

L'exemple ci-dessus recherchera dans le répertoire courant ( '.' ) Tous les "fichiers simples" ( '-type f' ) et définira la RECURSIVE_MANIFESTvariable make sur chaque fichier qu'il trouve. Vous pouvez ensuite utiliser des substitutions de modèles pour réduire cette liste ou, alternativement, fournir plus d'arguments à find pour restreindre ce qu'il renvoie. Consultez la page de manuel pour trouver .

Michael Shebanow
la source
1

Ma solution est basée sur celle ci-dessus, utilise sedau lieu de patsubstpour modifier la sortie defind ET échapper aux espaces.

Passer de flac / à ogg /

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

Mises en garde:

  1. Toujours barfs s'il y a des points-virgules dans le nom de fichier, mais ils sont assez rares.
  2. L'astuce $ (@ D) ne fonctionnera pas (produit du charabia), mais oggenc crée des répertoires pour vous!
utilisateur3199485
la source
1

Voici un script Python que j'ai rapidement piraté ensemble pour résoudre le problème d'origine: conserver une copie compressée d'une bibliothèque musicale. Le script convertira les fichiers .m4a (supposés être ALAC) au format AAC, sauf si le fichier AAC existe déjà et est plus récent que le fichier ALAC. Les fichiers MP3 de la bibliothèque seront liés, car ils sont déjà compressés.

Méfiez-vous simplement que l'annulation du script (ctrl-c ) laissera un fichier à moitié converti.

À l'origine, je voulais aussi écrire un Makefile pour gérer cela, mais comme il ne peut pas gérer les espaces dans les noms de fichiers (voir la réponse acceptée) et parce que l'écriture d'un script bash est garantie de me mettre dans un monde de douleur, c'est Python. C'est assez simple et court, et devrait donc être facile à adapter à vos besoins.

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])

Bien entendu, cela peut être amélioré pour effectuer le codage en parallèle. Cela est laissé comme exercice au lecteur ;-)

Brecht Machiels
la source
0

Si vous utilisez Bash 4.x, vous pouvez utiliser une nouvelle option de globbing , par exemple:

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

Ce type de joker récursif peut trouver tous les fichiers qui vous intéressent ( .flac , .mp3 ou autre). O

Kenorb
la source
1
Pour moi, même juste $ (wildcard flac / ** / *. Flac) semble fonctionner. OS X, Gnu Make 3.81
akauppi
2
J'ai essayé $ (wildcard ./**/*.py) et il s'est comporté de la même manière que $ (wildcard ./*/*.py). Je ne pense pas que make supporte réellement **, et cela n'échoue tout simplement pas lorsque vous utilisez deux * l'un à côté de l'autre.
lahwran
@lahwran Cela devrait quand vous invoquez des commandes via le shell Bash et que vous avez activé l' option globstar . Peut-être que vous n'utilisez pas GNU make ou autre chose. Vous pouvez également essayer cette syntaxe à la place. Consultez les commentaires pour quelques suggestions. Sinon, c'est une chose pour la nouvelle question.
kenorb
@kenorb non non, je n'ai même pas essayé votre truc parce que je voulais éviter l'invocation du shell pour cette chose particulière. J'utilisais la chose suggérée par akauppi. La chose avec laquelle je suis allé ressemblait à la réponse de Larskholte, bien que je l'ai eue ailleurs parce que les commentaires ici disaient que celle-ci était subtilement cassée. hausser les épaules :)
lahwran
@lahwran Dans ce cas **ne fonctionnera pas, car le globbing étendu est une chose bash / zsh.
kenorb