Citant dans les constructions de type ssh $ host $ FOO et ssh $ host «sudo su user -c $ FOO»

30

Je finis souvent par émettre des commandes complexes sur ssh; ces commandes impliquent un canalisation vers awk ou perl sur une ligne, et contiennent par conséquent des guillemets simples et des $. Je n'ai ni réussi à trouver une règle dure et rapide pour faire correctement la citation, ni trouvé une bonne référence pour cela. Par exemple, tenez compte des éléments suivants:

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

(Notez les guillemets supplémentaires dans l'instruction awk.)

Mais comment puis-je faire fonctionner cela, par exemple ssh $host "sudo su user -c '$CMD'"? Existe-t-il une recette générale pour gérer les devis dans de tels scénarios? ..

Leo Alekseyev
la source

Réponses:

35

Faire face à plusieurs niveaux de citation (vraiment, plusieurs niveaux d'analyse / d'interprétation) peut devenir compliqué. Il est utile de garder quelques points à l'esprit:

  • Chaque «niveau de cotation» peut potentiellement impliquer une langue différente.
  • Les règles de citation varient selon la langue.
  • Lorsqu'il s'agit de plus d'un ou de deux niveaux imbriqués, il est généralement plus facile de travailler «de bas en haut» (c'est-à-dire de l'intérieur à l'extérieur).

Niveaux de cotation

Examinons vos exemples de commandes.

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

Votre premier exemple de commande (ci-dessus) utilise quatre langages: votre shell, le regex dans pgrep , le regex dans grep (qui peut être différent du langage regex dans pgrep ) et awk . Il y a deux niveaux d'interprétation impliqués: le shell et un niveau après le shell pour chacune des commandes impliquées. Il n'y a qu'un seul niveau explicite de citation (citation de shell dans awk ).

ssh host 

Ensuite, vous avez ajouté un niveau de ssh en haut. Il s'agit en fait d'un autre niveau de shell: ssh n'interprète pas la commande elle-même, elle la remet à un shell à l'extrémité distante (via (par exemple) sh -c …) et ce shell interprète la chaîne.

ssh host "sudo su user -c …"

