it-swarm-es.com

Citando en las construcciones de tipo ssh $ Host $ FOO y ssh $ Host "Sudo su user -c $ FOO"

A menudo termino emitiendo comandos complejos sobre ssh; estos comandos implican canalizar a una línea awk o Perl y, como resultado, contienen comillas simples y $. No he podido descifrar una regla estricta y rápida para hacer las citas correctamente, ni he encontrado una buena referencia para ello. Por ejemplo, considere lo siguiente:

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

(Tenga en cuenta las comillas adicionales en la declaración awk).

Pero, ¿cómo hago para que esto funcione? ssh $Host "Sudo su user -c '$CMD'"? ¿Existe una receta general para administrar cotizaciones en tales escenarios? ...

31
Leo Alekseyev

Tratar con múltiples niveles de citas (en realidad, múltiples niveles de análisis/interpretación) puede resultar complicado. Es útil tener en cuenta algunas cosas:

  • Cada "nivel de cotización" puede involucrar potencialmente un idioma diferente.
  • Las reglas de cotización varían según el idioma.
  • Cuando se trata de más de uno o dos niveles anidados, suele ser más fácil trabajar "de abajo hacia arriba" (es decir, del más interior al más exterior).

Niveles de cotización

Veamos sus comandos de ejemplo.

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

Su primer comando de ejemplo (arriba) usa cuatro idiomas: su Shell, la expresión regular en pgrep , la expresión regular en grep (que puede ser diferente del lenguaje regex en pgrep ) y awk . Hay dos niveles de interpretación involucrados: el Shell y un nivel después del Shell para cada uno de los comandos involucrados. Solo hay un nivel explícito de citas (Shell entre comillas en awk ).

ssh Host …

A continuación, agregó un nivel de ssh en la parte superior. Este es efectivamente otro nivel de Shell: ssh no interpreta el comando en sí, lo entrega a un Shell en el extremo remoto (a través de (por ejemplo) sh -c …) y que Shell interpreta la cadena.

ssh Host "Sudo su user -c …"

