untuk vs temukan di Bash

28

Saat mengulang file, ada dua cara:

  1. gunakan for-loop:

    for f in *; do
        echo "$f"
    done
  2. gunakan find:

    find * -prune | while read f; do 
        echo "$f"
    done

Dengan asumsi dua loop ini akan menemukan daftar file yang sama, apa perbedaan dalam dua opsi dalam kinerja dan penanganan?

rubo77
sumber
1
Mengapa? findtidak membuka file yang ditemukannya. Satu-satunya hal yang saya bisa lihat menggigit Anda di sini sehubungan dengan sejumlah besar file adalah ARG_MAX .
kojiro
1
Lihat jawaban dan komentar yang memberi tahu Anda bahwa read fakan memecah-mecah nama file ketika membacanya (mis. Nama dengan blanko terkemuka). Juga find * -prunetampaknya menjadi cara yang sangat berbelit-belit untuk mengatakan hanya ls -1ya?
Ian D. Allen
4
Jangan berasumsi bahwa kedua loop akan menemukan kumpulan file yang sama; dalam banyak kasus, mereka tidak akan melakukannya. Juga, itu seharusnya find ., bukan find *.
alexis
1
@terdon Ya, parsing ls -ladalah ide yang buruk. Tapi parsing ls -1(itu 1bukan an l) tidak lebih buruk daripada parsing find * -prune. Keduanya gagal pada file dengan baris baru di namanya.
Ian D. Allen
5
Saya menduga bahwa kita masing-masing menghabiskan lebih banyak waktu membaca pertanyaan dan tanggapan ini daripada perbedaan total dalam kinerja selama masa pakai naskah yang dipertanyakan.
mpez0

Jawaban:

9

1.

Yang pertama:

for f in *; do
  echo "$f"
done

gagal untuk file yang dipanggil -n, -edan varian menyukai -nenedan dengan beberapa penyebaran bash, dengan nama file yang mengandung backslash.

Kedua:

find * -prune | while read f; do 
  echo "$f"
done

