Tratar con múltiples niveles de citas (realmente, múltiples niveles de análisis / interpretación) puede ser complicado. Ayuda a tener en cuenta algunas cosas:
- Cada "nivel de cita" puede implicar 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, generalmente es más fácil trabajar "de abajo hacia arriba" (es decir, de adentro hacia afuera).
Niveles de cotización
Veamos tus 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 de expresión regular 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 (citación de shell 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 ese shell interpreta la cadena.
ssh host "sudo su user -c …"
Luego, preguntó sobre 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 anidamiento ( awk → shell, shell → shell ( ssh ), shell → shell ( su user -c ), por lo que le aconsejo que utilice el enfoque "bottom, up". Asumiré que sus shells son compatibles con Bourne (por ejemplo , sh , ash , dash , 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
- Formule la cadena que desea representar en el nivel más interno.
- Seleccione un mecanismo de citas del repertorio de citas del siguiente idioma más alto.
- Cite la cadena deseada según el mecanismo de cotización seleccionado.
- A menudo hay muchas variaciones sobre cómo aplicar qué mecanismo de cotización. Hacerlo a mano suele ser una cuestión de práctica y experiencia. Al hacerlo programáticamente, generalmente es mejor elegir el más fácil de corregir (generalmente el "más literal" (menos escapes)).
- Opcionalmente, use la cadena citada resultante con código adicional.
- Si aún no ha alcanzado el nivel deseado de cita / interpretación, tome la cadena citada resultante (más cualquier código agregado) y úsela como la cadena inicial en el paso 2.
Citando la semántica varía
Lo que hay que tener en cuenta aquí es que cada idioma (nivel de comillas) puede proporcionar semánticas ligeramente diferentes (o incluso semánticas drásticamente diferentes) al mismo carácter de comillas.
La mayoría de los idiomas tienen un mecanismo de cita "literal", pero varían exactamente en su literalidad. La comilla simple de los shells tipo Bourne es literal (lo que significa que no se puede usar 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 con comillas simples de forma no literal (específicamente, \\
y \'
dan como resultado \
y '
, pero otras secuencias de barra invertida son en realidad literales).
Deberá leer la documentación de cada uno de sus idiomas para comprender sus reglas de comillas y la sintaxis general.
Su ejemplo
El nivel más interno de su ejemplo es un programa awk .
{print $1}
Vas a incrustar esto en una línea de comando de shell:
pgrep -fl java | grep -i datanode | awk …
Tenemos que proteger (como mínimo) el espacio y el $
en el awk programa. La opción obvia es usar comillas simples en el shell alrededor de todo el programa.
Sin embargo, hay otras opciones:
{print\ \$1}
escapar directamente del espacio y $
{print' $'1}
comilla simple solo el espacio y $
"{print \$1}"
comillas dobles el conjunto y escapar del $
{print" $"1}
comillas dobles solo el espacio y $
Esto puede estar doblando un poco las reglas (sin escapes $
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 tendríamos que citar o escapar de la coma o las llaves para evitar la "expansión de llaves" en algunos shells.
Lo seleccionamos '{print $1}'
e incrustamos en el resto del "código" del 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 igual que some-shell -c …
(excepto que se ejecuta bajo algún otro UID), por lo que su simplemente agrega otro nivel de shell. sudo no interpreta sus argumentos, por lo que no agrega ningún nivel de comillas.
Necesitamos otro nivel de shell para nuestra cadena de comandos. Podemos elegir comillas simples nuevamente, pero tenemos que dar un manejo 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 simples ( pgrep … awk
), una comilla simple con escape, el programa awk con comillas simples, otra comilla simple con comillas.
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, escapar del $
'pgrep -fl java | grep -i datanode | awk '"'"'{print \$1}'"'"
su 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 citas 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 (p ssh host …
. Ej .).
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í, sino que lo entrega a un shell en el extremo remoto (a través de (por ejemplo) sh -c …
) y ese shell interpreta la cadena.
ssh host …
El proceso es el mismo: toma la cadena, elige un método de comillas, úsala e incrúpela.
Usando comillas simples de nuevo:
'sudo su user -c '\''pgrep -fl java | grep -i datanode | awk '\'\\\'\''{print $1}'\'\\\'
Ahora hay once cadenas que se interpretan y se concatenan: 'sudo su user -c '
comillas simples escapadas, comillas simples 'pgrep … awk '
escapadas, barras invertidas escapadas, dos comillas simples escapadas, el programa awk con comillas simples, comillas simples escapadas, barras invertidas escapadas y comillas simples escapadas finales .
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 las comillas simples del shell facilita la automatización de una ligera 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")"
Vea la respuesta de Chris Johnsen para una explicación clara y profunda con una solución general. Voy a dar algunos consejos adicionales que ayudan en algunas circunstancias comunes.
Las comillas simples escapan de todo menos una comilla simple. Entonces, si sabe que el valor de una variable no incluye ninguna comilla simple, puede interpolarla de manera segura entre comillas simples en un script de shell.
Si su shell local es ksh93 o zsh, puede hacer frente a comillas simples en la variable reescribiéndolas en
'\''
. (Aunque bash también tiene la${foo//pattern/replacement}
construcción, su manejo de comillas simples no tiene sentido para mí).Otro consejo para evitar tener que lidiar con citas anidadas es pasar cadenas a través de variables de entorno tanto como sea posible. Ssh y sudo tienden a descartar la mayoría de las variables de entorno, pero a menudo están configuradas para dejar
LC_*
pasar, porque normalmente son muy importantes para la usabilidad (contienen información local) y rara vez se consideran sensibles a la seguridad.Aquí, desde
LC_CMD
contiene un fragmento de shell, debe proporcionarse literalmente al shell más interno. Por lo tanto, la variable se expande por el shell inmediatamente anterior. 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 utiliza la interpolación de shell, la utilidad tratará el valor de la variable como un comando, por ejemplo
sed "s/$pattern/$replacement/"
, no funcionará si las variables contienen/
. Por lo tanto, use awk (no sed) y su-v
opción o laENVIRON
matriz para pasar datos desde el shell (si lo haceENVIRON
, recuerde exportar las variables).fuente
Como Chris Johnson describe muy bien , aquí tiene algunos niveles de indirección citada; le indica a su local
shell
que instruya al control remoto ashell
través de lossh
que debe instruirsudo
asu
que instruya al control remotoshell
para que ejecute su canalizaciónpgrep -fl java | grep -i datanode | awk '{print $1}'
comouser
. Ese tipo de comando requiere mucho tedioso\'"quote quoting"\'
.Si sigues mi consejo, renunciarás a todas las tonterías y harás:
PARA MÁS:
Verifique mi respuesta (quizás demasiado larga) a las rutas de tuberías con diferentes tipos de comillas para la sustitución de barras en la que analizo algunas de las teorías detrás de por qué eso funciona.
-Micro
fuente
<<
simplemente reemplaza al primero. ¿Debería decir "zsh only" en alguna parte, o me perdí algo? (Truco inteligente, sin embargo, para tener un heredoc que está parcialmente sujeto a la expansión local)¿Qué tal usar más comillas dobles?
Entonces
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\""
. Creo que todo lo que tiene que hacer es escapar caracteres sensibles enCMD
:$
,\
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 ya está listo
ssh $host "sudo su user -c \"$(escape_my_var $CMD)\""
.fuente