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
/在本地运行它bash
,yash
如下所示:
$ "${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 不起作用。因此,我们最终在这里做的是调用sh
并sh
扩展这些$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")
对于更详细的解决方案,请阅读评论并检查另一个答案。
描述
好吧,我的解决方案不适用于非bash
shell。但假设它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"'