Caracteres curinga recursivos no GNU make?

91

Já faz um tempo desde que eu uso make, então tenha paciência comigo ...

Eu tenho um diretório flac,, contendo arquivos .FLAC. Eu tenho um diretório correspondente, mp3contendo arquivos MP3. Se um arquivo FLAC for mais recente que o arquivo MP3 correspondente (ou o arquivo MP3 correspondente não existir), quero executar vários comandos para converter o arquivo FLAC em MP3 e copiar as tags.

O kicker: eu preciso pesquisar o flacdiretório recursivamente e criar subdiretórios correspondentes no mp3diretório. Os diretórios e arquivos podem ter espaços nos nomes e são nomeados em UTF-8.

E eu quero usar makepara dirigir isso.

Roger Lipscombe
fonte
1
Alguma razão para selecionar o make para este propósito? Eu pensei que escrever um script bash seria mais simples
4
@Neil, o conceito de make como transformação do sistema de arquivos baseada em padrões é a melhor maneira de abordar o problema original. Talvez as implementações dessa abordagem tenham suas limitações, mas makeestá mais perto de implementá-la do que simples bash.
P Shved de
1
@Pavel Bem, um shscript que percorre a lista de arquivos flac ( find | while read flacname), faz um a mp3namepartir disso, executa "mkdir -p" no dirname "$mp3name"e então if [ "$flacfile" -nt "$mp3file"]converte "$flacname"em "$mp3name"não é realmente mágico. O único recurso que você está realmente perdendo em comparação com uma makesolução baseada é a possibilidade de executar Nprocessos de conversão de arquivos em paralelo com make -jN.
ndim
4
@ndim É a primeira vez que ouço a sintaxe do make ser descrita como "legal" :-)
1
Usar makee ter espaços nos nomes dos arquivos são requisitos contraditórios. Use uma ferramenta apropriada para o domínio do problema.
Jens

Respostas:

115

Eu tentaria algo nesse sentido

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

Algumas notas rápidas para makeiniciantes:

  • O @na frente dos comandos impede a makeimpressão do comando antes de realmente executá-lo.
  • $(@D)é a parte do diretório do nome do arquivo de destino ( $@)
  • Certifique-se de que as linhas com comandos shell começam com uma tabulação, não com espaços.

Mesmo que isso deva lidar com todos os caracteres UTF-8 e outras coisas, ele falhará em espaços nos nomes de arquivo ou diretório, pois makeusa espaços para separar coisas nos makefiles e não estou ciente de uma maneira de contornar isso. Então isso deixa você com apenas um script de shell, infelizmente: - /

ndim
fonte
É para onde eu estava indo ... dedos rápidos para a vitória. Embora pareça que você poderá fazer algo inteligente com ele vpath. Devo estudar isso um dia desses.
dmckee --- ex-moderador gatinho de
1
Não parece funcionar quando os diretórios têm espaços nos nomes.
Roger Lipscombe,
Não percebi que teria que pagar findpara obter os nomes recursivamente ...
Roger Lipscombe
1
@PaulKonova: Execute make -jN. Para Nusar o número de conversões que makedevem ser executadas em paralelo. Cuidado: Executar make -jsem um Niniciará todos os processos de conversão de uma vez em paralelo, o que pode ser equivalente a uma bomba fork.
ndim
1
@Adrian: A .PHONY: alllinha diz ao make que a receita para o allalvo deve ser executada mesmo se houver um arquivo chamado allmais recente que todos os $(MP3_FILES).
ndim
52

Você pode definir sua própria função curinga recursiva assim:

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

O primeiro parâmetro ( $1) é uma lista de diretórios e o segundo ( $2) é uma lista de padrões que você deseja corresponder.

Exemplos:

Para encontrar todos os arquivos C no diretório atual:

$(call rwildcard,.,*.c)

Para encontrar todas as .ce .harquivos em src:

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

Esta função é baseada na implementação deste artigo , com algumas melhorias.