gagal untuk bahkan lebih kasus (file disebut !, -H, -name, (, nama file yang dimulai atau berakhir dengan kosong atau berisi karakter baris baru ...)

Shell yang mengembang *, findtidak melakukan apa-apa selain mencetak file yang diterimanya sebagai argumen. Anda juga bisa menggunakan printf '%s\n'yang seperti printfyang dibangun juga akan menghindari potensi kesalahan terlalu banyak args .

2.

Perluasan *diurutkan, Anda dapat membuatnya sedikit lebih cepat jika Anda tidak perlu menyortir. Di zsh:

for f (*(oN)) printf '%s\n' $f

atau hanya:

printf '%s\n' *(oN)

bashtidak memiliki setara sejauh yang saya tahu, jadi Anda harus menggunakan find.

3.

find . ! -name . -prune ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

(di atas menggunakan -print0ekstensi non-standar GNU / BSD ).

Itu masih melibatkan pemunculan perintah find dan menggunakan while readloop lambat , jadi itu mungkin akan lebih lambat daripada menggunakan forloop kecuali daftar file sangat besar.

4.

Selain itu, bertentangan dengan ekspansi wildcard, findakan melakukan lstatpanggilan sistem pada setiap file, jadi tidak mungkin bahwa non-sorting akan mengimbangi itu.

Dengan GNU / BSD find, itu bisa dihindari dengan menggunakan -maxdepthekstensi mereka yang akan memicu optimasi menyimpan lstat:

find . -maxdepth 1 ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

Karena findmulai mengeluarkan nama file segera setelah ditemukan (kecuali untuk buffer keluaran stdio), yang mungkin lebih cepat adalah jika apa yang Anda lakukan dalam loop memakan waktu dan daftar nama file lebih dari buffer stdio (4 / 8 kB). Dalam hal ini, pemrosesan dalam loop akan dimulai sebelum findselesai menemukan semua file. Pada sistem GNU dan FreeBSD, Anda dapat menggunakannya stdbufuntuk menyebabkan hal itu terjadi lebih cepat (menonaktifkan buffering stdio).

5.

Cara POSIX / standar / portabel untuk menjalankan perintah untuk setiap file findadalah dengan menggunakan -execpredikat:

find . ! -name . -prune ! -name '.*' -exec some-cmd {} ';'

Dalam hal ini echo, itu kurang efisien daripada melakukan perulangan di shell karena shell akan memiliki versi builtin echosementara findperlu memunculkan proses baru dan mengeksekusi /bin/echodi dalamnya untuk setiap file.

Jika Anda perlu menjalankan beberapa perintah, Anda dapat melakukan:

find . ! -name . -prune ! -name '.*' -exec cmd1 {} ';' -exec cmd2 {} ';'

Namun waspadalah yang cmd2hanya dijalankan jika cmd1berhasil.

6.

Cara kanonik untuk menjalankan perintah kompleks untuk setiap file adalah dengan memanggil shell dengan -exec ... {} +:

find . ! -name . -prune ! -name '.*' -exec sh -c '
  for f do
    cmd1 "$f"
    cmd2 "$f"
  done' sh {} +

Saat itu, kami kembali menjadi efisien echokarena kami menggunakan versi bawaan shdan -exec +versi memunculkan sesedikit shmungkin.

7.

Dalam pengujian saya pada direktori dengan 200.000 file dengan nama pendek di ext4, yang zsh(paragraf 2.) sejauh ini tercepat, diikuti oleh for i in *loop sederhana pertama (meskipun seperti biasa, bashjauh lebih lambat daripada kerang lain untuk itu).

Stéphane Chazelas
sumber
apa yang !dilakukan dalam perintah find?
rubo77
@ rubo77, !untuk negasi. ! -name . -prune more...akan melakukan -prune(dan more...karena -pruneselalu mengembalikan true) untuk setiap file tetapi .. Jadi itu akan dilakukan more...pada semua file di ., tetapi akan mengecualikan .dan tidak akan turun ke subdirektori dari .. Jadi itu setara dengan standar GNU -mindepth 1 -maxdepth 1.
Stéphane Chazelas
18

Saya mencoba ini pada direktori dengan 2259 entri, dan menggunakan timeperintah.

Output dari time for f in *; do echo "$f"; done(minus file!) Adalah:

real    0m0.062s
user    0m0.036s
sys     0m0.012s

Output dari time find * -prune | while read f; do echo "$f"; done(minus file!) Adalah:

real    0m0.131s
user    0m0.056s
sys     0m0.060s

Saya menjalankan setiap perintah beberapa kali, sehingga dapat menghilangkan kesalahan cache. Ini menyarankan untuk menyimpannya di bash(untuk i di ...) lebih cepat daripada menggunakan finddan menyalurkan output (ke bash)

Hanya untuk kelengkapan, saya menjatuhkan pipa dari find, karena dalam contoh Anda, itu sepenuhnya berlebihan. Output dari adil find * -pruneadalah:

real    0m0.053s
user    0m0.016s
sys     0m0.024s

Juga, time echo *(keluaran tidak dipisahkan baris, sayangnya):

real    0m0.009s
user    0m0.008s
sys     0m0.000s

Pada titik ini, saya menduga alasannya echo *lebih cepat karena tidak menghasilkan begitu banyak baris baru, sehingga hasilnya tidak terlalu banyak. Ayo coba ...

time find * -prune | while read f; do echo "$f"; done > /dev/null

hasil:

real    0m0.109s
user    0m0.076s
sys     0m0.032s

sementara time find * -prune > /dev/nullhasil:

real    0m0.027s
user    0m0.008s
sys     0m0.012s

dan time for f in *; do echo "$f"; done > /dev/nullhasil:

real    0m0.040s
user    0m0.036s
sys     0m0.004s

dan akhirnya: time echo * > /dev/nullhasil:

real    0m0.011s
user    0m0.012s
sys     0m0.000s

Beberapa variasi dapat diperhitungkan oleh faktor acak, tetapi tampaknya jelas:

  • output lambat
  • biaya pipa sedikit
  • for f in *; do ...lebih lambat daripada find * -prunesendiri, tetapi untuk konstruksi di atas yang melibatkan pipa, lebih cepat.

Selain itu, di samping itu, kedua pendekatan tersebut tampaknya menangani nama dengan spasi.

EDIT:

Pengaturan waktu untuk find . -maxdepth 1 > /dev/nullvs find * -prune > /dev/null.:

time find . -maxdepth 1 > /dev/null:

real    0m0.018s
user    0m0.008s
sys     0m0.008s

find * -prune > /dev/null:

real    0m0.031s
user    0m0.020s
sys     0m0.008s

Jadi, kesimpulan tambahan:

  • find * -prunelebih lambat dari find . -maxdepth 1- di yang pertama, shell sedang memproses bola dunia, kemudian membangun baris perintah (besar) untuk find. NB: find . -prunehanya mengembalikan ..

Tes lebih lanjut time find . -maxdepth 1 -exec echo {} \; >/dev/null::

real    0m3.389s
user    0m0.040s
sys     0m0.412s

Kesimpulan:

  • cara paling lambat untuk melakukannya sejauh ini. Seperti yang ditunjukkan dalam komentar untuk jawaban di mana pendekatan ini disarankan, setiap argumen memunculkan shell.
Phil
sumber
Pipa mana yang redundan? dapatkah Anda menunjukkan garis yang digunakan tanpa pipa?
rubo77
2
@ rubo77 find * -prune | while read f; do echo "$f"; donememiliki pipa redundan - semua pipa lakukan adalah mengeluarkan findoutput apa sendiri. Tanpa pipa, itu akan menjadi sederhana find * -prune . Pipa hanya redundan khusus karena hal di sisi lain pipa hanya menyalin stdin ke stdout (untuk sebagian besar). Ini adalah no-op yang mahal. Jika Anda ingin melakukan hal-hal dengan output dari find, selain hanya meludahkannya kembali, itu berbeda.
Phil
Mungkin peminum utama adalah *. Seperti yang dikatakan BitsOfNix : Saya masih sangat menyarankan untuk tidak menggunakan *dan .sebagai findgantinya.
rubo77
@ rubo77 sepertinya begitu. Saya kira saya mengabaikan hal itu. Saya telah menambahkan temuan untuk sistem saya. Saya berasumsi find . -prunelebih cepat karena findakan membaca entri direktori kata demi kata, sementara shell akan melakukan hal yang sama, berpotensi cocok dengan glob (mungkin mengoptimalkan untuk *), kemudian membangun baris perintah besar untuk find.
Phil
1
find . -prunehanya mencetak .pada sistem saya. Ini hampir tidak berhasil sama sekali. Sama sekali tidak sama dengan find * -pruneyang menunjukkan semua nama di direktori saat ini. Telanjang read fakan memotong nama file dengan spasi terdepan.
Ian D. Allen
10

Saya pasti akan pergi dengan find walaupun saya akan mengubah temuan Anda menjadi hanya ini:

find . -maxdepth 1 -exec echo {} \;

Dari segi kinerja, findjauh lebih cepat tergantung kebutuhan Anda tentunya. Apa yang Anda miliki saat forini hanya akan menampilkan file / direktori di direktori saat ini tetapi tidak isi direktori. Jika Anda menggunakan find, itu juga akan menampilkan isi dari sub-direktori.

Saya katakan menemukan lebih baik karena dengan Anda foryang *akan harus diperluas pertama dan aku takut bahwa jika Anda memiliki sebuah direktori dengan sejumlah besar file mungkin memberikan kesalahan daftar argumen terlalu lama . Sama berlaku untukfind *

Sebagai contoh, di salah satu sistem yang saya gunakan saat ini ada beberapa direktori dengan lebih dari 2 juta file (masing-masing <100k):

find *
-bash: /usr/bin/find: Argument list too long
BitsOfNix
sumber
Saya menambahkan -pruneuntuk membuat dua contoh lebih mirip. dan saya lebih suka pipa dengan sementara sehingga lebih mudah untuk menerapkan lebih banyak perintah dalam loop
rubo77
mengubah batas keras bukanlah solusi yang tepat dari POV saya. Khususnya ketika berbicara tentang 2+ juta file. Tanpa penyimpangan dari Pertanyaan, untuk kasus-kasus sederhana sebagai direktori satu tingkat lebih cepat, tetapi jika Anda mengubah struktur file / direktori Anda akan lebih sulit untuk bermigrasi. Meskipun dengan menemukan dan itu adalah sejumlah besar pilihan Anda dapat lebih siap. Namun saya masih sangat menyarankan untuk tidak menggunakan * dan. untuk menemukan gantinya. Ini akan lebih portabel daripada * di mana Anda mungkin tidak dapat mengendalikan hardlimit ...
BitsOfNix
4
Itu akan menelurkan satu proses gema per file (sementara di shell untuk loop, itu gema builtin yang akan digunakan tanpa forking proses tambahan), dan akan turun ke direktori, sehingga akan jauh lebih lambat . Perhatikan juga bahwa itu akan termasuk file dot.
Stéphane Chazelas
Anda benar, saya menambahkan maxdepth 1 sehingga hanya menempel pada level saat ini.
BitsOfNix
7
find * -prune | while read f; do 
    echo "$f"
done

adalah penggunaan yang tidak berguna find- Apa yang Anda katakan efektif "untuk setiap file di direktori ( *), tidak menemukan file. Juga, itu tidak aman karena beberapa alasan:

  • Garis miring terbalik di jalur diperlakukan secara khusus tanpa -ropsi untuk read. Ini bukan masalah dengan forloop.
  • Baris baru di jalur akan memecah fungsionalitas non-sepele di dalam loop. Ini bukan masalah dengan forloop.

Penanganan setiap nama file dengan findyang sulit , sehingga Anda harus menggunakan foropsi lingkaran bila memungkinkan karena alasan itu saja. Juga, menjalankan program eksternal seperti findpada umumnya akan lebih lambat daripada menjalankan perintah loop internal seperti for.

l0b0
sumber
@ I0b0 Bagaimana dengan find -path './*' -prune atau find -path './[^.[*' -prune (untuk menghindari file dan direktori tersembunyi) sebagai konstruksi yang lebih baik - dalam bentuk lengkap: find -path ' ./* '-prune -print0 | xargs -0 sh -c '...'?
AsymLabs
1
Baik find' -print0maupun xargs' -0tidak kompatibel dengan POSIX, dan Anda tidak dapat memasukkan perintah sembarang sh -c ' ... '(tanda kutip tunggal tidak dapat diloloskan dalam tanda kutip tunggal), sehingga tidak terlalu sederhana.
l0b0
4

Tapi kami payah untuk pertanyaan kinerja! Permintaan untuk eksperimen ini membuat setidaknya dua asumsi yang membuatnya tidak terlalu valid.

A. Asumsikan mereka menemukan file yang sama ...

Yah, mereka akan menemukan file yang sama pada awalnya, karena mereka berdua mengulangi glob yang sama, yaitu *. Tetapi find * -prune | while read fmenderita beberapa kelemahan yang membuatnya sangat mungkin tidak akan menemukan semua file yang Anda harapkan:

  1. Menemukan POSIX tidak dijamin untuk menerima lebih dari satu argumen jalur. Sebagian besar findimplementasi memang, tetapi tetap saja, Anda tidak harus bergantung pada itu.
  2. find *dapat pecah ketika Anda menekan ARG_MAX. for f in *tidak akan, karena ARG_MAXberlaku untuk exec, bukan bawaan.
  3. while read fdapat putus dengan nama file mulai dan berakhir dengan spasi putih, yang akan dilucuti. Anda bisa mengatasi ini dengan while readdan parameter default-nya REPLY, tetapi itu masih tidak akan membantu Anda ketika datang ke nama file dengan baris baru di dalamnya.

B. echo. Tidak ada yang akan melakukan ini hanya untuk menggemakan nama file. Jika Anda menginginkannya, lakukan saja salah satu dari ini:

printf '%s\n' *
find . -mindepth 1 -maxdepth 1 # for dotted names, too

Pipa ke whileloop di sini membuat subshell implisit yang menutup ketika loop berakhir, yang mungkin tidak intuitif untuk beberapa orang.

Untuk menjawab pertanyaan, berikut adalah hasil dalam direktori saya yang memiliki 184 file dan direktori di dalamnya.

$ time bash -c 'for i in {0..1000}; do find * -prune | while read f; do echo "$f"; done >/dev/null; done'

real    0m7.998s
user    0m5.204s
sys 0m2.996s
$ time bash -c 'for i in {0..1000}; do for f in *; do echo "$f"; done >/dev/null; done'

real    0m2.734s
user    0m2.553s
sys 0m0.181s
$ time bash -c 'for i in {0..1000}; do printf '%s\n' * > /dev/null; done'

real    0m1.468s
user    0m1.401s
sys 0m0.067s

$ time bash -c 'for i in {0..1000}; do find . -mindepth 1 -maxdepth 1 >/dev/null; done '

real    0m1.946s
user    0m0.847s
sys 0m0.933s
kojiro
sumber
Saya tidak setuju dengan pernyataan itu sementara loop memunculkan subkulit - pada kasus terburuk, utas baru: berikut ini mencoba untuk ditampilkan sebelum dan sesudah, permintaan maaf untuk pemformatan yang buruk$ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20811 pts/1 R+ 0:00 grep bash $ while true; do while true; do while true; do while true; do while true; do sleep 100; done; done; done; done; done ^Z [1]+ Stopped sleep 100 $ bg [1]+ sleep 100 & $ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20924 pts/1 S+ 0:00 grep bash
Phil
Secara teknis saya salah bicara: pipa menyebabkan subshell implisit, bukan loop sementara. Saya akan mengedit.
kojiro
2

find *tidak akan berfungsi dengan benar jika *menghasilkan token yang terlihat seperti predikat daripada jalur.

Anda tidak dapat menggunakan --argumen yang biasa untuk memperbaikinya karena --menunjukkan akhir dari opsi, dan menemukan opsi datang sebelum jalan.

Untuk memperbaiki masalah ini, Anda dapat menggunakannya find ./*. Tapi kemudian itu tidak menghasilkan string yang sama persis dengan for x in *.

Perhatikan bahwa find ./* -prune | while read f ..sebenarnya tidak menggunakan fungsionalitas pemindaian find. Ini adalah sintaks globbing ./*yang sebenarnya melintasi direktori dan menghasilkan nama. Maka findprogram harus melakukan setidaknya statpemeriksaan pada masing-masing nama tersebut. Anda memiliki overhead untuk meluncurkan program dan memilikinya mengakses file-file ini, dan kemudian melakukan I / O untuk membaca outputnya.

Sulit membayangkan bagaimana itu bisa menjadi apa pun selain kurang efisien for x in ./* ....

Kaz
sumber
1

Yah sebagai permulaan foradalah kata kunci shell, dibangun ke Bash, sementara findmerupakan executable terpisah.

$ type -a for
for is a shell keyword

$ type -a find
find is /usr/bin/find

The forLoop hanya akan menemukan file dari karakter globstar ketika mengembang, hal itu tidak akan recurse ke setiap direktori yang ditemukan.

Temukan di sisi lain juga akan diberikan daftar yang diperluas oleh globstar, tetapi akan secara rekursif menemukan semua file dan direktori di bawah daftar yang diperluas ini dan menyalurkan masing-masing ke whileloop.

Kedua pendekatan ini mungkin dianggap berbahaya dalam arti bahwa mereka tidak menangani jalur atau nama file yang mengandung spasi.

Hanya itu yang bisa saya pikirkan untuk mengomentari kedua pendekatan ini.

slm
sumber
Saya menambahkan -proune ke perintah find, jadi mereka lebih mirip.
rubo77
0

Jika semua file yang dikembalikan oleh find dapat diproses dengan satu perintah (jelas tidak berlaku untuk contoh gema Anda di atas), Anda dapat menggunakan xargs:

find * |xargs some-command
rampok
sumber
0

Selama bertahun-tahun saya telah menggunakan ini: -

find . -name 'filename'|xargs grep 'pattern'|more

untuk mencari file-file tertentu (misalnya * .txt) yang berisi pola yang dapat dicari grep dan disalurkan menjadi lebih banyak sehingga tidak menggulung layar. Terkadang saya menggunakan pipa >> untuk menulis hasilnya ke file lain yang bisa saya lihat nanti.

Berikut contoh hasilnya: -

./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:In-Reply-To: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <448E53556A3F442ABC58203D6281923E@hypermax>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2011-April.txt:URL: http://mylist.net/private/rodgersorganusers/attachments/20110420/3f
Allen
sumber