Dlaczego mój skrypt powłoki dusi się spacjami lub innymi znakami specjalnymi?

284

Lub przewodnik wprowadzający do solidnej obsługi nazw plików i innych ciągów znaków przekazywanych w skryptach powłoki.

Napisałem skrypt powłoki, który działa dobrze przez większość czasu. Ale dusi się na niektórych danych wejściowych (np. Na niektórych nazwach plików).

Wystąpił problem, taki jak:

  • Mam nazwę pliku zawierającą spację hello world, która została potraktowana jako dwa osobne pliki helloi world.
  • Mam wiersz wejściowy z dwiema kolejnymi spacjami, które skurczyły się do jednego na wejściu.
  • Wiodące i końcowe białe znaki znikają z linii wejściowych.
  • Czasami, gdy dane wejściowe zawierają jeden ze znaków \[*?, są one zastępowane przez tekst, który w rzeczywistości jest nazwą plików.
  • W danych wejściowych znajduje się apostrof '(lub podwójny cytat ") i po tym punkcie wszystko stało się dziwne.
  • Wejście zawiera ukośnik odwrotny (lub: Używam Cygwin, a niektóre z moich nazw plików mają \separatory w stylu Windows ).

Co się dzieje i jak to naprawić?

Gilles
źródło
16
shellcheckpomagają poprawić jakość twoich programów.
aurelien
3
Oprócz technik ochronnych opisanych w odpowiedziach i chociaż jest to prawdopodobnie oczywiste dla większości czytelników, myślę, że warto skomentować, że gdy pliki mają być przetwarzane za pomocą narzędzi wiersza polecenia, dobrą praktyką jest unikanie fantazyjnych znaków w nazwiska w pierwszej kolejności, jeśli to możliwe.
bli
1
@bli Nie, to sprawia, że ​​tylko błędy pojawiają się dłużej. Ukrywa dziś błędy. A teraz nie znasz wszystkich nazw plików używanych później z twoim kodem.
Volker Siegel,
Po pierwsze, jeśli parametry zawierają spacje, należy je wpisać (w wierszu poleceń). Możesz jednak pobrać całą linię poleceń i parsować ją samodzielnie. Dwa pola nie zamieniają się w jedno pole; dowolna ilość miejsca informuje skrypt, który jest następną zmienną, więc jeśli wykonasz coś w rodzaju „echo 1 $ 2 $”, to twój skrypt umieszcza jedną spację między nimi. Użyj również „find (-exec)”, aby iterować pliki ze spacjami zamiast pętli for; łatwiej poradzisz sobie z przestrzenią.
Patrick Taylor

Odpowiedzi:

352

Zawsze należy używać w cudzysłowie zmiennych podstawienia i podstawień komend: "$foo","$(foo)"

Jeśli użyjesz bez $foocudzysłowu, twój skrypt będzie dławił się na danych wejściowych lub parametrach (lub danych wyjściowych polecenia, z $(foo)) zawierających białe znaki lub \[*?.

Tam możesz przestać czytać. Cóż, ok, oto kilka innych:

  • read- Aby czytać wiersz po wierszu za pomocą readwbudowanego, użyjwhile IFS= read -r line; do …
    Plain specjalnie readtraktuje ukośniki odwrotne i białe znaki specjalnie.
  • xargs- Unikajxargs . Jeśli musisz użyć xargs, zrób to xargs -0. Zamiast find … | xargs, woląfind … -exec … .
    xargstraktuje specjalnie białe znaki i znaki \"'.

Ta odpowiedź odnosi się do powłoki Bourne'a / POSIX-style ( sh, ash, dash, bash, ksh, mksh, yash...). Użytkownicy Zsh powinni go pominąć i przeczytać koniec Kiedy konieczne jest podwójne cytowanie? zamiast. Jeśli chcesz uzyskać cały drobiazg, przeczytaj standard lub instrukcję obsługi swojej powłoki.


Zauważ, że poniższe objaśnienia zawierają kilka przybliżeń (stwierdzenia, które są prawdziwe w większości warunków, ale może mieć na nie wpływ otaczający kontekst lub konfiguracja).

Dlaczego muszę pisać "$foo"? Co stanie się bez cytatów?

$foonie oznacza „weź wartość zmiennej foo”. Oznacza coś znacznie bardziej złożonego:

  • Najpierw weź wartość zmiennej.
  • Podział pól: potraktuj tę wartość jako rozdzieloną spacjami listę pól i utwórz wynikową listę. Na przykład, jeśli zmienna zawiera foo * bar ​to wynikiem tego etapu jest lista 3-elementowa foo, *, bar.
  • Generowanie nazw plików: traktuj każde pole jako glob, tj. Jako wzór wieloznaczny i zastąp je listą nazw plików pasujących do tego wzorca. Jeśli wzorzec nie pasuje do żadnego pliku, pozostaje niezmodyfikowany. W naszym przykładzie powoduje to listę zawierającą foo, następnie listę plików w bieżącym katalogu i na końcu bar. Jeśli bieżący katalog jest pusty, wynik jest foo, *, bar.

Zauważ, że wynikiem jest lista ciągów znaków. Składnia powłoki ma dwa konteksty: kontekst listy i kontekst łańcucha. Dzielenie pól i generowanie nazw plików odbywa się tylko w kontekście listy, ale to przez większość czasu. Podwójne cudzysłowy ograniczają kontekst łańcucha: cały ciąg cudzysłowu jest pojedynczym ciągiem, którego nie można dzielić. (Wyjątek: "$@"przejście do listy parametrów pozycyjnych, np. "$@"Jest równoważne, "$1" "$2" "$3"jeśli istnieją trzy parametry pozycyjne. Zobacz Jaka jest różnica między $ * a $ @? )

To samo dzieje się z zastępowaniem poleceń za pomocą $(foo)lub za pomocą `foo`. Na marginesie: nie używaj `foo`: jego reguły cytowania są dziwne i nieprzenośne, a wszystkie nowoczesne powłoki $(foo)są całkowicie równoważne, z wyjątkiem intuicyjnych reguł cytowania.

Wynik podstawienia arytmetycznego również podlega tym samym rozszerzeniom, ale zwykle nie stanowi to problemu, ponieważ zawiera tylko nierozwijalne znaki (zakładając, IFSże nie zawiera cyfr lub -).

Zobacz Kiedy konieczne jest podwójne cytowanie? po więcej szczegółów na temat przypadków, w których można pominąć cytaty.

O ile nie masz na myśli, że to wszystko się wydarzy, pamiętaj tylko, aby zawsze używać podwójnych cudzysłowów wokół podstawień zmiennych i poleceń. Uważaj: pomijanie cytatów może prowadzić nie tylko do błędów, ale i do luk w zabezpieczeniach .

Jak przetwarzać listę nazw plików?

Jeśli piszesz myfiles="file1 file2", ze spacjami do oddzielania plików, nie może to działać z nazwami plików zawierającymi spacje. Nazwy plików uniksowych mogą zawierać dowolny znak inny niż /(który zawsze jest separatorem katalogu) i bajty zerowe (których nie można używać w skryptach powłoki z większością powłok).

Ten sam problem z myfiles=*.txt; … process $myfiles. Kiedy to zrobisz, zmienna myfileszawiera 5-znakowy ciąg znaków *.txti wtedy, gdy piszesz $myfiles, symbol wieloznaczny jest rozwijany. Ten przykład będzie działał, dopóki nie zmienisz skryptu na myfiles="$someprefix*.txt"; … process $myfiles. Jeśli someprefixjest ustawiony na final report, to nie zadziała.

Aby przetworzyć dowolną listę (np. Nazwy plików), umieść ją w tablicy. Wymaga to mksh, ksh93, yash lub bash (lub zsh, który nie ma wszystkich tych problemów z cytowaniem); zwykła powłoka POSIX (taka jak ash lub dash) nie ma zmiennych tablicowych.

myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"

Ksh88 ma zmienne tablicowe z inną składnią przypisania set -A myfiles "someprefix"*.txt(patrz zmienna przypisania w innym środowisku ksh, jeśli potrzebujesz przenośności ksh88 / bash). Powłoki typu Bourne / POSIX mają pojedynczą tablicę, tablicę parametrów pozycyjnych, "$@"które ustawiasz seti które są lokalne dla funkcji:

set -- "$someprefix"*.txt
process -- "$@"

Co z nazwami plików, które zaczynają się od -?

W powiązanej notatce należy pamiętać, że nazwy plików mogą zaczynać się od -(myślnik / minus), co większość poleceń interpretuje jako oznaczające opcję. Jeśli masz nazwę pliku, która zaczyna się od części zmiennej, pamiętaj, aby podać --ją przedtem, tak jak we fragmencie powyżej. Wskazuje to komendzie, że osiągnęła koniec opcji, więc cokolwiek po tym jest nazwą pliku, nawet jeśli zaczyna się od -.

Możesz też upewnić się, że nazwy plików zaczynają się od znaku innego niż -. Bezwzględne nazwy plików rozpoczynają się od /, a można je dodawać ./na początku nazw względnych. Poniższy fragment kodu zmienia zawartość zmiennej fw „bezpieczny” sposób odwoływania się do tego samego pliku, od którego na pewno nie zaczniesz -.

case "$f" in -*) "f=./$f";; esac

Na ostatnią uwagę na ten temat, uważaj, że niektóre polecenia interpretują -jako standardowe wejście lub wyjście standardowe, nawet po --. Jeśli potrzebujesz odwołać się do rzeczywistego pliku o nazwie -lub jeśli wywołujesz taki program i nie chcesz, aby czytał ze standardowego wejścia lub zapisywał na standardowe wyjście, pamiętaj, aby przepisać -jak wyżej. Zobacz Jaka jest różnica między „du -sh *” a „du -sh ./*”? do dalszej dyskusji.

Jak przechowywać polecenie w zmiennej?

„Polecenie” może oznaczać trzy rzeczy: nazwę polecenia (nazwę jako plik wykonywalny, z pełną ścieżką lub bez, lub nazwę funkcji, wbudowanego lub aliasu), nazwę polecenia z argumentami lub fragment kodu powłoki. Istnieją odpowiednio różne sposoby przechowywania ich w zmiennej.

Jeśli masz nazwę polecenia, po prostu zapisz go i jak zwykle używaj zmiennej z podwójnymi cudzysłowami.

command_path="$1"

"$command_path" --option --message="hello world"

Jeśli masz polecenie z argumentami, problem jest taki sam jak w przypadku listy nazw plików powyżej: jest to lista ciągów, a nie ciąg. Nie możesz po prostu upchnąć argumentów w pojedynczy ciąg znaków ze spacjami między nimi, ponieważ jeśli to zrobisz, nie będziesz w stanie odróżnić spacji będących częścią argumentów od spacji oddzielających argumenty. Jeśli twoja powłoka ma tablice, możesz ich użyć.

cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"

Co jeśli używasz powłoki bez tablic? Nadal możesz używać parametrów pozycyjnych, jeśli nie masz nic przeciwko ich modyfikowaniu.

set -- /path/to/executable --option --message="hello world" --
set -- "$@" "$file1" "$file2"
"$@"

Co jeśli musisz przechowywać złożone polecenie powłoki, np. Z przekierowaniami, potokami itp.? A jeśli nie chcesz modyfikować parametrów pozycyjnych? Następnie możesz zbudować ciąg zawierający polecenie i użyć evalwbudowanego.

code='/path/to/executable --option --message="hello world" -- /path/to/file1 | grep "interesting stuff"'
eval "$code"

Zwróć uwagę na zagnieżdżone cudzysłowy w definicji code: pojedyncze cudzysłowy '…'ograniczają literał ciąg, tak że wartością zmiennej codejest ciąg /path/to/executable --option --message="hello world" -- /path/to/file1. evalWbudowane mówi powłoce do analizowania ciąg przekazany jako argument jakby wyglądał w skrypcie, więc w tym momencie cytaty i rura są analizowane, itd.

Korzystanie evaljest trudne. Zastanów się dokładnie, co zostanie przeanalizowane, kiedy. W szczególności nie możesz po prostu umieścić nazwy pliku w kodzie: musisz go zacytować, tak jak w przypadku pliku kodu źródłowego. Nie ma na to bezpośredniego sposobu. Coś jak code="$code $filename"przerw, jeśli nazwa pliku zawiera żadnej powłoki znak specjalny (spacje, $, ;, |, <, >, itd.). code="$code \"$filename\""wciąż się załamuje "$\`. Nawet code="$code '$filename'"psuje się, jeśli nazwa pliku zawiera '. Istnieją dwa rozwiązania.

  • Dodaj warstwę cytatów wokół nazwy pliku. Najłatwiej to zrobić, dodając pojedyncze cudzysłowy i zastępując je pojedynczymi '\''.

    quoted_filename=$(printf %s. "$filename" | sed "s/'/'\\\\''/g")
    code="$code '${quoted_filename%.}'"
  • Zachowaj rozszerzenie zmiennej w kodzie, aby było sprawdzane podczas oceny kodu, a nie po zbudowaniu fragmentu kodu. Jest to prostsze, ale działa tylko wtedy, gdy zmienna wciąż ma tę samą wartość w czasie wykonywania kodu, nie np. Jeśli kod jest wbudowany w pętlę.

    code="$code \"\$filename\""

Wreszcie, czy naprawdę potrzebujesz zmiennej zawierającej kod? Najbardziej naturalnym sposobem nadania nazwy blokowi kodu jest zdefiniowanie funkcji.

Co się dzieje z read?

Bez -r, readpozwala wierszy kontynuacji - jest to pojedyncza linia wejścia logiczne:

hello \
world

readdzieli linię wejściową na pola rozdzielone znakami w $IFS(bez -rodwrotnego ukośnika również je zastępuje). Na przykład, jeśli wejście jest linią zawierającą trzy słowa, wówczas read first second thirdustawia firstsię na pierwsze słowo wejścia, secondna drugie słowo i thirdna trzecie słowo. Jeśli jest więcej słów, ostatnia zmienna zawiera wszystko, co pozostało po ustawieniu poprzednich. Wiodące i końcowe białe znaki są przycinane.

Ustawienie IFSpustego łańcucha zapobiega przycinaniu. Zobacz, dlaczego tak często używa się `while IFS = read` zamiast` IFS =; podczas czytania ... dla dłuższego wyjaśnienia.

Co jest nie tak z xargs?

Format wejściowy xargsto ciągi rozdzielone spacjami, które mogą być opcjonalnie jedno- lub podwójnie cudzysłowione. Żadne standardowe narzędzie nie wyświetla tego formatu.

Dane wejściowe do xargs -L1lub xargs -lsą prawie listą linii, ale nie do końca - jeśli na końcu linii znajduje się spacja, następna linia jest linią kontynuacji.

Możesz użyć xargs -0tam, gdzie ma to zastosowanie (i jeśli jest dostępne: GNU (Linux, Cygwin), BusyBox, BSD, OSX, ale nie ma go w POSIX). Jest to bezpieczne, ponieważ bajty zerowe nie mogą pojawiać się w większości danych, w szczególności w nazwach plików. Aby utworzyć listę nazw plików rozdzieloną znakiem null, użyj find … -print0(lub możesz użyć, find … -exec …jak wyjaśniono poniżej).

Jak przetwarzać znalezione pliki find?

find  -exec some_command a_parameter another_parameter {} +

some_commandmusi być poleceniem zewnętrznym, nie może być funkcją powłoki ani aliasem. Jeśli potrzebujesz wywołać powłokę w celu przetworzenia plików, zadzwoń shjawnie.

find  -exec sh -c '
  for x do
    … # process the file "$x"
  done
' find-sh {} +

Mam inne pytanie

Przejrzyj na tej stronie, lub . (Kliknij „dowiedz się więcej…”, aby zobaczyć kilka ogólnych wskazówek i ręcznie wybraną listę często zadawanych pytań.) Jeśli szukałeś i nie możesz znaleźć odpowiedzi, zapytaj .

Gilles
źródło
6
@ John1024 To tylko funkcja GNU, więc pozostanę przy „braku standardowego narzędzia”.
Gilles
2
Potrzebujesz także cudzysłowów $(( ... ))(także $[...]w niektórych powłokach) oprócz zsh(w emulacji sh) i mksh.
Stéphane Chazelas
3
Zauważ, że xargs -0to nie jest POSIX. Z wyjątkiem FreeBSD xargs, na ogół chcesz xargs -r0zamiast xargs -0.
Stéphane Chazelas
2
@ John1024, nie, ls --quoting-style=shell-alwaysnie jest kompatybilny z xargs. Spróbujtouch $'a\nb'; ls --quoting-style=shell-always | xargs
Stéphane Chazelas
3
Kolejną przyjemną funkcją (tylko GNU) jest xargs -d "\n"to, że możesz uruchomić np. W locate PATTERN1 |xargs -d "\n" grep PATTERN2celu wyszukania nazw plików pasujących do PATTERN1 z zawartością pasującą do PATTERN2 . Bez GNU możesz to zrobić np. Jaklocate PATTERN1 |perl -pne 's/\n/\0/' |xargs -0 grep PATTERN1
Adam Katz
26

Chociaż odpowiedź Gillesa jest doskonała, w jego głównym punkcie zastanawiam się

Zawsze używaj podwójnych cudzysłowów wokół podstawień zmiennych i podstawień poleceń: „$ foo”, „$ (foo)”

Kiedy zaczynasz od powłoki podobnej do Basha, która dzieli słowa, tak, bezpieczną radą są zawsze cudzysłowy. Jednak podział słów nie zawsze jest wykonywany

§ Podział słów

Te polecenia można uruchamiać bez błędów

foo=$bar
bar=$(a command)
logfile=$logdir/foo-$(date +%Y%m%d)
PATH=/usr/local/bin:$PATH ./myscript
case $foo in bar) echo bar ;; baz) echo baz ;; esac

Nie zachęcam użytkowników do przyjęcia takiego zachowania, ale jeśli ktoś dobrze rozumie, kiedy dochodzi do dzielenia słów, powinien sam móc zdecydować, kiedy stosować cytaty.

Steven Penny
źródło
19
Jak wspomniałem w mojej odpowiedzi, zobacz unix.stackexchange.com/questions/68694/... w celu uzyskania szczegółowych informacji. Zwróć uwagę na pytanie: „Dlaczego mój skrypt powłoki dusi się?”. Najczęstszym problemem (z lat doświadczeń na tej stronie i poza nią) jest brak podwójnych cytatów. „Zawsze używaj podwójnych cudzysłowów” jest łatwiejsze do zapamiętania niż „zawsze używaj podwójnych cudzysłowów, z wyjątkiem przypadków, w których nie są one konieczne”.
Gilles
14
Zasady są trudne do zrozumienia dla początkujących. Na przykład, foo=$barjest OK, ale export foo=$barczy env foo=$varnie są (przynajmniej w niektórych muszli). Rada dla początkujących: zawsze podawaj swoje zmienne, chyba że wiesz, co robisz i masz dobry powód, aby tego nie robić .
Stéphane Chazelas
5
@StevenPenny Czy to naprawdę bardziej poprawne? Czy istnieją uzasadnione przypadki, w których cytaty złamałyby skrypt? W sytuacjach, w których w połowie przypadków należy użyć cudzysłowu , a w drugiej połówce można zastosować opcjonalnie - wtedy zalecenie „zawsze używaj cudzysłowów, na wszelki wypadek” to takie, o którym należy pomyśleć, ponieważ jest to prawdziwe, proste i mniej ryzykowne. Nauczanie takich list wyjątków dla początkujących jest powszechnie znane jako nieefektywne (bez kontekstu, nie będą ich pamiętać) i przynoszące efekt przeciwny do zamierzonego, ponieważ pomieszają potrzebne / niepotrzebne cytaty, łamią skrypty i demotywują je do dalszej nauki.
Peteris
6
Moje 0,02 $ byłoby takie, że zalecanie cytowania wszystkiego jest dobrą radą. Błędne cytowanie czegoś, czego nie potrzebuje, jest nieszkodliwe, błędne nieumiejętność cytowania czegoś, co tego potrzebuje, jest szkodliwe. Tak więc dla większości autorów skryptów powłoki, którzy nigdy nie zrozumieją zawiłości, kiedy dokładnie zachodzi dzielenie słów, cytowanie wszystkiego jest znacznie bezpieczniejsze niż próba cytowania tylko w razie potrzeby.
godlygeek
5
@Peteris i godlygeek: „Czy istnieją uzasadnione przypadki, w których cytaty złamałyby skrypt?” To zależy od twojej definicji „rozsądnego”. Jeśli skrypt jest ustawiony criteria="-type f", find . $criteriadziała, ale find . "$criteria"nie działa.
G-Man,
22

O ile mi wiadomo, są tylko dwa przypadki, w których konieczne jest podwójne cudzysłowy, i przypadki te obejmują dwa specjalne parametry powłoki "$@"i "$*"- które są określone, aby rozwijały się inaczej, gdy są ujęte w cudzysłowy. We wszystkich innych przypadkach (z wyjątkiem być może implementacji tablic specyficznych dla powłoki) zachowanie ekspansji jest konfigurowalne - istnieją na to opcje.

Nie oznacza to oczywiście, że należy unikać podwójnego cytowania - wręcz przeciwnie, jest to prawdopodobnie najbardziej dogodna i niezawodna metoda ograniczania rozszerzenia, które ma do zaoferowania powłoka. Ale myślę, że ponieważ alternatywy zostały już fachowo wyjaśnione, jest to doskonałe miejsce do dyskusji na temat tego, co dzieje się, gdy powłoka powiększa wartość.

Powłoka, w jego sercu i duszy (dla tych, którzy mają takie) , to interpreter poleceń - to parser, jak wielkie, interaktywne sed. Jeśli instrukcja powłoki dusi się na białych znakach lub w podobnych miejscach, jest to bardzo prawdopodobne, ponieważ nie w pełni zrozumiałeś proces interpretacji powłoki - szczególnie jak i dlaczego tłumaczy instrukcję wejściową na polecenie, które można wykonać. Zadaniem powłoki jest:

  1. zaakceptuj dane wejściowe

  2. zinterpretuj i podziel poprawnie na tokenizowane słowa wejściowe

    • słowa wejściowe są elementami składni powłoki, takimi jak $wordlubecho $words 3 4* 5

    • słowa są zawsze dzielone na białe znaki - to tylko składnia - ale tylko dosłowne znaki białych znaków podawane w powłoce w pliku wejściowym

  3. w razie potrzeby rozwiń je w wiele pól

    • pola wynikają z rozwinięć słów - stanowią one ostateczne polecenie wykonywalne

    • wyjątkiem "$@", $IFS field-rozszczepienie , a ekspansja ścieżka wejście słowo należy zawsze oceniać na jednym polu .

  4. a następnie wykonać wynikowe polecenie

    • w większości przypadków wiąże się to z przekazaniem wyników jego interpretacji w takiej czy innej formie

Ludzie często mówią, że powłoka jest klejem , a jeśli to prawda, to przykleja się do list argumentów - lub pól - do jednego lub drugiego procesu, gdy są one execnimi. Większość powłok nie NULradzi sobie dobrze z bajtem - jeśli w ogóle - a to dlatego, że już się na nim dzielą. Powłoka ma exec wiele do zrobienia i musi to zrobić z NULograniczoną tablicą argumentów, które przekazuje do jądra systemu w danym execmomencie. Jeśli miałbyś przełączyć ogranicznik powłoki z jej ograniczonymi danymi, to prawdopodobnie skorupa by to zepsuła. Wewnętrzne struktury danych - jak większość programów - polegają na tym ograniczniku. zsh, w szczególności nie psuje tego.

I tu właśnie $IFSpojawia się. $IFSJest zawsze obecnym - i podobnie ustawialnym - parametrem powłoki, który określa, w jaki sposób powłoka powinna dzielić rozwinięcia powłoki od słowa do pola - w szczególności na jakie wartości te pola powinny ograniczać. $IFSrozszczepia rozszerzenia powłoki na ograniczniki inne niż NUL- lub, innymi słowy, zamienniki powłoki bajtów wynikające z rozszerzania, które pasują do tych wartości $IFSz NUL, w jego wewnętrznych elementów zastosowaniem macierzy. Kiedy spojrzysz na to w ten sposób, możesz zacząć widzieć, że każde rozszerzenie powłoki podzielone na pola jest $IFStablicą danych -delimitowaną.

Ważne jest, aby zrozumieć, że ograniczają$IFS tylko rozszerzenia, które nie są jeszcze ograniczone w inny sposób - co można zrobić za pomocą podwójnych cudzysłowów. Cytując rozwinięcie, ograniczasz je na czele, a przynajmniej na końcu jego wartości. W takich przypadkach nie ma zastosowania, ponieważ nie ma pól do rozdzielenia. W rzeczywistości rozwinięcie podwójnego cudzysłowu wykazuje identyczne zachowanie dzielenia pola, co rozwinięcie niecytowane, gdy jest ustawione na pustą wartość."$IFSIFS=

O ile nie jest cytowany, $IFSsam jest $IFSograniczonym rozszerzeniem powłoki. Domyślnie jest to określona wartość <space><tab><newline>- z których wszystkie trzy wykazują specjalne właściwości, jeśli są zawarte $IFS. Podczas gdy każda inna wartość $IFSjest podana w celu oceny do pojedynczego pola na wystąpienie ekspansji , biała $IFS spacja - dowolna z tych trzech - jest określona, ​​aby eluować do pojedynczego pola na sekwencję ekspansji , a sekwencje wiodące / końcowe są całkowicie pomijane. Prawdopodobnie najłatwiej to zrozumieć na przykładzie.

slashes=///// spaces='     '
IFS=/; printf '<%s>' $slashes$spaces
<><><><><><     >
IFS=' '; printf '<%s>' $slashes$spaces
</////>
IFS=; printf '<%s>' $slashes$spaces
</////     >
unset IFS; printf '<%s>' "$slashes$spaces"
</////     >

Ale to tylko $IFS- tylko dzielenie słów lub białe znaki, jak pytano, więc co ze znakami specjalnymi ?

Powłoka - domyślnie - rozszerzy również niektóre niecytowane tokeny (takie ?*[jak wspomniano tutaj w innym miejscu) na wiele pól, gdy występują na liście. Nazywa się to rozszerzaniem nazw ścieżek lub globowaniem . Jest to niezwykle przydatne narzędzie, a ponieważ występuje po podzieleniu pól w kolejności analizowania powłoki, nie wpływa na nią IFS - pola generowane przez rozwinięcie nazwy ścieżki są rozdzielane na początku / na końcu samych nazw plików, niezależnie od tego, czy ich zawartość zawiera obecnie dowolne znaki $IFS. To zachowanie jest domyślnie włączone, ale w przeciwnym razie można je bardzo łatwo skonfigurować.

set -f

To instruuje powłokę, aby nie glob . Rozwinięcie nazwy ścieżki nie nastąpi przynajmniej dopóki to ustawienie nie zostanie w jakiś sposób cofnięte - na przykład jeśli bieżąca powłoka zostanie zastąpiona innym nowym procesem powłoki lub ...

set +f

... jest wydawany do powłoki. Podwójne cudzysłowy - podobnie jak w przypadku $IFS dzielenia pól - sprawiają, że to ustawienie globalne staje się niepotrzebne na rozwinięcie. Więc:

echo "*" *

... jeśli rozszerzenie ścieżki jest obecnie włączone, prawdopodobnie wygeneruje bardzo różne wyniki dla każdego argumentu - ponieważ pierwszy rozwinie się tylko do jego dosłownej wartości (pojedyncza gwiazdka, to znaczy wcale), a drugi tylko do tego samego jeśli bieżący katalog roboczy nie zawiera nazw plików, które mogłyby pasować (i pasuje do prawie wszystkich z nich) . Jeśli jednak to zrobisz:

set -f; echo "*" *

... wyniki dla obu argumentów są identyczne - *w tym przypadku nie rozwija się.

mikeserv
źródło
Właściwie zgadzam się z @ StéphaneChazelas, że (głównie) myli rzeczy bardziej niż pomaganie ... ale uważam to za pomocne, osobiście, więc wziąłem głos. Mam teraz lepszy pomysł (i kilka przykładów), jak to IFSnaprawdę działa. Co ja nie dostać to, dlaczego to zawsze dobry pomysł, aby ustawić IFSsię do czegoś innego niż domyślny.
Wildcard,
1
@Wildcard - to separator pola. jeśli masz wartość w zmiennej, którą chcesz rozwinąć do wielu pól, na których ją podzielisz $IFS. cd /usr/bin; set -f; IFS=/; for path_component in $PWD; do echo $path_component; doneWydruki \nnastępnie usr\npotem bin\n. Pierwszy echojest pusty, ponieważ /jest pustym polem. Ścieżka_komponenty może zawierać znaki nowej linii, spacje lub cokolwiek innego - nie ma znaczenia, ponieważ komponenty zostały podzielone, /a nie wartość domyślna. w awkkażdym razie ludzie robią to w / przez cały czas. twoja skorupa też to robi
mikeserv
3

Miałem duży projekt wideo ze spacjami w nazwach plików i spacjami w nazwach katalogów. Chociaż find -type f -print0 | xargs -0działa dla wielu celów i w różnych powłokach, stwierdzam, że użycie niestandardowego IFS (separatora pól wejściowych) daje większą elastyczność, jeśli używasz bash. Poniższy fragment używa bash i ustawia IFS tylko na nową linię; pod warunkiem, że w nazwach plików nie ma nowych linii:

(IFS=$'\n'; for i in $(find -type f -print) ; do
    echo ">>>$i<<<"
done)

Zwróć uwagę na użycie parens w celu wyizolowania redefinicji IFS. Czytałem inne posty o tym, jak odzyskać IFS, ale jest to po prostu łatwiejsze.

Więcej, ustawienie IFS na nowy wiersz pozwala wcześniej ustawić zmienne powłoki i łatwo je wydrukować. Na przykład mogę wyhodować zmienną V przyrostowo, używając nowych linii jako separatorów:

V=""
V="./Ralphie's Camcorder/STREAM/00123.MTS,04:58,05:52,-vf yadif"
V="$V"$'\n'"./Ralphie's Camcorder/STREAM/00111.MTS,00:00,59:59,-vf yadif"
V="$V"$'\n'"next item goes here..."

i odpowiednio:

(IFS=$'\n'; for v in $V ; do
    echo ">>>$v<<<"
done)

Teraz mogę „wymienić” ustawienie V za echo "$V"pomocą podwójnych cudzysłowów, aby wyprowadzić znaki nowego wiersza. (Podziękowania dla tego wątku dla $'\n'wyjaśnienia.)

Russ
źródło
3
Ale nadal będziesz mieć problemy z nazwami plików zawierającymi znaki nowej linii lub glob. Zobacz także: Dlaczego zapętlanie wyników wyszukiwania jest złą praktyką? . Jeśli używasz zsh, możesz używać IFS=$'\0'i używać -print0( zshnie robi globowania po rozszerzeniach, więc znaki globu nie stanowią problemu).
Stéphane Chazelas
1
Działa to z nazwami plików zawierającymi spacje, ale nie działa przeciwko potencjalnie wrogim nazwom plików lub przypadkowym „nonsensownym” nazwom plików. Dodając, możesz łatwo rozwiązać problem nazw plików zawierających znaki wieloznaczne set -f. Z drugiej strony, twoje podejście zasadniczo zawodzi w przypadku nazw plików zawierających znaki nowej linii. W przypadku danych innych niż nazwy plików nie działa również z pustymi elementami.
Gilles
Tak, moim zastrzeżeniem jest to, że nie będzie działać z nowymi liniami w nazwach plików. Uważam jednak, że musimy wytyczyć granicę po prostu szaleństwem ;-)
Russ
I nie jestem pewien, dlaczego otrzymano opinię negatywną. Jest to całkowicie rozsądna metoda iteracji nazw plików ze spacjami. Użycie -print0 wymaga xargs i są rzeczy, które są trudne w użyciu tego łańcucha. Przykro mi, że ktoś nie zgadza się z moją odpowiedzią, ale to nie jest powód, by głosować za nią.
Russ
0

Biorąc pod uwagę wszystkie implikacje bezpieczeństwa wspomniane powyżej oraz zakładając, że ufasz i masz kontrolę nad zmiennymi, które rozwijasz, możesz mieć wiele ścieżek z białymi spacjami eval. Ale bądź ostrożny!

$ FILES='"a b" c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
$ FILES='a\ b c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
Mattias Wadman
źródło