Bul tarafından döndürülen dosya adları arasında nasıl geçiş yapılır?


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

Bash kabuğunda yukarıdaki kod parçasını çalıştırırsam, elde ettiğim, bir liste değil, boşla ayrılmış birkaç dosya adı içeren bir dizedir.

Tabii ki, bir liste almak için onları boş olarak ayırabilirim, ancak eminim bunu yapmanın daha iyi bir yolu var.

Peki bir findkomutun sonuçları arasında geçiş yapmanın en iyi yolu nedir?


3
Dosya adları üzerinde döngü oluşturmanın en iyi yolu, aslında onunla ne yapmak istediğinize bağlıdır, ancak hiçbir dosyanın adında boşluk olmadığını garanti edemezseniz, bunu yapmak için harika bir yol değildir. Peki, dosyalar üzerinde döngü oluştururken ne yapmak istiyorsunuz?
Kevin

1
Ödülle ilgili olarak : buradaki ana fikir, olası tüm vakaları (yeni satırlı dosya adları, sorunlu karakterler ...) kapsayan kanonik bir cevap almaktır. Fikir, daha sonra bazı şeyler yapmak için bu dosya adlarını kullanmaktır (başka bir komut çağırın, bazı yeniden adlandırma gerçekleştirin ...). Teşekkürler!
fedorqui 'SO' zarar vermeyi durdurun '

Bir dosya veya klasör adının ".txt" ve ardından boşluk ve başka bir dize içerebileceğini unutmayın, örneğin "bir
şey.txt

Var değil dizi kullanın x=( $(find . -name "*.txt") ); echo "${x[@]}"Sonra döngüfor item in "${x[@]}"; { echo "$item"; }
Ivan

Yanıtlar:


394

TL; DR: En doğru cevap için buradaysanız, muhtemelen kişisel tercihimi istiyorsunuz find . -name '*.txt' -exec process {} \;(bu yazının altına bakınız). Zamanınız varsa, birçok farklı yol ve bunların çoğundaki sorunları görmek için geri kalanını okuyun.


Tam cevap:

En iyi yol ne yapmak istediğinize bağlıdır, ancak işte birkaç seçenek. Alt ağaçtaki hiçbir dosya veya klasör adında boşluk içermediği sürece, dosyalar üzerinde döngü yapabilirsiniz:

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

Marjinal olarak daha iyi, geçici değişkeni kesin x:

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

Mümkün olduğunda glob yapmak çok daha iyidir. Geçerli dizindeki dosyalar için beyaz boşluk kasası:

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

globstarSeçeneği etkinleştirerek, bu dizindeki ve tüm alt dizinlerdeki tüm eşleşen dosyaları glob edebilirsiniz:

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

Bazı durumlarda, örneğin dosya adları zaten bir dosyadaysa, şunları kullanmanız gerekebilir 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

readfindsınırlayıcıyı uygun şekilde ayarlayarak güvenli bir şekilde birlikte kullanılabilir :

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

Daha karmaşık aramalar için, muhtemelen kullanmak isteyeceğiniz findonun ile ya -execseçenek veya birlikte -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

find-execdiryerine kullanarak bir komutu çalıştırmadan önce her dosyanın dizinine cd yapabilir -execve -okyerine -exec(veya -okdiryerine ) kullanarak etkileşimli (her dosya için komutu çalıştırmadan önce sor -execdir) yapılabilir.

*: Teknik olarak, hem findve xargs(varsayılan olarak) komutu komut satırına sığabilecek kadar çok argümanla çalıştırır, tüm dosyaları almak için gereken kadar. Uygulamada, çok fazla sayıda dosyanız yoksa, önemli olmayacaktır ve uzunluğu aşar ancak hepsine aynı komut satırında ihtiyacınız varsa, SOL farklı bir yol bulur.


