Dlaczego opcje w cytowanej zmiennej zawodzą, ale działają, gdy nie są cytowane?

18

Czytałem o tym, że powinienem zacytować zmienne w bash, np. „$ Foo” zamiast $ foo. Jednak podczas pisania skryptu znalazłem przypadek, w którym działa on bez cudzysłowów, ale nie z nimi:

wget_options='--mirror --no-host-directories'
local_root="$1" # ./testdir recieved from command line
remote_root="$2" # ftp://XXX recieved from command line 
relative_path="$3" # /XXX received from command line

Ten działa:

wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

Ten nie ma (zwróć uwagę na podwójne cudzysłowy wokół $ wget_options):

wget "$wget_options" --directory_prefix="$local_root" "$remote_root$relative_path"
  • Jaki jest tego powód?

  • Czy pierwsza linia to dobra wersja; czy powinienem podejrzewać, że gdzieś jest ukryty błąd, który powoduje takie zachowanie?

  • Ogólnie, gdzie znajdę dobrą dokumentację, aby zrozumieć, jak działa bash i jego cytowanie? Podczas pisania tego skryptu czuję, że zacząłem pracować na zasadzie prób i błędów zamiast rozumieć zasady.

z32a7ul
źródło
3
Odpowiedź na twoje pytanie tutaj: mywiki.wooledge.org/BashFAQ/050
glenn jackman
3
Przejdź do źródła reguł: instrukcja bash . Zwróć szczególną uwagę na rozdział 3.5 „Rozszerzenia powłoki”, w szczególności dzielenie słów i rozwijanie nazw plików - te dwa czynniki służą do kontrolowania cudzysłowów.
glenn jackman
4
Myślę, że pomaga zrozumieć, w jaki sposób argumenty wiersza poleceń działają na niskim poziomie. Kiedy program jest wykonywany, otrzymuje argumenty jako listę list znaków (wystarczająco blisko). Każda wewnętrzna lista jest tym, co nazywamy „argumentem”. Większość programów zależy od logicznej separacji argumentów. Tutaj widzisz, że wgetnie wie, co --mirror --no-host-directoriesoznacza (jako jeden argument), ale obsługuje go, gdy jest podzielony na dwa argumenty. Bardzo niewiele programów specjalnie traktuje spacje i cudzysłowy, gdy znajdują się w wektorze argumentów. Problem polega na tym bash, że i inne powłoki mają być>
HTNW
2
> używane przez ludzi. Byłoby denerwujące, aby ręcznie zdefiniować granice między argumentami, więc powłoki dzielą się na białe znaki, aby zmienić linię (listę znaków) w wektor argumentu (listę list znaków). Zmienna ekspansja to jedna z pierwszych ekspansji bash, więc możesz sobie wyobrazić, że $ajest to odpowiednik bezpośredniego pisania jej zawartości. Teraz problem jest oczywisty: a="-a -b"; cmd "$a"rozwija się cmd "-a -b", ale cmdprawdopodobnie nie wie, co to znaczy. cmd $arozszerza się cmd -a -b, co prawdopodobnie czyni pracę.
HTNW,

Odpowiedzi:

28

Zasadniczo powinieneś podwójnie cytować rozszerzenia zmiennych, aby chronić je przed dzieleniem słów (i generowaniem nazw plików). Jednak w twoim przykładzie

wget_options='--mirror --no-host-directories'
wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

dzielenie słów jest dokładnie tym, czego chcesz .

Z "$wget_options"(cytowany) wgetnie wie, co zrobić z pojedynczym argumentem --mirror --no-host-directoriesi narzeka

wget: unknown option -- mirror --no-host-directories

Aby wgetzobaczyć dwie opcje --mirrori --no-host-directoriesjako osobne, musi wystąpić podział słów.

Istnieją bardziej niezawodne sposoby na zrobienie tego. Jeśli używasz bashlub jakiejkolwiek innej powłoki, która używa tablic takich jak bashdo, zobacz odpowiedź glenn jackman . Odpowiedź Gillesa dodatkowo opisuje alternatywne rozwiązanie dla bardziej płaskich pocisków, takie jak standard /bin/sh. Oba zasadniczo przechowują każdą opcję jako osobny element w tablicy.

Powiązane pytanie z dobrymi odpowiedziami: Dlaczego mój skrypt powłoki dusi się na białych znakach lub innych znakach specjalnych?


Dobrym pomysłem jest stosowanie podwójnego cytowania zmiennych. Zrób to . Zatem pamiętaj o niewielu przypadkach, w których nie powinieneś tego robić. Przedstawią się one za pośrednictwem komunikatów diagnostycznych, takich jak powyższy komunikat o błędzie.

Istnieje również kilka przypadków, w których nie trzeba cytować rozszerzeń zmiennych. Ale i tak łatwiej jest nadal używać podwójnych cudzysłowów, ponieważ nie robi to dużej różnicy. Jednym z takich przypadków jest

