Bash'de bulmak için


28

Dosyalar arasında dolaşırken iki yol vardır:

  1. Bir fordöngü kullanın :

    for f in *; do
        echo "$f"
    done
  2. kullanım find:

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

Bu iki döngünün aynı dosya listesini bulacağını varsayarsak, performans ve kullanımdaki bu iki seçenek arasındaki farklar nelerdir?


1
Niye ya? findbulduğu dosyaları açmaz. Çok sayıda dosya için sizi burada ısırmayı görebildiğim tek şey ARG_MAX .
kojiro

1
read fDosya adlarını okuduklarında değiştireceğini söyleyen cevaplara ve yorumlara bakın (örneğin, önde gelen boşluklara sahip adlar). Ayrıca find * -prunebasitçe ls -1evet demek için çok karmaşık bir yol gibi görünüyor ?
Ian D. Allen

4
Do iki döngüler aynı dosya kümesini bulacaksınız varsayalım; çoğu durumda, onlar olmaz. Ayrıca, bu olmalı find ., değil find *.
alexis,

1
@ terdon Evet, ayrıştırma ls -lkötü bir fikir. Ancak ayrıştırma ls -1(bu bir 1değil l) ayrıştırmadan daha kötü değildir find * -prune. Her ikisi de adlarda yeni satırlara sahip dosyalarda başarısız olur.
Ian D. Allen

5
Her birinin bu soruyu okumak için daha fazla zaman harcadığımızı ve söz konusu betiğin ömrü boyunca performanstaki toplam farktan yanıtlarını yanıtladığımızdan şüpheleniyorum.
m02-0

Yanıtlar:


9

1.

İlki:

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

denilen dosyalar için başarısız -n, -eve benzeri varyantları -nenedosya adları ters eğik çizgi içeren ve bazı bash dağıtımları ile.

İkinci:

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

daha fazla vaka için başarısız olur (çağrılan dosyalar ! , -H, -name, (... başlangıç veya bitiş kurusıkı veya yeni satır karakterleri içeren, dosya adları)

Genişleyen kabuk *,find argüman olarak aldığı dosyaları başka bir şey yapmaz Sen de kullanmış olabilir printf '%s\n'olarak hangi yerine printfyerleşik da önleyeceğini olduğunu çok fazla bağımsız değişken potansiyel hata.

2.

Genişlemesi * sıralanır, sıralamaya ihtiyacınız yoksa biraz daha hızlı hale getirebilirsiniz. İçinde zsh:

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

ya da sadece:

printf '%s\n' *(oN)

bashSöyleyebileceğim kadarıyla eşdeğeri yok, o yüzden başvurman gerekiyor find.

3.

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

(bir GNU / BSD kullanarak yukarıda -print0 standart olmayan bir uzantı kullanılarak).

Bu hala bir bulma komutunun oluşturulmasını ve yavaş bir while readdöngü kullanılmasını içerir, bu nedenle fordosya listesi çok büyük olmadıkça döngü kullanmaktan daha yavaş olacaktır .

4.

Ayrıca, joker karakter genişlemesinin aksine , her dosya findiçin bir lstatsistem çağrısı yapar; bu nedenle, sıralama dışı işlemlerin bunu telafi etmesi olası değildir.

GNU / BSD ile find, aşağıdakileri -maxdepthkazandıran bir optimizasyonu tetikleyecek uzantılarını kullanarak önlenebilir lstat:

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

Çünkü finddosya adlarını bulduktan hemen sonra çıkarmaya başlar (stdio çıktı arabelleği hariç), döngüde ne yaparsanız zaman alıcı ve dosya adlarının listesi bir stdio arabasından daha fazlaysa daha hızlı olabilir. / 8 kB). Bu durumda, döngü içindeki işlem findtüm dosyaları bulma işlemini bitirmeden önce başlayacaktır . GNU ve FreeBSD sistemlerinde, stdbufbunun daha erken olmasını sağlamak için kullanabilirsiniz (stdio tamponlamayı devre dışı bırakarak).

5.

Her dosya için komut çalıştırmanın POSIX / standart / taşınabilir yolu yüklemi findkullanmaktır -exec:

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

Durumunda echokabuk bir yerleşik sürümüne sahip olacak şekilde kabuğunda döngü yapmaktan daha az verimli olsa, echoise findyeni bir süreç yumurtlamaya ve yürütmek gerekir /bin/echoher dosya için içinde.

