Rekursive Platzhalter in GNU machen?


91

Es ist schon eine Weile her, seit ich es benutzt habe make, also nimm es mit ...

Ich habe ein Verzeichnis flacmit FLAC-Dateien. Ich habe ein entsprechendes Verzeichnis mp3mit MP3-Dateien. Wenn eine FLAC-Datei neuer als die entsprechende MP3-Datei ist (oder die entsprechende MP3-Datei nicht vorhanden ist), möchte ich eine Reihe von Befehlen ausführen, um die FLAC-Datei in eine MP3-Datei zu konvertieren und die Tags zu kopieren.

Der Kicker: Ich muss das flacVerzeichnis rekursiv durchsuchen und entsprechende Unterverzeichnisse im mp3Verzeichnis erstellen . Die Verzeichnisse und Dateien können Leerzeichen in den Namen enthalten und werden in UTF-8 benannt.

Und ich möchte makedamit fahren.


1
Gibt es einen Grund für die Auswahl von make für diesen Zweck? Ich habe einen Bash - Skript gedacht zu schreiben wäre einfacher

4
@Neil, das Konzept von make als musterbasierte Dateisystemtransformation ist der beste Weg, um das ursprüngliche Problem anzugehen. Vielleicht haben Implementierungen dieses Ansatzes ihre Grenzen, sind aber makenäher an der Implementierung als bloß bash.
P Shved

1
@Pavel Nun, ein shSkript , das die Liste der FLAC - Dateien geht durch ( find | while read flacname), macht eine mp3namedavon läuft „mkdir -p“ auf das dirname "$mp3name", und dann if [ "$flacfile" -nt "$mp3file"]wandelt "$flacname"in "$mp3name"nicht wirklich Magie. Die einzige Funktion, die Sie im Vergleich zu einer makebasierten Lösung tatsächlich verlieren, ist die Möglichkeit, NDateikonvertierungsprozesse parallel zu auszuführen make -jN.
Ndim

4
@ndim Das ist das erste Mal, dass ich gehört habe, dass die Syntax von make als "nett" beschrieben wird :-)

1
Die Verwendung makeund das Vorhandensein von Leerzeichen in Dateinamen sind widersprüchliche Anforderungen. Verwenden Sie ein für die Problemdomäne geeignetes Tool.
Jens

Antworten:


114

Ich würde etwas in diese Richtung versuchen

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

Ein paar kurze Hinweise für makeAnfänger:

  • Das @vor den Befehlen stehende makeBefehl verhindert, dass der Befehl gedruckt wird, bevor er tatsächlich ausgeführt wird.
  • $(@D)ist der Verzeichnisteil des Zieldateinamens ( $@)
  • Stellen Sie sicher, dass die Zeilen mit Shell-Befehlen mit einer Registerkarte beginnen, nicht mit Leerzeichen.

Selbst wenn dies alle UTF-8-Zeichen und -Stücke behandeln sollte, schlägt dies an Stellen in Datei- oder Verzeichnisnamen fehl, da makeLeerzeichen verwendet werden, um Sachen in den Makefiles zu trennen, und mir ist keine Möglichkeit bekannt, dies zu umgehen . Ich fürchte, Sie haben nur noch ein Shell-Skript: - /


Hier ging ich hin ... schnelle Finger für den Sieg. Obwohl es so aussieht, als könnten Sie etwas Kluges tun vpath. Muss das eines Tages studieren.
dmckee --- Ex-Moderator Kätzchen

1
Scheint nicht zu funktionieren, wenn die Verzeichnisse Leerzeichen in den Namen haben.
Roger Lipscombe

Ich wusste nicht, dass ich berappen musste find, um die Namen rekursiv zu erhalten ...
Roger Lipscombe

1
@ PaulKonova: Laufen make -jN. Zur NVerwendung die Anzahl der Konvertierungen, die makeparallel ausgeführt werden sollen. Achtung: Wenn Sie make -johne Nlaufen, werden alle Konvertierungsvorgänge gleichzeitig parallel gestartet, was möglicherweise einer Gabelbombe entspricht.
Ndim

1
@Adrian: Die .PHONY: allZeile sagt make, dass das Rezept für das allZiel ausgeführt werden soll, auch wenn es eine Datei gibt, die allneuer heißt als alle $(MP3_FILES).
ndim

52

Sie können Ihre eigene rekursive Platzhalterfunktion folgendermaßen definieren:

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

Der erste Parameter ( $1) ist eine Liste von Verzeichnissen und der zweite ( $2) ist eine Liste von Mustern, mit denen Sie übereinstimmen möchten.

Beispiele:

So finden Sie alle C-Dateien im aktuellen Verzeichnis:

$(call rwildcard,.,*.c)

So finden Sie alle .cund .hDateien in src:

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

Diese Funktion basiert auf der Implementierung aus diesem Artikel mit einigen Verbesserungen.


Das scheint bei mir nicht zu funktionieren. Ich habe die genaue Funktion kopiert und sie sieht immer noch nicht rekursiv aus.
Jeroen

3
Ich benutze GNU Make 3.81 und es scheint für mich zu funktionieren. Es funktioniert jedoch nicht, wenn einer der Dateinamen Leerzeichen enthält. Beachten Sie, dass die zurückgegebenen Dateinamen Pfade relativ zum aktuellen Verzeichnis haben, auch wenn Sie nur Dateien in einem Unterverzeichnis auflisten.
Larskholte