Ensuite, vous avez demandé d'ajouter un autre niveau de shell au milieu en utilisant su (via sudo , qui n'interprète pas ses arguments de commande, nous pouvons donc l'ignorer). À ce stade, vous avez trois niveaux d'imbrication en cours ( awk → shell, shell → shell ( ssh ), shell → shell ( su user -c ), donc je vous conseille d'utiliser l'approche "bottom, up". Je suppose que vos coquilles sont compatibles avec Bourne (par exemple sh , ash , dash , ksh , bash , zsh , etc.) Un autre type de coquille ( poisson , rc, etc.) peut nécessiter une syntaxe différente, mais la méthode s'applique toujours.

De bas en haut

  1. Formulez la chaîne que vous souhaitez représenter au niveau le plus intérieur.
  2. Sélectionnez un mécanisme de citation dans le répertoire de citation de la prochaine langue la plus élevée.
  3. Citez la chaîne souhaitée en fonction du mécanisme de citation que vous avez sélectionné.
    • Il existe souvent de nombreuses variantes sur la façon d'appliquer quel mécanisme de cotation. Le faire à la main est généralement une question de pratique et d'expérience. Lorsque vous le faites par programme, il est généralement préférable de choisir le plus facile à faire (généralement le «plus littéral» (le moins d'échappatoires)).
  4. Facultativement, utilisez la chaîne entre guillemets résultante avec du code supplémentaire.
  5. Si vous n'avez pas encore atteint le niveau souhaité de citation / interprétation, prenez la chaîne citée résultante (plus tout code ajouté) et utilisez-la comme chaîne de départ à l'étape 2.

Citer la sémantique varie

La chose à garder à l'esprit ici est que chaque langue (niveau de citation) peut donner une sémantique légèrement différente (ou même une sémantique radicalement différente) au même caractère de citation.

La plupart des langues ont un mécanisme de citation «littéral», mais elles varient exactement dans la mesure où elles sont littérales. La citation simple des shells de type Bourne est en fait littérale (ce qui signifie que vous ne pouvez pas l'utiliser pour citer elle-même un seul caractère de citation). D'autres langages (Perl, Ruby) sont moins littéraux en ce sens qu'ils interprètent certaines séquences de barre oblique inversée à l'intérieur de régions entre guillemets simples de manière non littérale (en particulier, \\et \'résultent en \et ', mais d'autres séquences de barre oblique inverse sont en réalité littérales).

Vous devrez lire la documentation de chacune de vos langues pour comprendre ses règles de citation et la syntaxe globale.

Votre exemple

Le niveau le plus profond de votre exemple est un programme awk .

{print $1}

Vous allez intégrer cela dans une ligne de commande shell:

pgrep -fl java | grep -i datanode | awk 

Nous devons protéger (au minimum) l'espace et $dans le awk programme. Le choix évident est d'utiliser des guillemets simples dans le shell autour de l'ensemble du programme.

  • '{print $1}'

Il existe cependant d'autres choix:

  • {print\ \$1} échapper directement à l'espace et $
  • {print' $'1} guillemet simple seulement l'espace et $
  • "{print \$1}" citer deux fois le tout et échapper à la $
  • {print" $"1}double guillemet uniquement l'espace et $
    cela peut plier un peu les règles (non échappé $à la fin d'une chaîne entre guillemets est littéral), mais il semble fonctionner dans la plupart des shells.

Si le programme utilisait une virgule entre les accolades ouvertes et fermées, nous devions également citer ou échapper à la virgule ou aux accolades pour éviter «l'expansion des accolades» dans certains coques.

Nous le sélectionnons '{print $1}'et l'intégrons dans le reste du «code» du shell:

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

Ensuite, vous vouliez l'exécuter via su et sudo .

sudo su user -c 

su user -c …est juste comme some-shell -c …(sauf sous un autre UID), donc su ajoute juste un autre niveau de shell. sudo n'interprète pas ses arguments, il n'ajoute donc aucun niveau de citation.

Nous avons besoin d'un autre niveau de shell pour notre chaîne de commande. Nous pouvons à nouveau choisir des guillemets simples, mais nous devons accorder une attention particulière aux guillemets simples existants. La façon habituelle ressemble à ceci:

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

Il y a quatre chaînes ici que le shell interprétera et concaténera: la première chaîne entre guillemets simples ( pgrep … awk), une citation simple échappée, le programme awk entre guillemets simples, une autre citation unique échappée.

Il existe bien sûr de nombreuses alternatives:

  • pgrep\ -fl\ java\ \|\ grep\ -i\ datanode\ \|\ awk\ \'{print\ \$1} échapper à tout ce qui est important
  • pgrep\ -fl\ java\|grep\ -i\ datanode\|awk\ \'{print\$1}le même, mais sans espace blanc superflu (même dans le programme awk !)
  • "pgrep -fl java | grep -i datanode | awk '{print \$1}'" citez le tout, échappez à la $
  • 'pgrep -fl java | grep -i datanode | awk '"'"'{print \$1}'"'"votre variation; un peu plus long que la façon habituelle en raison de l'utilisation de guillemets doubles (deux caractères) au lieu d'échappements (un caractère)

L'utilisation de citations différentes dans le premier niveau permet d'autres variations à ce niveau:

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

L'incorporation de la première variation dans la ligne de commande sudo / * su * donne ceci:

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

Vous pouvez utiliser la même chaîne dans n'importe quel autre contexte de niveau shell unique (par exemple ssh host …).

Ensuite, vous avez ajouté un niveau de ssh en haut. Il s'agit en fait d'un autre niveau de shell: ssh n'interprète pas la commande elle-même, mais il la remet à un shell à l'extrémité distante (via (par exemple) sh -c …) et ce shell interprète la chaîne.

ssh host 

Le processus est le même: prenez la chaîne, choisissez une méthode de citation, utilisez-la, incorporez-la.

Utiliser à nouveau des guillemets simples:

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

Il y a maintenant onze chaînes qui sont interprétées et concaténées 'sudo su user -c ':, guillemet 'pgrep … awk 'simple échappé,, guillemet simple échappé, barre oblique inversée échappée, deux guillemets simples échappés, programme awk guillemet simple, guillemet simple échappé, barre oblique inversée échappée et guillemet simple échappé final .

La forme finale ressemble à ceci:

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

C'est un peu difficile à taper à la main, mais la nature littérale des guillemets simples du shell facilite l'automatisation d'une légère variation:

#!/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
la source
5
Grande explication!
Gilles 'SO- arrête d'être méchant'
7

Voir la réponse de Chris Johnsen pour une explication claire et approfondie avec une solution générale. Je vais vous donner quelques conseils supplémentaires qui vous aideront dans certaines circonstances courantes.

Les guillemets simples échappent à tout sauf à un guillemet simple. Donc, si vous savez que la valeur d'une variable n'inclut aucun guillemet simple, vous pouvez l'interpoler en toute sécurité entre guillemets simples dans un script shell.

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

Si votre shell local est ksh93 ou zsh, vous pouvez gérer les guillemets simples dans la variable en les réécrivant '\''. (Bien que bash ait également la ${foo//pattern/replacement}construction, sa gestion des guillemets simples n'a pas de sens pour moi.)

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

Une autre astuce pour éviter d'avoir à traiter des citations imbriquées consiste à passer autant que possible des chaînes dans les variables d'environnement. Ssh et sudo ont tendance à supprimer la plupart des variables d'environnement, mais elles sont souvent configurées pour laisser LC_*passer, car elles sont normalement très importantes pour la convivialité (elles contiennent des informations sur les paramètres régionaux) et sont rarement considérées comme sensibles à la sécurité.

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

Ici, puisqu'il LC_CMDcontient un extrait de shell, il doit être fourni littéralement au shell le plus interne. Par conséquent, la variable est développée par le shell immédiatement au-dessus. La coquille "$LC_CMD"la plus intérieure mais une voit , et la coquille la plus intérieure voit les commandes.

Une méthode similaire est utile pour transmettre des données à un utilitaire de traitement de texte. Si vous utilisez l'interpolation shell, l'utilitaire traitera la valeur de la variable comme une commande, par exemple sed "s/$pattern/$replacement/"ne fonctionnera pas si les variables contiennent /. Utilisez donc awk (pas sed), et soit son -voption, soit le ENVIRONtableau pour transmettre les données du shell (si vous passez par là ENVIRON, n'oubliez pas d'exporter les variables).

awk -vpattern="$pattern" replacement="$replacement" '{gsub(pattern,replacement); print}'
Gilles, arrête de faire le mal
la source
2

Comme Chris Johnson le décrit très bien , vous avez ici quelques niveaux d'indirection cités; vous demandez à votre section locale shelld'instruire la télécommande shellvia sshqu'il doit indiquer sudoà sula télécommande shelld'exécuter votre pipeline en pgrep -fl java | grep -i datanode | awk '{print $1}'tant que user. Ce type de commande nécessite beaucoup de travail fastidieux \'"quote quoting"\'.

Si vous suivez mes conseils, vous renoncerez à toutes les absurdités et ferez:

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

POUR PLUS:

Vérifiez ma réponse (peut-être trop longue) aux chemins de tuyauterie avec différents types de guillemets pour la substitution de barre oblique dans laquelle je discute une partie de la théorie derrière pourquoi cela fonctionne.

-Mike

mikeserv
la source
Cela semble intéressant, mais je ne peux pas le faire fonctionner. Pourriez-vous publier un exemple de travail minimal?
John Lawrence Aspden,
Je crois que cet exemple nécessite zsh car il utilise plusieurs redirections vers stdin. Dans d'autres obus de type Bourne, le second <<remplace simplement le premier. Doit-il dire "zsh uniquement" quelque part, ou ai-je raté quelque chose? (Astuce astucieuse, pour avoir un hérédoc qui est en partie soumis à l'expansion locale)
Voici une version compatible bash: unix.stackexchange.com/questions/422489/…
dabest1
0

Que diriez-vous d'utiliser plus de guillemets doubles?

Ensuite, vous ssh $host $CMDdevriez très bien travailler avec celui-ci:

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

Passons maintenant au plus complexe, le ssh $host "sudo su user -c \"$CMD\"". Je suppose que tout ce que vous avez à faire est échapper à caractères sensibles CMD: $, \et ". Donc , je vais essayer et voir si cela fonctionne: echo $CMD | sed -e 's/[$\\"]/\\\1/g'.

Si cela semble correct, enveloppez l'écho + sed dans une fonction shell, et vous êtes prêt à continuer ssh $host "sudo su user -c \"$(escape_my_var $CMD)\"".

alex
la source