4
İle durumunda belirterek It yetmeyecek done < filenameve stdin'nin artık kullanılamaz borunun (döngü içinde artık interaktif malzeme →) ile aşağıdaki biri, ancak ihtiyaç duyulan durumlarda birini kullanabilirsiniz 3<yerine <ekleyebilir <&3veya -u3hiç readbölüm, temelde ayrı bir dosya tanıtıcı kullanarak. Ayrıca, read -d ''aynı olduğuna inanıyorum , read -d $'\0'ancak şu anda bu konuda resmi bir belge bulamıyorum.
phk

1
* .txt içindeki i için; eşleşen dosya yoksa, çalışmıyor. Bir xtra testi örneğin [[-e $ i]] gereklidir
Michael Brux

2
Bu kısımla kayboldum: -exec process {} \;ve tahminim bu başka bir soru - bunun anlamı nedir ve nasıl manipüle edebilirim? İyi bir soru-cevap veya doküman nerede? üstünde?
Alex Hall

1
@AlexHall her zaman man sayfalarına bakabilirsiniz ( man find). Bu durumda, (veya ) ile sonlandırılan ve yerine işlem yaptığı dosyanın adı (veya kullanılıyorsa, bu koşulu oluşturan tüm dosyalar ) ile değiştirilecek aşağıdaki komutu yürütmeyi -execsöyler . find;+{}+
Kevin

3
@ phk -d ''daha iyidir -d $'\0'. İkincisi sadece daha uzun değil, aynı zamanda boş bayt içeren argümanları iletebileceğinizi, ancak yapamayacağınızı gösterir. İlk null bayt dizenin sonunu belirtir. Bash $'a\0bc'ile aynı ave sadece boş dize ile $'\0'aynıdır . " Girdinin ilk karakterinin girdiyi sonlandırmak için kullanıldığını " belirtir. Boş dizedeki ilk karakter, dizenin sonunu her zaman işaretleyen boş bayttır (açıkça yazmasanız bile). $'\0abc'''help read''
Socowi

115

Ne yaparsanız yapın, bir fordöngü kullanmayın :

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

Üç neden:

  • For döngüsünün başlaması için, findişlemin tamamlanması gerekir.
  • Bir dosya adında boşluk varsa (boşluk, sekme veya yeni satır dahil), iki ayrı ad olarak değerlendirilir.
  • Şimdi olası olmasa da, komut satırı arabelleğinizi aşabilirsiniz. Komut satırı arabelleğinizin 32 KB içerdiğini ve döngünüzün 40 KB formetin döndürdüğünü düşünün . Bu son 8 KB fordöngüden çıkarılır ve asla bilemezsiniz.

Her zaman bir while readyapı kullanın :

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

findKomut yürütülürken döngü yürütülür. Ayrıca, içinde boşluk bulunan bir dosya adı döndürülse bile bu komut çalışacaktır. Ve, komut satırı arabelleğinizi taşmaz.

-print0Bir dosya ayırıcı yerine bir satır olarak NULL kullanacak ve -d $'\0'okurken ayırıcı olarak NULL kullanacaktır.


3
Dosya adlarında yeni satırlarla çalışmaz. Kullanım vurgunu -execyerine.
kullanıcı bilinmiyor

2
@userunknown - Bu konuda haklısın. -execen güvenli olanıdır, çünkü kabuğu hiç kullanmaz. Ancak, dosya adlarındaki NL oldukça nadirdir. Dosya adlarındaki boşluklar oldukça yaygındır. Ana nokta, forbirçok afişin önerdiği bir döngü kullanmak değildir .
David

1
@userunknown - İşte. Bunu düzelttim, bu yüzden şimdi yeni satırlar, sekmeler ve diğer beyaz boşluklarla dosyalara bakacak. Gönderinin asıl amacı OP'ye bununla for file $(find)ilgili sorunlar nedeniyle kullanılmamasını söylemektir .
David W.

