Ulangi daftar file dengan spasi


201

Saya ingin mengulang daftar file. Daftar ini adalah hasil dari suatu findperintah, jadi saya datang dengan:

getlist() {
  for f in $(find . -iname "foo*")
  do
    echo "File found: $f"
    # do something useful
  done
}

Tidak masalah kecuali jika file memiliki spasi dalam namanya:

$ ls
foo_bar_baz.txt
foo bar baz.txt

$ getlist
File found: foo_bar_baz.txt
File found: foo
File found: bar
File found: baz.txt

Apa yang bisa saya lakukan untuk menghindari pemisahan pada spasi?


Ini pada dasarnya adalah subcase spesifik dari Kapan untuk membungkus tanda kutip di sekitar variabel shell?
tripleee

Jawaban:


253

Anda bisa mengganti iterasi berbasis kata dengan iterasi berbasis baris:

find . -iname "foo*" | while read f
do
    # ... loop body
done

31
Ini sangat bersih. Dan membuat saya merasa lebih baik daripada mengubah IFS bersamaan dengan for loop
Derrick

15
Ini akan membagi satu jalur file yang berisi \ n. OK, mereka seharusnya tidak ada tetapi mereka dapat dibuat:touch "$(printf "foo\nbar")"
Ollie Saunders

4
Untuk mencegah interpretasi dari input (garis miring terbalik, spasi spasi awal dan spasi tambahan), gunakan IFS= while read -r fsaja.
mklement0

2
Jawaban ini menunjukkan kombinasi yang lebih aman dari finddan loop sementara.
moi

5
Sepertinya menunjukkan yang sudah jelas, tetapi dalam hampir semua kasus sederhana, -execakan menjadi lebih bersih daripada loop eksplisit: find . -iname "foo*" -exec echo "File found: {}" \;. Plus, dalam banyak kasus Anda dapat mengganti yang terakhir \;dengan +menaruh banyak file dalam satu perintah.
naught101

152

Ada beberapa cara yang bisa dilakukan untuk mencapai ini.

Jika Anda ingin tetap menggunakan versi asli Anda, itu bisa dilakukan dengan cara ini:

getlist() {
        IFS=$'\n'
        for file in $(find . -iname 'foo*') ; do
                printf 'File found: %s\n' "$file"
        done
}

Ini masih akan gagal jika nama file memiliki baris baru literal di dalamnya, tetapi spasi tidak akan merusaknya.

Namun, mengacaukan IFS tidak perlu. Inilah cara pilihan saya untuk melakukan ini:

