Citando in ssh $ host $ FOO e ssh $ host "sudo su user -c $ FOO" costrutti


30

Spesso finisco per inviare comandi complessi su ssh; questi comandi comportano il piping su awk o perl di una riga e di conseguenza contengono virgolette singole e $ 's. Non sono stato in grado di capire una regola dura e veloce per fare correttamente la citazione, né ho trovato un buon riferimento per questo. Ad esempio, considerare quanto segue:

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

(Nota le virgolette extra nell'istruzione awk.)

Ma come posso farlo funzionare, ad esempio ssh $host "sudo su user -c '$CMD'"? Esiste una ricetta generale per la gestione delle citazioni in tali scenari? ..

Risposte:


35

Gestire più livelli di quotazione (in realtà, più livelli di analisi / interpretazione) può diventare complicato. Aiuta a tenere a mente alcune cose:

  • Ogni "livello di quotazione" può potenzialmente coinvolgere una lingua diversa.
  • Le regole di citazione variano in base alla lingua.
  • Quando si ha a che fare con più di uno o due livelli nidificati, di solito è più facile lavorare "dal basso verso l'alto" (cioè dal più interno al più esterno).

Livelli di quotazione

Diamo un'occhiata ai tuoi comandi di esempio.

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

Il tuo primo comando di esempio (sopra) usa quattro lingue: la tua shell, il regex in pgrep , il regex in grep (che potrebbe essere diverso dal linguaggio regex in pgrep ) e awk . Esistono due livelli di interpretazione: la shell e un livello dopo la shell per ciascuno dei comandi coinvolti. C'è solo un livello esplicito di quotazione (shell quoting in awk ).

ssh host 

Successivamente hai aggiunto un livello di ssh in cima. Questo è effettivamente un altro livello di shell: ssh non interpreta il comando stesso, lo passa a una shell sull'estremità remota (via (es.) sh -c …) E quella shell interpreta la stringa.

ssh host "sudo su user -c …"

Quindi hai chiesto di aggiungere un altro livello di shell nel mezzo usando su (tramite sudo , che non interpreta i suoi argomenti di comando, quindi possiamo ignorarlo). A questo punto, hai tre livelli di annidamento in corso ( awk → shell, shell → shell ( ssh ), shell → shell ( su user -c ), quindi ti consiglio di usare l'approccio "bottom, up". le tue conchiglie sono compatibili con Bourne (es. sh , ash , dash , ksh , bash , zsh , ecc.) Alcuni altri tipi di shell ( fish , rc, ecc.) potrebbe richiedere una sintassi diversa, ma il metodo è ancora valido.

Dal basso verso l'alto

  1. Formulare la stringa che si desidera rappresentare al livello più interno.
  2. Seleziona un meccanismo di quotazione dal repertorio di citazioni della lingua successiva più alta.
  3. Cita la stringa desiderata in base al meccanismo di quotazione selezionato.
    • Esistono spesso molte varianti su come applicare quale meccanismo di quotazione. Farlo a mano è di solito una questione di pratica ed esperienza. Quando lo si fa in modo programmatico, di solito è meglio scegliere il più facile da ottenere (di solito il "più letterale" (il minor numero di fughe)).
  4. Facoltativamente, utilizzare la stringa tra virgolette risultante con codice aggiuntivo.
  5. Se non hai ancora raggiunto il livello desiderato di quotazione / interpretazione, prendi la stringa tra virgolette risultante (più qualsiasi codice aggiunto) e usala come stringa iniziale nel passaggio 2.

Citando la semantica varia

La cosa da tenere a mente qui è che ogni lingua (livello di quotazione) può dare una semantica leggermente diversa (o anche una semantica drasticamente diversa) allo stesso carattere di citazione.

La maggior parte delle lingue ha un meccanismo di quotazione "letterale", ma varia esattamente in quanto letterale. La singola citazione di conchiglie simili a Bourne è in realtà letterale (il che significa che non è possibile utilizzarla per citare un singolo carattere di citazione stesso). Altre lingue (Perl, Ruby) sono meno letterali in quanto interpretano non letteralmente alcune sequenze di barre rovesciate all'interno di singole regioni tra virgolette (in particolare, \\e \'risultano in \e ', ma altre sequenze di barre rovesciate sono in realtà letterali).

Dovrai leggere la documentazione per ciascuna delle tue lingue per comprendere le regole di quotazione e la sintassi generale.

Il tuo esempio

Il livello più interno del tuo esempio è un programma awk .

{print $1}

Lo incorporerai in una riga di comando della shell:

pgrep -fl java | grep -i datanode | awk 

Abbiamo bisogno di proteggere (come minimo) lo spazio e l' $nel awk programma. La scelta ovvia è quella di utilizzare la virgoletta singola nella shell attorno all'intero programma.

  • '{print $1}'

Ci sono altre scelte però:

  • {print\ \$1} sfuggire direttamente allo spazio e $
  • {print' $'1} citazione unica solo lo spazio e $
  • "{print \$1}" doppia citazione del tutto e sfuggire al $
  • {print" $"1}virgolette solo lo spazio e $
    questo potrebbe piegare un po 'le regole (senza caratteri di escape $alla fine di una stringa tra virgolette doppie è letterale), ma sembra funzionare nella maggior parte delle shell.

Se il programma utilizzava una virgola tra parentesi graffe aperte e chiuse, avremmo anche bisogno di citare o sfuggire alla virgola o alle parentesi graffe per evitare l'espansione delle parentesi graffe in alcune shell.

Lo selezioniamo '{print $1}'e lo incorporiamo nel resto del "codice" della shell:

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

Successivamente, si voleva eseguire questo tramite su e sudo .

sudo su user -c 

su user -c …è come some-shell -c …(tranne che in esecuzione con un altro UID), quindi su aggiunge solo un altro livello di shell. sudo non interpreta i suoi argomenti, quindi non aggiunge alcun livello di quotazione.

Abbiamo bisogno di un altro livello di shell per la nostra stringa di comando. Possiamo scegliere di nuovo le virgolette singole, ma dobbiamo dare una gestione speciale alle virgolette singole esistenti. Il solito modo è simile al seguente:

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

Qui ci sono quattro stringhe che la shell interpreterà e concatenerà: la prima stringa tra virgolette singole ( pgrep … awk), una virgoletta singola con escape, il programma awk con virgolette singole , un'altra virgoletta singola con escape.

Esistono ovviamente molte alternative:

  • pgrep\ -fl\ java\ \|\ grep\ -i\ datanode\ \|\ awk\ \'{print\ \$1} sfuggire a tutto ciò che è importante
  • pgrep\ -fl\ java\|grep\ -i\ datanode\|awk\ \'{print\$1}lo stesso, ma senza spazi bianchi superflui (anche nel programma awk !)
  • "pgrep -fl java | grep -i datanode | awk '{print \$1}'" doppia citazione l'intera cosa, sfuggire al $
  • 'pgrep -fl java | grep -i datanode | awk '"'"'{print \$1}'"'"la tua variazione; un po 'più lungo del solito a causa dell'utilizzo di virgolette doppie (due caratteri) invece di escape (un carattere)

L'uso di quotazioni diverse nel primo livello consente altre variazioni a questo livello:

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

Incorporando la prima variante nella riga di comando sudo / * su * si ottiene questo:

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

È possibile utilizzare la stessa stringa in qualsiasi altro contesto a livello di shell singola (ad es ssh host ….).

Successivamente, hai aggiunto un livello di ssh in cima. Questo è effettivamente un altro livello di shell: ssh non interpreta il comando stesso, ma lo passa a una shell sull'estremità remota (via (es.) sh -c …) E quella shell interpreta la stringa.

ssh host 

Il processo è lo stesso: prendi la stringa, scegli un metodo di quotazione, usalo, incorporalo.

Usando nuovamente le virgolette singole:

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

Ora ci sono undici le stringhe che vengono interpretati e concatenati: 'sudo su user -c ', sfuggito apostrofo, 'pgrep … awk ', sfuggito apostrofo, barra inversa sfuggito, due scapparono virgolette singole, il singolo citato awk programma, un sfuggito apostrofo, una barra rovesciata fuggito, e una finale sfuggito sola offerta .

Il modulo finale è simile al seguente:

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

Questo è un po 'ingombrante da digitare a mano, ma la natura letterale della virgoletta singola della shell rende facile automatizzare una leggera variazione:

#!/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")"


7

Vedi la risposta di Chris Johnsen per una spiegazione chiara e approfondita con una soluzione generale. Ho intenzione di dare alcuni suggerimenti extra che aiutano in alcune circostanze comuni.

Le virgolette singole sfuggono a tutto tranne una virgoletta singola. Quindi, se sai che il valore di una variabile non include alcuna virgoletta, puoi interpolarlo in modo sicuro tra virgolette singole in uno script di shell.

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

Se la shell locale è ksh93 o zsh, è possibile far fronte a virgolette singole nella variabile riscrivendole in '\''. (Anche se bash ha anche il ${foo//pattern/replacement}costrutto, la gestione delle virgolette singole non ha senso per me.)

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

Un altro suggerimento per evitare di dover gestire le quotazioni nidificate è di passare il più possibile le stringhe attraverso le variabili di ambiente. Ssh e sudo tendono a rilasciare la maggior parte delle variabili d'ambiente, ma sono spesso configurate per lasciar LC_*passare, perché di solito sono molto importanti per l'usabilità (contengono informazioni sulla locale) e raramente sono considerate sensibili alla sicurezza.

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

Qui, poiché LC_CMDcontiene uno snippet di shell, deve essere fornito letteralmente alla shell più interna. Pertanto la variabile viene espansa dalla shell immediatamente sopra. La "$LC_CMD"shell più interna vede una e la shell più interna vede i comandi.

Un metodo simile è utile per passare i dati a un'utilità di elaborazione del testo. Se si utilizza l'interpolazione della shell, l'utilità tratterà il valore della variabile come un comando, ad esempio sed "s/$pattern/$replacement/"non funzionerà se le variabili contengono /. Quindi usa awk (non sed) e la sua -vopzione o l' ENVIRONarray per passare i dati dalla shell (se passi attraverso ENVIRON, ricorda di esportare le variabili).

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

2

Come Chris Johnson descrive molto bene , qui ci sono alcuni livelli di riferimento indiretto; indichi al tuo locale shelldi istruire il telecomando shelltramite il sshquale dovrebbe sudoincaricare sudi istruire il telecomando shellper eseguire la pipeline pgrep -fl java | grep -i datanode | awk '{print $1}'come user. Questo tipo di comando richiede molto noioso \'"quote quoting"\'.

Se seguirai il mio consiglio, rinuncerai a tutte le sciocchezze e farai:

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

PER PIÙ:

Controlla la mia (forse troppo prolissa) risposta ai percorsi di tubazioni con diversi tipi di virgolette per la sostituzione della barra in cui discuto alcune delle teorie alla base del perché funzioni.

-Mike


Sembra interessante, ma non riesco a farlo funzionare. Potresti pubblicare un esempio minimo di lavoro?
John Lawrence Aspden,

Credo che questo esempio richieda zsh poiché utilizza reindirizzamenti multipli su stdin. In altre conchiglie simili a Bourne, la seconda <<sostituisce semplicemente la prima. Dovrebbe dire "solo zsh" da qualche parte o mi sono perso qualcosa? (Trucco intelligente, però, per avere una eredità in parte soggetta a espansione locale)

Ecco una versione compatibile bash: unix.stackexchange.com/questions/422489/…
dabest1

0

Che ne dici di usare più doppie virgolette?

Quindi ssh $host $CMDdovresti lavorare bene con questo:

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

Ora a quello più complesso, il ssh $host "sudo su user -c \"$CMD\"". Credo che tutto quello che dovete fare è scappare caratteri sensibili CMD: $, \e ". Quindi mi piacerebbe provare e vedere se questo funziona: echo $CMD | sed -e 's/[$\\"]/\\\1/g'.

Se sembra a posto, avvolgi echo + sed in una funzione shell e sei a posto ssh $host "sudo su user -c \"$(escape_my_var $CMD)\"".

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.