Właśnie przypisałem zmienną, ale zmienna echo $ pokazuje coś innego

104

Oto seria przypadków, w których echo $varmożna pokazać inną wartość niż ta, która została właśnie przypisana. Dzieje się tak niezależnie od tego, czy przypisana wartość była „w cudzysłowie”, „w pojedynczym cudzysłowie”, czy nie.

Jak sprawić, by powłoka poprawnie ustawiła moją zmienną?

Gwiazdki

Oczekiwany wynik to /* Foobar is free software */, ale zamiast tego otrzymuję listę nazw plików:

$ var="/* Foobar is free software */"
$ echo $var 
/bin /boot /dev /etc /home /initrd.img /lib /lib64 /media /mnt /opt /proc ...

Nawiasy kwadratowe

Oczekiwana wartość to [a-z], ale czasami zamiast tego otrzymuję pojedynczą literę!

$ var=[a-z]
$ echo $var
c

Kanały (nowe linie)

Oczekiwana wartość to lista oddzielnych linii, ale zamiast tego wszystkie wartości znajdują się w jednym wierszu!

$ cat file
foo
bar
baz

$ var=$(cat file)
$ echo $var
foo bar baz

Wiele przestrzeni

Spodziewałem się starannie wyrównanego nagłówka tabeli, ale zamiast tego wiele spacji znika lub jest zwiniętych w jedną!

$ var="       title     |    count"
$ echo $var
title | count

Tabs

Spodziewałem się dwóch wartości rozdzielonych tabulatorami, ale zamiast tego otrzymuję dwie wartości oddzielone spacjami!

$ var=$'key\tvalue'
$ echo $var
key value
ten inny facet
źródło
2
Dzięki za zrobienie tego. Często spotykam się z linią zasilającą. Więc var=$(cat file)jest dobrze, ale echo "$var"jest potrzebne.
snd
3
BTW, to także BashPitfalls # 14: mywiki.wooledge.org/BashPitfalls#echo_.24foo
Charles Duffy,

Odpowiedzi:

139

We wszystkich powyższych przypadkach zmienna jest poprawnie ustawiona, ale niepoprawnie odczytana! Właściwym sposobem jest użycie podwójnych cudzysłowów przy odwoływaniu się do :

echo "$var"

Daje to oczekiwaną wartość we wszystkich podanych przykładach. Zawsze cytuj odniesienia do zmiennych!


Czemu?

