Membuat skrip BASH `for` menangani nama file dengan spasi (atau solusi)

12

Sementara saya telah menggunakan BASH selama beberapa tahun, pengalaman saya dengan skrip BASH relatif terbatas.

Kode saya seperti di bawah ini. Seharusnya ambil seluruh struktur direktori dari dalam direktori saat ini dan direplikasi ke dalamnya $OUTDIR.

for DIR in `find . -type d -printf "\"%P\"\040"`
do
  echo mkdir -p \"${OUTPATH}${DIR}\"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done

Masalahnya adalah, ini contoh struktur file saya:

$ ls
Expect The Impossible-Stellar Kart
Five Iron Frenzy - Cheeses...
Five Score and Seven Years Ago-Relient K
Hello-After Edmund
I Will Go-Starfield
Learning to Breathe-Switchfoot
MMHMM-Relient K

Catat spasi: -S Dan forambil parameter kata demi kata, jadi output skrip saya terlihat seperti ini:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Learning"
Created Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot"
Created Breathe-Switchfoot

Tapi saya membutuhkannya untuk mengambil seluruh nama file (satu baris pada satu waktu) dari output find. Saya juga telah mencoba membuat findtanda kutip ganda di setiap nama file. Tetapi ini tidak membantu.

for DIR in `find . -type d -printf "\"%P\"\040"`

