Dlaczego nie mogę określić zmiennej środowiskowej i powtórzyć jej w tym samym wierszu poleceń?

91

Rozważ ten fragment:

$ SOMEVAR=AAA
$ echo zzz $SOMEVAR zzz
zzz AAA zzz

Tutaj mam ustawione $SOMEVARaby AAAna pierwszej linii - a kiedy echo go na drugiej linii, otrzymuję AAAzawartość zgodnie z oczekiwaniami.

Ale jeśli spróbuję określić zmienną w tym samym wierszu poleceń, co echo:

$ SOMEVAR=BBB echo zzz $SOMEVAR zzz
zzz AAA zzz

... nie dostaję BBBtak jak oczekiwałem - otrzymuję starą wartość ( AAA).

Czy tak właśnie powinno być? Jeśli tak, to dlaczego możesz określić zmienne, takie jak LD_PRELOAD=/... program args ...i sprawić, by działały? czego mi brakuje?

sdaau
źródło
2
Działa, gdy przypiszesz przypisanie do oddzielnej instrukcji lub gdy wywołujesz skrypt z własnym środowiskiem, ale nie podczas poprzedzania polecenia w bieżącym środowisku. Ciekawy!
Todd A. Jacobs
1
Powodem LD_PRELOADdziała to, że zmienna jest ustawiona w programie na środowisko - nie na swojej linii komend.
Wstrzymano do odwołania.

Odpowiedzi:

103

To, co widzisz, to oczekiwane zachowanie. Problem polega na tym, że powłoka macierzysta ocenia $SOMEVARw wierszu poleceń, zanim wywoła polecenie ze zmodyfikowanym środowiskiem. Musisz uzyskać ocenę $SOMEVARodroczoną do momentu ustawienia środowiska.

Twoje bezpośrednie opcje obejmują:

  1. SOMEVAR=BBB eval echo zzz '$SOMEVAR' zzz.
  2. SOMEVAR=BBB sh -c 'echo zzz $SOMEVAR zzz'.

Oba używają pojedynczych cudzysłowów, aby zapobiec wartościowaniu przez powłokę macierzystą $SOMEVAR; jest oceniany dopiero po ustawieniu go w środowisku (tymczasowo na czas trwania pojedynczego polecenia).

Inną opcją jest użycie notacji podpowłoki (co również zasugerował Marcus Kuhn w swojej odpowiedzi ):

(SOMEVAR=BBB; echo zzz $SOMEVAR zzz)

Zmienna jest ustawiana tylko w podpowłoce

Jonathan Leffler
źródło
Super, @JonathanLeffler - wielkie dzięki za wyjaśnienie; Twoje zdrowie!
sdaau
Dodatek z @ markus-kuhn jest trudny do przecenienia.
Alex Che
37

Problem ponownie odwiedzony

Szczerze mówiąc, podręcznik jest niejasny w tej kwestii. Podręcznik GNU Bash mówi:

Środowisko dla każdego prostego polecenia lub funkcji [zwróć uwagę, że wyklucza to polecenia wbudowane] można tymczasowo rozszerzyć, dodając do niego przedrostek przypisanymi parametrami, jak opisano w sekcji Parametry powłoki. Te instrukcje przypisania wpływają tylko na środowisko widziane przez to polecenie.

Jeśli naprawdę przeanalizujesz zdanie, to mówi, że środowisko polecenia / funkcji jest zmodyfikowane, ale nie środowisko dla procesu nadrzędnego. Więc to zadziała:

$ TESTVAR=bbb env | fgrep TESTVAR
TESTVAR=bbb

ponieważ środowisko dla polecenia env zostało zmodyfikowane przed jego wykonaniem. Jednak to nie zadziała:

$ set -x; TESTVAR=bbb echo aaa $TESTVAR ccc
+ TESTVAR=bbb
+ echo aaa ccc
aaa ccc

z powodu tego, kiedy interpretacja parametrów jest wykonywana przez powłokę.

Kroki tłumacza

Inną częścią problemu jest to, że Bash definiuje następujące kroki dla swojego interpretera:

  1. Odczytuje swoje dane wejściowe z pliku (zobacz Skrypty powłoki), z łańcucha dostarczonego jako argument opcji wywołania -c (zobacz Wywoływanie Bash) lub z terminala użytkownika.
  2. Dzieli dane wejściowe na słowa i operatory, przestrzegając zasad cytowania opisanych w cytowaniu. Te żetony są oddzielone metaznakami. Rozwijanie aliasów jest wykonywane w tym kroku (zobacz Aliasy).
  3. Przetwarza tokeny na proste i złożone polecenia (zobacz Polecenia powłoki).
  4. Wykonuje różne rozszerzenia powłoki (zobacz Rozszerzenia powłoki), dzieląc rozwinięte tokeny na listy nazw plików (zobacz Rozszerzanie nazw plików) oraz polecenia i argumenty.
  5. Wykonuje wszelkie niezbędne przekierowania (zobacz Przekierowania) i usuwa operatory przekierowania i ich operandy z listy argumentów.
  6. Wykonuje polecenie (patrz Wykonywanie poleceń).
  7. Opcjonalnie czeka na zakończenie polecenia i zbiera jego status wyjścia (zobacz Status wyjścia).