2
Dies ist wirklich ein Beispiel, das makeist eine Turing-Teergrube (siehe hier: yosefk.com/blog/fun-at-the-turing-tar-pit.html ). Es ist nicht einmal so schwer, aber man muss dies lesen: gnu.org/software/make/manual/html_node/Call-Function.html und dann "Wiederholung verstehen". SIE mussten dies rekursiv im wörtlichen Sinne schreiben; Es ist nicht das alltägliche Verständnis von "automatisch Dinge aus Unterverzeichnissen einschließen". Es ist tatsächlich RECURRENCE. Aber denken Sie daran: "Um die Wiederholung zu verstehen, müssen Sie die Wiederholung verstehen."
Tomasz Gandor

@TomaszGandor Du musst die Wiederholung nicht verstehen. Sie müssen die Rekursion verstehen, und um dies zu tun, müssen Sie zuerst die Rekursion verstehen.
user1129682

Mein schlechtes, ich verliebte mich in einen sprachlichen falschen Freund. Kommentare können nach so langer Zeit nicht mehr bearbeitet werden, aber ich hoffe, alle haben es verstanden. Und sie verstehen auch Rekursion.
Tomasz Gandor

2

FWIW, ich habe so etwas in einem Makefile verwendet :

RECURSIVE_MANIFEST = `find . -type f -print`

Im obigen Beispiel wird aus dem aktuellen Verzeichnis ( '.' ) Nach allen "einfachen Dateien" ( '-type f' ) gesucht und die RECURSIVE_MANIFESTVariable make auf jede gefundene Datei gesetzt. Sie können dann Musterersetzungen verwenden, um diese Liste zu verkleinern, oder alternativ mehr Argumente in find angeben, um die Rückgabe einzugrenzen. Weitere Informationen finden Sie in der Manpage .


1

Meine Lösung basiert auf der obigen, verwendet, sedanstatt patsubstdie Ausgabe von findAND zu entstellen, um die Leerzeichen zu verlassen.

Von flac / nach ogg /

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

Vorsichtsmaßnahmen:

  1. Immer noch Barfs, wenn der Dateiname Semikolons enthält, aber sie sind ziemlich selten.
  2. Der Trick $ (@ D) funktioniert nicht (gibt Kauderwelsch aus), aber oggenc erstellt Verzeichnisse für Sie!

1

Hier ist ein Python-Skript, das ich schnell zusammen gehackt habe, um das ursprüngliche Problem zu lösen: Behalte eine komprimierte Kopie einer Musikbibliothek. Das Skript konvertiert .m4a-Dateien (vermutlich ALAC) in das AAC-Format, es sei denn, die AAC-Datei ist bereits vorhanden und neuer als die ALAC-Datei. MP3-Dateien in der Bibliothek werden verknüpft, da sie bereits komprimiert sind.

Beachten Sie jedoch, dass beim Abbrechen des Skripts ( ctrl-c) eine halb konvertierte Datei zurückbleibt.

Ich wollte ursprünglich auch ein Makefile schreiben, um dies zu handhaben, aber da es keine Leerzeichen in Dateinamen verarbeiten kann (siehe die akzeptierte Antwort) und weil das Schreiben eines Bash-Skripts mich garantiert in eine Welt voller Schmerzen versetzt, ist es Python. Es ist ziemlich einfach und kurz und sollte daher leicht an Ihre Bedürfnisse anzupassen sein.

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

Dies kann natürlich verbessert werden, um die Codierung parallel durchzuführen. Das bleibt dem Leser als Übung überlassen ;-)


0

Wenn Sie Bash 4.x verwenden, können Sie eine neue Globbing-Option verwenden , zum Beispiel:

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

Diese Art von rekursivem Platzhalter kann alle Dateien von Interesse finden ( .flac , .mp3 oder was auch immer). Ö


1
Für mich scheint sogar nur $ (Wildcard Flac / ** / *. Flac) zu funktionieren. OS X, Gnu Make
3.81

2
Ich habe $ (Platzhalter ./**/*.py) ausprobiert und es hat sich genauso verhalten wie $ (Platzhalter ./*/*.py). Ich glaube nicht, dass make tatsächlich ** unterstützt, und es scheitert einfach nicht, wenn Sie zwei * s nebeneinander verwenden.
Lahwran

@lahwran Dies sollte der Fall sein, wenn Sie Befehle über die Bash-Shell aufrufen und die Globstar- Option aktiviert haben . Vielleicht verwenden Sie nicht GNU make oder etwas anderes. Sie können stattdessen auch diese Syntax ausprobieren . Überprüfen Sie die Kommentare auf einige Vorschläge. Ansonsten ist es eine Sache für die neue Frage.
Kenorb

@kenorb nein nein, ich habe dein Ding nicht einmal ausprobiert, weil ich einen Shell-Aufruf für dieses spezielle Ding vermeiden wollte. Ich habe Akauppis Vorschlag verwendet. Die Sache, mit der ich ging, sah aus wie die Antwort von Larskholte, obwohl ich sie von irgendwo anders bekommen habe, weil die Kommentare hier besagten, dass diese subtil gebrochen war. Achselzucken :)
Lahwran

@lahwran In diesem Fall **funktioniert es nicht, da das erweiterte Globbing eine Bash / Zsh-Sache ist.
Kenorb
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.