¿Los comodines recursivos en GNU hacen?


93

Ha pasado un tiempo desde que lo he usado make, así que tengan paciencia conmigo ...

Tengo un directorio flac, que contiene archivos .FLAC. Tengo un directorio correspondiente que mp3contiene archivos MP3. Si un archivo FLAC es más nuevo que el archivo MP3 correspondiente (o el archivo MP3 correspondiente no existe), entonces quiero ejecutar un montón de comandos para convertir el archivo FLAC en un archivo MP3 y copiar las etiquetas.

El truco: necesito buscar en el flacdirectorio de forma recursiva y crear los subdirectorios correspondientes en el mp3directorio. Los directorios y archivos pueden tener espacios en los nombres y se nombran en UTF-8.

Y quiero usarlo makepara conducir esto.


1
¿Alguna razón para seleccionar la marca para este propósito? Hubiera pensado que escribir un script de bash sería más simple

4
@Neil, el concepto de make como transformación del sistema de archivos basado en patrones es la mejor manera de abordar el problema original. Quizás las implementaciones de este enfoque tengan sus limitaciones, pero makeestá más cerca de implementarlo que de lo básico bash.
P Shved el

1
@Pavel Bueno, un shscript que recorre la lista de archivos flac ( find | while read flacname), hace un a mp3namepartir de eso, ejecuta "mkdir -p" en el dirname "$mp3name", y luego, if [ "$flacfile" -nt "$mp3file"]convierte "$flacname"en "$mp3name"no es realmente mágico. La única característica que realmente está perdiendo en comparación con una makesolución basada es la posibilidad de ejecutar Nprocesos de conversión de archivos en paralelo con make -jN.
ndim

4
@ndim Esa es la primera vez que escucho que la sintaxis de make se describe como "agradable" :-)

1
Usar makey tener espacios en los nombres de los archivos son requisitos contradictorios. Utilice una herramienta adecuada para el dominio del problema.
Jens

Respuestas:


117

Intentaría algo en esta línea

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

Un par de notas rápidas para makeprincipiantes:

  • El @frente de los comandos evita que se makeimprima el comando antes de ejecutarlo.
  • $(@D)es la parte del directorio del nombre del archivo de destino ( $@)
  • Asegúrese de que las líneas con comandos de shell comiencen con una pestaña, no con espacios.

Incluso si esto debería manejar todos los caracteres UTF-8 y demás, fallará en los espacios en los nombres de archivos o directorios, ya que makeusa espacios para separar cosas en los archivos MAKE y no conozco una forma de evitar eso. Entonces eso te deja con solo un script de shell, me temo: - /


Aquí es donde iba ... dedos rápidos para ganar. Aunque parece que puedes hacer algo inteligente con él vpath. Debo estudiar eso uno de estos días.
dmckee --- ex-moderador gatito

1
No parece funcionar cuando los directorios tienen espacios en los nombres.
Roger Lipscombe

No me di cuenta de que tendría que pagar findpara obtener los nombres de forma recursiva ...
Roger Lipscombe

1
@PaulKonova: Corre make -jN. Para Nutilizar el número de conversiones que makedeben ejecutarse en paralelo. Precaución: Ejecutar make -jsin un Niniciará todos los procesos de conversión a la vez en paralelo, lo que podría ser equivalente a una bomba de horquilla.
ndim

1
@Adrian: La .PHONY: alllínea le dice a make que la receta para el allobjetivo se ejecutará incluso si hay un archivo llamado allmás nuevo que todos los $(MP3_FILES).
ndim

53

Puede definir su propia función comodín recursiva como esta:

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

El primer parámetro ( $1) es una lista de directorios y el segundo ( $2) es una lista de patrones que desea hacer coincidir.

Ejemplos:

Para encontrar todos los archivos C en el directorio actual:

$(call rwildcard,.,*.c)

Para encontrar todos los archivos .cy .hen src:

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

Esta función se basa en la implementación de este artículo , con algunas mejoras.


Esto no parece funcionar para mí. He copiado la función exacta y todavía no se verá de forma recursiva.
Jeroen

3
Estoy usando GNU Make 3.81 y parece funcionar para mí. Sin embargo, no funcionará si alguno de los nombres de archivo tiene espacios. Tenga en cuenta que los nombres de archivo que devuelve tienen rutas relativas al directorio actual, incluso si solo está enumerando archivos en un subdirectorio.
larskholte