4
-Exec'i kullanabiliyorsanız daha iyi, ancak kabuğa geri verilen isme gerçekten ihtiyaç duyduğunuz zamanlar vardır. Örneğin, dosya uzantılarını kaldırmak istiyorsanız.
Ben Reser

5
Şu -rseçeneği kullanmalısınız 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

Not: bu yöntem ve bmargulies tarafından gösterilen (ikinci) yöntem, dosya / klasör adlarında beyaz boşlukla kullanmak için güvenlidir.

Dosya / klasör adlarında yer alan yeni satırların da - biraz egzotik - olması için, aşağıdaki gibi bir -execyüklemeye başvurmanız gerekir find:

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

{}Bulunan öğeyi için yer tutucudur ve \;sonlandırmak için kullanılır-exec yüklemi.

Ve eksiksizlik uğruna başka bir varyant eklememe izin verin - çok yönlülüğü için * nix yollarını sevmelisiniz:

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

Bu, yazdırılan öğeleri \0dosya veya klasör adlarında dosya sistemlerinden hiçbirinde izin verilmeyen bir karakterle, bildiklerime ayırır ve bu nedenle tüm üsleri kapsamalıdır. xargsonları birer birer alır sonra ...


3
Dosya adında yeni satır varsa başarısız olur.
kullanıcı bilinmiyor

2
@user unknown: haklısın, hiç düşünmediğim bir durum ve bence bu çok egzotik. Ama cevabımı buna göre ayarladım.
0xC0000022L

5
Muhtemelen değerinde olduğunu işaret find -print0ve xargs -0GNU uzantıları ve taşınabilir değil (POSIX) argümanlar her ikisi de. Yine de bu sistemlere sahip sistemlerde inanılmaz derecede faydalı!
Toby Speight

1
Bu, ters eğik çizgiler ( read -rdüzeltecek) içeren dosya adları veya boşlukla biten (düzeltilecek) dosya adları ile de başarısız olur IFS= read. Bu yüzden BashFAQ # 1 düşündürenwhile IFS= read -r filename; do ...
Charles Duffy

1
Bununla ilgili başka bir sorun , döngü gövdesinin aynı kabukta yürütüldüğü gibi görünüyor , ancak değil, bu yüzden örneğin exitbeklendiği gibi çalışmaz ve döngü gövdesinde ayarlanan değişkenler döngüden sonra kullanılamaz.
EM0

17

Dosya adları boşluklar ve hatta kontrol karakterleri içerebilir. Boşluklar (bash) kabuk genişlemesi için sınırlayıcılardır ve bunun sonucunda x=$(find . -name "*.txt")sorudan hiç tavsiye edilmez. Find, boşluklu bir dosya adı alırsa, örneğin bir döngüde "the file.txt"işlem yaparsanız, işlem için 2 ayrı dize alırsınız x. Bunu, sınırlayıcıyı (bash IFSDeğişken) olarak değiştirerek geliştirebilirsiniz \r\n, ancak dosya adları kontrol karakterleri içerebilir - bu nedenle bu (tamamen) güvenli bir yöntem değildir.

Benim bakış açımdan, dosyaları işlemek için önerilen 2 (ve güvenli) desen var:

1. döngü ve dosya adı genişletme için kullanın:

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. Find-read-while ve süreç ikamesini kullanın

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

Uyarılar

