Rekurencyjne symbole wieloznaczne w programie GNU make?

93

Minęło trochę czasu, odkąd używałem make, więc wytrzymaj ze mną ...

Mam katalog flaczawierający pliki .FLAC. Mam odpowiedni katalog mp3zawierający pliki MP3. Jeśli plik FLAC jest nowszy niż odpowiadający mu plik MP3 (lub odpowiadający mu plik MP3 nie istnieje), chcę uruchomić kilka poleceń, aby przekonwertować plik FLAC na plik MP3 i skopiować tagi w poprzek.

Kicker: Muszę przeszukać flackatalog rekurencyjnie i utworzyć odpowiednie podkatalogi w mp3katalogu. Katalogi i pliki mogą mieć spacje w nazwach i są nazywane w UTF-8.

I chcę to wykorzystać makedo prowadzenia tego.

Roger Lipscombe
źródło
1
Czy jest jakiś powód, aby wybrać markę do tego celu? Sądziłem, pisanie skryptu bash byłoby prostsze
4
@Neil, koncepcja make, polegająca na przekształceniu systemu plików w oparciu o wzorce, jest najlepszym sposobem rozwiązania pierwotnego problemu. Być może implementacje tego podejścia mają swoje ograniczenia, ale makejest bliższe realizacji niż gołe bash.
P Shved
1
@Pavel Cóż, shskrypt, który przechodzi przez listę plików flac ( find | while read flacname), tworzy mp3namez tego a, uruchamia "mkdir -p" na dirname "$mp3name", a następnie if [ "$flacfile" -nt "$mp3file"]konwertuje "$flacname"na "$mp3name"nie jest naprawdę magiczny. Jedyną cechą, którą faktycznie tracisz w porównaniu z makerozwiązaniem opartym na bazie, jest możliwość Nrównoległego uruchamiania procesów konwersji plików make -jN.
ndim
4
@ndim To pierwszy raz, kiedy słyszałem, że składnia make jest opisana jako „ładna” :-)
1
Używanie makei posiadanie spacji w nazwach plików to sprzeczne wymagania. Użyj narzędzia odpowiedniego dla problematycznej domeny.
Jens

Odpowiedzi:

117

Spróbowałbym czegoś podobnego

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

Kilka krótkich uwag dla makepoczątkujących:

  • Znak @na początku poleceń zapobiega ich makewydrukowaniu przed jego faktycznym uruchomieniem.
  • $(@D)jest częścią katalogu nazwy pliku docelowego ( $@)
  • Upewnij się, że wiersze zawierające polecenia powłoki zaczynają się tabulatorem, a nie spacjami.

Nawet jeśli powinno to obsłużyć wszystkie znaki UTF-8 i inne rzeczy, zakończy się niepowodzeniem w przypadku spacji w nazwach plików lub katalogów, ponieważ makeużywa spacji do oddzielenia rzeczy w plikach makefile i nie jestem świadomy sposobu obejścia tego. Zostaje więc tylko skrypt powłoki, obawiam się: - /

ndim
źródło
To jest miejsce, do którego zmierzałem ... szybkie palce na zwycięstwo. Chociaż wygląda na to, że możesz zrobić coś sprytnego vpath. Muszę przestudiować ten jeden z tych dni.
dmckee --- kociak byłego moderatora
1
Nie wydaje się działać, gdy katalogi mają spacje w nazwach.
Roger Lipscombe
Nie zdawali sobie sprawy, że będę musiał zapłacić, aby finduzyskać nazwy rekurencyjnie ...
Roger Lipscombe
1
@PaulKonova: Uruchom make -jN. Do Nwykorzystania liczby konwersji, które makepowinny przebiegać równolegle. Uwaga: Uruchomienie make -jbez Nuruchomi wszystkie procesy konwersji jednocześnie, co może być równoważne z bombą widelcową.
ndim
1
@Adrian: .PHONY: allLinia mówi make, że przepis na allcel ma zostać wykonany, nawet jeśli istnieje plik o nazwie allnowszy niż wszystkie $(MP3_FILES).
ndim
53

Możesz zdefiniować własną rekurencyjną funkcję wieloznaczną w następujący sposób:

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

Pierwszy parametr ( $1) to lista katalogów, a drugi ( $2) to lista wzorców, które chcesz dopasować.

Przykłady:

Aby znaleźć wszystkie pliki C w bieżącym katalogu:

$(call rwildcard,.,*.c)

Aby znaleźć wszystkie pliki .ci .hw src:

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

Ta funkcja jest oparta na implementacji z tego artykułu , z kilkoma ulepszeniami.