Dan output dengan baris yang diubah ini:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"""
Created ""
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"Learning"
Created "Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot""
Created Breathe-Switchfoot"

Sekarang, saya perlu beberapa cara yang bisa saya ulangi seperti ini, karena saya juga ingin menjalankan perintah yang lebih rumit yang melibatkan gstreamersetiap file dalam struktur serupa berikut. Bagaimana saya harus melakukan ini?

Sunting: Saya memerlukan struktur kode yang akan memungkinkan saya menjalankan beberapa baris kode untuk setiap direktori / file / loop. Maaf jika saya tidak jelas.

Solusi: Saya awalnya mencoba:

find . -type d | while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done

Ini berfungsi dengan baik untuk sebagian besar. Namun, saya kemudian menemukan bahwa karena pipa menghasilkan loop sementara berjalan dalam subkulit, variabel apa pun yang ditetapkan dalam loop kemudian tidak tersedia yang membuat menerapkan penghitung kesalahan cukup sulit. Solusi terakhir saya (dari jawaban ini di SO ):

while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done < <(find . -type d)

Ini kemudian memungkinkan saya untuk menambahkan variabel secara kondisional dalam loop yang akan tetap tersedia nanti dalam skrip.

Samuel Jaeschke
sumber
Why_would_you_ever_need_a_space_in_a_file_name?
Kevin Panko
Benar, bukan preferensi saya. Meskipun, untuk menghapus spasi, Anda harus menangani file dengan spasi terlebih dahulu;)
Samuel Jaeschke
1
Sebenarnya, nama file harus memberikan spasi. Saya akan membiarkan apa pun kecuali /karakter yang tidak diinginkan. Tapi apa pun diperbolehkan kecuali /dan \0sehingga Anda harus membiarkan mereka.
Kevin Panko

Jawaban:

11

Anda perlu menyalurkannya ke finddalam satu whilelingkaran.

find ... | while read -r dir
do
    something with "$dir"
done

Juga, Anda tidak perlu menggunakan -printfdalam hal ini.

Anda dapat membuat bukti ini terhadap file dengan baris baru di namanya, jika Anda mau, dengan menggunakan pembatas nullbyte (yang menjadi satu-satunya karakter yang tidak dapat muncul dalam * nix filepath):

find ... -print0 | while read -d '' -r dir
do
    something with "$dir"
done

Anda juga akan menemukan bahwa menggunakan $()backtick lebih fleksibel dan lebih mudah. Mereka dapat bersarang jauh lebih mudah dan mengutip dapat dilakukan dengan lebih mudah. Contoh yang dibuat-buat ini akan menggambarkan poin-poin ini:

echo "$(echo "$(echo "hello")")"

Coba lakukan itu dengan backticks.

Dijeda sampai pemberitahuan lebih lanjut.
sumber
2
Selain "$dir"itu, lebih baik digunakan "${dir}"- lebih mudah untuk membedakan antara $ {dir} name dan $ {dirname}, tetapi $ dirname dapat diartikan dengan cara apa pun.
James Polley
Yang penting di sini adalah readmembaca seluruh baris ${dir}, sehingga IFS tidak masalah.
James Polley
1
Terima kasih telah menemukan kesalahan ketik $ / ". Kawat gigi tidak diperlukan jika tidak ada yang mengikuti nama variabel.
Dijeda sampai pemberitahuan lebih lanjut.
4
Ini akan menangani nama path dengan spasi (U + 0020), tetapi masih gagal menangani nama path dengan feed garis (U + 000A). Saya lebih suka find … -print0 | xargs -0 …karena pembatas yang digunakan sesuai persis dengan satu-satunya karakter yang tidak diperbolehkan dalam pathanames POSIX: NUL (U + 0000).
Chris Johnsen
2
Sempurna! Apa yang saya cari. Tidak pernah terpikir oleh saya bahwa Anda mungkin dapat menyalurkan ke while. @ Chris Johnsen: Benar, tetapi bahkan program ripping musik tidak cenderung memasukkan umpan baris ke nama file mereka. Dan jika mereka melakukannya, saya ingin tahu (yaitu: ada yang tidak beres) dan segera singkirkan mereka ...
Samuel Jaeschke
8

Lihat jawaban ini yang saya tulis beberapa hari lalu untuk contoh skrip yang menangani nama file dengan spasi.

Ada cara yang sedikit lebih berbelit-belit (tetapi lebih ringkas) untuk mencapai apa yang Anda coba lakukan:

find . -type d -print0 | xargs -0 -I {} mkdir -p ../theredir/{}

-print0memberitahu find untuk memisahkan argumen dengan null; -0 ke xargs memerintahkannya untuk mengharapkan argumen yang dipisahkan oleh nol Ini berarti bahwa ia menangani ruang dengan baik.

-I {}memberitahu xargs untuk mengganti string {}dengan nama file. Ini juga menyiratkan bahwa hanya satu nama file yang harus digunakan per baris perintah (xargs biasanya akan memuat sebanyak yang sesuai pada baris)

Sisanya harus jelas.

James Polley
sumber
Saran Dennis Williamson, bagaimanapun (selain dari kesalahan ketik) jauh lebih mudah dibaca, dan dengan demikian lebih disukai dalam hampir setiap cara.
James Polley
Bekerja, untuk mkdir, tapi maaf saya seharusnya lebih jelas - Saya ingin menjalankan serangkaian perintah untuk setiap file. Anda lihat, untuk rutin serupa saya nanti saya ingin menghasilkan nama file keluaran berdasarkan nama file input (yang melibatkan pengupasan ekstensi .ogg dan menambahkan .mp3) dan kemudian menggunakan beberapa variabel ini dalam pipa saya ketika menjalankan peluncuran gst.
Samuel Jaeschke
5

Masalah yang Anda temui adalah pernyataan for sedang merespons temuan sebagai argumen terpisah. Pembatas ruang. Anda perlu menggunakan variabel IFS bash untuk tidak terpecah pada ruang.

Berikut ini tautan yang menjelaskan cara melakukan ini.

Variabel internal IFS

Salah satu cara mengatasi masalah ini adalah mengubah variabel IFS (Internal Field Separator) internal Bash sehingga ia membagi bidang dengan sesuatu selain spasi kosong (spasi, tab, baris baru), dalam hal ini, koma.

#!/bin/bash
IFS=$';'

for I in `find -type d -printf \"%P\"\;`
do
   echo "== $I =="
done

Setel temuan Anda untuk menghasilkan pembatas bidang Anda setelah% P dan setel IFS Anda dengan tepat. Saya memilih semi-colon karena sangat tidak mungkin ditemukan di nama file Anda.

Alternatif lain adalah memanggil mkdir dari find secara langsung via -execdo Anda dapat melewatkan for for loop sama sekali. Itu jika Anda tidak perlu melakukan parsing tambahan.

Aula Darren
sumber
Bagaimana jika nama file mengandung IFS? Maka Anda harus memilih yang berbeda. Tapi kemudian, bagaimana jika ...
Dijeda sampai pemberitahuan lebih lanjut.
3
Anda dapat memilih /pada POSIX, dan :pada sistem file DOS. Ada karakter ilegal untuk berbagai sistem file yang dapat Anda pilih untuk IFS. Ada yang lebih rumit dan Anda lebih baik menggunakan perl.
Darren Hall
2
Masalah dengan menggunakan / adalah pembatas direktori dan findmengembalikan nama file dengan path termasuk slash. Coba ubah tanda titik koma di skrip Anda menjadi garis miring dan gema akan mencetak direktori dan nama file pada baris terpisah.
Dijeda sampai pemberitahuan lebih lanjut.
Itu juga terlihat cukup berguna. Saya sudah menggunakan opsi pipa while, tetapi ini juga terlihat cukup bisa diterapkan. Ya, dalam struktur serupa saya nanti saya perlu melakukan parsing lebih lanjut. (Nama file input akan menjadi .ogg, yang akan dilewatkan seperti filesrcpada pipa gst, tetapi akhiran yang setara dengan .mp3 yang didasarkan pada direktori keluaran akan dihasilkan dan juga diteruskan ke pipa sebagai filesink, dan ini tentu saja perlu dilakukan untuk setiap file, bersama dengan beberapa echoke pengguna.)
Samuel Jaeschke
4

Jika isi loop Anda lebih dari satu perintah, dimungkinkan untuk menggunakan xargs untuk menggerakkan skrip shell:

export OUTPATH=/some/where/else/
find . -type d -print0 | xargs -0 bash -c 'for DIR in "$@"; do
  printf "mkdir -p %q\\n" "${OUTPATH}${DIR}"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done' -

Pastikan untuk menyertakan tanda hubung trailing (atau 'kata' lain) jika shellnya dari varietas Bourne / POSIX (digunakan untuk menetapkan $ 0 dalam skrip shell). Juga, harus diperhatikan dengan mengutip, karena skrip shell sedang ditulis di dalam string yang dikutip, bukan langsung pada prompt.

Chris Johnsen
sumber
Konsep lain yang menarik. Terima kasih - saya yakin saya akan menemukan kegunaan untuk ini nanti :)
Samuel Jaeschke
1

dalam pertanyaan Anda yang telah diperbarui

mkdir -p \"${OUTPATH}${DIR}\"

ini seharusnya

mkdir -p "${OUTPATH}${DIR}"
pengguna23307
sumber
Terima kasih. Tetap. Itu juga membaca untuk FILENAME bukannya DIR - copy-paste: P
Samuel Jaeschke
1
find . -type d -exec mkdir -p "{}\040" ';' -exec echo "Created {}\040" ';'
Vouze
sumber
0

atau untuk membuat semuanya jauh lebih rumit:

% rsync -av --include='*/' --exclude='*' SRC DST

ini mereplikasi struktur direktori SRC ke DST.

akira
sumber
Tidak, saya memerlukan struktur berulang seperti itu, yang memungkinkan saya untuk menjalankan beberapa baris kode untuk setiap file. "Sekarang, saya perlu beberapa cara agar saya bisa mengulanginya seperti ini, karena saya juga ingin menjalankan perintah yang lebih rumit yang melibatkan gstreamer pada setiap file dalam struktur serupa berikut ini." Maaf jika saya tidak jelas.
Samuel Jaeschke
perintah yang saya berikan menyelesaikan masalah yang Anda minta, tidak masalah jika ini hanya bagian dari 'pipa' yang lebih besar di sisi Anda. untuk orang lain yang memiliki masalah seperti yang dijelaskan dalam pertanyaan, pendekatan rsync akan berfungsi. jadi, tidak perlu minta maaf tentang potensi ketidakjelasan :)
akira
Ya. Tidak, maksud saya saya akan menggunakan struktur yang serupa while... do... donenanti untuk melakukan pemrosesan serupa dari find, yang akan membutuhkan beberapa baris kode untuk dijalankan pada setiap file (modifikasi string, gema, luncurkan gst, dll. ) dan rsynctidak akan mencapai ini. Itu sebabnya saya menentukan bahwa saya harus dapat menjalankan serangkaian perintah yang lebih rumit dalam strucutre yang sama. Script saya menggunakan struktur loop ini dua kali, jadi untuk pertanyaan saya memposting satu dengan kurang kasar di tengah.
Samuel Jaeschke
0

Jika Anda menginstal GNU Parallel http: // www.gnu.org/software/parallel/ Anda dapat melakukan ini:

find . -type d | parallel echo making {} ";" mkdir -p /tmp/outdir/{} ";" echo made {}

Tonton video intro untuk GNU Parallel untuk mempelajari lebih lanjut: http://www.youtube.com/watch?v=OpaiGYxkSuQ

Ole Tange
sumber