Bagaimana cara mengulang nama file yang dikembalikan oleh find?

223
x=$(find . -name "*.txt")
echo $x

jika saya menjalankan potongan kode di atas dalam shell Bash, yang saya dapatkan adalah string yang berisi beberapa nama file yang dipisahkan oleh kosong, bukan daftar.

Tentu saja, saya dapat memisahkan mereka dengan blank untuk mendapatkan daftar, tetapi saya yakin ada cara yang lebih baik untuk melakukannya.

Jadi apa cara terbaik untuk mengulang hasil findperintah?

Haiyuan Zhang
sumber
3
Cara terbaik untuk mengulang nama file tergantung sedikit pada apa yang sebenarnya ingin Anda lakukan dengannya, tetapi kecuali Anda dapat menjamin tidak ada file yang memiliki spasi putih dalam namanya, ini bukan cara yang bagus untuk melakukannya. Jadi apa yang ingin Anda lakukan dalam pengulangan file?
Kevin
1
Mengenai karunia : ide utama di sini adalah untuk mendapatkan jawaban kanonik yang mencakup semua kasus yang mungkin (nama file dengan baris baru, karakter bermasalah ...). Idenya adalah untuk kemudian menggunakan nama file ini untuk melakukan beberapa hal (panggil perintah lain, lakukan penggantian nama ...). Terima kasih!
fedorqui 'SO berhenti merugikan'
Jangan lupa bahwa file atau nama folder dapat berisi ".txt" diikuti oleh spasi dan string lain, contoh "something.txt sesuatu" atau "something.txt"
Yahya Yahyaoui
Gunakan array, bukan var. x=( $(find . -name "*.txt") ); echo "${x[@]}"Kemudian Anda bisa mengulangfor item in "${x[@]}"; { echo "$item"; }
Ivan

Jawaban:

392

TL; DR: Jika Anda hanya di sini untuk jawaban yang paling benar, Anda mungkin menginginkan preferensi pribadi saya, find . -name '*.txt' -exec process {} \;(lihat bagian bawah posting ini). Jika Anda punya waktu, baca sisanya untuk melihat beberapa cara berbeda dan masalah dengan sebagian besar dari mereka.


Jawaban lengkapnya:

Cara terbaik tergantung pada apa yang ingin Anda lakukan, tetapi di sini ada beberapa pilihan. Selama tidak ada file atau folder di subtree yang memiliki spasi putih dalam namanya, Anda bisa memutarnya:

for i in $x; do # Not recommended, will break on whitespace
    process "$i"
done

Secara marginal lebih baik, hilangkan variabel sementara x:

for i in $(find -name \*.txt); do # Not recommended, will break on whitespace
    process "$i"
done

Jauh lebih baik untuk glob ketika Anda bisa. Brankas ruang putih, untuk file di direktori saat ini:

for i in *.txt; do # Whitespace-safe but not recursive.
    process "$i"
done

Dengan mengaktifkan globstaropsi ini, Anda dapat menggumpalkan semua file yang cocok di direktori ini dan semua subdirektori:

# Make sure globstar is enabled
shopt -s globstar
for i in **/*.txt; do # Whitespace-safe and recursive
    process "$i"
done

Dalam beberapa kasus, misalnya jika nama file sudah ada dalam file, Anda mungkin perlu menggunakan read:

# IFS= makes sure it doesn't trim leading and trailing whitespace
# -r prevents interpretation of \ escapes.
while IFS= read -r line; do # Whitespace-safe EXCEPT newlines
    process "$line"
done < filename

readdapat digunakan dengan aman dalam kombinasi dengan finddengan mengatur pembatas secara tepat:

find . -name '*.txt' -print0 | 
    while IFS= read -r -d '' line; do 
        process "$line"
    done

Untuk pencarian yang lebih kompleks, Anda mungkin ingin menggunakan find, baik dengan -execopsi atau dengan -print0 | xargs -0:

# execute `process` once for each file
find . -name \*.txt -exec process {} \;

# execute `process` once with all the files as arguments*:
find . -name \*.txt -exec process {} +

# using xargs*
find . -name \*.txt -print0 | xargs -0 process

# using xargs with arguments after each filename (implies one run per filename)
find . -name \*.txt -print0 | xargs -0 -I{} process {} argument

findjuga dapat melakukan cd ke direktori masing-masing file sebelum menjalankan perintah dengan menggunakan -execdiralih-alih -exec, dan dapat dibuat interaktif (meminta sebelum menjalankan perintah untuk setiap file) menggunakan -okalih-alih -exec(atau -okdirbukannya -execdir).

*: Secara teknis, keduanya finddan xargs(secara default) akan menjalankan perintah dengan argumen sebanyak yang sesuai pada baris perintah, sebanyak yang diperlukan untuk melewati semua file. Dalam praktiknya, kecuali jika Anda memiliki jumlah file yang sangat besar itu tidak masalah, dan jika Anda melebihi panjangnya tetapi membutuhkan semuanya pada baris perintah yang sama, Anda SOL menemukan cara yang berbeda.

Kevin
sumber
4
Itu perlu dicatat bahwa dalam kasus dengan done < filenamedan mengikuti satu dengan pipa stdin tidak dapat digunakan lagi (→ tidak ada hal yang lebih interaktif dalam loop), tetapi dalam kasus di mana itu dibutuhkan satu dapat menggunakan 3<bukannya <dan menambahkan <&3atau -u3untuk ituread bagian, pada dasarnya menggunakan deskriptor file terpisah. Juga, saya percaya read -d ''sama dengan read -d $'\0'tetapi saya tidak dapat menemukan dokumentasi resmi tentang itu sekarang.
phk
1
untuk saya di * .txt; jangan bekerja, jika tidak ada file yang cocok. Diperlukan satu tes xtra misalnya [[-e $ i]]
Michael Brux
2
Saya bingung dengan bagian ini: -exec process {} \;dan dugaan saya adalah itu pertanyaan lain - apa artinya dan bagaimana saya memanipulasinya? Di mana T / A atau doc ​​yang baik? di atasnya?
Alex Hall
1
@AlexHall Anda selalu dapat melihat halaman manual ( man find). Dalam hal ini, -execsuruh finduntuk mengeksekusi perintah berikut, diakhiri oleh ;(atau +), di mana {}akan diganti dengan nama file yang sedang diproses (atau, jika +digunakan, semua file yang telah membuatnya ke kondisi itu).
Kevin
3
@ phk -d ''lebih baik daripada -d $'\0'. Yang terakhir tidak hanya lebih lama tetapi juga menunjukkan bahwa Anda bisa melewati argumen yang mengandung byte nol, tetapi Anda tidak bisa. Byte nol pertama menandai akhir dari string. Dalam bash $'a\0bc'sama dengan adan $'\0'sama dengan $'\0abc'atau hanya string kosong ''. help readmenyatakan bahwa " Karakter pertama'' pembatas digunakan untuk mengakhiri input " jadi menggunakan sebagai pembatas adalah sedikit peretasan. Karakter pertama dalam string kosong adalah byte nol yang selalu menandai akhir string (bahkan jika Anda tidak menuliskannya secara eksplisit).
Socowi
114

Apa pun yang Anda lakukan, jangan gunakan forloop :

# Don't do this
for file in $(find . -name "*.txt")
do
    code using "$file"
done

Tiga alasan:

  • Agar perulangan for mulai, yang findharus dijalankan untuk menyelesaikan.
  • Jika nama file memiliki spasi putih (termasuk spasi, tab, atau baris baru) di dalamnya, itu akan diperlakukan sebagai dua nama terpisah.
  • Meskipun sekarang tidak mungkin, Anda dapat membanjiri buffer baris perintah Anda. Bayangkan jika buffer baris perintah Anda menampung 32KB, dan forloop Anda mengembalikan 40KB teks. 8KB yang terakhir akan dijatuhkan langsung dari forloop Anda dan Anda tidak akan pernah mengetahuinya.

Selalu gunakan while readkonstruk:

find . -name "*.txt" -print0 | while read -d $'\0' file
do
    code using "$file"
done

Loop akan dieksekusi saat findperintah dieksekusi. Plus, perintah ini akan berfungsi bahkan jika nama file dikembalikan dengan spasi putih di dalamnya. Dan, Anda tidak akan membanjiri buffer baris perintah Anda.

The -print0akan menggunakan NULL sebagai pemisah file bukan baris baru dan -d $'\0'akan menggunakan NULL sebagai pemisah saat membaca.

David W.
sumber
3
Itu tidak akan bekerja dengan baris baru dalam nama file. Gunakan find's -execsaja.
pengguna tidak dikenal
2
@userunknown - Anda benar tentang itu. -execadalah yang paling aman karena tidak menggunakan shell sama sekali. Namun, NL dalam nama file cukup langka. Spasi dalam nama file cukup umum. Poin utamanya adalah jangan menggunakan forloop yang direkomendasikan banyak poster.
David W.
1
@userunknown - Di sini. Saya telah memperbaikinya, jadi sekarang file akan ditangani dengan baris baru, tab, dan spasi putih lainnya. Inti dari postingan ini adalah untuk memberitahu OP untuk tidak menggunakan for file $(find)karena masalah yang terkait dengan itu.
David W.
4
Jika Anda dapat menggunakan -exec lebih baik, tetapi ada kalanya Anda benar-benar membutuhkan nama yang diberikan kembali ke shell. Misalnya jika Anda ingin menghapus ekstensi file.
Ben Reser
5
Anda harus menggunakan -ropsi untuk read: -r raw input - disables interpretion of backslash escapes and line-continuation in the read data
Daira Hopwood
102
find . -name "*.txt"|while read fname; do
  echo "$fname"
done

Catatan: metode ini dan metode (kedua) yang ditunjukkan oleh bmargulies aman digunakan dengan spasi putih dalam nama file / folder.

Agar dapat memiliki - baris yang agak eksotis - baris baru dalam nama file / folder yang dicakup, Anda harus menggunakan -execpredikat findseperti ini:

find . -name '*.txt' -exec echo "{}" \;

The {}adalah tempat untuk item berhasil ditemukan dan \;digunakan untuk mengakhiri-exec predikat.

Dan demi kelengkapan, izinkan saya menambahkan varian lain - Anda harus menyukai cara * nix karena keserbagunaannya:

find . -name '*.txt' -print0|xargs -0 -n 1 echo

Ini akan memisahkan item yang dicetak dengan \0karakter yang tidak diperbolehkan dalam sistem file dalam nama file atau folder, sepengetahuan saya, dan karena itu harus mencakup semua pangkalan. xargsmengambilnya satu per satu lalu ...

0xC0000022L
sumber
3
Gagal jika baris baru dalam nama file.
pengguna tidak dikenal
2
@ Pengguna tidak diketahui: Anda benar, ini adalah kasus yang tidak saya pertimbangkan sama sekali dan itu, saya pikir, sangat eksotis. Tetapi saya menyesuaikan jawaban saya sesuai dengan itu.
0xC0000022L
5
Mungkin layak untuk menunjukkan itu find -print0dan xargs -0keduanya merupakan ekstensi GNU dan bukan argumen portabel (POSIX). Sangat berguna pada sistem yang memilikinya!
Toby Speight
1
Ini juga gagal dengan nama file yang mengandung garis miring terbalik (yang read -rakan memperbaiki), atau nama file berakhir dengan spasi putih (yang IFS= readakan memperbaiki). Karenanya BashFAQ # 1 menyarankanwhile IFS= read -r filename; do ...
Charles Duffy
1
Masalah lain dengan ini adalah bahwa sepertinya tubuh loop dijalankan dalam shell yang sama, tetapi tidak, jadi misalnya exittidak akan berfungsi seperti yang diharapkan dan variabel yang ditetapkan dalam tubuh loop tidak akan tersedia setelah loop.
EM0
17

Nama file dapat menyertakan spasi dan bahkan mengontrol karakter. Spasi adalah pembatas (default) untuk ekspansi shell di bash dan karenanya dari x=$(find . -name "*.txt")pertanyaan itu tidak disarankan sama sekali. Jika find mendapatkan nama file dengan spasi misalnya "the file.txt"Anda akan mendapatkan 2 string terpisah untuk diproses, jika Anda memproses xdalam satu lingkaran. Anda dapat meningkatkan ini dengan mengubah pembatas (bash IFSVariable) misalnya ke\r\n , tetapi nama file dapat menyertakan karakter kontrol - jadi ini bukan metode (sepenuhnya) aman.

Dari sudut pandang saya, ada 2 pola yang direkomendasikan (dan aman) untuk memproses file:

1. Gunakan untuk ekspansi loop & nama file:

for file in ./*.txt; do
    [[ ! -e $file ]] && continue  # continue, if file does not exist
    # single filename is in $file
    echo "$file"
    # your code here
done

2. Gunakan substitusi find-read-while & proses

while IFS= read -r -d '' file; do
    # single filename is in $file
    echo "$file"
    # your code here
done < <(find . -name "*.txt" -print0)

Catatan

pada Pola 1:

  1. bash mengembalikan pola pencarian ("* .txt") jika tidak ada file yang cocok ditemukan - sehingga baris tambahan "lanjutkan, jika file tidak ada" diperlukan. Lihat Bash Manual, Ekspansi Nama File
  2. opsi shell nullglobdapat digunakan untuk menghindari garis tambahan ini.
  3. "Jika failglobopsi shell diatur, dan tidak ada kecocokan yang ditemukan, pesan kesalahan dicetak dan perintah tidak dijalankan." (dari Bash Manual di atas)
  4. opsi shell globstar: "Jika diatur, pola '**' yang digunakan dalam konteks ekspansi nama file akan cocok dengan semua file dan nol atau lebih direktori dan subdirektori. Jika pola diikuti oleh '/', hanya direktori dan subdirektori yang cocok." lihat Bash Manual, Shopt Builtin
  5. pilihan lain untuk ekspansi nama file: extglob, nocaseglob, dotglob& variabel shellGLOBIGNORE

pada Pola 2:

  1. nama file dapat berisi kosong, tab, spasi, baris baru, ... untuk memproses nama file dengan cara yang aman, finddengan -print0digunakan: nama file dicetak dengan semua karakter kontrol & diakhiri dengan NUL. lihat juga Gnu Findutils Manpage, Penanganan Nama File Tidak Aman , Penanganan Nama File aman , karakter yang tidak biasa dalam nama file . Lihat David A. Wheeler di bawah ini untuk diskusi terperinci tentang topik ini.

  2. Ada beberapa pola yang mungkin untuk memproses hasil pencarian dalam loop sementara. Lainnya (kevin, David W.) telah menunjukkan cara melakukan ini menggunakan pipa:

    files_found=1 find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do # single filename in $file echo "$file" files_found=0 # not working example # your code here done [[ $files_found -eq 0 ]] && echo "files found" || echo "no files found"
    Ketika Anda mencoba potongan kode ini, Anda akan melihat, bahwa itu tidak berfungsi: files_foundselalu "benar" & kode akan selalu bergema "tidak ada file yang ditemukan". Alasannya adalah: setiap perintah pipeline dieksekusi dalam subshell terpisah, sehingga variabel yang diubah di dalam loop (subshell terpisah) tidak mengubah variabel dalam skrip shell utama. Inilah sebabnya saya merekomendasikan menggunakan proses substitusi sebagai pola "lebih baik", lebih bermanfaat, lebih umum.
    Lihat Saya mengatur variabel dalam satu lingkaran yang ada dalam pipa. Mengapa mereka menghilang ... (dari FAQ Bash Greg) untuk diskusi terperinci tentang topik ini.

Referensi & Sumber Tambahan:

Michael Brux
sumber
8

(Diperbarui untuk menyertakan peningkatan kecepatan eksekutif Socowi)

Dengan apa pun $SHELLyang mendukungnya (tanda hubung / zsh / bash ...):

find . -name "*.txt" -exec $SHELL -c '
    for i in "$@" ; do
        echo "$i"
    done
' {} +

Selesai


Jawaban asli (lebih pendek, tetapi lebih lambat):

find . -name "*.txt" -exec $SHELL -c '
    echo "$0"
' {} \;
pengguna569825
sumber
1
Lambat seperti molase (karena meluncurkan shell untuk setiap file) tetapi ini berhasil. +1
dawg
1
Alih-alih \;Anda dapat menggunakan +untuk mengirimkan file sebanyak mungkin ke satu exec. Kemudian gunakan "$@"di dalam skrip shell untuk memproses semua parameter ini.
Socowi
3
Ada bug dalam kode ini. Loop tidak memiliki hasil pertama. Itu karena $@menghilangkannya karena biasanya nama skrip. Kami hanya perlu menambahkan dummydi antara 'dan {}sehingga dapat menggantikan nama skrip, memastikan semua kecocokan diproses oleh loop.
BCartolo
Bagaimana jika saya membutuhkan variabel lain dari luar shell yang baru dibuat?
Jodo
OTHERVAR=foo find . -na.....seharusnya memungkinkan Anda untuk mengakses $OTHERVARdari dalam shell yang baru dibuat.
user569825
6
# Doesn't handle whitespace
for x in `find . -name "*.txt" -print`; do
  process_one $x
done

or

# Handles whitespace and newlines
find . -name "*.txt" -print0 | xargs -0 -n 1 process_one
bmargulies
sumber
3
for x in $(find ...)akan merusak setiap nama file dengan spasi putih di dalamnya. Sama dengan find ... | xargskecuali jika Anda menggunakan -print0dan-0
glenn jackman
1
Gunakan find . -name "*.txt -exec process_one {} ";"sebagai gantinya. Mengapa kita harus menggunakan xargs untuk mengumpulkan hasil, yang sudah kita miliki?
pengguna tidak diketahui
@ Penggunaunknown Nah itu semua tergantung pada apa process_oneitu. Jika itu adalah pengganti untuk perintah aktual , tentu itu akan berhasil (jika Anda memperbaiki kesalahan ketik dan menambahkan kutipan penutup setelah "*.txt). Tetapi jika process_onefungsi yang ditentukan pengguna, kode Anda tidak akan berfungsi.
toxalot
@toxalot: Ya, tapi itu tidak akan menjadi masalah untuk menulis fungsi dalam skrip untuk memanggil.
pengguna tidak diketahui
4

Anda dapat menyimpan findoutput dalam array jika Anda ingin menggunakan output nanti sebagai:

array=($(find . -name "*.txt"))

Sekarang untuk mencetak setiap elemen dalam baris baru, Anda dapat menggunakan forperulangan iterating ke semua elemen array, atau Anda dapat menggunakan pernyataan printf.

for i in ${array[@]};do echo $i; done

atau

printf '%s\n' "${array[@]}"

Anda juga bisa menggunakan:

for file in "`find . -name "*.txt"`"; do echo "$file"; done

Ini akan mencetak setiap nama file di baris baru

Untuk hanya mencetak findoutput dalam bentuk daftar, Anda dapat menggunakan salah satu dari berikut ini:

find . -name "*.txt" -print 2>/dev/null

atau

find . -name "*.txt" -print | grep -v 'Permission denied'

Ini akan menghapus pesan kesalahan dan hanya memberikan nama file sebagai output di baris baru.

Jika Anda ingin melakukan sesuatu dengan nama file, menyimpannya dalam array itu bagus, kalau tidak maka tidak perlu mengkonsumsi ruang itu dan Anda dapat langsung mencetak hasilnya find.

Rakholiya Jenish
sumber
1
Looping atas array gagal dengan spasi dalam nama file.
EM0
Anda harus menghapus jawaban ini. Itu tidak bekerja dengan spasi di nama file atau nama direktori.
jww
4

Jika Anda dapat menganggap nama file tidak mengandung baris baru, Anda dapat membaca output findmenjadi array Bash menggunakan perintah berikut:

readarray -t x < <(find . -name '*.txt')

catatan:

  • -tmenyebabkan readarraystrip baru.
  • Ini tidak akan berfungsi jika readarrayada di dalam pipa, maka proses substitusi.
  • readarray tersedia sejak Bash 4.

Bash 4.4 dan yang lebih tinggi juga mendukung -dparameter untuk menentukan pembatas. Menggunakan karakter nol, alih-alih baris baru, untuk membatasi nama file berfungsi juga dalam kasus yang jarang terjadi bahwa nama file berisi baris baru:

readarray -d '' x < <(find . -name '*.txt' -print0)

readarraydapat juga dipanggil mapfiledengan opsi yang sama.

Referensi: https://mywiki.wooledge.org/BashFAQ/005#Loading_lines_from_a_file_or_stream

Seppo Enarvi
sumber
Ini jawaban terbaik! Bekerja dengan: * Spaces dalam nama file * Tidak ada file yang cocok * exitketika mengulangi hasil
EM0
Tidak bekerja dengan semua kemungkinan nama file, meskipun - untuk itu, Anda harus menggunakanreadarray -d '' x < <(find . -name '*.txt' -print0)
Charles Duffy
3

Saya suka menggunakan find yang pertama kali ditugaskan ke variabel dan IFS beralih ke baris baru sebagai berikut:

FilesFound=$(find . -name "*.txt")

IFSbkp="$IFS"
IFS=$'\n'
counter=1;
for file in $FilesFound; do
    echo "${counter}: ${file}"
    let counter++;
done
IFS="$IFSbkp"

Hanya dalam kasus Anda ingin mengulangi lebih banyak tindakan pada set DATA yang sama dan menemukan sangat lambat pada server Anda (I / 0 utilisasi tinggi)

Paco
sumber
2

Anda bisa memasukkan nama file yang dikembalikan oleh findke dalam array seperti ini:

array=()
while IFS=  read -r -d ''; do
    array+=("$REPLY")
done < <(find . -name '*.txt' -print0)

Sekarang Anda bisa mengulang melalui array untuk mengakses item individual dan melakukan apa pun yang Anda inginkan dengannya.

Catatan: Ini adalah ruang putih yang aman.

Jahid
sumber
1
Dengan pesta 4.4 atau lebih tinggi Anda bisa menggunakan satu perintah bukan loop: mapfile -t -d '' array < <(find ...). Pengaturan IFStidak diperlukan untuk mapfile.
Socowi
1

berdasarkan jawaban dan komentar @phk lainnya, menggunakan fd # 3:
(yang masih memungkinkan untuk menggunakan stdin di dalam loop)

while IFS= read -r f <&3; do
    echo "$f"

done 3< <(find . -iname "*filename*")
Florian
sumber
-1

find <path> -xdev -type f -name *.txt -exec ls -l {} \;

Ini akan mencantumkan file dan memberikan detail tentang atribut.

chetangb
sumber
-5

Bagaimana jika Anda menggunakan grep daripada menemukan?

ls | grep .txt$ > out.txt

Sekarang Anda dapat membaca file ini dan nama file dalam bentuk daftar.

Dhruv Raj Singh Rathore
sumber
6
Tidak, jangan lakukan ini. Mengapa Anda tidak harus menguraikan output ls . Ini rapuh, sangat rapuh.
fedorqui 'SO stop harming'