Dlaczego wykrzyknik `!` Czasami denerwuje bash?

14

Zdaję sobie sprawę, że !ma to szczególne znaczenie w wierszu poleceń w kontekście historii wiersza poleceń, ale poza tym w wykrywającym skrypcie wykrzyknik może czasami powodować błąd analizy.
Myślę, że ma to coś wspólnego z event, ale nie mam pojęcia, co to za wydarzenie lub co robi. Mimo to to samo polecenie może zachowywać się inaczej w różnych sytuacjach.
Ostatni przykład poniżej powoduje błąd; ale dlaczego, kiedy ten sam kod działał poza podstawieniem polecenia? .. używając GNU bash 4.1.5

# This works, with or without a space between ! and p
  { echo -e "foo\nbar" | sed -nre '/foo/! p'
    echo -e "foo\nbar" | sed -nre '/foo/!p'; }
# bar
# bar

# This works, works when there is a space between ! and p
  var="$(echo -e "foo\nbar" | sed -nre '/foo/! p')"; echo "$var"
# bar

# This causes an ERROR, with NO space between ! and p
  var="$(echo -e "foo\nbar" | sed -nre '/foo/!p')"; echo "$var"
# bash: !p': event not found
Peter.O
źródło
@Warren .. Dzięki. Widziałem to QA, ale tak naprawdę mówi tylko o tym, jak uciec od ukośnika odwrotnego ... Mój problem dotyczy bardziej tego, dlaczego pozornie już
uniknięty
@fred: „pozornie już uciekł”? Nie widzę żadnych ucieczek, a ty używasz podwójnych cudzysłowów. Zobacz moją (poprawioną) odpowiedź. Jak myślisz, jaką część uciekł?
Caleb
@Caleb. Tak, użyłem niewłaściwego terminu. protectedByłoby bardziej odpowiednie. (chronione przez „pojedyncze cudzysłowy”)
Peter.O,
Jeśli zajmujesz się tylko prostymi zadaniami, możesz użyć var=$(…)(bez podwójnych cudzysłowów) i będzie działać tak, jak oczekuję. Jest jeszcze „bezpieczne”, ponieważ część wartość przypisania prostego nie podlega podziałowi na słowa lub globbing (choć może to nie być prawdą zadań wykonanych przez poleceń wbudowanych (np export, localitp) we wszystkich muszli). Niestety, nie wykracza to poza proste przypisania, ponieważ podwójne cudzysłowy są sposobem ochrony przed dzieleniem słów i dzieleniem tekstu, a jednocześnie zapewniają inne rodzaje ekspansji w innych kontekstach.
Chris Johnsen

Odpowiedzi:

12

!Charakter przywołuje historię podstawienie atakujących za. Po którym następuje ciąg znaków (jak w twoim przypadku, w którym nie powiodło się), próbuje rozwinąć się do ostatniego zdarzenia historii, które rozpoczęło się od tego ciągu. Podobnie jak $varzostanie rozwinięty do wartości tego ciągu, !echorozwinąłby się do ostatniego polecenia echa w twojej historii.

W takich rozszerzeniach przestrzeń jest przełomową postacią. Pierwsza uwaga, jak to by działało ze zmiennymi:

# var="like"
# echo "$var"
like
# echo "$"
$
# echo "Do you $var frogs?"
Do you like frogs?       <- as expected, variable name broken at space
# echo "Do you $varfrogs?"
Do you?                  <- $varfrogs not defined, replaced with blank
# echo "Do you $ var frogs?"
Do you $ var frogs?      <- $ not a valid variable name, ignored

To samo stanie się z rozszerzeniem historii. Znak huku ( !) rozpoczyna sekwencję zastępującą historię, ale tylko wtedy, gdy następuje po nim ciąg znaków. Podążając za spacją, uczyń to dosłownie hukiem zamiast częścią sekwencji zastępowania.

Można uniknąć tego rodzaju zamiany zarówno dla zmiennej, jak i historii, używając pojedynczych cudzysłowów. Twoje pierwsze przykłady używały pojedynczych cudzysłowów, więc działały dobrze. Twoje ostatnie przykłady są w podwójnych cudzysłowach, dlatego bash skanował je w poszukiwaniu sekwencji ekspansji, zanim zrobił cokolwiek innego. Jedynym powodem, dla którego pierwszy się nie potknął, jest to, że spacja jest znakiem przerwania, jak pokazano powyżej.

