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 plikihello
iworld
. - 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ć?
bash
shell
shell-script
quoting
whitespace
Gilles
źródło
źródło
shellcheck
pomagają poprawić jakość twoich programów.Odpowiedzi:
Zawsze należy używać w cudzysłowie zmiennych podstawienia i podstawień komend:
"$foo"
,"$(foo)"
Jeśli użyjesz bez
$foo
cudzysł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ąread
wbudowanego, użyjwhile IFS= read -r line; do …
Plain specjalnie
read
traktuje ukośniki odwrotne i białe znaki specjalnie.xargs
- Unikajxargs
. Jeśli musisz użyćxargs
, zrób toxargs -0
. Zamiastfind … | xargs
, woląfind … -exec …
.xargs
traktuje 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?$foo
nie oznacza „weź wartość zmiennejfoo
”. Oznacza coś znacznie bardziej złożonego:foo * bar
to wynikiem tego etapu jest lista 3-elementowafoo
,*
,bar
.foo
, następnie listę plików w bieżącym katalogu i na końcubar
. Jeśli bieżący katalog jest pusty, wynik jestfoo
,*
,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, zmiennamyfiles
zawiera 5-znakowy ciąg znaków*.txt
i wtedy, gdy piszesz$myfiles
, symbol wieloznaczny jest rozwijany. Ten przykład będzie działał, dopóki nie zmienisz skryptu namyfiles="$someprefix*.txt"; … process $myfiles
. Jeślisomeprefix
jest ustawiony nafinal 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.
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 ustawiaszset
i które są lokalne dla funkcji: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ść zmiennejf
w „bezpieczny” sposób odwoływania się do tego samego pliku, od którego na pewno nie zaczniesz-
.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.
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ć.
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.
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ć
eval
wbudowanego.Zwróć uwagę na zagnieżdżone cudzysłowy w definicji
code
: pojedyncze cudzysłowy'…'
ograniczają literał ciąg, tak że wartością zmiennejcode
jest ciąg/path/to/executable --option --message="hello world" -- /path/to/file1
.eval
Wbudowane 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
eval
jest 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ś jakcode="$code $filename"
przerw, jeśli nazwa pliku zawiera żadnej powłoki znak specjalny (spacje,$
,;
,|
,<
,>
, itd.).code="$code \"$filename\""
wciąż się załamuje"$\`
. Nawetcode="$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
'\''
.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ę.
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
,read
pozwala wierszy kontynuacji - jest to pojedyncza linia wejścia logiczne:read
dzieli linię wejściową na pola rozdzielone znakami w$IFS
(bez-r
odwrotnego ukośnika również je zastępuje). Na przykład, jeśli wejście jest linią zawierającą trzy słowa, wówczasread first second third
ustawiafirst
się na pierwsze słowo wejścia,second
na drugie słowo ithird
na 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
IFS
pustego ł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
xargs
to 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 -L1
lubxargs -l
są 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 -0
tam, 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żyjfind … -print0
(lub możesz użyć,find … -exec …
jak wyjaśniono poniżej).Jak przetwarzać znalezione pliki
find
?some_command
musi 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ńsh
jawnie.Mam inne pytanie
Przejrzyj cytat na tej stronie, powłokę lub skrypt powłoki . (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 .
źródło
$(( ... ))
(także$[...]
w niektórych powłokach) opróczzsh
(w emulacji sh) imksh
.xargs -0
to nie jest POSIX. Z wyjątkiem FreeBSDxargs
, na ogół chceszxargs -r0
zamiastxargs -0
.ls --quoting-style=shell-always
nie jest kompatybilny zxargs
. Spróbujtouch $'a\nb'; ls --quoting-style=shell-always | xargs
xargs -d "\n"
to, że możesz uruchomić np. Wlocate PATTERN1 |xargs -d "\n" grep PATTERN2
celu 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
Chociaż odpowiedź Gillesa jest doskonała, w jego głównym punkcie zastanawiam się
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
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.
źródło
foo=$bar
jest OK, aleexport foo=$bar
czyenv foo=$var
nie 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ć .criteria="-type f"
,find . $criteria
działa, alefind . "$criteria"
nie działa.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:zaakceptuj dane wejściowe
zinterpretuj i podziel poprawnie na tokenizowane słowa wejściowe
słowa wejściowe są elementami składni powłoki, takimi jak
$word
lubecho $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
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 .a następnie wykonać wynikowe polecenie
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
exec
nimi. Większość powłok nieNUL
radzi sobie dobrze z bajtem - jeśli w ogóle - a to dlatego, że już się na nim dzielą. Powłoka maexec
wiele do zrobienia i musi to zrobić zNUL
ograniczoną tablicą argumentów, które przekazuje do jądra systemu w danymexec
momencie. 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
$IFS
pojawia się.$IFS
Jest 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ć.$IFS
rozszczepia 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$IFS
zNUL
, 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$IFS
tablicą 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ść."
$IFS
IFS=
O ile nie jest cytowany,
$IFS
sam jest$IFS
ograniczonym 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ść$IFS
jest 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.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ć.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 ...
... 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:... 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:
... wyniki dla obu argumentów są identyczne -
*
w tym przypadku nie rozwija się.źródło
IFS
naprawdę działa. Co ja nie dostać to, dlaczego to zawsze dobry pomysł, aby ustawićIFS
się do czegoś innego niż domyślny.$IFS
.cd /usr/bin; set -f; IFS=/; for path_component in $PWD; do echo $path_component; done
Wydruki\n
następnieusr\n
potembin\n
. Pierwszyecho
jest 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. wawk
każdym razie ludzie robią to w / przez cały czas. twoja skorupa też to robiMiałem duży projekt wideo ze spacjami w nazwach plików i spacjami w nazwach katalogów. Chociaż
find -type f -print0 | xargs -0
dział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: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:
i odpowiednio:
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.)źródło
zsh
, możesz używaćIFS=$'\0'
i używać-print0
(zsh
nie robi globowania po rozszerzeniach, więc znaki globu nie stanowią problemu).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.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!źródło