getlist() {
    while IFS= read -d $'\0' -r file ; do
            printf 'File found: %s\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

Jika Anda menemukan < <(command)sintaksis tidak dikenal, Anda harus membaca tentang substitusi proses . Keuntungan dari ini for file in $(find ...)adalah bahwa file dengan spasi, baris baru dan karakter lainnya ditangani dengan benar. Ini berfungsi karena finddengan -print0akan menggunakan null(alias \0) sebagai terminator untuk setiap nama file dan, tidak seperti baris baru, null bukan karakter hukum dalam nama file.

Keuntungannya dibandingkan versi yang hampir setara

getlist() {
        find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
                printf 'File found: %s\n' "$file"
        done
}

Apakah itu setiap tugas variabel dalam tubuh loop sementara dipertahankan. Artinya, jika Anda pipa whileseperti di atas maka tubuh whiledalam subkulit yang mungkin tidak seperti yang Anda inginkan.

Keuntungan dari versi substitusi proses find ... -print0 | xargs -0adalah minimal: xargsVersi ini baik-baik saja jika Anda hanya perlu mencetak satu baris atau melakukan satu operasi pada file, tetapi jika Anda perlu melakukan beberapa langkah, versi loop lebih mudah.

EDIT : Ini skrip pengujian yang bagus sehingga Anda bisa mendapatkan ide tentang perbedaan antara berbagai upaya dalam memecahkan masalah ini

#!/usr/bin/env bash

dir=/tmp/getlist.test/
mkdir -p "$dir"
cd "$dir"

touch       'file not starting foo' foo foobar barfoo 'foo with spaces'\
    'foo with'$'\n'newline 'foo with trailing whitespace      '

# while with process substitution, null terminated, empty IFS
getlist0() {
    while IFS= read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

# while with process substitution, null terminated, default IFS
getlist1() {
    while read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

# pipe to while, newline terminated
getlist2() {
    find . -iname 'foo*' | while read -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# pipe to while, null terminated
getlist3() {
    find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# for loop over subshell results, newline terminated, default IFS
getlist4() {
    for file in "$(find . -iname 'foo*')" ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# for loop over subshell results, newline terminated, newline IFS
getlist5() {
    IFS=$'\n'
    for file in $(find . -iname 'foo*') ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}


# see how they run
for n in {0..5} ; do
    printf '\n\ngetlist%d:\n' $n
    eval getlist$n
done

rm -rf "$dir"

1
Menerima jawaban Anda: paling lengkap dan menarik - saya tidak tahu tentang $IFSdan < <(cmd)sintaksisnya. Masih satu hal tetap mengaburkan kepada saya, mengapa $di $'\0'? Terima kasih banyak.
gregseth

2
+1, tetapi Anda harus menambahkan ... while IFS= read... untuk menangani file yang dimulai atau diakhiri dengan spasi putih.
Gordon Davisson

1
Ada satu peringatan untuk solusi substitusi proses. Jika Anda memiliki prompt di dalam loop (atau membaca dari STDIN dengan cara lain), input akan diisi oleh barang yang Anda masukkan ke dalam loop. (mungkin ini harus ditambahkan ke jawabannya?)
andsens

2
@ uvsmtid: Pertanyaan ini ditandai bashjadi saya merasa aman menggunakan fitur spesifik bash. Substitusi proses tidak mudah dibawa ke shell lain (ia sendiri kemungkinan tidak akan pernah menerima pembaruan yang signifikan).
sorpigal

2
Menggabungkan IFS=$'\n'dengan formencegah pemisahan baris-kata internal, tetapi masih membuat garis yang dihasilkan tunduk pada globbing, sehingga pendekatan ini tidak sepenuhnya kuat (kecuali Anda juga mematikan globbing dulu). Saat read -d $'\0'berfungsi, ini sedikit menyesatkan karena menunjukkan bahwa Anda dapat menggunakan $'\0'untuk membuat NUL - Anda tidak dapat: a \0dalam string ANSI C yang dikutip secara efektif mengakhiri string, sehingga -d $'\0'secara efektif sama dengan -d ''.
mklement0

29

Ada juga solusi yang sangat sederhana: andalkan bash globbing

$ mkdir test
$ cd test
$ touch "stupid file1"
$ touch "stupid file2"
$ touch "stupid   file 3"
$ ls
stupid   file 3  stupid file1     stupid file2
$ for file in *; do echo "file: '${file}'"; done
file: 'stupid   file 3'
file: 'stupid file1'
file: 'stupid file2'

Perhatikan bahwa saya tidak yakin perilaku ini adalah yang default tetapi saya tidak melihat pengaturan khusus di shopt saya jadi saya akan pergi dan mengatakan bahwa itu harus "aman" (diuji pada osx dan ubuntu).


13
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"

6
sebagai catatan tambahan, ini hanya akan berfungsi jika Anda ingin menjalankan perintah. Shell bawaan tidak akan berfungsi seperti ini.
Alex

11
find . -name "fo*" -print0 | xargs -0 ls -l

Lihat man xargs.


6

Karena Anda tidak melakukan pemfilteran jenis apa pun lainnya find, Anda dapat menggunakan yang berikut pada bash4.0:

shopt -s globstar
getlist() {
    for f in **/foo*
    do
        echo "File found: $f"
        # do something useful
    done
}

The **/akan cocok dengan nol atau lebih direktori, sehingga pola penuh akan cocok foo*di direktori saat ini atau subdirektori apapun.


3

Saya sangat suka untuk loop dan array iterasi, jadi saya pikir saya akan menambahkan jawaban ini ke dalam campuran ...

Saya juga suka contoh file bodoh marchelbling. :)

$ mkdir test
$ cd test
$ touch "stupid file1"
$ touch "stupid file2"
$ touch "stupid   file 3"

Di dalam direktori tes:

readarray -t arr <<< "`ls -A1`"

Ini menambahkan setiap baris daftar file ke dalam array bash yang dinamai arrdengan setiap baris tambahan yang dihapus.

Katakanlah kita ingin memberikan nama-nama file ini lebih baik ...

for i in ${!arr[@]}
do 
    newname=`echo "${arr[$i]}" | sed 's/stupid/smarter/; s/  */_/g'`; 
    mv "${arr[$i]}" "$newname"
done

$ {! arr [@]} meluas ke 0 1 2 jadi "$ {arr [$ i]}" adalah elemen ke- i dari array. Kutipan di sekitar variabel penting untuk menjaga jarak.

Hasilnya adalah tiga file berganti nama:

$ ls -1
smarter_file1
smarter_file2
smarter_file_3

2

findmemiliki -execargumen yang mengulang hasil pencarian dan mengeksekusi perintah arbitrer. Sebagai contoh:

find . -iname "foo*" -exec echo "File found: {}" \;

Di sini {}mewakili file yang ditemukan, dan membungkusnya ""memungkinkan perintah shell yang dihasilkan untuk berurusan dengan spasi dalam nama file.

Dalam banyak kasus, Anda dapat mengganti yang terakhir \;(yang memulai perintah baru) dengan \+, yang akan menempatkan banyak file dalam satu perintah (meskipun tidak harus semuanya sekaligus, lihat man finddetail lebih lanjut).


0

Dalam beberapa kasus, di sini jika Anda hanya perlu menyalin atau memindahkan daftar file, Anda dapat mengirimkan daftar tersebut ke awk juga.
Penting di \"" "\"sekitar bidang $0(singkatnya file Anda, satu baris-daftar = satu file).

find . -iname "foo*" | awk '{print "mv \""$0"\" ./MyDir2" | "sh" }'

0

Ok - posting pertama saya di Stack Overflow!

Meskipun masalah saya dengan ini selalu di csh tidak bash solusi yang saya sajikan akan, saya yakin, bekerja di keduanya. Masalahnya adalah dengan interpretasi shell dari pengembalian "ls". Kita dapat menghapus "ls" dari masalah hanya dengan menggunakan ekspansi shell dari *wildcard - tetapi ini memberikan kesalahan "tidak cocok" jika tidak ada file dalam folder saat ini (atau folder tertentu) - untuk menyiasatinya, kita cukup memperluas ekspansi untuk memasukkan file-dot dengan demikian: * .*- ini akan selalu menghasilkan hasil sejak file. dan .. akan selalu hadir. Jadi di csh kita bisa menggunakan konstruk ini ...

foreach file (* .*)
   echo $file
end

jika Anda ingin menyaring dot-file standar maka itu cukup mudah ...

foreach file (* .*)
   if ("$file" == .) continue
   if ("file" == ..) continue
   echo $file
end

Kode di posting pertama di utas ini akan ditulis demikian: -

getlist() {
  for f in $(* .*)
  do
    echo "File found: $f"
    # do something useful
  done
}

Semoga ini membantu!


0

Solusi lain untuk pekerjaan ...

Sasarannya adalah:

  • pilih / filter nama file secara rekursif dalam direktori
  • menangani setiap nama (ruang apa pun di jalur ...)
#!/bin/bash  -e
## @Trick in order handle File with space in their path...
OLD_IFS=${IFS}
IFS=$'\n'
files=($(find ${INPUT_DIR} -type f -name "*.md"))
for filename in ${files[*]}
do
      # do your stuff
      #  ....
done
IFS=${OLD_IFS}


Terima kasih untuk komentar yang membangun, tetapi: 1 - ini adalah masalah yang sebenarnya, 2 - shell dapat berevolusi pada waktu ... seperti yang saya duga semua orang; 3 - Tidak ada jawaban di atas yang dapat memenuhi resolusi pb langsung tanpa mengubah masalah atau disertasi :-)
Vince B
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.