Cytowanie w konstrukcjach typu ssh $ host $ FOO i ssh $ host „sudo su user -c $ FOO”


30

Często kończę na wydawaniu złożonych poleceń przez ssh; polecenia te obejmują potokowanie do awk lub perl jednowierszowe, w wyniku czego zawierają pojedyncze cudzysłowy i $. Nie byłem w stanie wypracować twardej i szybkiej zasady prawidłowego cytowania, ani nie znalazłem dla niej dobrego odniesienia. Rozważ na przykład:

# what I'd run locally:
CMD='pgrep -fl java | grep -i datanode | awk '{print $1}'
# this works with ssh $host "$CMD":
CMD='pgrep -fl java | grep -i datanode | awk '"'"'{print $1}'"'"

(Zwróć uwagę na dodatkowe cytaty w instrukcji awk.)

Ale jak mam to zrobić, na przykład ssh $host "sudo su user -c '$CMD'"? Czy istnieje ogólny przepis na zarządzanie ofertami w takich scenariuszach? ..

Odpowiedzi:


35

Radzenie sobie z wieloma poziomami cytowania (tak naprawdę, wieloma poziomami parsowania / interpretacji) może być skomplikowane. Pomaga pamiętać o kilku rzeczach:

  • Każdy „poziom cytowania” może potencjalnie obejmować inny język.
  • Zasady cytowania różnią się w zależności od języka.
  • W przypadku więcej niż jednego lub dwóch zagnieżdżonych poziomów zwykle najłatwiej jest pracować „od dołu do góry” (tj. Od środka do najbardziej na zewnątrz).

Poziomy cytowania

Spójrzmy na twoje przykładowe polecenia.

pgrep -fl java | grep -i datanode | awk '{print $1}'

Twoje pierwsze przykładowe polecenie (powyżej) używa czterech języków: twojej powłoki, wyrażenia regularnego w pgrep , wyrażenia regularnego w grep (które mogą być inne niż język wyrażenia regularnego w pgrep ) i awk . Istnieją dwa poziomy interpretacji: powłoka i jeden poziom za powłoką dla każdego z zaangażowanych poleceń. Istnieje tylko jeden wyraźny poziom cytowania (cytowanie powłoki w awk ).

ssh host 

Następnie dodałeś poziom ssh na górze. Jest to faktycznie kolejny poziom powłoki: ssh nie interpretuje samego polecenia, przekazuje je powłoce na zdalnym końcu (przez (np.) sh -c …) I ta powłoka interpretuje ciąg znaków.

ssh host "sudo su user -c …"

Następnie zapytałeś o dodanie kolejnego poziomu powłoki za pomocą su (via sudo , który nie interpretuje argumentów poleceń, więc możemy go zignorować). W tym momencie masz trzy poziomy zagnieżdżania ( awk → powłoka, powłoka → powłoka ( ssh ), powłoka → powłoka ( su user -c ), więc radzę stosować podejście „od dołu do góry”. twoje muszle są kompatybilne z Bourne (np. sh , ash , dash , ksh , bash , zsh itp.) Niektóre inne rodzaje muszli ( ryby , rcitp.) może wymagać innej składni, ale metoda nadal obowiązuje.

Dół, góra

  1. Sformułuj ciąg, który chcesz reprezentować na najbardziej wewnętrznym poziomie.
  2. Wybierz mechanizm cytowania z repertuaru cytowań następnego najwyższego języka.
  3. Podaj żądany ciąg zgodnie z wybranym mechanizmem cytowania.
    • Często istnieje wiele wariantów zastosowania mechanizmu cytowania. Robienie tego ręcznie jest zwykle kwestią praktyki i doświadczenia. Robiąc to programowo, zazwyczaj najlepiej jest wybrać najłatwiejszy do zrobienia (zwykle „najbardziej dosłowny” (najmniej ucieczek)).
  4. Opcjonalnie możesz użyć wynikowego cudzysłowu z dodatkowym kodem.
  5. Jeśli nie osiągnąłeś jeszcze pożądanego poziomu cytowania / interpretacji, weź otrzymany ciąg cytowany (plus dowolny dodany kod) i użyj go jako łańcucha początkowego w kroku 2.

Cytując Semantykę Zmieniaj się

Należy pamiętać, że każdy język (poziom cytowania) może nadawać nieco inną semantykę (lub nawet drastycznie inną semantykę) temu samemu znakowi cytowania.

Większość języków ma „dosłowny” mechanizm cytowania, ale różnią się dokładnie pod względem dosłowności. Pojedynczy cytat z powłok podobnych do Bourne'a jest w rzeczywistości dosłowny (co oznacza, że ​​nie można go używać do cytowania samego znaku cytowania). Inne języki (Perl, Ruby) są mniej dosłowne w tym, że interpretują pewne sekwencje backslash w pojedynczych regionach cytowane dosłownie non-(konkretnie, \\a \'wynik w \i ', ale inne sekwencje ukośnik odwrotny to rzeczywiście dosłowny).

