No creo que ninguna implementación ssh
tenga una forma nativa de pasar un comando del cliente al servidor sin involucrar un shell.
Ahora, las cosas pueden ser más fáciles si puede decirle al shell remoto que solo ejecute un intérprete específico (por ejemplo sh
, para el que conocemos la sintaxis esperada) y le dé al código que se ejecute por otro medio.
Esa otra media puede ser, por ejemplo, una entrada estándar o una variable de entorno .
Cuando ninguno de los dos se puede utilizar, propongo una tercera solución hacky a continuación.
Usando stdin
Si no necesita alimentar ningún dato al comando remoto, esa es la solución más fácil.
Si sabe que el host remoto tiene un xargs
comando que admite la -0
opción y el comando no es demasiado grande, puede hacer lo siguiente:
printf '%s\0' "${cmd[@]}" | ssh user@host 'xargs -0 env --'
Esa xargs -0 env --
línea de comando se interpreta de la misma manera con todas esas familias de shell. xargs
lee la lista de argumentos delimitados por nulos en stdin y los pasa como argumentos a env
. Eso supone que el primer argumento (el nombre del comando) no contiene =
caracteres.
O puede usar sh
en el host remoto después de haber citado cada elemento utilizando la sh
sintaxis de cita.
shquote() {
LC_ALL=C awk -v q=\' '
BEGIN{
for (i=1; i<ARGC; i++) {
gsub(q, q "\\" q q, ARGV[i])
printf "%s ", q ARGV[i] q
}
print ""
}' "$@"
}
shquote "${cmd[@]}" | ssh user@host sh
Usar variables de entorno
Ahora, si necesita alimentar algunos datos del cliente al stdin del comando remoto, la solución anterior no funcionará.
ssh
Sin embargo, algunas implementaciones de servidor permiten pasar variables de entorno arbitrarias del cliente al servidor. Por ejemplo, muchas implementaciones de openssh en sistemas basados en Debian permiten pasar variables cuyo nombre comienza con LC_
.
En esos casos, podría tener una LC_CODE
variable que, por ejemplo, contenga el código entrecomillado sh
como se indicó anteriormente y ejecutarla sh -c 'eval "$LC_CODE"'
en el host remoto después de haberle dicho a su cliente que pase esa variable (de nuevo, esa es una línea de comandos que se interpreta igual en cada shell):
LC_CODE=$(shquote "${cmd[@]}") ssh -o SendEnv=LC_CODE user@host '
sh -c '\''eval "$LC_CODE"'\'
Construyendo una línea de comando compatible con todas las familias de shell
Si ninguna de las opciones anteriores es aceptable (porque necesita stdin y sshd no acepta ninguna variable, o porque necesita una solución genérica), deberá preparar una línea de comando para el host remoto que sea compatible con todos Conchas soportadas.
Eso es particularmente complicado porque todos esos shells (Bourne, csh, rc, es, fish) tienen su propia sintaxis diferente y, en particular, diferentes mecanismos de cotización y algunos de ellos tienen limitaciones que son difíciles de solucionar.
Aquí hay una solución que se me ocurrió, la describo más abajo:
#! /usr/bin/perl
my $arg, @ssh, $preamble =
q{printf '%.0s' "'\";set x=\! b=\\\\;setenv n "\
";set q=\';printf %.0s "\""'"';q='''';n=``()echo;x=!;b='\'
printf '%.0s' '\'';set b \\\\;set x !;set -x n \n;set q \'
printf '%.0s' '\'' #'"\"'";export n;x=!;b=\\\\;IFS=.;set `echo;echo \.`;n=$1 IFS= q=\'
};
@ssh = ('ssh');
while ($arg = shift @ARGV and $arg ne '--') {
push @ssh, $arg;
}
if (@ARGV) {
for (@ARGV) {
s/'/'\$q\$b\$q\$q'/g;
s/\n/'\$q'\$n'\$q'/g;
s/!/'\$x'/g;
s/\\/'\$b'/g;
$_ = "\$q'$_'\$q";
}
push @ssh, "${preamble}exec sh -c 'IFS=;exec '" . join "' '", @ARGV;
}
exec @ssh;
Ese es un perl
guión envolvente ssh
. Yo lo llamo sexec
. Lo llamas así:
sexec [ssh-options] user@host -- cmd and its args
entonces en tu ejemplo:
sexec user@host -- "${cmd[@]}"
Y el contenedor se convierte cmd and its args
en una línea de comando que todos los shells terminan interpretando como llamadas cmd
con sus argumentos (independientemente de su contenido).
Limitaciones:
- El preámbulo y la forma en que se cita el comando significa que la línea de comando remota termina siendo significativamente más grande, lo que significa que el límite en el tamaño máximo de una línea de comando se alcanzará antes.
- Solo lo he probado con: Bourne shell (de heirloom toolchest), dash, bash, zsh, mksh, lksh, yash, ksh93, rc, es, akanga, csh, tcsh, fish como se encuentra en un sistema Debian reciente y / bin / sh, / usr / bin / ksh, / bin / csh y / usr / xpg4 / bin / sh en Solaris 10.
- Si
yash
es el shell de inicio de sesión remoto, no puede pasar un comando cuyos argumentos contengan caracteres no válidos, pero esa es una limitación en el sentido de yash
que no puede evitarlo de todos modos.
- Algunos shells como csh o bash leen algunos archivos de inicio cuando se invocan a través de ssh. Asumimos que esos no cambian el comportamiento dramáticamente para que el preámbulo aún funcione.
- además
sh
, también supone que el sistema remoto tiene el printf
comando.
Para comprender cómo funciona, necesita saber cómo funciona la cita en los diferentes shells:
- Bourne:
'...'
son citas fuertes sin carácter especial. "..."
son comillas débiles donde "
se puede escapar con una barra invertida.
csh
. Igual que Bourne, excepto que "
no se puede escapar por dentro "..."
. También se debe ingresar un carácter de nueva línea con una barra diagonal inversa. Y !
causa problemas incluso dentro de comillas simples.
rc
. Las únicas citas son '...'
(fuertes). Una comilla simple dentro de comillas simples se ingresa como ''
(like '...''...'
). Las comillas dobles o las barras invertidas no son especiales.
es
. Igual que rc, excepto que las comillas externas, la barra diagonal inversa puede escapar de una comilla simple.
fish
: igual que Bourne, excepto que la barra invertida se escapa '
dentro '...'
.
Con todas esas restricciones, es fácil ver que uno no puede citar de manera confiable los argumentos de la línea de comandos para que funcione con todos los shells.
Usando comillas simples como en:
'foo' 'bar'
funciona en todos pero:
'echo' 'It'\''s'
no funcionaría en rc
.
'echo' 'foo
bar'
no funcionaría en csh
.
'echo' 'foo\'
no funcionaría en fish
.
Sin embargo, deberíamos poder solucionar la mayoría de esos problemas si logramos almacenar esos caracteres problemáticos en variables, como la barra diagonal inversa $b
, la comilla simple $q
, la nueva línea $n
(y !
en la $x
expansión del historial de csh) de una manera independiente del shell.
'echo' 'It'$q's'
'echo' 'foo'$b
funcionaría en todos los depósitos. Sin embargo, eso todavía no funcionaría para Newline csh
. Si $n
contiene nueva línea, en csh
, debe escribirlo $n:q
para que se expanda a una nueva línea y eso no funcionará para otros shells. Entonces, lo que terminamos haciendo aquí es llamar sh
y sh
expandirlos $n
. Eso también significa tener que hacer dos niveles de cotización, uno para el shell de inicio de sesión remoto y otro para sh
.
El $preamble
en ese código es la parte más complicada. Se hace uso de las distintas reglas diferentes de cotización en todas las cáscaras tenían algunas secciones del código interpretado por una sola de las conchas (mientras se está comentada para los demás) cada uno de los cuales acaba de definir los $b
, $q
, $n
, $x
variables para su respectiva concha.
Aquí está el código de shell que sería interpretado por el shell de inicio de sesión del usuario remoto host
para su ejemplo:
printf '%.0s' "'\";set x=\! b=\\;setenv n "\
";set q=\';printf %.0s "\""'"';q='''';n=``()echo;x=!;b='\'
printf '%.0s' '\'';set b \\;set x !;set -x n \n;set q \'
printf '%.0s' '\'' #'"\"'";export n;x=!;b=\\;IFS=.;set `echo;echo \.`;n=$1 IFS= q=\'
exec sh -c 'IFS=;exec '$q'printf'$q' '$q'<%s>'$b'n'$q' '$q'arg with $and spaces'$q' '$q''$q' '$q'even'$q'$n'$q'* * *'$q'$n'$q'newlines'$q' '$q'and '$q$b$q$q'single quotes'$q$b$q$q''$q' '$q''$x''$x''$q
Ese código termina ejecutando el mismo comando cuando lo interpretan cualquiera de los shells compatibles.
cmd
argumento fuera/bin/sh -c
que terminaríamos con un shell posix en el 99% de todos los casos, ¿no? Por supuesto, escapar de caracteres especiales es un poco más doloroso de esta manera, pero ¿resolvería el problema inicial?