2011-08-12 14 views
153

Chcę iterować listę plików. Lista ta jest wynikiem polecenia find, więc wymyśliłem:Powtórzenie listy plików ze spacjami

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

Jest w porządku z wyjątkiem, jeśli plik zawiera spacje w nazwie:

$ 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 

Co mogę zrobić, aby uniknąć rozłamu w przestrzeni?

Odpowiedz

177

Można zastąpić słowo iteracji opartych o linię na bazie jednego:

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

Jest to bardzo czysty. I sprawia, że ​​czuję się lepiej niż zmiana IFS w połączeniu z pętlą for. – Derrick

+13

Spowoduje to podzielenie ścieżki pliku, która zawiera \ n. OK, te nie powinny być w pobliżu, ale można je utworzyć: 'touch" $ (printf "foo \ nbar") "' –

+2

@OllieSaunders: zawsze coś jest! –

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

Zobacz man xargs.

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

na marginesie, to zadziała tylko wtedy, gdy chcesz wykonać polecenie. Wbudowana powłoka nie działa w ten sposób. – Alex

141

Istnieje kilka wykonalnych sposobów, aby tego dokonać.

Jeśli chciał ściśle trzymać się pierwotnej wersji można to zrobić w ten sposób:

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

ten będzie nadal się niepowodzeniem, jeśli nazwy plików mają dosłowne znaki nowej linii w nich, ale obowiązuje go nie złamie.

Jednak zakłócanie komunikacji z IFS nie jest konieczne. Oto mój preferowany sposób to zrobić:

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

Jeśli okaże składnia < <(command) zaznajomiony powinieneś przeczytać o process substitution. Zaletą tego ponad for file in $(find ...) jest poprawne przetwarzanie plików ze spacjami, znakami nowej linii i innymi znakami. Działa to, ponieważ find z -print0 użyje null (alias \0) jako terminatora dla każdej nazwy pliku i, w przeciwieństwie do nowej linii, null nie jest prawnym znakiem w nazwie pliku.

Zaletą tego na niemal równoważnej wersji

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

jest, że każda zmienna przydzieleniem ciała pętli jest zachowana. To znaczy, jeśli podłączysz się do while jak wyżej, korpus while znajduje się w podpowłoce, która może nie być tym, czego potrzebujesz.

Zaletą wersji zastępującej proces nad jest minimalna: Wersja xargs jest w porządku, jeśli wystarczy wydrukować linię lub wykonać pojedynczą operację na pliku, ale jeśli trzeba wykonać wiele kroków, wersja pętli jest łatwiej.

EDIT: Oto miły skrypt testowy, dzięki czemu można uzyskać wyobrażenie o różnicy pomiędzy różnymi próbami rozwiązania tego problemu

#!/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

Przyjęto twoją odpowiedź: najbardziej kompletną i interesującą - nie wiedziałem o składni '$ IFS' i' <<(cmd) '. Jeszcze jedno pozostaje dla mnie niejasne, dlaczego '$' in '$ '\ 0''? Wielkie dzięki. – gregseth

+0

@gregseth: To jest składnia bash dla literalnej postaci escape. Na przykład, jeśli powiesz CTRL + V, a następnie naciśnij TAB, wstawisz literalną kartę. Nie będzie to jednak wyglądało poprawnie, gdy zostanie skopiowane i wklejone w innym miejscu, ale składnia '$ '\ t'' zostanie oceniona jako karta i działa w ten sam sposób. Jest to po prostu wygodny sposób przekazywania określonych postaci do komend bez martwienia się o ich przekręcanie. – Sorpigal

+0

Ok. Z moim pierwszym postem nie myślałem, aby uzyskać tak wiele odpowiedzi na moje nieformalne pytania;) - Jeszcze raz dziękuję. – gregseth

24

Istnieje również bardzo proste rozwiązanie: polegać na bash globbing

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

Należy zauważyć, że nie jestem pewien, czy to zachowanie jest domyślne, ale nie widzę żadnego specjalnego ustawienia w moim sklepie, więc chciałbym powiedzieć, że powinno być "bezpieczne" (testowane na osx i ubuntu).

+1

Składnia '" $ {plik} "działa cuda dla mnie. –

+3

Najlepsza odpowiedź! Wydaje się to o wiele bardziej zwięzłe niż inne próby wielu poleceń. –

+3

Prosty i czysty. Dałbym dwie przegrane, jeśli bym mógł. –

6

Ponieważ nie robią żadnego innego rodzaju filtrowanie z find, można użyć następujących dzień bash 4.0:

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

**/ pasuje zero lub więcej katalogów, więc pełny wzór będzie pasował foo* w bieżącym katalogu lub dowolnych podkatalogach.

0

W niektórych przypadkach tutaj, jeśli wystarczy skopiować lub przenieść listę plików, można również przesłać tę listę do awk.
Ważne wokół pola $0 (w skrócie Twoje pliki, jedna lista liniowa = jeden plik).

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

bardzo podoba mi do pętli i macierzy iteracji, więc postać dodam tę odpowiedź do mieszanki ...

Lubiłem też głupi przykład plik marchelbling użytkownika. :)

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

wewnątrz katalogu testu:

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

to dodaje każdy plik z listą linii do tablicy bash nazwie arr z każdej nowej linii spływu usunięte.

Powiedzmy chcemy dać te pliki lepszych nazw ...

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

$ {! Arr [@]} rozszerza się do 0 1 2 tak "$ {arr [$ i]}" jest i th element tablicy. Cytaty dotyczące zmiennych są ważne dla zachowania przestrzeni.

Rezultatem jest trzy zmienionych nazwach plików:

$ ls -1 
smarter_file1 
smarter_file2 
smarter_file_3 
Powiązane problemy