Citando nas construções do tipo ssh $ host $ FOO e ssh $ host “sudo su user -c $ FOO”

30

Costumo emitir comandos complexos sobre ssh; esses comandos envolvem canalizações para awk ou perl one-lines e, como resultado, contêm aspas simples e $ 's. Não consegui descobrir uma regra rígida e rápida para fazer a citação corretamente, nem encontrei uma boa referência para isso. Por exemplo, considere o seguinte:

# 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}'"'"

(Observe as aspas extras na instrução awk.)

Mas como faço para que isso funcione, por exemplo ssh $host "sudo su user -c '$CMD'"? Existe uma receita geral para gerenciar cotações em tais cenários?

Leo Alekseyev
fonte

Respostas:

35

Lidar com vários níveis de citação (realmente, vários níveis de análise / interpretação) pode ser complicado. Isso ajuda a manter algumas coisas em mente:

  • Cada "nível de citação" pode envolver um idioma diferente.
  • As regras de cotação variam de acordo com o idioma.
  • Ao lidar com mais de um ou dois níveis aninhados, geralmente é mais fácil trabalhar “de baixo para cima” (ou seja, da parte interna para a externa).

Níveis de cotação

Vamos dar uma olhada nos seus comandos de exemplo.

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

Seu primeiro exemplo de comando (acima) usa quatro idiomas: seu shell, o regex no pgrep , o regex no grep (que pode ser diferente do idioma do regex no pgrep ) e o awk . Existem dois níveis de interpretação envolvidos: o shell e um nível após o shell para cada um dos comandos envolvidos. Há apenas um nível explícito de citação (shell citando no awk ).

ssh host 

Em seguida, você adicionou um nível de ssh na parte superior. Este é efetivamente outro nível de shell: o ssh não interpreta o comando em si, entrega-o a um shell na extremidade remota (via (por exemplo) sh -c …) e esse shell interpreta a string.

ssh host "sudo su user -c …"