Luego preguntó acerca de agregar otro nivel de Shell en el medio usando su (a través de Sudo , que no interpreta sus argumentos de comando, por lo que podemos ignorarlo). En este punto, tiene tres niveles de anidación en curso ( awk → Shell, Shell → Shell ( ssh ), Shell → Shell ( su user -c ), por lo que recomiendo usar el enfoque "de abajo hacia arriba". Asumiré que sus shells son compatibles con Bourne (por ejemplo, sh , ash , guión , ksh , bash , zsh , etc.). Algún otro tipo de Shell ( fish , rc , etc.) pueden requerir una sintaxis diferente, pero el método aún se aplica.

De abajo hacia arriba

  1. Formule la cadena que desea representar en el nivel más interno.
  2. Seleccione un mecanismo de citas del repertorio de citas del siguiente idioma más alto.
  3. Cotice la cadena deseada de acuerdo con su mecanismo de cotización seleccionado.
    • A menudo hay muchas variaciones sobre cómo aplicar qué mecanismo de cotización. Hacerlo a mano suele ser cuestión de práctica y experiencia. Al hacerlo de manera programática, generalmente es mejor elegir el más fácil de hacer bien (generalmente el "más literal" (menos escapes)).
  4. Opcionalmente, use la cadena entre comillas resultante con código adicional.
  5. Si aún no ha alcanzado el nivel deseado de citación/interpretación, tome la cadena entre comillas resultante (más cualquier código agregado) y utilícela como cadena de inicio en el paso 2.

La semántica de las citas varía

Lo que hay que tener en cuenta aquí es que cada idioma (nivel de comillas) puede dar una semántica ligeramente diferente (o incluso una semántica drásticamente diferente) al mismo carácter de comillas.

La mayoría de los idiomas tienen un mecanismo de citación "literal", pero varían exactamente en lo literal que son. La comilla simple de conchas tipo Bourne es en realidad literal (lo que significa que no puede usarla para citar un carácter de comilla simple). Otros lenguajes (Perl, Ruby) son menos literales en el sentido de que interpretan algunas secuencias de barra invertida dentro de regiones entre comillas simples de forma no literal (específicamente, \\ y \' resulta en \ y ', pero otras secuencias de barra invertida son realmente literales).

Tendrá que leer la documentación de cada uno de sus idiomas para comprender sus reglas de cotización y la sintaxis general.

Tu ejemplo

El nivel más interno de su ejemplo es un programa awk .

{print $1}

Vas a incrustar esto en una línea de comandos de Shell:

pgrep -fl Java | grep -i datanode | awk …

Necesitamos proteger (como mínimo) el espacio y el $ en el programa awk . La opción obvia es utilizar comillas simples en el Shell en todo el programa.

  • '{print $1}'

Sin embargo, hay otras opciones:

  • {print\ \$1} escapar directamente del espacio y $
  • {print' $'1} comillas simples solo el espacio y $
  • "{print \$1}" comillas dobles el todo y no el $
  • {print" $"1} comillas dobles solo el espacio y $
    Esto puede estar doblando un poco las reglas (sin escapar $ al final de una cadena entre comillas dobles es literal), pero parece funcionar en la mayoría de los shells.

Si el programa usara una coma entre las llaves abiertas y cerradas, también necesitaríamos citar o escapar de la coma o de las llaves para evitar la “expansión de llaves” en algunos shells.

Elegimos '{print $1}' e incrustarlo en el resto del "código" de Shell:

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

A continuación, quería ejecutar esto a través de su y Sudo .

Sudo su user -c …

su user -c … es como some-Shell -c … (excepto que se ejecuta bajo algún otro UID), entonces su simplemente agrega otro nivel de Shell. Sudo no interpreta sus argumentos, por lo que no agrega ningún nivel de cotización.

Necesitamos otro nivel de Shell para nuestra cadena de comandos. Podemos elegir entre comillas simples nuevamente, pero tenemos que dar un tratamiento especial a las comillas simples existentes. La forma habitual se ve así:

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

Aquí hay cuatro cadenas que el Shell interpretará y concatenará: la primera cadena entre comillas (pgrep … awk), una comilla simple de escape, el programa de comillas simples awk , otra comilla simple de escape.

Existen, por supuesto, muchas alternativas:

  • pgrep\ -fl\ Java\ \|\ grep\ -i\ datanode\ \|\ awk\ \'{print\ \$1} escapar de todo lo importante
  • pgrep\ -fl\ Java\|grep\ -i\ datanode\|awk\ \'{print\$1} lo mismo, pero sin espacios en blanco superfluos (incluso en el programa awk !)
  • "pgrep -fl Java | grep -i datanode | awk '{print \$1}'" comillas dobles todo el asunto, escapar del $
  • 'pgrep -fl Java | grep -i datanode | awk '"'"'{print \$1}'"'" tu variación; un poco más de lo habitual debido al uso de comillas dobles (dos caracteres) en lugar de escapes (un carácter)

El uso de cotizaciones diferentes en el primer nivel permite otras variaciones en este nivel:

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

Incrustar la primera variación en la línea de comando Sudo /* su * da esto:

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

Puede usar la misma cadena en cualquier otro contexto de nivel de Shell único (por ejemplo, ssh Host …).

A continuación, agregó un nivel de ssh en la parte superior. Este es efectivamente otro nivel de Shell: ssh no interpreta el comando en sí, pero lo entrega a un Shell en el extremo remoto (a través de (por ejemplo) sh -c …) y que Shell interpreta la cadena.

ssh Host …

El proceso es el mismo: toma la cadena, elige un método de cotización, úsalo, incrustalo.

Usando comillas simples nuevamente:

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

Ahora hay once cadenas que se interpretan y concatenan: 'Sudo su user -c ', comillas simples de escape, 'pgrep … awk ', comillas simples de escape, barra invertida de escape, dos comillas simples de escape, el programa de comillas simples awk , una comilla simple de escape, una barra invertida de escape y un comilla simple escapada final.

La forma final se ve así:

ssh Host 'Sudo su user -c '\''pgrep -fl Java | grep -i datanode | awk '\'\\\'\''{print $1}'\'\\\'

Esto es un poco difícil de escribir a mano, pero la naturaleza literal de la cita única de Shell hace que sea fácil automatizar una pequeña variación:

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

Consulte Respuesta de Chris Johnsen para obtener una explicación clara y detallada con una solución general. Voy a dar algunos consejos adicionales que ayudan en algunas circunstancias comunes.

Las comillas simples escapan a todo menos a una comilla simple. Entonces, si sabe que el valor de una variable no incluye comillas simples, puede interpolarlo de manera segura entre comillas simples en un script de Shell.

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

Si su Shell local es ksh93 o zsh, puede hacer frente a las comillas simples en la variable reescribiéndolas en '\''. (Aunque bash también tiene el ${foo//pattern/replacement} construct, su manejo de comillas simples no tiene sentido para mí.)

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

Otro consejo para evitar tener que lidiar con comillas anidadas es pasar cadenas a través de variables de entorno tanto como sea posible. Ssh y Sudo tienden a eliminar la mayoría de las variables de entorno, pero a menudo están configuradas para permitir que LC_* mediante, porque normalmente son muy importantes para la usabilidad (contienen información de la configuración regional) y rara vez se consideran sensibles a la seguridad.

LC_CMD='what you would use locally' ssh $Host 'Sudo su user -c "$LC_CMD"'

Aquí, desde LC_CMD contiene un fragmento de Shell, debe proporcionarse literalmente al Shell más interno. Por lo tanto, el Shell expande la variable inmediatamente arriba. El Shell más interno pero uno ve "$LC_CMD", y el Shell más interno ve los comandos.

Un método similar es útil para pasar datos a una utilidad de procesamiento de texto. Si usa la interpolación de Shell, la utilidad tratará el valor de la variable como un comando, p. Ej. sed "s/$pattern/$replacement/" no funcionará si las variables contienen /. Entonces use awk (no sed), y su -v o la matriz ENVIRON para pasar datos desde el Shell (si pasa por ENVIRON, recuerde exportar las variables).

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

Como Chris Johnson describe muy bien , tiene algunos niveles de indirección citada aquí; le indica a su Shell local que indique al remoto Shell a través de ssh que debe indicar a Sudo que indique a su que le indique al mando Shell para ejecutar su canalización pgrep -fl Java | grep -i datanode | awk '{print $1}' como user. Ese tipo de comando requiere un montón de tediosos \'"quote quoting"\'.

Si sigue mi consejo, renunciará a todas las tonterías y hará:

% 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 MÁS:

Verifique mi respuesta (quizás demasiado larga) a Tuberías de rutas con diferentes tipos de citas para la sustitución de barras en la que analizo parte de la teoría detrás de por qué funciona.

-Miguel

2
mikeserv

¿Qué tal usar más comillas dobles?

Entonces tu ssh $Host $CMD Debería funcionar bien con este:

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

Ahora al más complejo, el ssh $Host "Sudo su user -c \"$CMD\"". Supongo que todo lo que tienes que hacer es escapar de los caracteres sensibles en CMD: $, \ Y ". Así que trataría de ver si esto funciona: echo $CMD | sed -e 's/[$\\"]/\\\1/g'.

Si eso se ve bien, envuelva el echo + sed en una función de Shell, y estará listo para ir con ssh $Host "Sudo su user -c \"$(escape_my_var $CMD)\"".

0
alex