To, co się tutaj dzieje, to fakt, że elementy wbudowane nie otrzymują własnego środowiska wykonawczego, więc nigdy nie widzą zmodyfikowanego środowiska. Ponadto, proste polecenia (np / bin / echo) mają otrzymać zmodyfikowany ennvironment (dlatego przykładem env pracował), ale rozszerzenie powłoki odbywa się w bieżącym środowisku w kroku # 4.

Innymi słowy, nie przekazujesz „aaa $ TESTVAR ccc” do / bin / echo; przekazujesz interpolowany ciąg (rozszerzony w bieżącym środowisku) do / bin / echo. W tym przypadku, ponieważ bieżące środowisko nie ma TESTVAR , po prostu przekazujesz komendę „aaa ccc”.

Podsumowanie

Dokumentacja mogłaby być dużo bardziej przejrzysta. Dobrze, że jest przepełnienie stosu!

Zobacz też

http://www.gnu.org/software/bash/manual/bashref.html#Command-Execution-Environment

Todd A. Jacobs
źródło
Głosowałem już za tym - ale właśnie wróciłem do tego pytania, a ten post zawiera dokładnie te wskazówki, których potrzebuję; wielkie dzięki, @CodeGnome!
sdaau
Nie wiem, czy Bash się zmieniło w tej dziedzinie, ponieważ ta odpowiedź została wysłana, ale poprzedzone przypisania zmiennej zrobić pracę z builtins teraz. Na przykład FOO=foo eval 'echo $FOO'drukuje foozgodnie z oczekiwaniami. Oznacza to, że możesz robić takie rzeczy jak IFS="..." read ....
Will Vousden
Myślę, że to, co się dzieje, polega na tym, że Bash tymczasowo modyfikuje własne środowisko i przywraca je po zakończeniu polecenia, co może mieć dziwne skutki uboczne.
Will Vousden
22

Aby osiągnąć to, co chcesz, użyj

( SOMEVAR=BBB; echo zzz $SOMEVAR zzz )

Powód:

  • Musisz oddzielić przypisanie średnikiem lub nową linią od następnego polecenia, w przeciwnym razie nie zostanie ono wykonane przed interpretacją parametrów dla następnego polecenia (echo).

  • Musisz dokonać przypisania w środowisku podpowłoki , aby upewnić się, że nie będzie trwał poza bieżącą linią.

To rozwiązanie jest krótsze, schludniejsze i bardziej wydajne niż niektóre inne sugerowały, w szczególności nie tworzy nowego procesu.

Markus Kuhn
źródło
3
Dla przyszłych pracowników Google, którzy tu trafią: to prawdopodobnie najlepsza odpowiedź na to pytanie. Aby to jeszcze bardziej skomplikować, jeśli chcesz, aby przydział był dostępny w środowisku polecenia, musisz go wyeksportować. Podpowłoka nadal zapobiega utrwalaniu przypisania. (export SOMEVAR=BBB; python -c "from os import getenv; print getenv('SOMEVAR')")
eaj
@eaj Aby wyeksportować zmienną powłoki do jednego zewnętrznego wywołania programu, jak w twoim przykładzie, po prostu użyjSOMEVAR=BBB python -c "from os import getenv; print getenv('SOMEVAR')"
Markus Kuhn
10

Powodem jest to, że ustawia to zmienną środowiskową dla jednej linii. Ale echonie robi rozszerzenia, bashrobi. W związku z tym zmienna jest faktycznie rozwijana przed wykonaniem polecenia, nawet jeśli SOME_VARznajduje się BBBw kontekście polecenia echo.

Aby zobaczyć efekt, możesz zrobić coś takiego:

$ SOME_VAR=BBB bash -c 'echo $SOME_VAR'
BBB

Tutaj zmienna nie jest rozwijana, dopóki nie zostanie wykonany proces potomny, więc zobaczysz zaktualizowaną wartość. jeśli SOME_VARIABLEponownie sprawdzisz w powłoce nadrzędnej, nadal jest AAA, zgodnie z oczekiwaniami.

Błąd krytyczny
źródło
1
+1 za poprawne wyjaśnienie, dlaczego nie działa tak, jak napisano, i wykonalne obejście.
Jonathan Leffler
1
SOMEVAR=BBB; echo zzz $SOMEVAR zzz

Użyć ; aby oddzielić instrukcje, które znajdują się w tym samym wierszu.

Kyros
źródło
1
To działa, ale nie o to chodzi. Chodzi o to, aby ustawić środowisko tylko dla jednego polecenia, a nie na stałe, jak to robi rozwiązanie.
Jonathan Leffler
Dzięki za to @Kyros; nie wiem dlaczego już to przegapiłem :) Wciąż błądząc jak LD_PRELOADi takie mogą działać przed plikiem wykonywalnym bez średnika, choć ... Jeszcze raz wielkie dzięki - brawa!
sdaau
@JonathanLeffler - rzeczywiście, taki był zamysł; Nie zdawałem sobie sprawy, że średnik sprawia, że ​​zmiana jest trwała - dziękuję, że to zauważyłeś!
sdaau
1

Oto jedna alternatywa:

SOMEVAR=BBB && echo zzz $SOMEVAR zzz
brian
źródło
Niezależnie od tego, czy używasz poleceń, &&czy ;oddzielasz je, przypisanie pozostaje niezmienione, co nie jest pożądanym zachowaniem OP. Markus Kuhn ma poprawną wersję tej odpowiedzi.
eaj