Birkaç komut çalıştırmanız gerekirse, şunları yapabilirsiniz:

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

Ancak , başarılı cmd2olursa cmd1, yürütüldüğüne dikkat edin .

6.

Her dosya için karmaşık komutları çalıştırmanın kurallı bir yolu, aşağıdakileri içeren bir kabuk çağırmaktır -exec ... {} +:

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

O zamanlar, yerleşik olanını kullandığımızdan ve sürüm mümkün olduğu kadar az çıktığından echoberi verimli olmaya geri döndük .sh-exec +sh

7.

In 200.000 dosyaları ile bir dizin benim testlerde ext4 kısa adlarla, zshbir (paragraf 2.) İlk basit ardından uzak hızlı gereğidir for i in *döngü (her zamanki gibi olsa bashçok daha yavaş diğer kabukları daha o içindir).


!find komutunda ne yapar ?
rubo77

@ rubo77, !olumsuzlama içindir. her dosya için ! -name . -prune more...yapacağım -prune(ve more...o zamandan beri -pruneher zaman doğru olur) .. Böylece more...tüm dosyalar üzerinde çalışacak ., fakat dışlanacak .ve alt dizinlerine inmeyecek .. Yani bu GNU'ların standart karşılığıdır -mindepth 1 -maxdepth 1.
Stéphane Chazelas

18

Bunu 2259 girişli bir dizinde denedim ve timekomutu kullandım .

Çıktısı time for f in *; do echo "$f"; done(eksi dosyalar!):

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

Çıktısı time find * -prune | while read f; do echo "$f"; done(eksi dosyalar!):

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

