Skutki bezpieczeństwa dla zapomnienia o cytowaniu zmiennej w powłokach bash / POSIX

206

Jeśli od jakiegoś czasu obserwujesz unix.stackexchange.com, powinieneś wiedzieć, że pozostawienie zmiennej niecytowanej w kontekście listy (jak w echo $var) w powłokach Bourne / POSIX (wyjątek to zsh) ma bardzo szczególne znaczenie i nie należy tego robić, chyba że masz ku temu dobry powód.

To omówiono obszernie w wielu Q & A tutaj (przykłady: ? Dlaczego mój skrypt powłoki zadławić spacji lub innych znaków specjalnych , kiedy to dwukrotnie powołując konieczne? , Ekspansja zmiennej powłoki i efekt glob i podzielona na nim , Cytowany vs niecytowane rozwinięcie łańcucha)

Tak było od pierwszego wydania powłoki Bourne'a pod koniec lat 70. i nie została zmieniona przez powłokę Korna (jedno z największych żalów Davida Korna (pytanie nr 7) ) lub bashktóra w większości skopiowała powłokę Korna, i to jest jak to zostało określone przez POSIX / Unix.

Teraz nadal widzimy tutaj wiele odpowiedzi, a nawet czasami publicznie wydawany kod powłoki, w którym zmienne nie są cytowane. Można by pomyśleć, że ludzie już by się nauczyli.

Z mojego doświadczenia wynika, że ​​są 3 typy ludzi, którzy pomijają cytowanie swoich zmiennych:

  • początkujący. Można to usprawiedliwić, ponieważ co prawda jest to całkowicie nieintuicyjna składnia. Naszą rolą na tej stronie jest ich edukowanie.

  • zapominalscy ludzie.

  • ludzie, którzy nie są przekonani nawet po wielokrotnym wbijaniu, którzy myślą, że z pewnością autor powłoki Bourne'a nie zamierzał cytować wszystkich naszych zmiennych .

Może uda nam się ich przekonać, jeśli ujawnimy ryzyko związane z tego rodzaju zachowaniami.

Co jest najgorsze niż może się zdarzyć, jeśli zapomnisz podać swoje zmienne. Czy to naprawdę takie złe?

O jakiej podatności tutaj mówimy?

W jakich kontekstach może to stanowić problem?

Stéphane Chazelas
źródło
8
Myślę , że BashPitfalls spodoba ci się.
pawel7318,
link
zwrotny
5
Chciałbym zasugerować dodanie czwartej grupy: osób, które zbyt wiele razy zostały trafione w głowę za nadmierne cytowanie, być może przez członków trzeciej grupy wyładowujących frustrację na innych (ofiara staje się tyranem). Smutne jest oczywiście to, że ci z czwartej grupy mogą w końcu nie cytować rzeczy, kiedy ma to największe znaczenie.
ack

Odpowiedzi:

201

Preambuła

Po pierwsze, powiedziałbym, że nie jest to właściwy sposób rozwiązania problemu. To trochę jak powiedzenie „ nie powinieneś mordować ludzi, bo inaczej pójdziesz do więzienia ”.

Podobnie nie podajesz swojej zmiennej, ponieważ w przeciwnym razie wprowadzasz luki w zabezpieczeniach. Cytujesz swoje zmienne, ponieważ błędem jest nie (ale jeśli strach przed więzieniem może pomóc, dlaczego nie).

Małe podsumowanie dla tych, którzy właśnie wskoczyli do pociągu.

W większości powłok pozostawienie cudzysłowu zmiennego (choć to (i reszta tej odpowiedzi) dotyczy także podstawiania poleceń ( `...`lub $(...)) i rozszerzania arytmetycznego ( $((...))lub $[...])) ma bardzo szczególne znaczenie. Najlepszym sposobem na opisanie tego jest to, że przypomina to wywoływanie jakiegoś domyślnego operatora split + glob operator¹.

cmd $var

w innym języku byłoby napisane coś takiego:

cmd(glob(split($var)))

$varjest najpierw dzielony na listę słów zgodnie ze złożonymi regułami obejmującymi $IFSspecjalny parametr ( część podzielona ), a następnie każde słowo powstałe w wyniku tego podziału jest uważane za wzorzec, który jest rozwijany do listy pasujących plików ( część globalna ) .

