Boşluklu dosyaların bir listesi üzerinde yineleme


202

Bir dosya listesi üzerinden yineleme yapmak istiyorum. Bu liste bir findkomutun sonucudur , bu yüzden geldim:

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

Bir dosyanın adında boşluk olması dışında sorun yoktur:

$ 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

Boşluklarda bölünmeyi önlemek için ne yapabilirim?


Yanıtlar:


253

Sözcük tabanlı yinelemeyi satır tabanlı bir yinelemeyle değiştirebilirsiniz:

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

31
Bu son derece temiz. Ve bir for döngüsü ile birlikte IFS değiştirmekten daha iyi hissetmemi sağlıyor
Derrick

15
Bu, \ n içeren tek bir dosya yolunu böler. Tamam, bunlar etrafta olmamalı ama yaratılabilirler:touch "$(printf "foo\nbar")"
Ollie Saunders

4
Girişin (ters eğik çizgiler, öndeki ve sondaki boşluk) yorumlanmasını önlemek için IFS= while read -r fkullanın.
mklement0

2
Bu yanıtfind ve bir while döngüsünün daha güvenli bir kombinasyonunu gösterir .
moi

5
Bariz işaret gibi görünüyor, ama neredeyse tüm basit vakalarda, -execaçık bir döngü daha temiz olacak: find . -iname "foo*" -exec echo "File found: {}" \;. Ayrıca, birçok durumda , tek komutta çok sayıda dosya koymak için bunu en son \;ile değiştirebilirsiniz +.
naught101

153

Bunu başarmanın birkaç uygulanabilir yolu vardır.

Orijinal sürümünüze yakından bağlı kalmak istiyorsanız, bu şekilde yapılabilir:

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

Dosya adlarında gerçekte yeni satırlar varsa bu yine de başarısız olur, ancak boşluklar bunu kırmaz.

Ancak, IFS ile uğraşmak gerekli değildir. İşte bunu yapmak için tercih ettiğim yol:

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

< <(command)Sözdizimini tanıdık bulmazsanız, işlem ikamesi hakkında bilgi okumalısınız . Bunun avantajı, for file in $(find ...)boşluklu, satırsonu ve diğer karakterlere sahip dosyaların doğru şekilde işlenmesidir. Bunun nedeni çalışır findile -print0bir kullanacaktır null(aka \0yeni satır aksine, her bir dosya adı için terminatör gibi) ve null bir dosya adında yasal bir karakter değildir.

Neredeyse eşdeğer versiyona göre bunun avantajı

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

While döngüsünün gövdesinde herhangi bir değişken ataması korunur mu? Yani, whileyukarıdaki gibi boru yaparsanız , gövdesi whilebir alt kabuktadır ve bu da istediğiniz şey olmayabilir.

İşlem ikame sürümünün avantajı find ... -print0 | xargs -0asgari düzeydedir: xargsİhtiyacınız olan tek şey dosyaya bir satır yazdırmak veya tek bir işlem gerçekleştirmekse, ancak çok sayıda adım gerçekleştirmeniz gerekiyorsa döngü sürümü daha kolaysa, sürüm iyidir.

EDIT : İşte güzel bir test komut dosyası, böylece bu sorunu çözmek için farklı girişimler arasındaki fark hakkında bir fikir edinebilirsiniz

#!/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
Cevabınızı kabul ettim: en eksiksiz ve ilginç - bilmiyordum $IFSve < <(cmd)sözdizimi. Hala bir şey kalıntıları neden bana gizlemek $içinde $'\0'? Çok teşekkürler.
gregseth

2
+1, ancak while IFS= readboşlukla başlayan veya biten dosyaları işlemek için ... ... eklemelisiniz .
Gordon Davisson

1
Proses ikame çözeltisine bir uyarı vardır. Döngünün içinde herhangi bir isteminiz varsa (veya STDIN'den başka bir şekilde okuyorsanız), giriş döngüye beslediğiniz şeyler tarafından doldurulur. (belki de cevaba eklenmesi gerekir?)
13'te

2
@uvsmtid: Bu soru etiketlendi, bashbu yüzden bash'a özgü özellikleri kullanarak güvende hissettim. İşlem ikamesi diğer kabuklara taşınabilir değildir (sh'in kendisinin böyle önemli bir güncelleme alması muhtemel değildir).
sorpigal

