Karakter pengganti rekursif di GNU make?

93

Sudah lama sejak saya menggunakannya make, jadi bersabarlah ...

Saya punya direktori flac, berisi file .FLAC. Saya punya direktori yang sesuai, mp3berisi file MP3. Jika file FLAC lebih baru dari file MP3 yang sesuai (atau file MP3 yang sesuai tidak ada), maka saya ingin menjalankan banyak perintah untuk mengonversi file FLAC ke file MP3, dan menyalin tag.

Penendang: Saya perlu mencari flacdirektori secara rekursif, dan membuat subdirektori yang sesuai di mp3direktori. Direktori dan file dapat memiliki spasi pada namanya, dan diberi nama dalam UTF-8.

Dan saya ingin menggunakan makeuntuk mengendarai ini.

Roger Lipscombe
sumber
1
Ada alasan untuk memilih make untuk tujuan ini? Saya mengira menulis skrip bash akan lebih sederhana
4
@Neil, konsep make sebagai transformasi sistem file berbasis pola adalah cara terbaik untuk mendekati masalah asli. Mungkin implementasi dari pendekatan ini memiliki keterbatasan, tetapi makelebih mendekati penerapannya daripada telanjang bash.
P Shved
1
@Pavel Nah, shskrip yang menelusuri daftar file flac ( find | while read flacname), membuat a mp3namedari itu, menjalankan "mkdir -p" pada dirname "$mp3name", dan kemudian, if [ "$flacfile" -nt "$mp3file"]mengubahnya "$flacname"menjadi "$mp3name"tidak benar-benar ajaib. Satu-satunya fitur yang benar-benar hilang dibandingkan dengan makesolusi berbasis adalah kemungkinan untuk menjalankan Nproses konversi file secara paralel make -jN.
ndim
4
@ndim Itulah pertama kalinya saya mendengar sintaks make dijelaskan sebagai "nice" :-)
1
Menggunakan makedan memiliki spasi pada nama file adalah persyaratan yang kontradiktif. Gunakan alat yang sesuai untuk domain masalah.
Jens

Jawaban:

117

Saya akan mencoba sesuatu seperti ini

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

Beberapa catatan singkat untuk makepemula:

  • Bagian @depan perintah mencegah makepencetakan perintah sebelum benar-benar menjalankannya.
  • $(@D)adalah bagian direktori dari nama file target ( $@)
  • Pastikan bahwa baris dengan perintah shell di dalamnya dimulai dengan tab, bukan spasi.

Bahkan jika ini harus menangani semua karakter dan item UTF-8, itu akan gagal pada spasi dalam nama file atau direktori, karena makemenggunakan spasi untuk memisahkan hal-hal dalam makefile dan saya tidak mengetahui cara untuk mengatasinya. Sehingga Anda hanya memiliki skrip shell, saya khawatir: - /

ndim
sumber
Di sinilah saya pergi ... jari cepat untuk menang. Meskipun sepertinya Anda bisa melakukan sesuatu yang pintar vpath. Harus mempelajarinya suatu hari nanti.
dmckee --- mantan moderator anak kucing
1
Tampaknya tidak berfungsi saat direktori memiliki spasi pada namanya.
Roger Lipscombe
Tidak menyadari bahwa saya harus keluar uang finduntuk mendapatkan nama secara rekursif ...
Roger Lipscombe
1
@PaulKonova: Lari make -jN. Untuk Nmenggunakan jumlah konversi yang makeharus dijalankan secara paralel. Perhatian: Berjalan make -jtanpa Nakan memulai semua proses konversi sekaligus secara paralel yang mungkin setara dengan bom garpu.
ndim
1
@ Adrian: .PHONY: allBaris memberitahu make bahwa resep untuk alltarget akan dieksekusi meskipun ada file bernama alllebih baru dari semua $(MP3_FILES).
ndim
53

Anda dapat menentukan fungsi wildcard rekursif Anda sendiri seperti ini:

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

Parameter pertama ( $1) adalah daftar direktori, dan yang kedua ( $2) adalah daftar pola yang ingin Anda cocokkan.

Contoh:

Untuk menemukan semua file C di direktori saat ini:

$(call rwildcard,.,*.c)

Untuk menemukan semua .cdan .hfile di src:

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

Fungsi ini didasarkan pada implementasi dari artikel ini , dengan beberapa perbaikan.