Gdy zmienna nie jest cytowana , będzie:

  1. Poddaj się podziałowi na pola, w którym wartość jest dzielona na wiele słów w odstępach (domyślnie):

    Przed: /* Foobar is free software */

    Po: /*, Foobar, is, free, software,*/

  2. Każde z tych słów zostanie rozwinięte , a wzorce zostaną rozwinięte do pasujących plików:

    Przed: /*

    Po: /bin, /boot, /dev, /etc, /home, ...

  3. Na koniec wszystkie argumenty są przekazywane do echa, które wypisuje je oddzielone pojedynczymi odstępami , dając

    /bin /boot /dev /etc /home Foobar is free software Desktop/ Downloads/

    zamiast wartości zmiennej.

Kiedy zmienna jest cytowana, to:

  1. Zastąp jego wartość.
  2. Nie ma kroku 2.

Dlatego zawsze należy cytować wszystkie odwołania do zmiennych , chyba że wyraźnie wymaga się dzielenia na słowa i rozwijania nazw plików. Narzędzia takie jak shellcheck służą pomocą i ostrzegają o brakujących cudzysłowach we wszystkich powyższych przypadkach.

ten inny facet
źródło
to nie zawsze działa. Mogę podać przykład: paste.ubuntu.com/p/8RjR6CS668
recolic
1
Tak, $(..)paski końcowe linii wysuwów. Możesz var=$(cat file; printf x); var="${var%x}"go obejść.
ten drugi facet
17

Możesz chcieć wiedzieć, dlaczego tak się dzieje. Razem ze świetnym wyjaśnieniem tego innego gościa znajdź odniesienie do Dlaczego mój skrypt powłoki dławi się białymi znakami lub innymi znakami specjalnymi? napisane przez Gillesa w systemach Unix i Linux :

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

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

  • Najpierw weź wartość zmiennej.
  • Dzielenie pól: traktuj tę wartość jako listę pól oddzielonych białymi znakami i buduj 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 wzorzec wieloznaczny, i zamień je na listę nazw plików, które pasują do tego wzorca. Jeśli wzorzec nie pasuje do żadnych plików, pozostaje niezmieniony. W naszym przykładzie daje to listę zawierającą foo, po której następuje lista 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. W składni powłoki istnieją dwa konteksty: kontekst listy i kontekst ciągu. Dzielenie pól i generowanie nazw plików odbywa się tylko w kontekście listowym, ale jest to przeważnie. Podwójne cudzysłowy ograniczają kontekst ciągu: cały ciąg w cudzysłowie jest pojedynczym ciągiem, którego nie można podzielić. (Wyjątek: "$@"aby rozwinąć listę 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 podstawianiem poleceń z $(foo)lub z `foo`. Na marginesie, nie używaj `foo`: jego zasady cytowania są dziwne i nieprzenośne, a wszystkie współczesne powłoki obsługują, $(foo)co jest absolutnie równoważne, z wyjątkiem intuicyjnych reguł cytowania.

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

Zobacz Kiedy konieczne jest podwójne cytowanie? aby uzyskać więcej informacji na temat przypadków, w których możesz pominąć cytaty.

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

fedorqui 'SO przestań szkodzić'
źródło
7

Oprócz innych problemów spowodowanych brakiem cytowania -ni -emoże być używany echojako argumenty. (Tylko ta pierwsza jest legalna zgodnie ze specyfikacją POSIX echo, ale kilka typowych implementacji narusza specyfikację i także konsumuje -e).

Aby tego uniknąć, użyj printfzamiast echokiedy szczegóły mają znaczenie.

A zatem:

$ vars="-e -n -a"
$ echo $vars      # breaks because -e and -n can be treated as arguments to echo
-a
$ echo "$vars"
-e -n -a

Jednak prawidłowe cytowanie nie zawsze uratuje Cię podczas korzystania z echo:

$ vars="-n"
$ echo $vars
$ ## not even an empty line was printed

... podczas gdy to będzie cię uratować z printf:

$ vars="-n"
$ printf '%s\n' "$vars"
-n
Charles Duffy
źródło
Tak, potrzebujemy do tego dobrego dedupu! Zgadzam się, że pasuje to do tytułu pytania, ale nie sądzę, aby uzyskało tutaj taką widoczność, na jaką zasługuje. Co powiesz na nowe pytanie à la „Dlaczego mój ukośnik -e/ -n/ nie jest wyświetlany?” W razie potrzeby możemy dodać linki z tego miejsca.
ten inny facet
Czy chodziło Ci spożywać -njak również ?
PesaThe
1
@PesaThe, nie, miałem na myśli -e. Standard for echonie określa wyjścia, gdy jego pierwszy argument ma wartość -n, czyniąc jakiekolwiek / wszystkie możliwe wyjście legalnym w tym przypadku; nie ma takiego przepisu -e.
Charles Duffy,
Och ... nie umiem czytać. Obwiniajmy za to mój angielski. Dziękuję za wyjaśnienie.
PesaThe
6

podwójny cudzysłów użytkownika, aby uzyskać dokładną wartość. lubię to:

echo "${var}"

i poprawnie odczyta twoją wartość.

vanishedzhou
źródło
Działa ... Dzięki
Tshilidzi Mudau
2

echo $varwyjście w dużym stopniu zależy od wartości IFSzmiennej. Domyślnie zawiera spacje, tabulatory i znaki nowej linii:

[ks@localhost ~]$ echo -n "$IFS" | cat -vte
 ^I$

Oznacza to, że gdy powłoka dokonuje podziału na pola (lub na słowa), używa wszystkich tych znaków jako separatorów słów. Dzieje się tak, gdy odwołujemy się do zmiennej bez podwójnych cudzysłowów, aby ją powtórzyć ( $var), a zatem oczekiwane dane wyjściowe są zmieniane.

Jednym ze sposobów zapobiegania dzieleniu słów (oprócz stosowania podwójnych cudzysłowów) jest ustawienie IFSwartości null. Zobacz http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_05 :

Jeżeli wartość IFS jest zerowa, nie przeprowadza się podziału na pola.

Ustawienie na null oznacza ustawienie na pustą wartość:

IFS=

Test:

[ks@localhost ~]$ echo -n "$IFS" | cat -vte
 ^I$
[ks@localhost ~]$ var=$'key\nvalue'
[ks@localhost ~]$ echo $var
key value
[ks@localhost ~]$ IFS=
[ks@localhost ~]$ echo $var
key
value
[ks@localhost ~]$ 
ks1322
źródło
2
Musiałbyś także set -fzapobiec globbingowi
temu drugiemu facetowi
@thatotherguy, czy jest to naprawdę konieczne w pierwszym przykładzie z rozszerzaniem ścieżki? W przypadku IFSustawienia wartości null echo $varzostanie rozwinięty do, echo '/* Foobar is free software */'a rozwinięcie ścieżki nie jest wykonywane wewnątrz ciągów w pojedynczych cudzysłowach.
ks1322
1
Tak. Jeśli mkdir "/this thing called Foobar is free software etc/"zobaczysz, że nadal się rozszerza. Na przykład jest to oczywiście bardziej praktyczne [a-z].
ten inny facet
Rozumiem, na [a-z]przykład ma to sens .
ks1322
2