Będziesz musiał przeczytać dokumentację dla każdego języka, aby zrozumieć zasady cytowania i ogólną składnię.

Twój przykład

Najbardziej wewnętrzny poziom twojego przykładu to program awk .

{print $1}

Zamieścisz to w linii poleceń powłoki:

pgrep -fl java | grep -i datanode | awk 

Musimy chronić (przynajmniej) w przestrzeni i $w awk programie. Oczywistym wyborem jest użycie pojedynczego cudzysłowu w powłoce wokół całego programu.

  • '{print $1}'

Istnieją jednak inne opcje:

  • {print\ \$1} bezpośrednio uciec z przestrzeni i $
  • {print' $'1} pojedynczy cytat tylko przestrzeń i $
  • "{print \$1}" podwójnie zacytuj całość i uciec $
  • {print" $"1}podwójny cudzysłów tylko spacja i $
    to może nieco wyginać reguły (nieosłonięte $na końcu łańcucha podwójnego cudzysłowu jest dosłowne), ale wydaje się, że działa w większości powłok.

Gdyby program używał przecinka między otwartymi i zamkniętymi nawiasami klamrowymi, musielibyśmy również zacytować lub uciec albo przecinek, albo nawiasy klamrowe, aby uniknąć „rozszerzenia nawiasów klamrowych” w niektórych powłokach.

Wybieramy '{print $1}'i osadzamy go w pozostałej części „kodu” powłoki:

pgrep -fl java | grep -i datanode | awk '{print $1}'

Następnie chciałeś uruchomić to za pomocą su i sudo .

sudo su user -c 

su user -c …jest podobny some-shell -c …(oprócz działania pod innym identyfikatorem UID), więc su po prostu dodaje kolejny poziom powłoki. sudo nie interpretuje swoich argumentów, więc nie dodaje żadnych poziomów cytowania.

Potrzebujemy innego poziomu powłoki dla naszego ciągu poleceń. Możemy ponownie wybrać pojedyncze cytaty, ale musimy specjalnie traktować istniejące pojedyncze cytaty. Zwykły sposób wygląda następująco:

'pgrep -fl java | grep -i datanode | awk '\''{print $1}'\'

Są tutaj cztery ciągi, które powłoka będzie interpretować i konkatenować: pierwszy ciąg pojedynczego cudzysłowu ( pgrep … awk), pojedynczy cytat ze znakiem ucieczki, program awk z pojedynczym cudzysłowem , inny pojedynczy cytat ze znakiem ucieczki.

Istnieje oczywiście wiele alternatyw:

  • pgrep\ -fl\ java\ \|\ grep\ -i\ datanode\ \|\ awk\ \'{print\ \$1} uciec od wszystkiego, co ważne
  • pgrep\ -fl\ java\|grep\ -i\ datanode\|awk\ \'{print\$1}to samo, ale bez zbędnych białych znaków (nawet w programie awk !)
  • "pgrep -fl java | grep -i datanode | awk '{print \$1}'" podwójnie zacytuj całość, ucieknij $
  • 'pgrep -fl java | grep -i datanode | awk '"'"'{print \$1}'"'"twoja odmiana; nieco dłuższy niż zwykle ze względu na użycie podwójnych cudzysłowów (dwa znaki) zamiast znaków ucieczki (jeden znak)

Użycie różnych cytatów na pierwszym poziomie pozwala na inne warianty na tym poziomie:

  • 'pgrep -fl java | grep -i datanode | awk "{print \$1}"'
  • 'pgrep -fl java | grep -i datanode | awk {print\ \$1}'