2
Birleştiren IFS=$'\n'ile forbu yaklaşım tamamen sağlam değil bu yüzden (siz de ilk globbing kapatmazsanız) hala önler hat-içi sözcük bölme, ancak, globbing için çıkan çizgiler konuyu yapar. Çalışırken read -d $'\0', $'\0'NUL oluşturmak için kullanabileceğinizi düşündürmesi biraz yanıltıcıdır - yapamazsınız: ANSI C alıntılı bir dizedeki a \0, dizeyi etkili bir şekilde sonlandırır , böylece -d $'\0'etkili bir şekilde aynıdır -d ''.
mklement0

29

Ayrıca çok basit bir çözüm var: bas globbing'e güvenin

$ 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'

Bu davranışın varsayılan olduğundan emin değilim ama benim gidip "güvenli" (osx ve ubuntu üzerinde test edilmiş) gerektiğini söylemek benim shopt herhangi bir özel ayar görmüyorum unutmayın.


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

6
yan not olarak, bu yalnızca bir komut yürütmek istiyorsanız çalışır. Bir kabuk yerleşimi bu şekilde çalışmaz.
Alex

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

Bkz man xargs.


6

Başka bir tür filtreleme yapmadığınızdan find, bash4.0 itibarıyla aşağıdakileri kullanabilirsiniz :

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

**/Tam desen maç olacak, böylece sıfır veya daha fazla dizinleri maç olacak foo*geçerli dizinden veya altdizinlere.


3

Döngüler ve dizi yinelemesi için gerçekten seviyorum, bu yüzden bu cevabı karışıma ekleyeceğim ...

Ayrıca marchelbling'in aptal dosya örneğini de beğendim. :)

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

Test dizininin içinde:

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

Bu, her dosya listeleme satırını, arrsondaki yeni satır kaldırılmış olarak adlandırılan bir bash dizisine ekler .

Diyelim ki bu dosyalara daha iyi adlar vermek istiyoruz ...

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

$ {! dizi [@]} "$ {dizi [$ i]}" i için 0 1 ila 2 genişler inci dizi elemanı. Değişkenlerin etrafındaki alıntılar boşlukları korumak için önemlidir.

Sonuç üç yeniden adlandırılmış dosyadır:

$ ls -1
smarter_file1
smarter_file2
smarter_file_3

2

find-execsonuçları bulmak için döngü oluşturan ve rasgele bir komut yürüten bir argüman var. Örneğin:

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

Burada {}bulunan dosyaları temsil eder ve içine sarmak "", sonuçtaki kabuk komutunun dosya adındaki boşluklarla uğraşmasına izin verir.

Birçok durumda, sonuncuyu \;(yeni bir komut başlatan) bir komutla değiştirebilirsiniz \+, bu da bir komutta birden fazla dosya koyacaktır (bir anda hepsinin birden olması gerekmez, man finddaha fazla ayrıntı için bkz .).


0

Bazı durumlarda, burada bir dosya listesini kopyalamanız veya taşımanız gerekiyorsa, bu listeyi awk için de bağlayabilirsiniz. Alanın çevresinde
önemli (kısacası dosyalarınız, bir satır listesi = bir dosya).\"" "\"$0

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

0

Tamam - Stack Overflow'daki ilk yazım!

Bununla ilgili sorunlarım her zaman csh içinde olmasına rağmen sunduğum çözüm bash değil, eminim, her ikisinde de çalışacaktır. Sorun kabuk "ls" döner yorumlanması ile. Biz sadece *joker kabuk genişletme kullanarak "ls" sorundan kaldırabilirsiniz - ama geçerli (veya belirtilen klasör) hiçbir dosya yoksa "eşleşme" hatası verir - bu sorunu çözmek için nokta dosyalarını içerecek şekilde genişletme: * .*- bu her zaman dosyalardan beri sonuç verir. ve .. daima mevcut olacak. Yani csh biz bu yapı kullanabilirsiniz ...

foreach file (* .*)
   echo $file
end

standart nokta dosyalarını filtrelemek istiyorsanız o zaman bu kadar kolay ...

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

Bu konudaki ilk yazının kodu şöyle yazılır: -

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

Bu yardımcı olur umarım!


0

İş için başka bir çözüm ...

Amaç:

  • dizinlerde dosya adlarını tekrar tekrar seçme / filtreleme
  • her isimleri (yoldaki boşluk ne olursa olsun ...)
#!/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}


Yapıcı sözler için teşekkürler, ama: 1- bu gerçek bir problem, 2- kabuk zaman içinde evrilmiş olabilir ... varsaydığım herkes gibi; 3- Yukarıdaki yanıtların hiçbiri, sorunu değiştirmeden veya tez değiştirmeden pb'nin DOĞRUDAN bir çözünürlüğünü karşılayamaz :-)
Vince B
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.