¿Cómo ejecutar una línea de comando compleja a través de ssh?

¿Cómo ejecutar una línea de comando compleja a través de ssh?

A veces quiero que el entorno de un contenedor acoplable se ejecute en un host remoto. Para hacer esto inicio sesión en el host:

ssh [email protected]

y luego ejecuto este comando:

sudo docker exec -it `sudo docker ps | grep mycontainername | awk '{print $1;}'` env

Ahora quiero hacer esto con un solo comando (hazlo más de 3 veces y quiero automatizarlo... :-)).

Esto funciona:

 ssh -t [email protected] sudo docker ps | grep mycontainername

pero cuando hago esto

ssh -t [email protected] sudo docker exec -it `sudo docker ps | grep mycontainername | awk '{print $1;}'` env

yo obtengo

"docker exec" requires at least 2 arguments.
See 'docker exec --help'.

Usage:  docker exec [OPTIONS] CONTAINER COMMAND [ARG...]

Run a command in a running container
Connection to 123.456.789.10 closed.

¿Alguien sabe cómo puedo hacer que esto funcione?

Respuesta1

Observaciones generales

Hay algunas palabras clave en Bash que afectan el análisis de lo que viene después de ellas, por ejemplo [[. Pero sshno es uno de ellos, es un comando normal. Esto significa que:

  • La ssh …línea completa normalmente es analizada por sulocalcaparazón; caracteres como |, ;, *, "o $espacio significan algo para el shell, no llegarán a ssh, a menos que los cite o los escape (con pocas excepciones, por ejemplo, lenguado $como palabra separada no es especial). Este es el primer nivel de análisis e interpretación.

  • Cualesquiera que sean los argumentos a los que lleguen ssh(o cualquier otro comando normal) después de que el shell haga su trabajo, son solo argumentos, cadenas. Ahora es el trabajo de la herramienta interpretarlos. Este es el segundo nivel.

En el caso de sshalgunos (cero, uno o más) de sus argumentos de línea de comando, se interpretan como un comando que se ejecutará en el lado del servidor. En general sshes capaz de construir un comando a partir de muchos argumentos. El efecto es como si invocaras algo como esto en el servidor:

"$SHELL" -c "$command_line_built_by_ssh"

(No estoy afirmando que seaexactamenteasí, pero ciertamente está lo suficientemente cerca como para entender lo que sucede. Lo escribí como si fuera invocado en un shell, por lo que me resulta familiar; Pero en realidad todavía no hay caparazón. Y no existe $command_line…como variable, solo estoy usando este nombre para referirme a alguna cadena a los efectos de esta respuesta).

Luego, $SHELLen el servidor se analiza $command_line…por sí solo. Este es el tercer nivel.


Observaciones específicas

El comando que falló

ssh -t [email protected] sudo docker exec -it `sudo docker ps | grep mycontainername | awk '{print $1;}'` env

falló porque 123.456.789.10no es una dirección IP válida.

Vale, entiendo 123.456.789.10que es un marcador de posición, pero aún así no es válido. :)

El comando falló porque se ejecutó sudo docker ps | grep mycontainername | awk '{print $1;}'localmente. La salida probablemente estaba vacía. Entonces $command_line_built_by_sshestaba lejos de lo que querías.

Tenga en cuenta que ssh … | grep mycontainernamese ejecuta greplocalmente (es posible que lo sepa o no).


Discusión

Para tener el control de lo que obtendrá el shell remoto $command_line_built_by_ssh, es necesario comprender, predecir y planificar el análisis y la interpretación que suceden antes. Necesitas crear tu comando local, así quedespuésel shell local y sshdigerirlo, se convierte en exactamente $command_line…lo que desea ejecutar en el lado remoto.

Puede ser bastante complicado si realmente desea que su shell local se expanda o sustituya algo antes de que llegue el resultado ssh. Tu caso es más sencillo porque ya tienes la cadena textual que deseas como $command_line_built_by_ssh. La cadena es:

sudo docker exec -it $(sudo docker ps | grep mycontainername | awk '{print $1;}') env

Notas:

  • Utilicé la sustitución de comandos en forma de $(), no comillas invertidas. Hayrazones para preferir$().
  • No lo sé dockeren absoluto, no puedo decir si $(…)debería estar entre comillas dobles. En generalno citar casi siempre es malo. Pregúntese qué sucede cuando la sustitución devuelve varias palabras (es decir, ingresan varias líneas awk). Este es un problema diferente (si es que alguna vez lo es en este caso) y no lo abordaré en esta respuesta.

Para proteger todo de ser expandido/interpretado por el shell local, debe citar o escapar (con \) correctamente todos los caracteres que pueden desencadenar la expansión o pueden interpretarse. En este caso, cite $, (, ), |, ;, {, }y '(tal vez u opcionalmente) espacios.

Dije "tal vez u opcionalmente cite o escape espacios" por cómo ssh … some commandfunciona. Si encuentra dos o más argumentos que interpreta como código para ejecutar en el servidor, los concatenará añadiendo espacios simples entre ellos. Así $command_line_built_by_sshse construye. Si no cita ni escapa espacios en lo que parece código para el shell remoto, entonces el shell local consumirá espacios (y tabulaciones) al dividir palabras y luego sshagregará espacios. Es posible que el resultado no sea exactamente el que desea si hay tabulaciones o varios espacios consecutivos. Por ejemplo:

ssh user@server echo a     b

sshobtiene user@server, echo, a, b. El comando remoto será echo a by echoobtendrá a,. bSe imprimirá a b.

Luego esto:

ssh user@server 'echo a     b'

sshobtiene user@server, echo a b. El comando remoto será echo a by echoobtendrá a,. bSe imprimirá a b.

Y finalmente esto:

ssh user@server 'echo "a     b"'

sshobtiene user@server, echo "a b". El comando remoto será echo "a b"y echoobtendrá a b. Se imprimirá a b.

La conclusión es que debes citar en el contexto del shell local ypor separadoen el contexto del shell remoto. Tenga en cuenta que cuando se trata de expandir cosas mediante un caparazón,las comillas exteriores importan.


Solución

Reunir toda esta información (y aún asumir que desea protegertodode ser expandido/interpretado por el shell local), recomiendo lo siguiente:

  • Citar o escapar.
  • PreferircitandoEscapar en exceso porque un solo par de comillas puede proteger muchos caracteres, mientras que una sola \protege un solo carácter. Lo más probable es que necesites muchas barras invertidas para proteger todo; A menudo se puede lograr el mismo resultado con un solo par de comillas.
  • Preferircomillas simples( '), pueden proteger todo menos '. Por otro lado, las comillas dobles ( ") pueden proteger ', pero no $ni "(ni \a veces, ni !a veces en Bash), a menos que $y tales también se escapen (es decir, se escapenycitado, es decir, escapado entre comillas dobles; excepto! cual es molesto).
  • Prefiero proporcionar comandos comosolteroargumento a ssh.

Esto lleva al siguiente procedimiento:

  1. Prepare un comando textual que desee ejecutar en el lado remoto.
  2. Reemplace cada 'con '"'"'o con '\''(puede elegir de forma independiente para cada uno ').
  3. Abraza toda la cadena resultante entre comillas simples.
  4. Añadir ssh …al frente.

Su comando textual es:

sudo docker exec -it $(sudo docker ps | grep mycontainername | awk '{print $1;}') env

El procedimiento da como resultado:

ssh -t user@server 'sudo docker exec -it $(sudo docker ps | grep mycontainername | awk '\''{print $1;}'\'') env'
# single-quoted     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^    ^^^^^^^^^^^    ^^^^^
# escaped                                                                                ^              ^

Tenga en cuenta que el procedimiento puede conducir o no a la cadena más corta posible. Con conocimiento, a veces se puede "optimizar" la cadena. Pero el procedimiento es bastantesimpley totalmente confiable. Si solo sabe que desea proteger todo para que el shell local no lo expanda/interprete, el procedimiento en sí no requiere más información.


Automatización

De hecho, el procedimiento se puede automatizar. Existen herramientas que pueden agregar comillas a una cadena y preservar adecuadamente las comillas existentes. Bash en sí es esa herramienta.Esta respuesta míaproporciona una manera de hacer esto con solo presionar una tecla en Bash. Una posible solución ajustada a su caso es:

  1. En su shell local defina la siguiente función personalizada y enlace:

     _prepend_ssh() { READLINE_LINE="ssh  ${READLINE_LINE@Q}"; READLINE_POINT=4; }
     bind -x '"\C-x\C-h":_prepend_ssh'
    
  2. Aún en su shell local, escriba (o pegue) el comando que desea ejecutar en el shell remoto; no ejecutar. El comando debe serexactamentecomo desea que esté en el shell remoto.

  3. Presiona Ctrl+ x, Ctrl+ h. El shell local se encargará de cotizar. También se agregará sshal frente y colocará el cursor justo después.

  4. Agregue (escriba) los argumentos que faltan (p. ej. -t user@server). Para su comodidad, el cursor ya está en la posición correcta para hacer esto.

  5. Enter


Alternativas

Hay otra forma de pasar comandos textuales a un shell remoto. En algunos casos puedestuboellos a través de ssh. Supongamos que la línea de comando remota debería ser:

echo "$PATH"; date

Podríamos proceder como arriba, agregar comillas simples y ejecutar localmente así:

ssh user@server 'echo "$PATH"; date'

El ejemplo es simple pero en general agregar comillas no siempre es tan fácil. Alternativamente, podemos canalizar el comando de esta manera (el primero echopor simplicidad;printfes mejor):

echo 'echo "$PATH"; date' | ssh user@server bash

que todavía requiere estas comillas simples. Pero si tiene los comandos en un archivo, entonces:

<file ssh user@server bash

O incluso sin ningún archivo (aquí documento):

ssh user@server bash <<'EOF'
echo "$PATH"
date
EOF

(Tenga en cuenta que las comillas <<'EOF'evitan $PATHque se expandan localmente).

Ventajas:

  • Puede pasar fácilmente comandos/fragmentos/scripts de varias líneas (lo divido echo … ; datesolo para mostrar esto).
  • No se requiere ninguna capa adicional de cotización.
  • Puede elegir explícitamente un intérprete remoto que no tiene que ser un shell (por ejemplo , o o bash) .zshpython

Desventajas:

  • Debe especificar explícitamente un intérprete remoto; de lo contrario, el valor predeterminadoaccesoSe generará el shell, tal vez se imprima el mensaje del día. Aún puede usar el shell predeterminado como un shell sin inicio de sesión especificando entre comillas correctamente exec "$SHELL"(la línea será como ssh … 'exec "$SHELL"' <<'EOF').
  • La entrada estándar de sshno es una terminal, por lo que no puedes usarla -t(es por eso que no usé tu comando original como ejemplo).
  • Los comandos llegan al intérprete remoto ( bashen el ejemplo) a través de su entrada estándar. Posibles problemas:
    • Los procesos secundarios (o incorporados) utilizarán la misma entrada estándar. Si alguno de ellos lee desde su entrada estándar, leerá la misma secuencia y posiblemente leerá los siguientes comandos destinados al intérprete. Este comportamiento se puede suprimir o incluso (abusar) de forma creativa, aunque no daré más detalles.
    • No puedes usar fácilmente este canal para canalizar nada más.

información relacionada