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

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

ssh有一个恼人的功能,当你运行时:

ssh user@host cmd and "here's" "one arg"

它将参数cmd与空格host连接起来cmd,并运行 shell 来host解释生成的字符串(我猜这就是为什么它被称为ssh而不是sexec)。

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

有办法解决这个问题吗?

假设我有一个命令作为存储在数组中的参数列表bash,每个参数都可能包含任何非空字节序列,有没有办法让它以一致的方式执行,host无论user该命令的登录 shell 是user什么host(我们假设它是主要的 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'"
  '!!'
)

我可以使用ksh// zsh/在本地运行它bashyash如下所示:

$ "${cmd[@]}"
<arg with $and spaces>
<>
<even
* * *
newlines>
<and 'single quotes'>
<!!>

或者

env "${cmd[@]}"

或者

xterm -hold -e "${cmd[@]}"
...

我将如何运行它host作为user结束ssh

ssh user@host "${cmd[@]}"

显然行不通。

ssh user@host "$(printf ' %q' exec "${cmd[@]}")"

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

答案1

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

现在,如果您可以告诉远程 shell 仅运行特定的解释器(例如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

要不就:

print -r -- ${(qq)cmd} | ssh user@host sh

如果使用 zsh 作为本地 shell。

使用环境变量

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

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

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

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

或者:

LC_CODE=${(qqj[ ])cmd} ssh -o SendEnv=LC_CODE user@host '
  sh -c '\''eval "$LC_CODE"'\'

zsh

构建与所有 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使用其参数进行调用(无论其内容如何)。

限制:

  • 前导码和引用命令的方式意味着远程命令行最终会变得明显更大,这意味着将更快达到命令行最大大小的限制。
  • 我只测试过: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您无论如何都无法解决。
  • 某些 shell(如 csh 或 bash)在通过 ssh 调用时会读取一些启动文件。我们假设这些不会显着改变行为,因此序言仍然有效。
  • 除此之外sh,它还假设远程系统具有该printf命令。

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

  • Bourne:'...'是强有力的引述其中的特殊字符。"..."是弱引号,"可以用反斜杠转义。
  • csh。和谍影重重一样,只是"里面无法逃脱"..."。此外,还必须输入换行符并以反斜杠为前缀。!即使在单引号内也会引起问题。
  • rc。唯一的引号是'...'(强)。单引号内的单引号输入为''(如'...''...')。双引号或反斜杠并不特殊。
  • es。与 rc 相同,除了外部引号之外,反斜杠可以转义单引号。
  • fish: 与 Bourne 相同,只是反斜杠'在内部转义'...'

有了所有这些限制,很容易看出无法可靠地引用命令行参数,以便它适用于所有 shell。

使用单引号,如下所示:

'foo' 'bar'

适用于所有情况,但:

'echo' 'It'\''s'

不会在 中工作rc

'echo' 'foo
bar'

不会在 中工作csh

'echo' 'foo\'

不会在 中工作fish

然而,如果我们设法以独立于 shell 的方式将这些有问题的字符存储在变量中,例如 中的反斜杠$b、 中的单引号$q、 中的换行符$n(以及csh 历史扩展中!的 in $x),我们应该能够解决大多数问题。

'echo' 'It'$q's'
'echo' 'foo'$b

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

该代码中的$preamble是最棘手的部分。它利用所有 shell 中的各种不同的引用规则,使代码的某些部分仅由一个 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

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

答案2

太长了;博士

ssh USER@HOST -p PORT $(printf "%q" "cmd") $(printf "%q" "arg1") \
    $(printf "%q" "arg2")

对于更详细的解决方案,请阅读评论并检查另一个答案

描述

好吧,我的解决方案不适用于非bashshell。但假设它bash在另一端,事情就会变得更简单。我的想法是重用printf "%q"以逃避。一般来说,在另一端有一个接受参数的脚本更具可读性。但如果命令很短,则内联它可能没问题。以下是在脚本中使用的一些示例函数:

local.sh:

#!/usr/bin/env bash
set -eu

ssh_run() {
    local user_host_port=($(echo "$1" | tr '@:' ' '))
    local user=${user_host_port[0]}
    local host=${user_host_port[1]}
    local port=${user_host_port[2]-22}
    shift 1
    local cmd=("$@")
    local a qcmd=()
    for a in ${cmd[@]+"${cmd[@]}"}; do
        qcmd+=("$(printf "%q" "$a")")
    done
    ssh "$user"@"$host" -p "$port" ${qcmd[@]+"${qcmd[@]}"}
}

ssh_cmd() {
    local user_host_port=$1
    local cmd=$2
    shift 2
    local args=("$@")
    ssh_run "$user_host_port" bash -lc "$cmd" - ${args[@]+"${args[@]}"}
}

ssh_run USER@HOST ./remote.sh "1  '  \"  2" '3  '\''  "  4'
ssh_cmd USER@HOST:22 "for a; do echo \"'\$a'\"; done" "1  '  \"  2" '3  '\''  "  4'
ssh_cmd USER@HOST:22 'for a; do echo "$a"; done' '1  "2' "3'  4"

remote.sh:

#!/usr/bin/env bash
set -eu
for a; do
    echo "'$a'"
done

输出:

'1  '  "  2'
'3  '  "  4'
'1  '  "  2'
'3  '  "  4'
1  "2
3'  4

或者,printf如果您知道自己在做什么,您也可以自己完成工作:

ssh USER@HOST ./1.sh '"1  '\''  \"  2"' '"3  '\''  \"  4"'

相关内容