Her komutu birkaç kez çalıştırdım, böylece önbellek eksikliğini ortadan kaldırıyordum. Bu, bash(i'de ... için) içeride tutmanın findçıkışı kullanmaktan ve borulamaktan daha hızlı olduğunu gösterir.bash )

Sadece tamlık için boruyu düşürdüm, findörneğinizde tamamen gereksiz. Sadece çıktısı find * -prune:

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

Ayrıca, time echo *(çıktı newline ayrılmış değil, ne yazık ki):

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

Bu noktada, sebebinin echo *daha hızlı olduğundan şüpheleniyorum , bu kadar çok yeni satır çıkmıyor, bu yüzden çıktı kadar kaydırma yapmıyor. Hadi test edelim ...

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

verim:

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

ise time find * -prune > /dev/nullverimi:

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

ve time for f in *; do echo "$f"; done > /dev/nullverimler:

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

ve son olarak: time echo * > /dev/nullverim:

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

Varyasyonların bazıları rastgele faktörlerle açıklanabilir, ancak açık görünüyor:

  • çıkış yavaş
  • boru maliyetleri biraz
  • for f in *; do ...tek başına daha yavaştır find * -prune, ancak yukarıdaki boruları içeren yapılar için daha hızlıdır.

Ayrıca, bir yana, her iki yaklaşım da boşlukları iyi olan isimleri ele almak için görünür.

DÜZENLE:

İçin Zamanlamaları 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

Yani, ek sonuç:

  • find * -pruneöncekinden daha yavaştır find . -maxdepth 1, kabuk bir küre işliyor, sonra için bir (büyük) komut satırı oluşturuyor find. NB: find . -prunesadece döner ..

Daha fazla test time find . -maxdepth 1 -exec echo {} \; >/dev/null::

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

Sonuç:

  • şimdiye kadar yapmanın en yavaş yolu. Bu yaklaşımın önerildiği cevabın yorumunda da belirtildiği gibi, her argüman bir kabuk ortaya çıkarmaktadır.

Hangi boru gereksiz? Borusuz kullandığınız çizgiyi gösterebilir misiniz?
rubo77

2
@ rubo77 find * -prune | while read f; do echo "$f"; doneyedek boruya sahip - borunun yaptığı tek şey tam olarak findkendi çıktısını alıyor . Bir boru olmadan, basitçe olacaktır find * -prune . Boru yalnızca yedeklidir, çünkü borunun diğer tarafındaki şey sadece stdout'a (çoğunlukla) stdin kopyalar. Pahalı bir ameliyat değil. Bulgunun çıktısıyla bir şeyler yapmak istiyorsanız, tekrar geri tükürmek dışında, bu farklı.
Phil

Belki de ana zaman tüketimi *. As BitsOfNix belirtti: Hala güçlü kullanmayın önermek *ve .için findyerine.
rubo77 22:13

@ rubo77 bu şekilde görünüyor. Sanırım bunu görmezden geldim. Sistemim için bulgular ekledim. Sanırım find . -prunedaha hızlı, çünkü findbir dizin girişi verbatim okuyor olacak, kabuk aynı şekilde, potansiyel olarak glob ile eşleşecek (bunun için optimize edilebilir *), sonra büyük komut satırı oluşturacak find.
Phil

1
find . -pruneyalnızca .sistemime yazdırıyor . Neredeyse hiç iş yapmaz. find * -pruneGeçerli dizindeki tüm adları gösteren ile aynı değildir . Çıplak read f, dosya adlarını baştaki boşluklarla dolaştırır.
Ian D. Allen

10

Bulgunuzu sadece bu şekilde değiştirmeme rağmen kesinlikle bulmaya giderdim:

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

Performans bilge, findtabii ki ihtiyaçlarınıza bağlı olarak çok daha hızlı. Şu anda sahip forolduğunuz şey yalnızca geçerli dizindeki dosyaları / dizinleri görüntüler, ancak dizin içeriğini görüntülemez. Bulursanız alt dizinlerin içeriğini de gösterir.

Ben bulmak ile beri iyi olduğunu söylemek senin ilk genişletilmiş gerekecektir ve ben dosyaların büyük miktarda bir dizin varsa hata verebilir korkuyorum argüman listesi çok uzunfor* . Aynı şey için de geçerlifind *

Örnek olarak, şu anda kullandığım sistemlerden birinde, 2 milyonun üzerinde dosya içeren birkaç dizin var (her biri <100k):

find *
-bash: /usr/bin/find: Argument list too long

-pruneİki örneği daha benzer yapmak için ekledim . ve boruyu tercih ederken döngüde daha fazla komut uygulamak daha kolay
rubo77


Sabit limiti değiştirmek, POV'mdan pek uygun bir geçici çözüm değil. Özellikle 2 milyondan fazla dosya hakkında konuşurken. Sorudan kısma olmadan, basit durumlar için tek seviye bir dizin için daha hızlıdır, ancak dosya / dizin yapınızı değiştirirseniz, taşınması zorlaşır. Bul ve çok büyük seçeneklerle birlikte daha iyi hazırlanabilirsiniz. Yine de * ve kullanmamayı şiddetle tavsiye ediyorum. bunun yerine bulmak için. Zor sınırlarını kontrol edemeyeceğin * yerlerden daha taşınabilir ...
BitsOfNix

4
Bu, dosya başına bir yankı işlemi ortaya çıkarır (döngü kabuğundayken, fazladan bir işlem yapmadan kullanılacak olan yankı yerleşimidir) ve dizinlere iner, bu yüzden çok daha yavaş olacaktır . Ayrıca nokta dosyaları içereceğini de unutmayın.
Stéphane Chazelas

Haklısınız, maxdepth 1'i yalnızca geçerli seviyeye yapışması için ekledim.
BitsOfNix

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

yararsız bir kullanımıdır find- Söylediğiniz şey etkilidir "dizindeki her dosya için ( *), hiçbir dosya bulamayın. Ayrıca, birkaç nedenden dolayı güvenli değildir:

  • Yollardaki ters eğik çizgiler özel -rseçeneği olmadan tedavi edilir read. Bu fordöngü ile ilgili bir sorun değil .
  • Yollardaki yeni çizgiler, döngü içinde önemsiz olmayan işlevleri bozar. Bu fordöngü ile ilgili bir sorun değil .

İle herhangi bir dosya adı Handling findise zor kullanmanız gerektiğini bu nedenle, formümkün olan her durumda bu nedenle yalnız için döngü seçeneği. Ayrıca, gibi bir harici program çalıştırma find, genel olarak bir dahili döngü komutu çalıştırmaya kıyasla daha yavaş olacaktır for.


@ I0b0 Ne -path './*' -prune veya -path './CL^' i bulun / /. ./* '-prune -print0 | xargs -0 sh -c '...'?
AsymLabs

1
Ne find'ın -print0ne de xargs' -0POSIX uyumludur ve keyfi komutları koyamazsınız sh -c ' ... 'o kadar basit değil bu yüzden, (tek tırnak tek tırnak içindeki çıkışı olamaz).
l0b0

4

Ama performans soruları için enayileriz! Bu deneme isteği, son derece geçerli olmayan kılan en az iki varsayımda bulunur.

A. Aynı dosyaları bulduğunu varsayalım…

Eh, onlar olacaktır onlar yani aynı topak üzerinde hem ilerlerken çünkü, ilk başta aynı dosyaları bulabilirsiniz *. Ancak find * -prune | while read fbeklediğiniz tüm dosyaları bulamayacağını mümkün kılan birçok kusurdan muzdarip:

  1. POSIX bulmasının birden fazla yol argümanını kabul etmesi garanti edilmez. Çoğu finduygulama yapar, ancak yine de buna güvenmemelisin.
  2. find *Vurulduğunda kırılabilir ARG_MAX. for f in *çünkü olmaz, ARG_MAXuygulandığı exec, builtins değil.
  3. while read fboşluklarla başlayıp biten dosya isimleriyle kırılabilir, bu da sıyrılacak. Bunu while readve onun varsayılan parametresiyle bunun üstesinden gelebilirsiniz REPLY, ancak bu, içinde yeni satır bulunan dosya adlarına gelince size yardımcı olmayacaktır.

B. echo. Kimse bunu sadece dosya adını tekrarlamak için yapmayacak. Bunu istiyorsan, şunlardan birini yap:

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

Buradaki ilmeğe giden boru while, ilmek sona erdiğinde kapanan, bazıları için sezgisel olmayan, örtük bir alt kabuk yaratır.

Soruyu cevaplamak için, işte 184 dosya ve dizin içeren bir dizindeki sonuçlar.

$ 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

While döngüsünün bir alt kabuk ortaya çıkardığı ifadesine katılmıyorum - en kötü durumda, yeni bir konu: aşağıdakiler önce ve sonra, kötü biçimlendirme için özür dilemek için göstermeye çalışıyor$ 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

Teknik olarak ben yanlış konuştum: boru bir süre döngüsüne değil, örtük alt kabuğa neden oluyor. Düzenleyeceğim.
kojiro

2

find **yollar yerine yüklemler gibi görünen belirteçler üretiyorsa doğru çalışmayacaktır .

Bunu --düzeltmek için normal argümanı kullanamazsınız, çünkü --seçeneklerin sonunu ve bulma seçeneklerinin yollardan önce geldiğini gösterir.

Bu sorunu çözmek için find ./*bunun yerine kullanabilirsiniz . Ama sonra tam olarak aynı dizeleri üretmiyor for x in *.

Bunun find ./* -prune | while read f ..aslında tarama işlevini kullanmadığını unutmayın find. ./*Dizini geçen ve isimleri üreten genel sözdizimidir . Daha sonra findprogramın stat, bu isimlerden her biri üzerinde en az bir kontrol yapması gerekecek . Programı başlatma ve bu dosyalara erişme yükü sizde ve ardından çıktısını okumak için G / Ç yapıyorsunuz.

Nasıl bir şey olabileceğini hayal etmek zor ama ondan daha az verimli for x in ./* ....


1

Yeni başlayanlar foriçin Bash içine yerleştirilmiş, ancak findayrı bir çalıştırılabilir bir kabuk anahtar kelimedir .

$ type -a for
for is a shell keyword

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

forGenişlediğinde döngü yalnızca bulduğu dizinlere recurse olmaz, globstar karakteri dosyaları bulacaksınız.

Öte yandan bul, ayrıca globstar tarafından genişletilmiş bir liste de verilecek, ancak bu genişletilmiş listenin altındaki tüm dosyaları ve dizinleri tekrar tekrar bulacak ve her birini whiledöngüye aktaracaktır .

Her iki yaklaşım da boşluk içeren yolları veya dosya adlarını kullanmamaları açısından tehlikeli olarak kabul edilebilir.

Bu 2 yaklaşım hakkında yorum yapmaya değer bulabildiğim kadarıyla ilgili.


Find komutuna -prune ekledim, bu yüzden daha çok benziyorlar.
rubo77

0

Find tarafından döndürülen dosyaların tümü tek bir komutla işlenebilirse (açıkça yukarıdaki eko örneğiniz için geçerli değildir), xargs kullanabilirsiniz:

find * |xargs some-command

0

Yıllardır bunu kullanıyorum: -

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

grep araştırabilecek ve ekranı kaydırmayacak şekilde düzenleyebilecek bir desen içeren belirli dosyaları (örneğin * .txt) aramak için. Bazen sonuçları daha sonra bakabileceğim başka bir dosyaya yazmak için >> piposunu kullanıyorum.

İşte sonucun bir örneği: -

./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
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.