Osadzenie pierwszej odmiany w wierszu polecenia sudo / * su * daje:

sudo su user -c 'pgrep -fl java | grep -i datanode | awk '\''{print $1}'\'

Możesz użyć tego samego łańcucha w dowolnym innym kontekście pojedynczego poziomu powłoki (np ssh host ….).

Następnie dodałeś poziom ssh na górze. Jest to w rzeczywistości kolejny poziom powłoki: ssh nie interpretuje samego polecenia, ale przekazuje je powłoce na zdalnym końcu (przez (np.) sh -c …) I ta powłoka interpretuje ciąg znaków.

ssh host 

Proces jest taki sam: weź ciąg, wybierz metodę cytowania, użyj go, umieść go.

Ponowne użycie pojedynczych cudzysłowów:

'sudo su user -c '\''pgrep -fl java | grep -i datanode | awk '\'\\\'\''{print $1}'\'\\\'

Teraz jest jedenaście ciągów, które są interpretowane i łączone:, 'sudo su user -c 'uniknął pojedynczego cytatu, 'pgrep … awk 'uniknął pojedynczego cytatu, uniknął odwrotnego ukośnika, dwóch unikniętych pojedynczych cytatów, pojedynczego cytowanego programu awk , unikniętego pojedynczego cytatu, unikniętego odwrotnego ukośnika i ostatniego unikniętego pojedynczego cytatu .

Ostateczna forma wygląda następująco:

ssh host 'sudo su user -c '\''pgrep -fl java | grep -i datanode | awk '\'\\\'\''{print $1}'\'\\\'

Pisanie ręczne jest trochę niewygodne, ale dosłowny charakter pojedynczego cytowania powłoki ułatwia automatyzację niewielkich zmian:

#!/bin/sh

sq() { # single quote for Bourne shell evaluation
    # Change ' to '\'' and wrap in single quotes.
    # If original starts/ends with a single quote, creates useless
    # (but harmless) '' at beginning/end of result.
    printf '%s\n' "$*" | sed -e "s/'/'\\\\''/g" -e 1s/^/\'/ -e \$s/\$/\'/
}

# Some shells (ksh, bash, zsh) can do something similar with %q, but
# the result may not be compatible with other shells (ksh uses $'...',
# but dash does not recognize it).
#
# sq() { printf %q "$*"; }

ap='{print $1}'
s1="pgrep -fl java | grep -i datanode | awk $(sq "$ap")"
s2="sudo su user -c $(sq "$s1")"

ssh host "$(sq "$s2")"

5
Świetne wyjaśnienie!
Gilles „SO - przestań być zły”,

7

Zobacz odpowiedź Chrisa Johnsena, aby uzyskać jasne, dogłębne wyjaśnienie z ogólnym rozwiązaniem. Dam kilka dodatkowych wskazówek, które pomogą w niektórych typowych okolicznościach.

Pojedyncze cytaty unikają wszystkiego oprócz jednego cytatu. Jeśli więc wiesz, że wartość zmiennej nie zawiera pojedynczego cudzysłowu, możesz bezpiecznie interpolować ją między pojedynczymi cudzysłowami w skrypcie powłoki.

su -c "grep '$pattern' /root/file"  # assuming there is no ' in $pattern

