如何在不知道远程用户的登录 shell 的情况下通过 ssh 执行任意简单的命令?

Sté*_*las 28 ssh quoting portability

ssh 运行时有一个烦人的功能:

ssh user@host cmd and "here's" "one arg"
Run Code Online (Sandbox Code Playgroud)

它不是cmd用它的参数运行它host,而是将它cmd和带有空格的参数连接起来,并运行一个 shellhost来解释结果字符串(我想这就是为什么它被调用ssh而不是sexec)。

更糟糕的是,您不知道将使用什么 shell 来解释该字符串,因为它的登录 shelluser甚至不能保证像 Bourne 一样,因为仍然有人使用tcsh作为他们的登录 shell 并且fish还在增加。

有没有办法解决这个问题?

假设我有一个命令作为存储一个在参数列表bash阵列,其每一个可以包含的非空字节的任何序列,是否有任何方式把它上执行hostuser以一致的方式,无论该登录壳的userhost(我们假设它是主要的 Unix shell 系列之一:Bourne、csh、rc/es、fish)?

我应该能够做出的另一个合理假设是,有一个sh与Bourne 兼容的命令host可用$PATH

例子:

cmd=(
  'printf'
  '<%s>\n'
  'arg with $and spaces'
  '' # empty
  $'even\n* * *\nnewlines'
  "and 'single quotes'"
  '!!'
)
Run Code Online (Sandbox Code Playgroud)

我可以在本地运行它与ksh/ zsh/ bash/yash如下:

$ "${cmd[@]}"
<arg with $and spaces>
<>
<even
* * *
newlines>
<and 'single quotes'>
<!!>
Run Code Online (Sandbox Code Playgroud)

或者

env "${cmd[@]}"
Run Code Online (Sandbox Code Playgroud)

或者

xterm -hold -e "${cmd[@]}"
...
Run Code Online (Sandbox Code Playgroud)

我将如何运行它host作为userssh

ssh user@host "${cmd[@]}"
Run Code Online (Sandbox Code Playgroud)

显然是行不通的。

ssh user@host "$(printf ' %q' exec "${cmd[@]}")"
Run Code Online (Sandbox Code Playgroud)

仅当远程用户的登录 shell 与本地 shell 相同(或理解printf %q以与本地 shell 中产生它的方式相同的方式进行引用)并在同一语言环境中运行时才有效。

Sté*_*las 19

我不认为 的任何实现都ssh具有在不涉及 shell 的情况下将命令从客户端传递到服务器的本机方式。

现在,如果你可以告诉远程 shell 只运行一个特定的解释器(比如sh,我们知道预期的语法)并通过另一种方式执行代码,事情会变得更容易。

其他意思可以是例如标准输入环境变量

当两者都不能使用时,我提出了下面的第三种解决方案。

使用标准输入

如果您不需要向远程命令提供任何数据,这是最简单的解决方案。

如果你知道远程主机有xargs支持该-0选项的命令并且命令不是太大,你可以这样做:

printf '%s\0' "${cmd[@]}" | ssh user@host 'xargs -0 env --'
Run Code Online (Sandbox Code Playgroud)

xargs -0 env --命令行的解释与所有这些 shell 系列相同。xargs在 stdin 上读取以 null 分隔的参数列表,并将它们作为参数传递给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
Run Code Online (Sandbox Code Playgroud)

使用环境变量

现在,如果您确实需要从客户端向远程命令的 stdin 提供一些数据,则上述解决方案将不起作用。

ssh然而,一些服务器部署允许将任意环境变量从客户端传递到服务器。例如,基于 Debian 的系统上的许多 openssh 部署允许传递名称以LC_.

在这些情况下,您可以拥有一个LC_CODE变量,例如包含上述shquoted sh代码,并sh -c 'eval "$LC_CODE"'在告诉您的客户端传递该变量后在远程主机上运行(同样,这是一个在每个 shell 中解释相同的命令行):

