Korzystanie z pętli, takiej jak
for i in `find . -name \*.txt`
pęknie, jeśli niektóre nazwy plików mają spacje.
Jakiej techniki mogę użyć, aby uniknąć tego problemu?
Korzystanie z pętli, takiej jak
for i in `find . -name \*.txt`
pęknie, jeśli niektóre nazwy plików mają spacje.
Jakiej techniki mogę użyć, aby uniknąć tego problemu?
Odpowiedzi:
Idealnie nie robisz tego w ogóle, ponieważ prawidłowe analizowanie nazw plików w skrypcie powłoki jest zawsze trudne (napraw to spacje, nadal będziesz mieć problemy z innymi osadzonymi znakami, w szczególności nową linią). Jest to nawet wymienione jako pierwszy wpis na stronie BashPitfalls.
To powiedziawszy, istnieje sposób, aby prawie zrobić to, co chcesz:
oIFS=$IFS
IFS=$'\n'
find . -name '*.txt' | while read -r i; do
# use "$i" with whatever you're doing
done
IFS=$oIFS
Pamiętaj, aby cytować także $i
podczas korzystania z niego, aby uniknąć innych interpretacji spacji później. Pamiętaj również, aby $IFS
zrezygnować po jego użyciu, ponieważ nieprzestrzeganie tego spowoduje później oszałamiające błędy.
Ma to jeszcze jedno zastrzeżenie: to, co dzieje się w while
pętli, może mieć miejsce w podpowłoce, w zależności od dokładnie używanej powłoki, więc ustawienia zmiennych mogą nie zostać zachowane. Wersja for
pętli unika tego, ale za cenę, która nawet jeśli zastosujesz $IFS
rozwiązanie w celu uniknięcia problemów ze spacjami, będziesz mieć kłopoty, jeśli find
zwróci zbyt wiele plików.
W pewnym momencie poprawną poprawką do tego wszystkiego staje się robienie tego w języku takim jak Perl lub Python zamiast powłoki.
Użyj find -print0
i potokuj go xargs -0
lub napisz własny mały program w języku C i potokuj go do małego programu w języku C. Po to -print0
i -0
wymyślono.
Skrypty powłoki nie są najlepszym sposobem na obsługę nazw plików ze spacjami: możesz to zrobić, ale robi się nieporęcznie.
Możesz ustawić „separator pola wewnętrznego” ( IFS
) na coś innego niż miejsce na dzielenie argumentów pętli, np
ORIGIFS=${IFS}
NL='
'
IFS=${NL}
for i in $(find . -name '*.txt'); do
IFS=${ORIGIFS}
#do stuff
done
IFS=${ORIGIFS}
Resetuję IFS
po użyciu w find, głównie dlatego, że chyba ładnie wygląda. Nie widziałem żadnych problemów z ustawieniem go na nową linię, ale myślę, że jest to „czystsze”.
Inną metodą, zależnie od tego, co chcesz zrobić z danymi wyjściowymi find
, jest albo bezpośrednie użycie -exec
z find
poleceniem, albo użycie -print0
i wpięcie do niego xargs -0
. W pierwszym przypadku find
dba o ucieczkę nazwy pliku. W takim -print0
przypadku find
wypisuje dane wyjściowe z separatorem zerowym, a następnie xargs
dzieli na to. Ponieważ żadna nazwa pliku nie może zawierać tego znaku (o czym wiem), jest to również zawsze bezpieczne. Jest to szczególnie przydatne w prostych przypadkach; i zwykle nie jest doskonałym zamiennikiem pełnej for
pętli.
find -print0
zxargs -0
Używanie w find -print0
połączeniu z xargs -0
jest całkowicie odporne na legalne nazwy plików i jest jedną z najbardziej rozszerzalnych dostępnych metod. Załóżmy na przykład, że chcesz wyświetlić listę wszystkich plików PDF w bieżącym katalogu. Mógłbyś pisać
$ find . -iname '*.pdf' -print0 | xargs -0 -n 1 echo
Znajduje każdy plik PDF (przez -iname '*.pdf'
) w bieżącym katalogu ( .
) i dowolnym podkatalogu i przekazuje każdy z nich jako argument do echo
polecenia. Ponieważ podaliśmy tę -n 1
opcję, xargs
przekażemy tylko jeden argument na raz echo
. Gdybyśmy pominęli tę opcję, xargs
przekazalibyśmy jej jak najwięcej echo
. (Możesz echo short input | xargs --show-limits
zobaczyć, ile bajtów jest dozwolonych w wierszu poleceń).
xargs
dokładnie robi ?Możemy wyraźnie zobaczyć, jaki wpływ xargs
ma na jego wejście - aw szczególności efekt -n
- za pomocą skryptu, który powtarza swoje argumenty w sposób bardziej precyzyjny niż echo
.
$ cat > echoArgs.sh <<'EOF'
#!/bin/bash
echo "Number of arguments: $#"
[[ $# -eq 0 ]] && exit
for i in $(seq 1 $#); do
echo "Arg $i: <$1>"
shift
done
EOF
$ find . -iname '*.pdf' -print0 | xargs -0 ./echoArgs.sh
$ find . -iname '*.pdf' -print0 | xargs -0 -n 1 ./echoArgs.sh
Pamiętaj, że doskonale radzi sobie ze spacjami i znakami nowej linii,
$ touch 'A space-age
new line of vending machines.pdf'
$ find . -iname '*space*' -print0 | xargs -0 -n 1 ./echoArgs.sh
co byłoby szczególnie kłopotliwe w przypadku następującego wspólnego rozwiązania:
chmod +x ./echoArgs.sh
for file in $(ls *spacey*); do
./echoArgs.sh "$file"
done
Notatki
Nie zgadzam się z bash
podstawkami, ponieważ bash
wraz z zestawem narzędzi * nix jest dość biegły w obsłudze plików (w tym tych, których nazwy mają osadzone białe znaki).
W rzeczywistości find
daje ci drobną kontrolę nad wyborem plików do przetworzenia ... Po stronie bash naprawdę musisz tylko zdać sobie sprawę, że musisz zrobić łańcuchy bash words
; zazwyczaj przy użyciu „podwójnych cudzysłowów” lub innego mechanizmu, takiego jak IFS lub find's{}
Zauważ, że w większości / wielu sytuacjach nie musisz ustawiać i resetować IFS; po prostu użyj IFS lokalnie, jak pokazano w poniższych przykładach. Wszystkie trzy dobrze trzymają białe znaki. Także nie trzeba „standard” strukturę pętli, ponieważ znaleźć na \;
to skutecznie pętla; wystarczy umieścić logikę pętli w funkcji bash (jeśli nie wywołujesz standardowego narzędzia).
IFS=$'\n' find ~/ -name '*.txt' -exec function-or-util {} \;
I jeszcze dwa przykłady
IFS=$'\n' find ~/ -name '*.txt' -exec printf 'Hello %s\n' {} \;
IFS=$'\n' find ~/ -name '*.txt' -exec echo {} \+ |sed 's/home//'
'znajdź also allows you to pass multiple filenames as args to you script ..(if it suits your need: use
+ instead
\; `)
find -print0
ixargs -0
.