Caleb
źródło
Dzięki Caleb .. Kolejna z moich wstępnych koncepcji została odrzucona ... Myślałem, że parsowanie bash zostało wykonane z najbardziej wewnętrznego wspornika lub klamry, a potem zadziałało na zewnątrz ... Wygląda na to, że bash parsuje się inaczej niż przypuszczam.
Peter.O,
1
Cytowanie jest dość mylące w bashu bez zmiany zagnieżdżonych ciągów. W tej chwili wymiana następuje na bardzo wczesnym etapie procesu. Rozważ ten przykład:var=word; echo "test '$var'"; echo 'test "$var"'
Caleb
.. Tak, niezrozumiany. Byłem świadomy zagnieżdżania cudzysłowów w cudzysłowach ... Moje nieporozumienie polegało na tym, że myślałem, że kod w nawiasach podstawienia polecenia zostanie parsowany osobno, do tego, co otacza te nawiasy; ale najwyraźniej nie ... dzięki.
Peter.O,
6

Jak już powiedział Caleb , !służy do wywołania podstawienia historii basha.

Jeśli tak jak ja czujesz, że nie potrzebujesz takiej funkcji, możesz ją wyłączyć, wstawiając następujący wiersz ~/.bashrc:

set +H

Nie potrzebuję tego, ponieważ historię można odzyskać za pomocą strzałki w górę i Ctrl- rprzyrostowego wyszukiwania wstecznego. Zobacz stronę podręcznika bash, sekcja Polecenia do manipulowania historią, aby uzyskać szczegółową listę skrótów.

enzotib
źródło
2
Jak żyjesz bez !!?
Caleb
Dzięki. Myślę, że może to być problem, jeśli chodzi o przenośność, ale używanie set +Hw skrypcie działa równie dobrze :) +1
Peter.O
2
@fred: dziwne, generalnie rozszerzenie historii jest włączone tylko dla interaktywnych powłok.
enzotib
@enzo .. Jeszcze raz dziękuję .. Przetestowałem to z wiersza poleceń .. Ach! Gdyby nauka nie była tak zabawna, byłaby nudna ... czy wspomniałem o kawie? to też pomaga :)
Peter.O
Tak, to jest gotcha. Właśnie z tego powodu skopiowałem wklejony kod ze skryptów, które nie powiodły się w wierszu poleceń. Ekspansja historii nie stanowiła problemu w skrypcie, ale znajduje się w interaktywnej powłoce.
Caleb
2

twój pierwszy przykład:

{ echo -e "foo\nbar" | sed -nre '/foo/! p'
    echo -e "foo\nbar" | sed -nre '/foo/!p'; }

można zredukować do

echo '! p' 
echo '!p'

W pojedynczych cudzysłowach wszystkie znaki zachowują swoje dosłowne wartości. W !ten sposób utraciło swoje szczególne znaczenie, a ekspansja historii nie jest przeprowadzana.

twój drugi i trzeci przykład:

var="$(echo -e "foo\nbar" | sed -nre '/foo/! p')"; echo "$var"

var="$(echo -e "foo\nbar" | sed -nre '/foo/!p')"; echo "$var"

można zredukować do

echo "'! p'"

echo "'!p'"

'! p'i '!p'są zasadniczo częściami podwójnie cytowanych ciągów.

Wewnątrz cudzysłowów wszystkie znaki zachować ich wartości literalne except $ , `, \i !.

Oznacza to, że pojedyncze cytaty '! p'i '!p'utraciły swoje specjalne znaczenie (tj. „Niezdolność do ucieczki” !), ale !nadal zachowują swoje specjalne znaczenie, dlatego następuje ekspansja historii.

Jednak !po znaku spacji rozszerzenie historii nie jest wykonywane.

Cytowanie z man bash:

CYTOWANIE

[...]

Umieszczanie znaków w pojedynczych cudzysłowach zachowuje dosłowną wartość każdego znaku w cudzysłowach. [...]

Umieszczanie znaków w podwójnych cudzysłowach zachowuje dosłowną wartość wszystkich znaków w cudzysłowach, z wyjątkiem $, `, \ oraz, gdy włączone jest rozszerzanie historii,!. [...] Jeśli ta opcja jest włączona, rozszerzanie historii będzie wykonywane, chyba że! występowanie w podwójnych cudzysłowach jest poprzedzane znakiem odwrotnego ukośnika. Odwrotny ukośnik poprzedzający! nie jest usuwany.

ROZSZERZENIE HISTORII

[...]

Rozszerzenia historii są wprowadzane przez pojawienie się postaci rozszerzenia historii, która jest! domyślnie. Tylko ukośnik odwrotny (\) i pojedyncze cudzysłowy mogą cytować znak rozwinięcia historii.

Kilka znaków wstrzymuje ekspansję historii, jeśli zostanie znalezione bezpośrednio po znaku ekspansji historii, nawet jeśli nie jest cytowana: spacja, tabulator, nowa linia, powrót karetki i =. Jeśli włączona jest opcja powłoki extglob, (spowoduje także wstrzymanie ekspansji.

kieroi
źródło