Então você perguntou sobre adicionar outro nível de shell no meio usando su (via sudo , que não interpreta seus argumentos de comando, para que possamos ignorá-lo). Neste ponto, você tem três níveis de aninhamento ( awk → shell, shell → shell ( ssh ), shell → shell ( su user -c ), portanto, aconselho o uso da abordagem "bottom-up". suas conchas são compatíveis com Bourne (por exemplo , sh , cinza , traço , ksh , bash , zsh , etc.) Algum outro tipo de concha ( peixe , rc, etc.) podem exigir sintaxe diferente, mas o método ainda se aplica.

Debaixo para cima

  1. Formule a sequência que você deseja representar no nível mais interno.
  2. Selecione um mecanismo de citação no repertório de citação do próximo idioma mais alto.
  3. Cite a sequência desejada de acordo com o mecanismo de cotação selecionado.
    • Muitas vezes, existem muitas variações de como aplicar qual mecanismo de cotação. Fazer isso manualmente é geralmente uma questão de prática e experiência. Ao fazer isso de maneira programática, geralmente é melhor escolher o mais fácil de acertar (geralmente o "mais literal" (o menor número de fugas)).
  4. Opcionalmente, use a string entre aspas resultante com código adicional.
  5. Se você ainda não atingiu o nível desejado de citação / interpretação, pegue a sequência entre aspas resultante (mais qualquer código adicionado) e use-a como a sequência inicial na etapa 2.

A citação da semântica varia

O que deve ser lembrado aqui é que cada idioma (nível de citação) pode fornecer semântica ligeiramente diferente (ou mesmo semântica drasticamente diferente) ao mesmo caractere de citação.

A maioria dos idiomas possui um mecanismo de citação "literal", mas eles variam exatamente em como são literais. A citação simples de shells tipo Bourne é realmente literal (o que significa que você não pode usá-lo para citar um caractere de aspas simples). Outros idiomas (Perl, Ruby) são menos literais, pois interpretam algumas seqüências de barra invertida dentro de regiões entre aspas simples não literalmente (especificamente, \\e \'resultam em \e ', mas outras seqüências de barra invertida são realmente literais).

Você precisará ler a documentação de cada um dos seus idiomas para entender suas regras de cotação e a sintaxe geral.

Seu exemplo

O nível mais interno do seu exemplo é um programa awk .

{print $1}

Você vai incorporar isso em uma linha de comando do shell:

pgrep -fl java | grep -i datanode | awk 

Precisamos proteger (no mínimo) o espaço eo $no awk programa. A escolha óbvia é usar aspas simples no shell em todo o programa.

  • '{print $1}'

Existem outras opções, porém:

  • {print\ \$1} escapar diretamente do espaço e $
  • {print' $'1} aspas simples apenas o espaço e $
  • "{print \$1}" aspas duplas do todo e escapar do $
  • {print" $"1}aspas duplas apenas o espaço e $
    Isso pode estar dobrando um pouco as regras (sem escape $no final de uma cadeia de caracteres entre aspas duplas é literal), mas parece funcionar na maioria dos shells.

Se o programa usasse uma vírgula entre as chaves de abertura e fechamento, também precisaríamos citar ou escapar da vírgula ou das chaves para evitar a "expansão da chave" em algumas conchas.

Nós escolhemos '{print $1}'e incorporamos no restante do “código” do shell:

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

Em seguida, você queria executar isso via su e sudo .

sudo su user -c 

su user -c …é exatamente como some-shell -c …(exceto executando sob outro UID), então su apenas adiciona outro nível de shell. O sudo não interpreta seus argumentos, portanto, não adiciona nenhum nível de citação.

Precisamos de outro nível de shell para nossa cadeia de comandos. Podemos escolher aspas simples novamente, mas precisamos dar um tratamento especial às aspas simples existentes. A maneira usual é assim:

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

Existem quatro strings aqui que o shell interpretará e concatenará: a primeira string entre aspas simples ( pgrep … awk), uma aspas simples com escape, o programa awk com aspas simples , outra aspas simples com escape.

Existem, é claro, muitas alternativas:

  • pgrep\ -fl\ java\ \|\ grep\ -i\ datanode\ \|\ awk\ \'{print\ \$1} escapar de tudo que é importante
  • pgrep\ -fl\ java\|grep\ -i\ datanode\|awk\ \'{print\$1}o mesmo, mas sem espaço em branco supérfluo (mesmo no programa awk !)
  • "pgrep -fl java | grep -i datanode | awk '{print \$1}'" aspas duplas a coisa toda, escape do $
  • 'pgrep -fl java | grep -i datanode | awk '"'"'{print \$1}'"'"sua variação; um pouco mais do que o habitual devido ao uso de aspas duplas (dois caracteres) em vez de escapes (um caractere)

O uso de aspas diferentes no primeiro nível permite outras variações nesse nível:

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

A incorporação da primeira variação na linha de comando sudo / * su * fornece:

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

Você pode usar a mesma string em qualquer outro contexto de nível de shell único (por exemplo ssh host …).

Em seguida, você adicionou um nível de ssh na parte superior. Este é efetivamente outro nível de shell: o ssh não interpreta o comando em si, mas o entrega a um shell na extremidade remota (via (por exemplo) sh -c …) e esse shell interpreta a string.

ssh host 

O processo é o mesmo: pegue a string, escolha um método de citação, use-o, incorpore-o.

Usando aspas simples novamente:

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

Agora, existem onze cadeias de caracteres que são interpretadas e concatenadas 'sudo su user -c ':, aspas simples escapadas, aspas simples 'pgrep … awk 'escapadas, barra invertida escapada, duas aspas simples escapadas, o programa awk entre aspas simples , uma aspas simples escapada, uma barra invertida escapada e uma única aspira final escapada .

A forma final é assim:

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

É um pouco difícil de digitar manualmente, mas a natureza literal das aspas simples do shell facilita a automação de uma pequena variação:

#!/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")"
Chris Johnsen
fonte
5
Ótima explicação!
Gilles 'SO- stop be evil'
7

Veja a resposta de Chris Johnsen para uma explicação clara e aprofundada com uma solução geral. Vou dar algumas dicas extras que ajudam em algumas circunstâncias comuns.

As aspas simples escapam de tudo, exceto uma única. Portanto, se você souber que o valor de uma variável não inclui aspas simples, é possível interpolá-lo com segurança entre aspas simples em um script de shell.

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

Se o seu shell local for ksh93 ou zsh, você poderá lidar com aspas simples na variável reescrevendo-as para '\''. (Embora o bash também tenha a ${foo//pattern/replacement}construção, seu tratamento de aspas simples não faz sentido para mim.)

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

Outra dica para evitar ter que lidar com aspas aninhadas é passar seqüências de caracteres pelas variáveis ​​de ambiente o máximo possível. Ssh e sudo tendem a descartar a maioria das variáveis ​​de ambiente, mas geralmente são configuradas para permitir a LC_*passagem, porque normalmente são muito importantes para a usabilidade (contêm informações de localidade) e raramente são consideradas sensíveis à segurança.

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

Aqui, como LC_CMDcontém um trecho de shell, ele deve ser fornecido literalmente ao shell mais interno. Portanto, a variável é expandida pelo shell imediatamente acima. O shell mais interno mas um vê "$LC_CMD", e o shell mais interno vê os comandos.

Um método semelhante é útil para passar dados para um utilitário de processamento de texto. Se você usar interpolação de shell, o utilitário tratará o valor da variável como um comando, por exemplo sed "s/$pattern/$replacement/", não funcionará se as variáveis ​​contiverem /. Portanto, use awk (não sed) e sua -vopção ou o ENVIRONarray para transmitir dados do shell (se você passar por isso ENVIRON, lembre-se de exportar as variáveis).

awk -vpattern="$pattern" replacement="$replacement" '{gsub(pattern,replacement); print}'
Gilles 'SO- parar de ser mau'
fonte
2

Como Chris Johnson descreve muito bem , você tem alguns níveis de indireção citada aqui; você instrui seu local shella instruir o controle remoto shellvia sshque ele deve sudoinstruir supara instruir o controle remoto shella executar seu pipeline pgrep -fl java | grep -i datanode | awk '{print $1}'como user. Esse tipo de comando requer muito tedioso \'"quote quoting"\'.

Se você seguir meu conselho, renunciará a toda bobagem e fará:

% 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>

PARA MAIS:

Verifique minha resposta (talvez exagerada) aos caminhos de tubulação com diferentes tipos de cotações para substituição de barra, na qual discuto algumas das teorias por trás de por que isso funciona.

-Mike

mikeserv
fonte
Parece interessante, mas não consigo fazê-lo funcionar. Você poderia postar um exemplo de trabalho mínimo?
John Lawrence Aspden
Eu acredito que este exemplo requer o zsh, pois usa vários redirecionamentos para o stdin. Em outras conchas tipo Bourne, a segunda <<simplesmente substitui a primeira. Deveria dizer "zsh only" em algum lugar ou perdi alguma coisa? (Inteligente truque, porém, ter um heredoc que é parcialmente sujeitos a expansão local)
Aqui está uma versão compatível com o bash: unix.stackexchange.com/questions/422489/…
dabest1:
0

Que tal usar mais aspas duplas?

Então você ssh $host $CMDdeve funcionar bem com este:

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

Agora, para o mais complexo, o ssh $host "sudo su user -c \"$CMD\"". Eu acho que tudo que você tem a fazer é escapar caracteres sensíveis em CMD: $, \e ". Então, eu ia tentar e ver se isso funciona: echo $CMD | sed -e 's/[$\\"]/\\\1/g'.

Se isso parecer bom, envolva o echo + sed em uma função shell e você estará pronto para usá-lo ssh $host "sudo su user -c \"$(escape_my_var $CMD)\"".

alex
fonte