Na przykład, jeśli $varzawiera *.txt,/var/*.xmli $IFS zawiera ,, cmdzostanie wywołany z wieloma argumentami, z których pierwszy cmdto txt pliki, a następne to pliki w bieżącym katalogu i xmlpliki w /var.

Jeśli chcesz zadzwonić cmdza pomocą dwóch dosłownych argumentów cmd i *.txt,/var/*.xmlpiszesz:

cmd "$var"

który byłby w twoim innym, bardziej znanym języku:

cmd($var)

Co rozumiemy przez podatność w powłoce ?

W końcu od samego początku wiadomo, że skryptów powłoki nie należy używać w kontekstach wrażliwych pod względem bezpieczeństwa. Z pewnością OK pozostawienie zmiennej bez cudzysłowu jest błędem, ale to nie może wyrządzić tyle szkody, prawda?

Cóż, pomimo faktu, że ktoś powiedziałby ci, że skrypty powłoki nigdy nie powinny być używane do internetowych interfejsów graficznych, lub że na szczęście większość systemów nie pozwala obecnie na skrypty powłoki setuid / setgid, jedną z rzeczy, które shellshock (zdalnie wykorzystywany błąd bash, który spowodował nagłówki z września 2014 r.) ujawniły, że powłoki są nadal szeroko stosowane tam, gdzie prawdopodobnie nie powinny: w CGI, w skryptach przechwytujących klienta DHCP, w poleceniach sudoers, wywoływanych przez (jeśli nie jako ) polecenia setuid ...

Czasem nieświadomie. Na przykład system('cmd $PATH_INFO') w skrypcie php/ perl/ pythonCGI wywołuje powłokę, aby zinterpretować ten wiersz poleceń (nie wspominając o tym, że cmdsam może być skryptem powłoki, a jego autor nigdy nie spodziewał się, że zostanie wywołany z CGI).

Masz lukę, gdy istnieje ścieżka do eskalacji uprawnień, to znaczy, gdy ktoś (nazwijmy go atakującym ) jest w stanie zrobić coś, do czego nie jest przeznaczony.

Niezmiennie oznacza to, że atakujący dostarcza dane, które są przetwarzane przez uprzywilejowanego użytkownika / proces, który przypadkowo robi coś, czego nie powinien robić, w większości przypadków z powodu błędu.

Zasadniczo masz problem, gdy Twój błędny kod przetwarza dane pod kontrolą atakującego .

Teraz nie zawsze jest oczywiste, skąd pochodzą te dane , i często trudno jest stwierdzić, czy Twój kod kiedykolwiek przetworzy niezaufane dane.

Jeśli chodzi o zmienne, w przypadku skryptu CGI jest dość oczywiste, że dane to parametry GET / POST CGI i parametry takie jak pliki cookie, ścieżka, parametry hosta ...

W przypadku skryptu setuid (uruchamianego jako jeden użytkownik, gdy jest wywoływany przez innego), są to argumenty lub zmienne środowiskowe.

Innym bardzo częstym wektorem są nazwy plików. Jeśli otrzymujesz listę plików z katalogu, możliwe, że atakujący umieścił tam pliki .

W tym względzie, nawet po zachęcie interaktywnej powłoki, możesz być narażony (np. Podczas przetwarzania plików w /tmplub ~/tmp na przykład).

Nawet a ~/.bashrcmoże być podatny na atak (na przykład bashzinterpretuje go, gdy zostanie wywołany, sshaby uruchomić ForcedCommand podobnie jak we gitwdrożeniach serwera z niektórymi zmiennymi pod kontrolą klienta).

Teraz skrypt nie może być wywoływany bezpośrednio w celu przetwarzania niezaufanych danych, ale może być wywoływany przez inne polecenie, które to robi. Albo twój niepoprawny kod może zostać wklejony do skryptów, które to robią (przez ciebie 3 lata później lub jeden z twoich kolegów). Jednym z miejsc, w którym jest to szczególnie ważne, są odpowiedzi na stronach z pytaniami i odpowiedziami, ponieważ nigdy nie wiadomo, gdzie mogą znaleźć się kopie kodu.

W dół do biznesu; jak bardzo jest źle?

Pozostawienie cudzysłowu zmiennej (lub podstawienia polecenia) jest zdecydowanie najważniejszym źródłem luk w zabezpieczeniach związanych z kodem powłoki. Częściowo dlatego, że te błędy często przekładają się na luki w zabezpieczeniach, ale także dlatego, że tak często można zobaczyć niecytowane zmienne.

W rzeczywistości, szukając luk w kodzie powłoki, pierwszą rzeczą do zrobienia jest poszukiwanie niecytowanych zmiennych. Jest łatwa do wykrycia, często jest dobrym kandydatem, na ogół łatwa do prześledzenia do danych kontrolowanych przez osobę atakującą.

Istnieje nieskończona liczba sposobów, w jakie niecytowana zmienna może przekształcić się w podatność na atak. Podam tylko kilka wspólnych trendów.

Ujawnienie informacji

Większość ludzi wpadnie na błędy związane z niecytowanymi zmiennymi ze względu na podzieloną część (na przykład często pliki mają spacje w swoich nazwach, a spacja ma domyślną wartość IFS). Wiele osób przeoczy część globalną . Część glob jest co najmniej tak samo niebezpieczna jak część podzielona .

Globowanie wykonywane na nieautoryzowanych wejściach zewnętrznych oznacza, że osoba atakująca może zmusić Cię do przeczytania zawartości dowolnego katalogu.

W:

echo You entered: $unsanitised_external_input

jeśli $unsanitised_external_inputzawiera /*, oznacza to, że atakujący może zobaczyć zawartość /. Nie ma sprawy. Staje się bardziej interesująca choć /home/*co daje listę nazw użytkowników na maszynie /tmp/*, /home/*/.forwardna podpowiedzi w innych niebezpiecznych praktyk, /etc/rc*/*dla służb obsługujących ... Nie ma potrzeby, aby wymienić je indywidualnie. Wartość /* /*/* /*/*/*...spowoduje tylko wyświetlenie całego systemu plików.

