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


26

ssh 在运行时具有令人讨厌的功能:

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

而不是cmd使用参数将其打开host,而是cmd使用空格将其和参数连接在一起,然后运行shell host来解释结果字符串(我猜这就是为什么调用它ssh而不是它的原因sexec)。

更糟糕的是,您不知道将使用什么shell来解释该字符串,因为user甚至登录的shell 都无法保证是Bo​​urne,因为仍然有人们在使用它tcsh作为登录shell,并且这种fish情况还在增加。

有办法解决吗?

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

另一种合理的假设,我应该能够提出的是,有是sh在命令host中可用的$PATH是Bourne兼容。

例:

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

我可以使用ksh/ zsh/ bash/ 在本地运行yash

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

要么

env "${cmd[@]}"

要么

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

我将如何运行它host作为userssh

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

显然行不通。

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

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


3
如果cmd论点是/bin/sh -c我们将在所有案例的99%中使用posix shell,不是吗?当然,以这种方式转义特殊字符会比较麻烦,但是会解决最初的问题吗?
Bananguin 2015年

@Bananguin,如果您运行ssh host sh -c 'some cmd',与相同,则不会ssh host 'sh -c some cmd',它具有远程用户的登录外壳解释该sh -c some cmd命令行。我们需要以正确的语法为该shell编写命令(并且我们不知道它是哪一个),以便sh在那里使用-cand some cmd参数调用该命令。
斯特凡Chazelas

1
@Otheus,是的,在所有这些shell中,sh -c 'some cmd'some cmd命令行的解释都相同。现在,如果我想echo \'在远程主机上运行Bourne命令行怎么办?echo command-string | ssh ... /bin/sh是我在回答中提供的一种解决方案,但这意味着您无法将数据馈送到该远程命令的标准输入。
斯特凡Chazelas

1
听起来更持久的解决方案是ssh的rexec插件,例如ftp插件。
奥瑟斯

1
@myrdd,不,不是,您需要空格或制表符来分隔Shell命令行中的参数。如果cmdcmd=(echo "foo bar"),在shell命令行传递到ssh应该像`“回声”“富巴” . The *first* space (the one before 回声) is superflous, but doen't harm. The other one (the ones before “富酒吧” ) is needed. With “%Q” , we'd pass a “echo''foo bar'`命令行。
斯特凡Chazelas

Answers:


19

我不认为任何实现都没有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。因此,我们最终在这里所做的只是调用shsh扩展了这些$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解释时,该代码最终将运行相同的命令。


1
SSH协议(RFC 4254§6.5)将远程命令定义为字符串。由服务器决定如何解释该字符串。在Unix系统上,通常的解释是将字符串传递给用户的登录shell。对于受限帐户,这可能类似于rssh或rush,它们不接受任意命令。帐户或密钥上甚至可能存在强制命令,导致客户端发送的命令字符串被忽略。
吉尔斯(Gilles)'所以

1
@Gilles,感谢RFC参考。是的,此问答的假设是远程用户的登录外壳可用(如我可以运行我要运行的远程命令),并且是POSIX系统上的主要外壳系列之一。我对受限制的外壳程序或非外壳程序,强制命令或任何不允许我运行该远程命令的内容都不感兴趣。
斯特凡Chazelas

1
可以在Hyperpolyglot上找到有用的参考,以了解一些常见shell之间的语法主要区别。
lcd047

0

tl; dr

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"'

1
假定远程用户的登录外壳是bash(如bash的printf%q以bash方式引用)并且bash在远程计算机上可用。还有一些缺少引号的问题,这会导致空格和通配符出现问题。
斯特凡Chazelas

@StéphaneChazelas确实,我的解决方案可能只针对bashshell。但是希望人们会发现它有用。我试图解决其他问题。随时告诉我,我是否想念其他bash东西。
x-yuri

1
请注意,它仍然无法与问题(ssh_run user@host "${cmd[@]}")中的示例命令一起使用。您仍然缺少一些报价。
斯特凡Chazelas

1
这样更好 请注意,printf %q在不同的语言环境中使用bash的输出是不安全的(并且也存在很多问题;例如,在使用BIG5字符集的语言环境中,它(4.3.48)的引用εα`!)。为此,最好是仅使用shquote()我的答案中的所有内容并用单引号引起来。
斯特凡Chazelas
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.