variable=$other_variable

Innym jest

case $variable in
    ...) ... ;;
esac
Kusalananda
źródło
2
Przed użyciem operatora split + glob konieczne może być sprawdzenie, czy $IFSzawiera on odpowiednią wartość. Tutaj musisz podzielić na spację, a tekst nie zawiera tabulatora ani nowego wiersza, więc domyślna wartość $IFSby to zrobiła, ale jeśli ten kod ma być użyty w funkcji, która może być wywołana w kontekście, w którym $IFSmógł zostać zmodyfikowany , chcesz $IFSwcześniej ustawić (i ewentualnie przywrócić go później lub użyć lokalnego zasięgu, jeśli reszta kodu zakłada niezmodyfikowaną $IFS)
Stéphane Chazelas
32

Najbardziej niezawodny sposób na użycie kodu:

wget_options=(
    --mirror 
    --no-host-directories
    --directory_prefix="$1"
)
wget "${wget_options[@]}" "$2/$3"
Glenn Jackman
źródło
To jest właściwa odpowiedź. Odniesienie
l0b0
2
To dobra odpowiedź, więc głosowałem za nią, ale Kusalanda pomógł mi lepiej zrozumieć, dlaczego mój kod był błędny i mogę zaakceptować tylko jeden.
z32a7ul
Wpadłem w świat kłopotów, dopóki ktoś z listy rsync nie pokazał mi tej konstrukcji. Jest to szczególnie pomocne, jeśli niektóre elementy mogą być pustymi łańcuchami. To powoduje, że puste ciągi znikają. Niektóre polecenia jak cpi rsyncbędą robić nieoczekiwane rzeczy, jeśli twoje polecenie rozwinie się do czegoś podobnego rsync '' rest of parameters. Jest to świetne do warunkowego budowania polecenia kawałek po kawałku, a następnie po prostu uruchamianie go raz w jednym miejscu.
Joe
17

Próbujesz zapisać listę ciągów w zmiennej łańcuchowej. To nie pasuje. Bez względu na sposób dostępu do zmiennej coś jest zepsute.

wget_options='--mirror --no-host-directories'ustawia zmienną wget_optionsna ciąg znaków, który zawiera spację. W tym momencie nie ma sposobu, aby wiedzieć, czy przestrzeń ma być częścią opcji, czy też separatorem między opcjami.

Gdy uzyskujesz dostęp do zmiennej z cytowanym podstawieniem wget "$wget_options", wartość zmiennej jest używana jako ciąg. Oznacza to, że jest przekazywany jako pojedynczy parametr do wget, więc jest to jedna opcja. To się psuje w twoim przypadku, ponieważ zamierzałeś oznaczać wiele opcji.

Gdy używasz zastępowania bez cudzysłowu wget $wget_options, wartość zmiennej łańcuchowej podlega procesowi rozszerzenia o nazwie „split + glob”:

  1. Weź wartość zmiennej i podziel ją na części rozdzielane białymi znakami (zakładając, że nie zmodyfikowałeś $IFSzmiennej). Daje to pośrednią listę ciągów.
  2. Jeśli dla każdego elementu listy pośredniej jest to wzór wieloznaczny pasujący do jednego lub więcej plików, zastąp ten element listą pasujących plików.

Dzieje się tak w twoim przykładzie, ponieważ proces podziału zamienia spację w separator, ale ogólnie nie działa, ponieważ opcja może zawierać spacje i znaki wieloznaczne.

W ksh, bash, yash i zsh możesz użyć zmiennej tablicowej. Tablica w terminologii powłoki to lista ciągów, więc nie ma utraty informacji. Aby utworzyć zmienną tablicową, podczas przypisywania wartości do zmiennej umieść nawiasy wokół elementów tablicy. Aby uzyskać dostęp do wszystkich elementów tablicy, użyj - jest to uogólnienie , które tworzy listę z elementów tablicy. Pamiętaj, że potrzebujesz tutaj podwójnych cudzysłowów, w przeciwnym razie każdy element zostanie podzielony na glob."${VARIABLE[@]}""$@"

wget_options=(--mirror --no-host-directories --user-agent="I can haz spaces")
wget "${wget_options[@]}" 

W zwykłym sh nie ma zmiennych tablicowych. Jeśli nie masz nic przeciwko utracie argumentów pozycyjnych, możesz użyć ich do zapisania jednej listy ciągów.

set -- --mirror --no-host-directories --user-agent="I can haz spaces"
wget "$@" 

Aby uzyskać więcej informacji, zobacz Dlaczego mój skrypt powłoki dusi się na białych znakach lub innych znakach specjalnych?

Gilles „SO- przestań być zły”
źródło
Dla zwykłego sh do czynienia z podpowłoką by zachować pozycyjnych argumentów: (set -- ...; exec wget "$@" ...).
John Kugelman wspiera Monikę