Luki w zabezpieczeniach typu „odmowa usługi”.

Biorąc poprzednią sprawę trochę za daleko i mamy DoS.

W rzeczywistości każda niecytowana zmienna w kontekście listy z niezaszyfrowanymi danymi wejściowymi stanowi przynajmniej lukę w zabezpieczeniach DoS.

Nawet eksperci od skryptów powłoki często zapominają cytować takie rzeczy jak:

#! /bin/sh -
: ${QUERYSTRING=$1}

:jest poleceniem no-op. Co może pójść nie tak?

Która jest przeznaczona do przypisania $1do $QUERYSTRINGjeśli $QUERYSTRING był wyłączony. To szybki sposób na wywołanie skryptu CGI również z wiersza poleceń.

Jest $QUERYSTRINGto jednak nadal rozwinięte, a ponieważ nie jest cytowane, wywoływany jest operator split + glob .

Obecnie istnieją globusy, których rozbudowa jest szczególnie droga. Ten /*/*/*/*jest wystarczająco zły, ponieważ oznacza wyświetlanie list katalogów do 4 poziomów w dół. Oprócz aktywności dysku i procesora oznacza to przechowywanie dziesiątek tysięcy ścieżek do plików (40 tys. Tutaj na minimalnej maszynie wirtualnej serwera, z czego 10 tys. Katalogów).

Teraz /*/*/*/*/../../../../*/*/*/*oznacza 40k x 10k i /*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*wystarcza, aby rzucić nawet najpotężniejszą maszynę na kolana.

Wypróbuj sam (choć przygotuj się na awarię lub zawieszenie komputera):

a='/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*' sh -c ': ${a=foo}'

Oczywiście, jeśli kod to:

echo $QUERYSTRING > /some/file

Następnie możesz wypełnić dysk.

Wystarczy zrobić wyszukiwania Google na CGI powłoki lub bash cgi lub ksh CGI , a znajdziesz kilka stron, które pokazują, jak napisać CGI w muszli. Zauważ, że połowa z tych, które przetwarzają parametry, jest wrażliwa.

Nawet własny David Korn jest podatny na atak (patrz obsługa plików cookie).

aż do luk w wykonywaniu dowolnego kodu

Wykonanie dowolnego kodu jest najgorszym rodzajem podatności, ponieważ jeśli atakujący może uruchomić dowolne polecenie, nie ma ograniczeń co do tego, co może zrobić.

Na ogół jest to podzielona część, która do nich prowadzi. Podział powoduje przekazanie kilku poleceń do poleceń, gdy tylko jeden jest oczekiwany. Podczas gdy pierwszy z nich zostanie użyty w oczekiwanym kontekście, pozostałe będą w innym kontekście, więc potencjalnie będą interpretowane inaczej. Lepiej z przykładem:

awk -v foo=$external_input '$2 == foo'

Tutaj celem było przypisanie zawartości $external_inputzmiennej powłoki do foo awkzmiennej.

Teraz:

$ external_input='x BEGIN{system("uname")}'
$ awk -v foo=$external_input '$2 == foo'
Linux

Drugie słowo wynikające z podziału $external_input nie jest przypisywane, fooale traktowane jako awkkod (tutaj, które wykonuje dowolne polecenie:) uname.

To przede wszystkim problem dla poleceń, które może wykonywać inne polecenia ( awk, env, sed(GNU jeden) perl, find...), zwłaszcza z wariantów GNU (które akceptują opcje po argumentach). Zdarza się, że nie będzie podejrzewał polecenia, aby móc wykonać jak inni ksh, bashlub zsh„S [lub printf...

for file in *; do
  [ -f $file ] || continue
  something-that-would-be-dangerous-if-$file-were-a-directory
done

Jeśli utworzymy katalog o nazwie x -o yes, test staje się pozytywny, ponieważ oceniamy to wyrażenie warunkowe zupełnie inne.

Co gorsza, jeśli utworzymy plik o nazwie x -a a[0$(uname>&2)] -gt 1, zawierający co najmniej wszystkie implementacje ksh (w tym sh większość komercyjnych uniksów i niektóre BSD), który jest wykonywany, uname ponieważ te powłoki wykonują obliczenia arytmetyczne na numerycznych operatorach porównania [polecenia.

$ touch x 'x -a a[0$(uname>&2)] -gt 1'
$ ksh -c 'for f in *; do [ -f $f ]; done'
Linux

To samo bashdotyczy nazwy pliku takiej jak x -a -v a[0$(uname>&2)].

Oczywiście, jeśli nie mogą uzyskać arbitralnej egzekucji, atakujący może zadowolić się mniejszymi obrażeniami (co może pomóc w uzyskaniu arbitralnej egzekucji). Można użyć dowolnego polecenia, które może zapisywać pliki lub zmieniać uprawnienia, własność lub mieć jakikolwiek efekt główny lub uboczny.

Za pomocą nazw plików można wykonywać różne czynności.

$ touch -- '-R ..'
$ for file in *; do [ -f "$file" ] && chmod +w $file; done

I w końcu ..zapisujesz (rekurencyjnie w GNU chmod).

Skrypty wykonujące automatyczne przetwarzanie plików w miejscach, w których można publicznie zapisywać, /tmpnależy pisać bardzo ostrożnie.

Co powiesz na [ $# -gt 1 ]

Irytuje mnie to. Niektórzy ludzie zadają sobie trud zastanowienia się, czy dane rozszerzenie może być problematyczne, aby zdecydować, czy mogą pominąć cytaty.

To jak mówienie. Hej, wygląda na to, że $#nie może podlegać operatorowi split + glob, zapytajmy powłokę o split + glob . Albo hej, napiszmy niepoprawny kod tylko dlatego, że błąd prawdopodobnie nie zostanie trafiony .

Teraz jak mało prawdopodobne? OK $#(lub $!, $?albo dowolny arytmetyka substytucja) może zawierać tylko cyfry (lub -dla niektórych), więc glob część jest na zewnątrz. Aby część podzielona mogła coś zrobić, wszystko, czego potrzebujemy, to $IFSzawierać cyfry (lub -).

Niektóre powłoki $IFSmogą być dziedziczone ze środowiska, ale jeśli środowisko nie jest bezpieczne, i tak gra się kończy.

Teraz, jeśli napiszesz funkcję taką jak:

my_function() {
  [ $# -eq 2 ] || return
  ...
}

Oznacza to, że zachowanie twojej funkcji zależy od kontekstu, w którym jest ona wywoływana. Innymi słowy, $IFS staje się jednym z danych wejściowych. Ściśle mówiąc, kiedy piszesz dokumentację API dla swojej funkcji, powinna ona wyglądać mniej więcej tak:

# my_function
#   inputs:
#     $1: source directory
#     $2: destination directory
#   $IFS: used to split $#, expected not to contain digits...

Kod wywołujący twoją funkcję musi się upewnić, $IFSże nie zawiera cyfr. Wszystko to dlatego, że nie miałeś ochoty pisać 2 podwójnych znaków cudzysłowu.

Teraz, aby ten [ $# -eq 2 ]błąd mógł stać się podatny na atak, musisz w jakiś sposób uzyskać $IFSkontrolę nad atakującym . Możliwe, że normalnie tak by się nie stało, gdyby atakujący nie wykorzystał innego błędu.

Nie jest to jednak niespotykane. Częstym przypadkiem jest to, że ludzie zapominają o odkażaniu danych przed użyciem ich w wyrażeniach arytmetycznych. Widzieliśmy już powyżej, że może pozwolić na wykonanie dowolnego kodu w niektórych powłokach, ale we wszystkich pozwala atakującemu na podanie dowolnej zmiennej wartości całkowitej.

Na przykład:

n=$(($1 + 1))
if [ $# -gt 2 ]; then
  echo >&2 "Too many arguments"
  exit 1
fi

A przy $1wartościach (IFS=-1234567890)ta ocena arytmetyczna ma efekt uboczny ustawień IFS, a następna [ komenda kończy się niepowodzeniem, co oznacza, że ​​sprawdzanie zbyt wielu argumentów jest pomijane.

Co się stanie, gdy operator split + glob nie zostanie wywołany?

Jest inny przypadek, w którym potrzebne są cudzysłowy wokół zmiennych i innych rozszerzeń: gdy jest on używany jako wzorzec.

[[ $a = $b ]]   # a `ksh` construct also supported by `bash`
case $a in ($b) ...; esac

nie sprawdzaj, czy $ai $bsą takie same (oprócz z zsh), ale czy $apasuje do wzorca w $b. I musisz zacytować, $bjeśli chcesz porównać jako łańcuchy (to samo w "${a#$b}"lub "${a%$b}"lub "${a##*$b*}"gdzie $bnależy je zacytować, jeśli nie należy tego traktować jako wzorzec).

Oznacza to, że [[ $a = $b ]]może zwrócić wartość true w przypadkach, gdy $ajest różna od $b(na przykład, kiedy $ajest anythingi $bjest *) lub może zwrócić false, gdy są identyczne (na przykład, gdy oba są $ai $b[a]).

Czy może to stanowić lukę w zabezpieczeniach? Tak, jak każdy błąd. W tym miejscu atakujący może zmienić logiczny przepływ kodu skryptu i / lub złamać założenia, które przyjmuje twój skrypt. Na przykład z kodem takim jak:

if [[ $1 = $2 ]]; then
   echo >&2 '$1 and $2 cannot be the same or damage will incur'
   exit 1
fi

Atakujący może ominąć czek, przekazując '[a]' '[a]'.

Teraz, jeśli nie stosuje się ani dopasowania wzorca, ani operatora split + glob , jakie jest niebezpieczeństwo pozostawienia zmiennej bez cudzysłowu?

Muszę przyznać, że piszę:

a=$b
case $a in...

Cytowanie nie szkodzi, ale nie jest absolutnie konieczne.

Jednak jednym z efektów ubocznych pomijania cytatów w tych przypadkach (na przykład w odpowiedziach na pytania i odpowiedzi) jest to, że może wysłać niewłaściwą wiadomość do początkujących: że dobrze jest nie cytować zmiennych .

Na przykład mogą zacząć myśleć, że jeśli a=$bjest OK, to export a=$brównież będzie (co nie jest w wielu powłokach, ponieważ jest w argumentach exportpolecenia, więc w kontekście listy) lub env a=$b.

Co zsh?

zshnaprawiono większość tych niezręczności projektowych. W zsh(przynajmniej jeśli nie w sh / tryb emulacji ksh), jeśli chcesz, podział lub globbing lub pasujące do wzorca , musisz poprosić go wyraźnie: $=vardo podziału, a $~varna glob lub za treść zmiennej należy traktować jako wzorzec.

Jednak dzielenie (ale nie globowanie) wciąż odbywa się w sposób dorozumiany po niecytowanym zastąpieniu polecenia (jak w echo $(cmd)).

Ponadto czasami niepożądanym efektem ubocznym nie cytowania zmiennej jest usunięcie pustki . zshZachowanie jest podobne do tego, co można osiągnąć w innych skorup wyłączając globbing całkowicie (z set -f) i dzielenie (z IFS=''). Jeszcze w:

cmd $var

Nie będzie podziału + glob , ale jeśli $varjest pusty, zamiast otrzymać jeden pusty argument, cmdnie otrzyma żadnego argumentu.

Może to powodować błędy (jak oczywiste [ -n $var ]). Może to zepsuć oczekiwania i założenia skryptu i spowodować luki w zabezpieczeniach, ale nie mogę teraz wymyślić niezbyt dalekiego przykładu).

A co kiedy zrobić potrzebujemy podziału + glob operatora?

Tak, zwykle wtedy, gdy chcesz pozostawić zmienną bez cudzysłowu. Ale musisz upewnić się, że odpowiednio dostroiłeś operatorów split i glob przed użyciem. Jeśli chcesz tylko część podzieloną, a nie część globalną (co ma miejsce przez większość czasu), musisz wyłączyć globbing ( set -o noglob/ set -f) i naprawić $IFS. W przeciwnym razie spowodujesz również luki w zabezpieczeniach (jak wspomniany wyżej przykład CGI Davida Korna).

Wniosek

Krótko mówiąc, pozostawienie cudzysłowu w powłoce zmiennej (lub podstawieniu polecenia lub interpretacji arytmetycznej) może być bardzo niebezpieczne, szczególnie, gdy jest wykonywane w niewłaściwych kontekstach, i bardzo trudno jest ustalić, które z tych niewłaściwych kontekstów.

To jeden z powodów, dla których uważa się to za złą praktykę .

Dzięki za przeczytanie do tej pory. Jeśli przejdzie ci przez głowę, nie martw się. Nie można oczekiwać, że wszyscy zrozumieją wszystkie konsekwencje pisania kodu w taki sposób, w jaki go piszą. Dlatego mamy zalecenia dotyczące dobrych praktyk , dzięki czemu można je stosować, niekoniecznie rozumiejąc dlaczego.

(a jeśli nie jest to jeszcze oczywiste, unikaj pisania poufnych kodów w powłokach).

I proszę podać swoje zmienne w odpowiedziach na tej stronie!


¹ W ksh93i pdkshpochodnych interpretacja nawiasów jest również wykonywana, chyba że globbing jest wyłączony (w przypadku ksh93wersji do ksh93u +, nawet gdy braceexpandopcja jest wyłączona).

Stéphane Chazelas
źródło
Zauważ, że z [[, należy podać tylko RHS porównań:if [[ $1 = "$2" ]]; then
mirabilos
2
@mirabilos, tak, ale nie musi LHS nie być cytowany, więc nie ma istotnych powodów, aby nie cytować go tam (jeśli jesteśmy podjąć świadomą decyzję, aby zacytować domyślnie , ponieważ wydaje się być najrozsądniejszym rozwiązaniem ). Zauważ też, że [[ $* = "$var" ]]to nie to samo, [[ "$*" = "$var" ]]jakby pierwszym znakiem $IFSnie było spacja bash(a także mkshjeśli $IFSjest puste, ale w takim przypadku nie jestem pewien, co $*jest równe, czy powinienem zgłosić błąd?).
Stéphane Chazelas,
1
Tak, możesz tam cytować domyślnie. Proszę nie więcej błędów związanych z dzieleniem pól w tej chwili, wciąż muszę naprawić te, o których wiem (od ciebie i innych), zanim będziemy mogli to ponownie ocenić.
mirabilos
2
@Barmar, zakładając, że miałeś na myśli foo='bar; rm *', nie, nie będzie, jednak wyświetli zawartość bieżącego katalogu, który może być uznany za ujawnienie informacji. print $fooin ksh93(gdzie printjest zamiennik dla echotych adresów niektóre z jego wad) ma jednak lukę w iniekcji kodu (na przykład z foo='-f%.0d z[0$(uname>&2)]') (naprawdę potrzebujesz print -r -- "$foo". echo "$foo"jest nadal niepoprawny i nie można go naprawić (choć ogólnie mniej szkodliwy)).
Stéphane Chazelas,
3
Nie jestem ekspertem od bash, ale koduję w nim od ponad dekady. Często używałem cytatów, ale głównie do obsługi osadzonych pustych miejsc. Teraz użyję ich o wiele więcej! Byłoby miło, gdyby ktoś rozwinął tę odpowiedź, aby trochę łatwiej było przyswoić sobie wszystkie dobre punkty. Mam go dużo, ale też bardzo tęskniłem. To już długi post, ale wiem, że jest o wiele więcej do nauki. Dzięki!
Joe
34

[Zainspirowany tą odpowiedzią autorstwa cas .]

Ale co gdyby …?

Ale co, jeśli mój skrypt ustawi zmienną na znaną wartość przed użyciem? W szczególności, co jeśli ustawia zmienną na jedną z dwóch lub więcej możliwych wartości (ale zawsze ustawia ją na coś znanego) i żadna z wartości nie zawiera znaków spacji ani znaków globalnych? Nie jest to bezpieczne, aby używać go bez cudzysłowów w tej sprawie ?

A co, jeśli jedną z możliwych wartości jest pusty ciąg, a ja polegam na „usunięciu opróżnienia”? To znaczy, jeśli zmienna zawiera pusty ciąg, nie chcę uzyskać pustego ciągu w moim poleceniu; Chcę nic nie dostać. Na przykład,

jeśli jakiś warunek
następnie
    ignorecase = "- i"
jeszcze
    ignorecase = ""
fi
                                        # Zauważ, że cytaty w powyższych poleceniach nie są ściśle potrzebne. 
grep $ ignorecase   other_ grep _args

Nie mogę powiedzieć ; to zawiedzie, jeśli jest pusty ciąg.grep "$ignorecase" other_grep_args$ignorecase

Odpowiedź; reakcja; reagowanie; odzew; oddźwięk:

Jak omówiono w drugiej odpowiedzi, to nadal nie powiedzie się, jeśli IFSzawiera a -lub an i. Jeśli IFSupewniłeś się, że nie zawiera żadnych znaków w zmiennej (i jesteś pewien, że twoja zmienna nie zawiera żadnych znaków globalnych), prawdopodobnie jest to bezpieczne.

Istnieje jednak sposób, który jest bezpieczniejszy (choć jest nieco brzydki i nieintuicyjny): użyj ${ignorecase:+"$ignorecase"}. Ze specyfikacji języka poleceń powłoki POSIX , w części  2.6.2 Rozszerzanie parametrów ,

${parameter:+[word]}

    Użyj wartości alternatywnej.   Jeżeli parameterjest nieustawiony lub zerowy, null zastępuje się; w przeciwnym razie rozwinięcie word (lub pusty ciąg, jeśli wordzostanie pominięty) zostanie zastąpione.

Sztuką tutaj, taką jaka jest, jest to, że używamy ignorecasejako parameter i "$ignorecase"jako word. To ${ignorecase:+"$ignorecase"}znaczy

Jeśli $ignorecasejest nieustawione lub zerowe (tj. Puste), null (tzn. Nic nie cytowane ) należy zastąpić; w przeciwnym razie rozszerzenie "$ignorecase"zostanie zastąpione.

To prowadzi nas tam, gdzie chcemy iść: jeśli zmienna jest ustawiona na pusty ciąg, zostanie „usunięta” (całe to zwinięte wyrażenie nic nie da - nawet pusty ciąg), a jeśli zmienna ma wartość inną niż -pustą wartość, otrzymamy tę wartość, podaną.


Ale co gdyby …?

Ale co, jeśli mam zmienną, którą chcę / muszę podzielić na słowa? (W innym przypadku jest tak jak w pierwszym przypadku; mój skrypt ustawił zmienną i jestem pewien, że nie zawiera żadnych znaków globalnych. Ale może zawierać spacje i chcę podzielić je na osobne argumenty w spacji granice.
PS Nadal chcę usunąć opróżnianie.)

Na przykład,

jeśli jakiś warunek
następnie
    kryteria = „- typ f”
jeszcze
    kryteria = „”
fi
jeśli jakiś inny_warunek
następnie
    kryteria = „$ kryteria -mtime +42”
fi
znajdź „$ katalog_początkowy” $ kryteria   inne_ znajdź _args

Odpowiedź; reakcja; reagowanie; odzew; oddźwięk:

Możesz pomyśleć, że jest to przypadek użycia eval.  Nie!   Oprzyj się pokusie, aby nawet pomyśleć o skorzystaniu z evaltego miejsca.

Ponownie, jeśli upewniłeś się, że IFSnie zawiera żadnych znaków w zmiennej (z wyjątkiem spacji, które chcesz uhonorować) i jesteś pewien, że twoja zmienna nie zawiera żadnych znaków globu, to powyższe jest prawdopodobnie bezpieczny.

Ale jeśli używasz bash (lub ksh, zsh lub yash), istnieje bezpieczniejszy sposób: użyj tablicy:

jeśli jakiś warunek
następnie
    kryteria = (- typ f) # Można powiedzieć `kryteria = (" - typ "" f ")`, ale to naprawdę niepotrzebne.
jeszcze
    kryteria = () # Nie używaj cudzysłowów w tym poleceniu!
fi
jeśli jakiś inny_warunek
następnie
    kryteria + = (- mtime +42) # Uwaga: nie `=`, ale ` + =`, aby dodać (dołączyć) do tablicy.
fi
znajdź „$ katalog_początkowy” „$ {kryteria [@]}”   inny_ znajdź _args

Z bash (1) ,

Do dowolnego elementu tablicy można się odwoływać za pomocą . … Jeśli jest lub  , słowo rozszerza się na wszystkich członków . Te indeksy dolne różnią się tylko wtedy, gdy słowo pojawia się w cudzysłowie. Jeśli słowo jest cytowane podwójnie,… rozwija każdy element do osobnego słowa.${name[subscript]}subscript@*name${name[@]}name

"${criteria[@]}"Rozwija się więc do (w powyższym przykładzie) zera, dwóch lub czterech elementów criteriatablicy, z których każdy jest cytowany. W szczególności, jeśli żaden z warunków  s nie jest spełniony, criteriatablica nie ma treści (zgodnie z criteria=()instrukcją) i "${criteria[@]}"ocenia na nic (nawet niewygodny pusty ciąg).


Staje się to szczególnie interesujące i skomplikowane, gdy masz do czynienia z wieloma słowami, z których niektóre są dynamicznymi (użytkownika) danymi wejściowymi, których nie znasz z góry i mogą zawierać spacje lub inne znaki specjalne. Rozważać:

printf „Wpisz nazwę pliku, którego szukasz:”
przeczytaj fname
if ["$ fname"! = ""]
następnie
    kryteria + = (- nazwa „$ fname”)
fi

Uwaga: $fnamejest cytowana przy każdym użyciu. Działa to nawet wtedy, gdy użytkownik wprowadzi coś takiego jak foo barlub foo*"${criteria[@]}"ocenia do -name "foo bar"lub -name "foo*". (Pamiętaj, że każdy element tablicy jest cytowany).

Tablice nie działają we wszystkich powłokach POSIX; tablice to ksh / bash / zsh / yash-ism. Z wyjątkiem… istnieje jedna tablica obsługiwana przez wszystkie powłoki: lista argumentów, alias "$@". Jeśli skończysz z listą argumentów, z którą zostałeś wywołany (np. Skopiowałeś wszystkie „parametry pozycyjne” (argumenty) do zmiennych lub w inny sposób je przetworzyłeś), możesz użyć listy arg jako tablicy:

jeśli jakiś warunek
następnie
    set - -type f # Można powiedzieć `set -" -type "" f "`, ale to naprawdę niepotrzebne.
jeszcze
    zestaw -
fi
jeśli jakiś inny_warunek
następnie
    set - „$ @” -mtime +42
fi
# Podobnie: set - "$ @" -name "$ fname"
znajdź „$ katalog_początkowy” „$ @”   inny_ znajdź _args

"$@"Konstrukt (co historycznie było pierwsze) ma taką samą semantykę jak - rozszerza każdy argument (czyli każdy element listy argumentów) do osobnego słowa, jakby były wpisywane ."${name[@]}""$1" "$2" "$3" …

Fragment specyfikacji języka poleceń powłoki POSIX , w 2.5.2 Parametry specjalne ,

@

    Rozwija się do parametrów pozycyjnych, zaczynając od jednego, początkowo wytwarzając jedno pole dla każdego ustawionego parametru pozycyjnego. … Początkowe pola należy zachować jako oddzielne pola,… Jeśli nie ma parametrów pozycyjnych, rozwinięcie @pola spowoduje wygenerowanie zerowych pól, nawet jeśli @zawiera się w cudzysłowach; …

Pełny tekst jest nieco tajemniczy; kluczową kwestią jest to, że określa, że "$@"ma generować zero pól, gdy nie ma parametrów pozycyjnych. Uwaga historyczna: kiedy po "$@"raz pierwszy wprowadzono go w powłoce Bourne'a (poprzednik bash) w 1979 r., Miał błąd, który "$@"został zastąpiony pojedynczym pustym łańcuchem, gdy nie było parametrów pozycyjnych; zobacz Co to ${1+"$@"}znaczy w skrypcie powłoki i czym się różni "$@"? The Traditional Bourne Shell FamilyCo to ${1+"$@"}znaczy ... i gdzie jest to konieczne? i "$@"kontra${1+"$@"} .


Tablice pomagają również w pierwszej sytuacji:

jeśli jakiś warunek
następnie
    ignorecase = (- i) # Można powiedzieć `ignorecase = (" - i ")`, ale to naprawdę niepotrzebne.
jeszcze
    ignorecase = () # Nie używaj cudzysłowów w tym poleceniu!
fi
grep "$ {ignorecase [@]}"   other_ grep _args

____________________

PS (csh)

Powinno to być oczywiste, ale z korzyścią dla osób, które są tutaj nowe: csh, tcsh itp., Nie są powłokami Bourne / POSIX. To cała inna rodzina. Koń w innym kolorze. Cała inna gra w piłkę. Inna rasa kota. Ptaki innego pióra. A szczególnie inna puszka robaków.

Niektóre z wypowiedzi na tej stronie dotyczą csh; takich jak: dobrze jest zacytować wszystkie zmienne, chyba że masz dobry powód, aby tego nie robić, i jesteś pewien, że wiesz, co robisz. Ale w csh każda zmienna jest tablicą - tak się składa, że ​​prawie każda zmienna jest tablicą tylko jednego elementu i działa dość podobnie do zwykłej zmiennej powłoki w powłokach Bourne / POSIX. A składnia jest okropnie inna (i mam na myśli okropnie ). Nie powiemy więc nic więcej o powłokach z rodziny csh.

G-Man
źródło
1
Zauważ, że w csh, wolisz użyć $var:qniż, "$var"ponieważ ten ostatni nie działa dla zmiennych zawierających znaki nowego wiersza (lub jeśli chcesz cytować elementy tablicy indywidualnie, zamiast łączyć je spacjami w pojedynczy argument).
Stéphane Chazelas
W bash bitrate="--bitrate 320"działa ${bitrate:+$bitrate}i bitrate=--bitrate 128nie działa, ${bitrate:+"$bitrate"}ponieważ psuje polecenie. Czy korzystanie ${variable:+$variable}z niego jest bezpieczne "?
Freedo
@ Freedo: Twój komentarz jest niejasny. Szczególnie niejasne jest to, co chcesz wiedzieć, czego jeszcze nie powiedziałem. Zobacz drugi nagłówek „Ale co jeśli” - „Ale co, jeśli mam zmienną, którą chcę / muszę podzielić na słowa?” To twoja sytuacja i powinieneś postępować zgodnie z nią. Ale jeśli nie możesz (np. Ponieważ używasz powłoki innej niż bash, ksh, zsh lub yash i używasz  $@do czegoś innego) lub po prostu odmawiasz użycia tablicy z innych powodów, odsyłam do „Jak omówiono w drugiej odpowiedzi”. … (Ciąg dalszy)
G-Man
(Ciąg dalszy) ... Twoja sugestia (korzystając ${variable:+$variable}ze nie ") nie powiedzie się, jeśli IFS zawiera  -,  0, 2, 3, a, b, e, i, rlub  t.
G-Man
Cóż, moja zmienna ma zarówno znaki a, e, b, i 2 i 3 i nadal działa dobrze${bitrate:+$bitrate}
Freedo
11

Byłem sceptycznie nastawiony do odpowiedzi Stéphane'a, jednak można nadużyć $#:

$ set `seq 101`

$ IFS=0

$ echo $#
1 1

lub $ ?:

$ IFS=0

$ awk 'BEGIN {exit 101}'

$ echo $?
1 1

Są to wymyślone przykłady, ale potencjał istnieje.

Steven Penny
źródło