Desen 1'de:

  1. Eşleşen bir dosya bulunmazsa bash arama desenini ("* .txt") döndürür - dolayısıyla "dosya yoksa, devam et" gerekir. bkz. Bash Kılavuzu, Dosya Adı Genişletme
  2. nullglobBu ekstra çizgiden kaçınmak için kabuk seçeneği kullanılabilir.
  3. " failglobKabuk seçeneği ayarlanmışsa ve hiçbir eşleşme bulunmazsa, bir hata mesajı yazdırılır ve komut yürütülmez." (yukarıdaki Bash Manual'dan)
  4. shell option globstar: "Ayarlanmışsa, dosya adı genişletme bağlamında kullanılan '**' kalıbı tüm dosyalarla ve sıfır veya daha fazla dizin ve alt dizinle eşleşir. Deseni bir '/' izlerse, yalnızca dizinler ve alt dizinler eşleşir." bkz. Bash Manual, Shopt Builtin
  5. Dosyaismi için diğer seçenekler: extglob, nocaseglob, dotglobve kabuk değişkeniGLOBIGNORE

Desen 2'de:

  1. dosya boşlukları, sekme, boşluklar, yeni satır içerebilir, ... güvenli bir şekilde işlem dosya için, findbirlikte -print0kullanılır: Dosya tüm kontrol karakterleri ile basılır ve NUL ile sona erdirildi. ayrıca bkz. Gnu Findutils Manpage, Güvenli Olmayan Dosya Adı İşleme , güvenli Dosya Adı İşleme , dosya adlarında olağandışı karakterler . Bu konunun ayrıntılı tartışması için aşağıdaki David A. Wheeler'a bakınız.

  2. Bulma sonuçlarını bir while döngüsünde işlemek için bazı olası desenler vardır. Diğerleri (kevin, David W.) bunu boruları kullanarak nasıl yapacağını gösterdi:

    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"
    Bu kod parçasını denediğinizde, çalışmadığını göreceksiniz: files_foundher zaman "true" dır ve kod her zaman "dosya bulunamadı" yankılanacaktır. Sebep: bir boru hattının her komutu ayrı bir alt kabukta yürütülür, bu nedenle döngü içindeki değişen değişken (ayrı alt kabuk) ana kabuk betiğindeki değişkeni değiştirmez. Bu yüzden süreç ikamesini "daha iyi", daha kullanışlı, daha genel bir model olarak kullanmanızı öneririm.
    Bkz . Bir boru hattındaki bir döngüde değişkenler ayarladım. Bu konu hakkında ayrıntılı bir tartışma için neden kayboluyorlar ... (Greg'in Bash SSS bölümünden).

Ek Referanslar ve Kaynaklar:


8

(@ Socowi'nin üstün hız iyileştirmesini içerecek şekilde güncellendi)

$SHELLBunu destekleyen herhangi biriyle (tire / zsh / bash ...):

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

Bitti.


Orijinal cevap (daha kısa ama daha yavaş):

find . -name "*.txt" -exec $SHELL -c '
    echo "$0"
' {} \;

1
Pekmez gibi yavaş (her dosya için bir kabuk başlattığından) ama bu işe yarıyor. +1
dawg

1
Bunun yerine tek bir dosyaya mümkün olduğu kadar çok dosya aktarmak \;için kullanabilirsiniz . Ardından tüm bu parametreleri işlemek için kabuk betiğinin içinde kullanın . +exec"$@"
Socowi

3
Bu kodda bir hata var. Döngüde ilk sonuç eksik. Çünkü $@genellikle komut dosyasının adı olduğu için bunu atlar. Sadece dummyarasına eklemeliyiz 've {}böylece tüm eşleşmelerin döngü tarafından işlenmesini sağlayarak kod adının yerini alabilir.
BCartolo

Yeni oluşturulan kabuğun dışından başka değişkenlere ihtiyacım olursa ne olur?
Jodo

OTHERVAR=foo find . -na.....$OTHERVARyeni oluşturulan kabuğun içinden erişmenize izin vermelidir .
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

3
for x in $(find ...)içinde boşluk olan herhangi bir dosya adı için kırılacaktır. İle aynı find ... | xargsKullanmak sürece -print0ve-0
Glenn Jackman

1
find . -name "*.txt -exec process_one {} ";"Bunun yerine kullanın . Sonuçları toplamak için neden xargs kullanmalıyız?
kullanıcı bilinmiyor

@userunknown Her şey ne olduğuna bağlı process_one. Gerçek bir komut için yer tutucuysa , bunun işe yarayacağından emin olun (yazım hatasını düzeltir ve sonra kapanış tırnak işaretleri eklerseniz "*.txt). Ancak process_onekullanıcı tanımlı bir işlevse, kodunuz çalışmaz.
Mart'ta toxalot

@toxalot: Evet, ancak işlevi çağrılacak bir komut dosyasına yazmak sorun olmaz.
kullanıcı bilinmiyor

4

findÇıkışı daha sonra şu şekilde kullanmak isterseniz , çıktınızı dizide saklayabilirsiniz :

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

Şimdi her bir öğeyi yeni bir satıra yazdırmak için for, dizinin tüm öğelerine döngü yinelemesini kullanabilir veya printf deyimini kullanabilirsiniz.

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

veya

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

Ayrıca kullanabilirsiniz:

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

Bu, her dosya adını yeni satıra yazdırır

findÇıkışı yalnızca liste biçiminde yazdırmak için aşağıdakilerden birini kullanabilirsiniz:

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

veya

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

Bu, hata mesajlarını kaldıracak ve sadece yeni satırda çıktı olarak dosya adını verecektir.

Dosya adlarıyla bir şey yapmak istiyorsanız, dizide saklamak iyidir, aksi takdirde bu alanı tüketmenize gerek yoktur ve çıktıyı doğrudan yazdırabilirsiniz find.


1
Dizi üzerinde döngü, dosya adlarındaki boşluklarla başarısız olur.
EM0

Bu yanıtı silmelisiniz. Dosya adlarındaki veya dizin adlarındaki boşluklarla çalışmaz.
jww

4

Dosya adlarının yeni satır içermediğini varsayabilirseniz find, aşağıdaki komutu kullanarak bir Bash dizisinin çıktısını okuyabilirsiniz :

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

Not:

  • -t nedenleri readarraysatırsonu soymaya .
  • Bir borudaysa çalışmaz readarray, dolayısıyla işlem ikamesi.
  • readarray Bash 4'ten beri kullanılabilir.

Bash 4.4 ve üstü -d, sınırlayıcıyı belirleme parametresini de destekler . Dosya adlarını sınırlamak için yeni satır yerine null karakterini kullanmak, dosya adlarının yeni satır içerdiği nadir durumlarda da çalışır:

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

readarray olarak da çağrılabilir mapfileaynı seçeneklerde .

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


Bu en iyi cevap! Şununla çalışır: * Dosya adlarındaki boşluklar * Eşleşen dosyalar yok * exitsonuçlar üzerinde döngü yaparken
EM0

Bununla birlikte, tüm olası dosya adlarıyla çalışmaz - bunun için kullanmalısınızreadarray -d '' x < <(find . -name '*.txt' -print0)
Charles Duffy

3

Ben ilk değişken olarak atanan bulmak ve IFS aşağıdaki gibi yeni bir satıra geçti kullanmak istiyorum:

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

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

Aynı DATA setinde daha fazla işlemi tekrarlamak ve sunucunuzda çok yavaş bulmak istiyorsanız (I / 0 yüksek kullanım)


2

Döndürülen dosya adlarını aşağıdaki findgibi bir diziye koyabilirsiniz :

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

Artık tek tek öğelere erişmek ve onlarla ne istersen yapmak için dizi arasında gezinebilirsin.

Not: Beyaz alan güvenlidir.


1
Bash 4.4 veya üstü ile loop: yerine tek bir komut kullanabilirsiniz mapfile -t -d '' array < <(find ...). Ayar IFSgerekli değildir mapfile.
Socowi

1

fd # 3 kullanarak diğer cevaplara ve @phk yorumlarına dayanarak:
(hala döngü içinde stdin kullanımına izin verir)

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

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

-1

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

Böylece dosyalar listelenir ve öznitelikler hakkında ayrıntılar verilir.


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.