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 to xargs -0
. Zamiast find … | 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ść 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 myfiles
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 na myfiles="$someprefix*.txt"; … process $myfiles
. Jeśli someprefix
jest 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 set
i 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 f
w „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ć eval
wbudowanego.
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 code
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ś 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
, read
pozwala wierszy kontynuacji - jest to pojedyncza linia wejścia logiczne:
hello \
world
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ówczas read first second third
ustawia first
się na pierwsze słowo wejścia, second
na drugie słowo i third
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
lub xargs -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ż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_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.
find … -exec sh -c '
for x do
… # process the file "$x"
done
' find-sh {} +
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 .
shellcheck
pomagają poprawić jakość twoich programów.