larskholte
źródło
To chyba nie działa dla mnie. Skopiowałem dokładną funkcję i nadal nie będzie wyglądać rekurencyjnie.
Jeroen,
3
Używam GNU Make 3.81 i wydaje mi się, że działa. Jednak nie zadziała, jeśli którakolwiek z nazw plików zawiera spacje. Zwróć uwagę, że nazwy plików, które zwraca, mają ścieżki względne w stosunku do bieżącego katalogu, nawet jeśli wymieniasz tylko pliki w podkatalogu.
larskholte
2
To jest naprawdę przykład, to makejest Turing Tar Pit (patrz tutaj: yosefk.com/blog/fun-at-the-turing-tar-pit.html ). Nie jest to nawet takie trudne, ale trzeba przeczytać to: gnu.org/software/make/manual/html_node/Call-Function.html, a następnie „zrozumieć powtarzalność”. TY musieliście to napisać rekurencyjnie, dosłownie; nie jest to codzienne rozumienie „automatycznie dołączaj rzeczy z podkatalogów”. To rzeczywisty nawrót. Ale pamiętaj - „Aby zrozumieć nawrót, musisz zrozumieć nawrót”.
Tomasz Gandor
@TomaszGandor Nie musisz rozumieć powtarzalności. Musisz zrozumieć rekurencję i aby to zrobić, musisz najpierw zrozumieć rekurencję.
user1129682
Moja wina, dałem się nabrać na językowego fałszywego przyjaciela. Komentarze nie mogą być edytowane po tak długim czasie, ale mam nadzieję, że wszyscy zrozumieli. Rozumieją także rekurencję.
Tomasz Gandor
2

FWIW, użyłem czegoś takiego w pliku Makefile :

RECURSIVE_MANIFEST = `find . -type f -print`

Powyższy przykład wyszuka w bieżącym katalogu ( „.” ) Wszystkie „zwykłe pliki” ( „-type f” ) i ustawi RECURSIVE_MANIFESTzmienną make na każdy znaleziony plik. Następnie możesz użyć podstawień wzorców, aby zredukować tę listę, lub alternatywnie, podać więcej argumentów, aby znaleźć, aby zawęzić to, co zwraca. Zobacz stronę podręcznika, aby znaleźć .

Michael Shebanow
źródło
1

Moje rozwiązanie jest oparte na powyższym, używa sedzamiast patsubstmodyfikować wyjście findAND ucieczki ze spacji.

Przejście z flac / do ogg /

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

Ostrzeżenia:

  1. Nadal barfs, jeśli w nazwie pliku znajdują się średniki, ale są one dość rzadkie.
  2. Sztuczka $ (@ D) nie zadziała (wyświetla bełkot), ale oggenc tworzy dla Ciebie katalogi!
user3199485
źródło
1

Oto skrypt w Pythonie, który szybko zhakowałem, aby rozwiązać pierwotny problem: zachowaj skompresowaną kopię biblioteki muzycznej. Skrypt skonwertuje pliki .m4a (zakłada się, że jest to ALAC) do formatu AAC, chyba że plik AAC już istnieje i jest nowszy niż plik ALAC. Pliki MP3 w bibliotece zostaną połączone, ponieważ są już skompresowane.

Uważaj tylko, że przerwanie skryptu ( ctrl-c) spowoduje pozostawienie w połowie przekonwertowanego pliku.

Początkowo chciałem również napisać Makefile, aby to obsłużyć, ale ponieważ nie może on obsługiwać spacji w nazwach plików (zobacz akceptowaną odpowiedź) i ponieważ napisanie skryptu bash gwarantuje, że wprowadzi mnie w świat bólu, jest nim Python. Jest dość prosty i krótki, a zatem powinien być łatwy do dostosowania do twoich potrzeb.

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

Oczywiście można to ulepszyć, aby wykonywać kodowanie równolegle. Czytelnikowi pozostawiam to jako ćwiczenie ;-)

Brechta Machielsa
źródło
0

Jeśli używasz Bash 4.x, możesz użyć nowej opcji globowania , na przykład:

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

Ten rodzaj rekursywnego symbolu wieloznacznego może znaleźć wszystkie interesujące Cię pliki ( .flac , .mp3 lub cokolwiek innego). O

kenorb
źródło
1
Wydaje mi się, że nawet $ (wildcard flac / ** / *. Flac) działa. OS X, Gnu Make 3.81
akauppi
2
Próbowałem $ (wildcard ./**/*.py) i zachowywał się tak samo jak $ (wildcard ./*/*.py). Myślę, że make nie obsługuje ** i po prostu nie zawodzi, gdy używasz dwóch * obok siebie.
lahwran
@lahwran Powinno, gdy wywołujesz polecenia przez powłokę Bash i masz włączoną opcję globstar . Może nie używasz make GNU ani czegoś innego. Zamiast tego możesz również wypróbować tę składnię . Sprawdź komentarze, aby uzyskać sugestie. W przeciwnym razie to kwestia nowego pytania.
kenorb
@kenorb nie, nie, nawet nie próbowałem, ponieważ chciałem uniknąć wywołania powłoki dla tej konkretnej rzeczy. Korzystałem z sugestii Akauppi. To, z czym poszedłem, wyglądało jak odpowiedź Larskholte, chociaż dostałem ją z innego miejsca, ponieważ komentarze tutaj mówiły, że ta jest subtelnie zepsuta. wzruszając ramionami :)
lahwran
@lahwran W tym przypadku **nie zadziała, ponieważ rozszerzony globbing to rzecz bash / zsh.
kenorb