2
Este es verdaderamente un ejemplo, makees un Turing Tar Pit (ver aquí: yosefk.com/blog/fun-at-the-turing-tar-pit.html ). Ni siquiera es tan difícil, pero hay que leer esto: gnu.org/software/make/manual/html_node/Call-Function.html y luego "comprender la recurrencia". USTED tuvo que escribir esto de forma recursiva, en el sentido literal; no es la comprensión cotidiana de "incluir automáticamente cosas de subdirectorios". Es RECURRENCIA real. Pero recuerde: "Para comprender la recurrencia, debe comprender la recurrencia".
Tomasz Gandor

@TomaszGandor No es necesario que comprenda la recurrencia. Debe comprender la recursividad y, para hacerlo, primero debe comprender la recursividad.
user1129682

Mi mal, me enamoré de un falso amigo lingüístico. Los comentarios no se pueden editar después de tanto tiempo, pero espero que todos entiendan el punto. Y también entienden la recursividad.
Tomasz Gandor

2

FWIW, he usado algo como esto en un Makefile :

RECURSIVE_MANIFEST = `find . -type f -print`

El ejemplo anterior buscará en el directorio actual ( '.' ) Todos los "archivos sin formato" ( '-type f' ) y establecerá la RECURSIVE_MANIFESTvariable make para cada archivo que encuentre. A continuación, puede utilizar sustituciones de patrones para reducir esta lista o, alternativamente, proporcionar más argumentos en find para reducir lo que devuelve. Consulte la página de manual para encontrar .


1

Mi solución se basa en la anterior, usa en sedlugar de patsubstdestrozar la salida de findY escapar de los espacios.

Pasando de flac / a ogg /

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

Advertencias:

  1. Todavía barfs si hay punto y coma en el nombre del archivo, pero son bastante raros.
  2. El truco $ (@ D) no funcionará (genera galimatías), ¡pero oggenc crea directorios para usted!

1

Aquí hay un script de Python que pirateé rápidamente para resolver el problema original: mantener una copia comprimida de una biblioteca de música. El script convertirá archivos .m4a (se supone que son ALAC) al formato AAC, a menos que el archivo AAC ya exista y sea más reciente que el archivo ALAC. Los archivos MP3 de la biblioteca se vincularán, ya que ya están comprimidos.

Solo tenga cuidado de que abortar el script (ctrl-c ) dejará un archivo a medio convertir.

Originalmente también quería escribir un Makefile para manejar esto, pero dado que no puede manejar espacios en los nombres de archivo (ver la respuesta aceptada) y porque escribir un script bash está garantizado para ponerme en un mundo de dolor, Python lo es. Es bastante sencillo y corto, por lo que debería ser fácil de ajustar a sus necesidades.

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

Por supuesto, esto se puede mejorar para realizar la codificación en paralelo. Eso se deja como ejercicio para el lector ;-)


0

Si está usando Bash 4.x, puede usar una nueva opción de globbing , por ejemplo:

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

Este tipo de comodín recursivo puede encontrar todos los archivos de su interés ( .flac , .mp3 o lo que sea). O


1
Para mí, incluso solo $ (wildcard flac / ** / *. Flac) parece funcionar. OS X, Gnu Make 3.81
akauppi

2
Probé $ (comodín ./**/*.py) y se comportó igual que $ (comodín ./*/*.py). No creo que make realmente admita **, y simplemente no falla cuando usas dos * s uno al lado del otro.
Lahwran

@lahwran Debería hacerlo cuando invoca comandos a través del shell Bash y ha habilitado la opción globstar . Tal vez no esté usando GNU make o algo más. En su lugar, también puede probar esta sintaxis . Consulte los comentarios para ver algunas sugerencias. De lo contrario, es una cosa para la nueva pregunta.
Kenorb

@kenorb no no, ni siquiera probé lo tuyo porque quería evitar la invocación de shell para esta cosa en particular. Estaba usando lo sugerido por akauppi. La cosa con la que fui parecía la respuesta de larskholte, aunque la obtuve de otra parte porque los comentarios aquí decían que esta estaba sutilmente rota. encogimiento de hombros :)
lahwran

@lahwran En este caso **no funcionará, porque el globbing extendido es una cosa bash / zsh.
kenorb
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.