我不认为任何实现都没有ssh在不涉及外壳的情况下将命令从客户端传递到服务器的本机方法。
现在,如果您可以告诉远程外壳程序仅运行特定的解释器(例如sh,我们知道其预期语法的)并以另一种方式执行代码,则事情会变得更加容易。
该其他平均值可以是例如标准输入或环境变量。
当两种方法都无法使用时,我在下面提出了一个棘手的第三种解决方案。
使用标准输入
如果您不需要向远程命令提供任何数据,那是最简单的解决方案。
如果您知道远程主机具有xargs支持该-0选项的命令,并且该命令不太大,则可以执行以下操作:
printf '%s\0' "${cmd[@]}" | ssh user@host 'xargs -0 env --'
该xargs -0 env --命令行与所有这些shell系列的解释相同。xargs读取stdin上以空值分隔的参数列表,并将其作为参数传递给env。假定第一个参数(命令名称)不包含=字符。
或者,您可以sh在使用sh引用语法对每个元素进行引用之后,在远程主机上使用。
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
使用环境变量
现在,如果您确实需要从客户端向远程命令的stdin提供一些数据,则上述解决方案将不起作用。
ssh但是,某些服务器部署允许将任意环境变量从客户端传递到服务器。例如,基于Debian的系统上的许多openssh部署都允许传递名称以开头的变量LC_。
在那种情况下,您可能有一个LC_CODE变量,例如包含上面的短引号的 sh代码,并sh -c 'eval "$LC_CODE"'在告诉客户端传递该变量后再次在远程主机上运行该变量(同样,这是一个命令行,在每个shell中解释都相同):
LC_CODE=$(shquote "${cmd[@]}") ssh -o SendEnv=LC_CODE user@host '
sh -c '\''eval "$LC_CODE"'\'
构建与所有Shell系列兼容的命令行
如果上述选项都不可接受(因为您需要stdin且sshd不接受任何变量,或者因为您需要通用解决方案),那么您将必须为与所有主机兼容的远程主机准备命令行支持的外壳。
这特别棘手,因为所有这些shell(Bourne,csh,rc,es,fish)都有各自不同的语法,尤其是不同的引用机制,其中一些具有难以解决的局限性。
这是我想出的一个解决方案,下面将对其进行详细描述:
#! /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;
那是一个perl包装脚本ssh。我叫它sexec。您这样称呼它:
sexec [ssh-options] user@host -- cmd and its args
因此在您的示例中:
sexec user@host -- "${cmd[@]}"
包装器变成cmd and its args一个命令行,所有shell最终将其解释为cmd使用其args 调用(不考虑其内容)。
局限性:
- 序言和命令的引用方式意味着远程命令行最终会大大变大,这意味着将尽快达到命令行最大大小的限制。
- 我仅使用以下工具对其进行了测试:伯恩壳(来自传家宝工具箱),破折号,bash,zsh,mksh,lksh,yash,ksh93,rc,es,akanga,csh,tcsh,fish(在最近的Debian系统中发现)和/在Solaris 10上为bin / sh,/ usr / bin / ksh,/ bin / csh和/ usr / xpg4 / bin / sh。
- 如果
yash是远程登录外壳,则无法传递参数包含无效字符的命令,但这是一个局限性,yash因为您无论如何都无法解决。
- 通过SSH调用时,某些外壳(如csh或bash)会读取一些启动文件。我们假设这些行为不会显着改变行为,因此序言仍然有效。
- 在旁边
sh,还假定远程系统具有该printf命令。
要了解它是如何工作的,您需要知道报价在不同shell中的工作方式:
- 伯恩(Bourne):
'...'引号很强,没有特殊字符。"..."是弱引号,"可以使用反斜杠进行转义。
csh。与Bourne相同,除了"不能在内部逃脱"..."。此外,还必须输入换行符,并在前面加上反斜杠。而且!,即使里面的单引号的原因的问题。
rc。唯一的引号是'...'(强)。在单引号内的单引号输入为''(如'...''...')。双引号或反斜杠并不特殊。
es。与rc相同,除了外部引号外,反斜杠可以转义单个引号。
fish:与Bourne相同,除了反斜线在'内部逸出'...'。
有了所有这些约束,很容易看出一个人不能可靠地引用命令行参数,因此它可以与所有shell一起使用。
使用单引号,如下所示:
'foo' 'bar'
除以下以外,均可使用:
'echo' 'It'\''s'
不会在中工作rc。
'echo' 'foo
bar'
不会在中工作csh。
'echo' 'foo\'
不会在中工作fish。
然而,我们应该能够解决这些问题最如果我们管理这些问题的字符存储在变量,如反斜线$b,单引号$q,新行中$n(以及!在$x对csh历史扩展)在shell独立的方式。
'echo' 'It'$q's'
'echo' 'foo'$b
可以在所有外壳中使用。但是,这对于换行符仍然不起作用csh。如果$n包含换行符,则csh必须在中编写它,$n:q以使其扩展为换行符,而该命令不适用于其他shell。因此,我们最终在这里所做的只是调用sh并sh扩展了这些$n。这还意味着必须执行两个级别的引用,一个用于远程登录shell,一个用于sh。
该$preamble代码中的是最棘手的部分。它利用的所有弹各种不同的报价规则,让仅一个壳解释(而它的注释掉的其他人)的代码每一个刚刚定义的部分路段$b,$q,$n,$x为各自的shell变量。
这是host您的示例将由远程用户的登录shell解释的shell代码:
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
当由任何受支持的shell解释时,该代码最终将运行相同的命令。
cmd论点是/bin/sh -c我们将在所有案例的99%中使用posix shell,不是吗?当然,以这种方式转义特殊字符会比较麻烦,但是会解决最初的问题吗?