Odpowiedź od ks1322 pomógł mi zidentyfikować ten problem podczas korzystania z docker-compose exec:

Jeśli pominiesz -Tflagę, docker-compose execdodaj znak specjalny, który przerywa wyjście, bzamiast 1b:

$ test=$(/usr/local/bin/docker-compose exec db bash -c "echo 1")
$ echo "${test}b"
b
echo "${test}" | cat -vte
1^M$

Z -Tflagą docker-compose execdziała zgodnie z oczekiwaniami:

$ test=$(/usr/local/bin/docker-compose exec -T db bash -c "echo 1")
$ echo "${test}b"
1b
glin
źródło
-2

Oprócz umieszczenia zmiennej w cudzysłowie, można również przetłumaczyć wynik zmiennej za pomocą tri zamieniając spacje na znaki nowej linii.

$ echo $var | tr " " "\n"
foo
bar
baz

Chociaż jest to trochę bardziej zawiłe, dodaje więcej różnorodności do danych wyjściowych, ponieważ można zastąpić dowolny znak jako separator między zmiennymi tablicowymi.

Alek
źródło
2
Ale to zastępuje wszystkie spacje znakami nowej linii. Cytowanie zachowuje istniejące znaki nowej linii i spacje.
user000001
Prawda, tak. Przypuszczam, że zależy to od tego, co znajduje się w zmiennej. Właściwie używam trodwrotnego sposobu tworzenia tablic z plików tekstowych.
Alek
3
Tworzenie problemu przez niepoprawne cytowanie zmiennej, a następnie obejście jej za pomocą dodatkowego, utrudnionego procesu nie jest dobrym programowaniem.
tripleee
@Alek, ... err, co? Nie ma trpotrzeby prawidłowego / poprawnego tworzenia tablicy z pliku tekstowego - możesz określić dowolny separator, ustawiając IFS. Na przykład: IFS=$'\n' read -r -d '' -a arrayname < <(cat file.txt && printf '\0')działa przez całą drogę wstecz do bash 3.2 (najstarsza wersja w szerokim obiegu) i poprawnie ustawia status wyjścia na false, jeśli twój błąd się catnie powiódł. A jeśli chciał, powiedzmy, zakładki zamiast znaki nowej linii, to że po prostu wymienić $'\n'się $'\t'.
Charles Duffy
1
@Alek, ... jeśli robisz coś takiego arrayname=( $( cat file | tr '\n' ' ' ) ), to jest to zepsute na wielu warstwach: globalizuje twoje wyniki (więc *zmienia się w listę plików w bieżącym katalogu) i działałoby równie dobrze beztr ( lub cat, jeśli o to chodzi; można by po prostu użyć arrayname=$( $(<file) )i zostałoby złamane w ten sam sposób, ale mniej nieefektywnie).
Charles Duffy