larskholte.dll
sumber
Ini sepertinya tidak berhasil untuk saya. Saya telah menyalin fungsi yang tepat dan masih tidak akan terlihat secara rekursif.
Jeroen
3
Saya menggunakan GNU Make 3.81, dan sepertinya berhasil untuk saya. Ini tidak akan berfungsi jika salah satu nama file memiliki spasi di dalamnya. Perhatikan bahwa nama file yang dikembalikan memiliki jalur yang sesuai dengan direktori saat ini, meskipun Anda hanya mencantumkan file dalam subdirektori.
larskholte
2
Ini benar-benar sebuah contoh, yaitu makeTuring Tar Pit (lihat di sini: yosefk.com/blog/fun-at-the-turing-tar-pit.html ). Ini bahkan tidak terlalu sulit, tetapi kita harus membaca ini: gnu.org/software/make/manual/html_node/Call-Function.html dan kemudian "memahami kekambuhan". ANDA harus menulis ini secara rekursif, dalam arti kata demi kata; ini bukan pemahaman sehari-hari tentang "secara otomatis menyertakan barang dari subdir". Ini RECURRENCE sebenarnya. Tapi ingat - "Untuk memahami kekambuhan, Anda harus memahami kekambuhan".
Tomasz Gandor
@TomaszGandor Anda tidak harus memahami kekambuhan. Anda harus memahami rekursi dan untuk melakukannya Anda harus memahami rekursi terlebih dahulu.
user1129682
Saya buruk, saya jatuh cinta pada teman palsu linguistik. Komentar tidak dapat diedit setelah sekian lama, tetapi saya harap semua orang mengerti. Dan mereka juga memahami rekursi.
Tomasz Gandor
2

FWIW, saya telah menggunakan sesuatu seperti ini di Makefile :

RECURSIVE_MANIFEST = `find . -type f -print`

Contoh di atas akan mencari dari direktori saat ini ( '.' ) Untuk semua "file biasa" ( '-type f' ) dan menyetel RECURSIVE_MANIFESTvariabel make ke setiap file yang ditemukannya. Anda kemudian dapat menggunakan substitusi pola untuk mengurangi daftar ini, atau sebagai alternatif, berikan lebih banyak argumen ke dalam find untuk mempersempit apa yang dikembalikannya. Lihat halaman manual untuk menemukan .

Michael Shebanow
sumber
1

Solusi saya didasarkan pada yang di atas, menggunakan sedalih-alih patsubstmengacaukan output dari findDAN melarikan diri dari ruang.

Beralih dari flac / ke ogg /

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

Peringatan:

  1. Masih muntah jika ada titik koma di nama file, tapi cukup jarang.
  2. Trik $ (@ D) tidak akan berfungsi (menghasilkan omong kosong), tetapi oggenc membuat direktori untuk Anda!
pengguna3199485
sumber
1

Berikut ini skrip Python yang saya retas dengan cepat untuk menyelesaikan masalah asli: simpan salinan pustaka musik yang dikompresi. Skrip akan mengubah file .m4a (diasumsikan ALAC) ke format AAC, kecuali file AAC sudah ada dan lebih baru dari file ALAC. File MP3 di perpustakaan akan ditautkan, karena sudah dikompresi.

Berhati-hatilah karena membatalkan skrip ( ctrl-c) akan meninggalkan file yang setengah dikonversi.

Saya awalnya juga ingin menulis Makefile untuk menangani ini, tetapi karena Makefile tidak dapat menangani spasi dalam nama file (lihat jawaban yang diterima) dan karena menulis skrip bash dijamin akan menempatkan saya di dunia yang menyakitkan, Python itu. Ini cukup mudah dan singkat, dan karenanya harus mudah disesuaikan dengan kebutuhan Anda.

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

Tentu saja, ini dapat ditingkatkan untuk melakukan pengkodean secara paralel. Itu dibiarkan sebagai latihan bagi pembaca ;-)

Brecht Machiels
sumber
0

Jika Anda menggunakan Bash 4.x, Anda dapat menggunakan opsi globbing baru , misalnya:

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

Wildcard rekursif semacam ini dapat menemukan semua file yang Anda minati ( .flac , .mp3 atau apa pun). HAI

kenorb
sumber
1
Bagi saya, bahkan hanya $ (wildcard flac / ** / *. Flac) tampaknya berhasil. OS X, Gnu Make 3.81
akauppi
2
Saya mencoba $ (wildcard ./**/*.py) dan perilakunya sama seperti $ (wildcard ./*/*.py). Saya tidak berpikir make benar-benar mendukung **, dan itu tidak gagal ketika Anda menggunakan dua * s di samping satu sama lain.
lahwran
@lahwran Seharusnya saat Anda menjalankan perintah melalui shell Bash dan Anda telah mengaktifkan opsi globstar . Mungkin Anda tidak menggunakan GNU make atau yang lainnya. Anda juga dapat mencoba sintaks ini . Periksa komentar untuk beberapa saran. Kalau tidak, itu adalah masalah untuk pertanyaan baru.
kenorb
@kenorb tidak, tidak, saya bahkan tidak mencoba hal Anda karena saya ingin menghindari pemanggilan shell untuk hal khusus ini. Saya menggunakan hal yang disarankan akauppi. Hal yang saya lakukan tampak seperti jawaban larskholte, meskipun saya mendapatkannya dari tempat lain karena komentar di sini mengatakan yang ini agak rusak. mengangkat bahu :)
lahwran
@lahwran Dalam hal ini **tidak akan berfungsi, karena globbing yang diperluas adalah hal bash / zsh.
kenorb