Larskholte
fonte
Isso não parece funcionar para mim. Copiei a função exata e ainda não parecerá recursivamente.
Jeroen
3
Estou usando o GNU Make 3.81 e parece funcionar para mim. Não funcionará se algum dos nomes de arquivo contiver espaços. Observe que os nomes de arquivo que ele retorna têm caminhos relativos ao diretório atual, mesmo se você estiver listando apenas os arquivos em um subdiretório.
larskholte
2
Este é realmente um exemplo, que makeé um Turing Tar Pit (veja aqui: yosefk.com/blog/fun-at-the-turing-tar-pit.html ). Não é nem tão difícil, mas é preciso ler isso: gnu.org/software/make/manual/html_node/Call-Function.html e então "entender a recorrência". VOCÊ teve que escrever isso recursivamente, no sentido literal; não é o entendimento diário de "incluir automaticamente coisas de subdiretórios". É RECORRÊNCIA real. Mas lembre-se - "Para entender a recorrência, você tem que entender a recorrência".
Tomasz Gandor
@TomaszGandor Você não precisa entender a recorrência. Você tem que entender a recursão e para fazer isso você deve primeiro entender a recursão.
user1129682
Que pena, apaixonei-me por um falso amigo linguístico. Os comentários não podem ser editados depois de tanto tempo, mas espero que todos tenham entendido. E eles também entendem a recursão.
Tomasz Gandor
2

FWIW, usei algo assim em um Makefile :

RECURSIVE_MANIFEST = `find . -type f -print`

O exemplo acima irá pesquisar no diretório atual ( '.' ) Por todos os "arquivos simples" ( '-tipo f' ) e definir a RECURSIVE_MANIFESTvariável make para cada arquivo que encontrar. Você pode então usar substituições de padrão para reduzir essa lista ou, alternativamente, fornecer mais argumentos em find para restringir o que ela retorna. Veja a página do manual para encontrar .

Michael Shebanow
fonte
1

Minha solução é baseada na acima, usa ao sedinvés de patsubstmangle a saída de findAND escapar dos espaços.

Indo de flac / para ogg /

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

Ressalvas:

  1. Ainda barfs se houver ponto e vírgula no nome do arquivo, mas eles são muito raros.
  2. O truque $ (@ D) não funcionará (gera gibberish), mas oggenc cria diretórios para você!
user3199485
fonte
1

Aqui está um script Python que eu criei rapidamente para resolver o problema original: mantenha uma cópia compactada de uma biblioteca de música. O script converterá os arquivos .m4a (supostamente do ALAC) para o formato AAC, a menos que o arquivo AAC já exista e seja mais recente que o arquivo ALAC. Os arquivos MP3 na biblioteca serão vinculados, uma vez que já estão compactados.

Apenas tome cuidado, pois abortar o script ( ctrl-c) deixará um arquivo convertido pela metade.

Originalmente, eu também queria escrever um Makefile para lidar com isso, mas como ele não pode lidar com espaços em nomes de arquivos (veja a resposta aceita) e porque escrever um script bash certamente me colocará em um mundo de dor, Python é. É bastante simples e curto e, portanto, deve ser fácil de ajustar às suas necessidades.

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

Claro, isso pode ser aprimorado para realizar a codificação em paralelo. Isso é deixado como um exercício para o leitor ;-)

Brecht Machiels
fonte
0

Se você estiver usando o Bash 4.x, poderá usar uma nova opção de globbing , por exemplo:

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

Este tipo de curinga recursivo pode localizar todos os arquivos de seu interesse ( .flac , .mp3 ou qualquer outro). O

Kenorb
fonte
1
Para mim, mesmo apenas $ (wildcard flac / ** / *. Flac) parece funcionar. OS X, Gnu Make 3.81
akauppi
2
Tentei $ (wildcard ./**/*.py) e ele se comportou da mesma forma que $ (wildcard ./*/*.py). Eu não acho que o make realmente suporte **, e ele simplesmente não falha quando você usa dois * s próximos um do outro.
lahwran
@lahwran Deve quando você invoca comandos via shell Bash e você habilitou a opção globstar . Talvez você não esteja usando GNU make ou qualquer outra coisa. Você também pode tentar esta sintaxe . Verifique os comentários para algumas sugestões. Caso contrário, é uma coisa pela nova questão.
kenorb
@kenorb não não, eu nem tentei o seu negócio porque queria evitar a invocação do shell para este item em particular. Eu estava usando a sugestão do akauppi. A coisa com a qual escolhi parecia a resposta de larskholte, embora a tenha obtido de outro lugar porque os comentários aqui diziam que esta estava sutilmente quebrada. encolher de ombros :)
lahwran
@lahwran Neste caso **não funcionará, porque o globbing estendido é uma coisa bash / zsh.
kenorb