Jeśli twoją lokalną powłoką jest ksh93 lub zsh, możesz poradzić sobie z pojedynczymi cudzysłowami w zmiennej, przepisując je do '\''. (Chociaż bash ma również ${foo//pattern/replacement}konstrukcję, jego obsługa pojedynczych cudzysłowów nie ma dla mnie sensu).

su -c "grep '${pattern//'/'\''}' /root/file"  # if the outer shell is zsh
su -c "grep '${pattern//\'/\'\\\'\'}' /root/file"  # if the outer shell is ksh93

Kolejną wskazówką, aby uniknąć konieczności radzenia sobie z zagnieżdżonym cytowaniem, jest przepuszczanie ciągów przez zmienne środowiskowe w jak największym stopniu. Ssh i sudo mają tendencję do upuszczania większości zmiennych środowiskowych, ale często są skonfigurowane do przepuszczania LC_*, ponieważ są one zwykle bardzo ważne dla użyteczności (zawierają informacje o lokalizacji) i rzadko są uważane za wrażliwe na bezpieczeństwo.

LC_CMD='what you would use locally' ssh $host 'sudo su user -c "$LC_CMD"'

Tutaj, ponieważ LC_CMDzawiera fragment powłoki, musi być dostarczony dosłownie do najbardziej wewnętrznej powłoki. Dlatego zmienna jest rozszerzana przez powłokę bezpośrednio powyżej. Powłoka najbardziej wewnętrzna-jedna widzi "$LC_CMD", a powłoka najbardziej wewnętrzna widzi polecenia.

Podobna metoda jest przydatna do przekazywania danych do narzędzia do przetwarzania tekstu. Jeśli użyjesz interpolacji powłoki, narzędzie potraktuje wartość zmiennej jako polecenie, np. sed "s/$pattern/$replacement/"Nie będzie działać, jeśli zmienne zawierają /. Więc użyj awk (nie sed) i jego -vopcji lub ENVIRONtablicy, aby przekazać dane z powłoki (jeśli przejdziesz ENVIRON, pamiętaj, aby wyeksportować zmienne).

awk -vpattern="$pattern" replacement="$replacement" '{gsub(pattern,replacement); print}'

2

Jak Chris bardzo dobrze opisuje , masz tutaj kilka cytowanych poziomów pośrednich; poinstruować lokalny shellpouczać pilota shellvia sshże powinna pouczać sudopouczyć susię instruować pilota shelluruchomić rurociąg pgrep -fl java | grep -i datanode | awk '{print $1}'jako user. Tego rodzaju polecenie wymaga wielu żmudnych działań \'"quote quoting"\'.

Jeśli posłuchasz mojej rady, porzucisz wszystkie bzdury i zrobisz:

% ssh $host <<REM=LOC_EXPANSION <<'REMOTE_CMD' |
> localhost_data='$(commands run on localhost at runtime)' #quotes don't affect expansion
> more_localhost_data="$(values set at heredoc expansion)" #remote shell will receive m_h_d="result"
> REM=LOC_EXPANSION
> commands typed exactly as if located at 
> the remote terminal including variable 
> "${{more_,}localhost_data}" operations
> 'quotes and' \all possibly even 
> a\wk <<'REMOTELY_INVOKED_HEREDOC' |
> {as is often useful with $awk
> so long as the terminator for}
> REMOTELY_INVOKED_HEREDOC
> differs from that of REM=LOC_EXPANSION and
> REMOTE_CMD
> and here you can | pipeline operate on |\
> any output | received from | ssh as |\
> run above | in your local | terminal |\
> however | tee > ./you_wish.result
<desired output>

WIĘCEJ:

Sprawdź moją (być może zbyt długą) odpowiedź na ścieżki rurociągów z różnymi rodzajami cytatów dotyczących zastępowania cięciami, w których omawiam niektóre teorie leżące u podstaw tego, dlaczego to działa.

-Mikrofon


Wygląda to interesująco, ale nie mogę go uruchomić. Czy możesz opublikować minimalny działający przykład?
John Lawrence Aspden

Uważam, że ten przykład wymaga zsh, ponieważ używa wielu przekierowań do standardowego wejścia. W innych pociskach podobnych do Bourne'a druga <<po prostu zastępuje pierwszą. Czy powinno być gdzieś napisane „tylko zsh”, czy może coś przeoczyłem? (Sprytna sztuczka, aby mieć heredok, który częściowo podlega lokalnej ekspansji)

Oto wersja kompatybilna z bash: unix.stackexchange.com/questions/422489/…
dabest1

0

Co powiesz na użycie więcej podwójnych cudzysłowów?

W takim razie ssh $host $CMDpowinieneś dobrze pracować z tym:

CMD="pgrep -fl java | grep -i datanode | awk '{print $1}'"

Teraz do bardziej złożonego - ssh $host "sudo su user -c \"$CMD\"". Chyba wszystko co musisz zrobić, to uciec znaków wrażliwe na CMD: $, \i ". Więc chciałbym spróbować i zobaczyć, czy to działa: echo $CMD | sed -e 's/[$\\"]/\\\1/g'.

Jeśli to wygląda OK, zawiń echo + sed w funkcję powłoki i dobrze jest zacząć ssh $host "sudo su user -c \"$(escape_my_var $CMD)\"".

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.