LC_CODE=$(shquote "${cmd[@]}") ssh -o SendEnv=LC_CODE user@host '
  sh -c '\''eval "$LC_CODE"'\'
Run Code Online (Sandbox Code Playgroud)

构建与所有 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;
Run Code Online (Sandbox Code Playgroud)

这是一个perl围绕ssh. 我称之为sexec。你这样称呼它:

sexec [ssh-options] user@host -- cmd and its args
Run Code Online (Sandbox Code Playgroud)

所以在你的例子中:

sexec user@host -- "${cmd[@]}"
Run Code Online (Sandbox Code Playgroud)

并且包装器变成cmd and its args了一个命令行,所有 shell 最终都会将其解释为cmd使用其 args调用(不管它们的内容如何)。

限制:

  • 序言和命令引用的方式意味着远程命令行最终会明显更大,这意味着将更快地达到命令行最大大小的限制。
  • 我只测试过:Bourne shell(来自传家宝工具箱)、dash、bash、zsh、mksh、lksh、yash、ksh93、rc、es、akanga、csh、tcsh、在最近的 Debian 系统上发现的鱼和/ Solaris 10 上的 bin/sh、/usr/bin/ksh、/bin/csh 和 /usr/xpg4/bin/sh。
  • 如果yash是远程登录 shell,则不能传递参数包含无效字符的命令,但这是一个限制,yash因为无论如何您都无法解决。
  • 当通过 ssh 调用时,一些 shell(如 csh 或 bash)会读取一些启动文件。我们假设这些不会显着改变行为,因此序言仍然有效。
  • 此外sh,它还假定远程系统具有该printf命令。

要了解它是如何工作的,您需要知道引用在不同的 shell 中是如何工作的:

  • Bourne:'...'是强引号,其中没有特殊字符。"..."是弱引号,"可以用反斜杠转义。
  • csh. 与 Bourne 相同,只是"不能在里面逃脱"..."。此外,必须输入带有反斜杠前缀的换行符。而且!,即使里面的单引号的原因的问题。
  • rc. 唯一的引号是'...'(强)。单引号中的单引号输入为''(如'...''...')。双引号或反斜杠并不特殊。
  • es. 与 rc 相同,除了引号之外,反斜杠可以转义单引号。
  • fish: 与 Bourne 相同,只是反斜杠在'里面转义'...'

有了所有这些限制,很容易看出不能可靠地引用命令行参数以使其适用于所有 shell。

使用单引号如下:

'foo' 'bar'
Run Code Online (Sandbox Code Playgroud)

除了:

'echo' 'It'\''s'
Run Code Online (Sandbox Code Playgroud)

不会在rc.

'echo' 'foo
bar'
Run Code Online (Sandbox Code Playgroud)

不会在csh.

'echo' 'foo\'
Run Code Online (Sandbox Code Playgroud)

不会在fish.

然而,我们应该能够解决这些问题最如果我们管理这些问题的字符存储在变量,如反斜线$b,单引号$q,新行中$n(以及!$x对csh历史扩展)在shell独立的方式。

'echo' 'It'$q's'
'echo' 'foo'$b
Run Code Online (Sandbox Code Playgroud)

将在所有壳中工作。尽管如此,这仍然不适用于换行符csh。如果$n包含换行符 in csh,则必须将其编写为$n:q将其扩展为换行符,并且这不适用于其他外壳。所以,我们最终在这里做的是调用shsh扩展那些$n. 这也意味着必须进行两级引用,一级用于远程登录 shell,一级用于sh.

$preamble代码中的 是最棘手的部分。它利用所有 shell 中的各种不同引用规则使代码的某些部分仅由一个 shell 解释(而对于其他 shell 则被注释掉),每个部分只为各自的 shell定义那些$b, $q, $n,$x变量。

以下是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
Run Code Online (Sandbox Code Playgroud)

当被任何受支持的 shell 解释时,该代码最终会运行相同的命令。