リモートユーザーのログインシェルを知らず、SSHを介して任意の単純なコマンドをどのように実行できますか?

リモートユーザーのログインシェルを知らず、SSHを介して任意の単純なコマンドをどのように実行できますか?

ssh実行時に迷惑な機能があります。

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

cmd引数を空白でhost連結し、cmdシェルを実行してhost結果文字列を解釈します(それでssh代わりに呼び出されるようですsexec)。

userさらに悪いことに、ログインシェルがBourneのログインシェルと同じであるという保証さえないため、その文字列を解釈するためにどのシェルが使用されるのかわかりません。それでもtcshログインシェルとして使用している人がいて、fishその数が増えているからです。

この問題を解決する方法はありますか?

配列に格納されている引数のリストとしてコマンドがあるとしますbash。各引数にはnull以外のバイトシーケンスを含めることができます。コマンドのログインシェルに関係なく、一貫した方法で実行する方法はありますかhost?主要なUnixシェルファミリ(Bourne、csh、rc / es、fish)の1つですか?useruserhost

私ができるはずのもう一つの合理的な仮定は、shBourneと互換性のあるコマンドがあるということです。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[@]}")"

このコマンドは、リモートユーザーのログインシェルがローカルシェルと同じ(またはprintf %qローカルシェルで作成されたのと同じ方法で参照を理解し)、同じロケールで実行されている場合にのみ機能します。

答え1

ssh私はどの実装にもシェルを使わずにクライアントからサーバーにコマンドを渡す基本的な方法はないと思います。

これで、リモートシェルに特定のインタプリタのみを実行するように指示し(たとえば、予想される構文を知っている)、他の方法で実行されるコードを提供できる場合は、sh作業が簡単になります。

たとえば、別のアプローチは次のとおりです。標準入力または環境変数

どちらもうまくいかない場合は、以下の3番目の解決策を思い出しました。

標準入力の使用

これは、リモートコマンドにデータを提供する必要がない場合の最も簡単なソリューションです。

リモートホストにxargsこの-0オプションをサポートするコマンドがあり、コマンドが大きすぎない場合は、次のことができます。

printf '%s\0' "${cmd[@]}" | ssh user@host 'xargs -0 env --'

コマンドxargs -0 env --ラインは、これらのすべてのシェルファミリに対して同じように解釈されます。xargsstdinから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

それ以外の場合:

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

zsh をローカルシェルとして使用する場合。

環境変数の使用

これで、クライアントがリモートコマンドの標準入力としていくつかのデータを供給する必要がある場合、上記の解決策は機能しません。

ただし、一部sshのサーバー展開では、任意の環境変数をクライアントからサーバーに渡すことができます。たとえば、Debianベースのシステムの多くのopensshディストリビューションではLC_

LC_CODEこのような場合には、たとえば、次のような変数を持つことができます。shが引用 sh上記のようにコーディングし、sh -c 'eval "$LC_CODE"'クライアントにその変数を渡すように指示した後、リモートホストで実行します(これもすべてのシェルで同じように解釈されるコマンドラインです)。

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

すべてのシェルスイートと互換性のあるコマンドラインの構築

上記のオプションのいずれも許可されていない場合(stdinが必要で、sshdがどの変数も受け入れないか、または汎用ソリューションが必要なため)、互換性のあるすべてのコマンドラインサポートを備えたリモートホスト用のシェルを準備する必要があります。

これらのすべてのシェル(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解釈するコマンドラインになります。cmd

限界:

  • 序文とコマンドを引用する方法は、リモートコマンドラインがはるかに大きくなることを意味します。
  • 私はBourneシェル(ガボツールボックスにあります)、dash、bash、zsh、mksh、lksh、yash、ksh93、rc、es、akanga、csh、tcsh、最近Debianシステムで見つかったfishおよび/ Solaris bin /でのみテストしましたしました。 sh、/usr/bin/ksh、/bin/csh、および/usr/xpg4/bin/sh(10)。
  • yashTelnetシェルの場合、引数に無効な文字を含むコマンドを渡すことはできませんが、これはとにかくyash解決できない制限です。
  • 一部のシェル(cshやbashなど)は、sshを介して呼び出されるといくつかの起動ファイルを読み込みます。私たちはこれが行動を大きく変えないと仮定するので、序文は有効です。
  • とりわけ、shリモートシステムにprintfコマンドがあるとします。

これがどのように機能するかを理解するには、他のシェルで引用がどのように機能するかを知る必要があります。

  • Bourne:'...'強力な引用です。いいえその中に特殊文字があります。"..."弱い引用符で、"バックスラッシュでエスケープできます。
  • csh"出口がないことを除いて、The Bourne Supremacyと同じです"..."。また、改行文字を入力し、前にバックスラッシュを付ける必要があります。!一重引用符の内側にも問題が発生する可能性があります。
  • rc。唯一の引用符は'...'(strong)です。一重引用符内の一重引用符は、''(例えば)として入力されます'...''...'。二重引用符やバックスラッシュは特別ではありません。
  • es。外部引用符を除くとrcと同じで、バックスラッシュは単一引用符をエスケープできます。
  • fish':バックスラッシュが内部的にエスケープされることを除いて、Bourneと同じです'...'

これらすべての制限により、すべてのシェルで動作するようにコマンドライン引数を確実に引用する方法がないことが簡単にわかります。

次のように単一引用符を使用してください。

'foo' 'bar'

以下を除くすべての状況に適用されます。

'echo' 'It'\''s'

では動作しませんrc

'echo' 'foo
bar'

では動作しませんcsh

'echo' 'foo\'

では動作しませんfish

$bしかし、これらの問題のある文字をバックスラッシュ、一重引用符、$q改行(および$ncshレコード拡張)などのシェルに依存しない方法で変数に保存すると、ほとんどの問題を解決できます。!$x

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

すべてのシェルで動作します。ただし、これは改行文字ではまだ機能しませんcsh$n改行文字が含まれている場合は、他のシェルでは機能しない改行文字に拡張するようにcsh作成する必要があります。したがって、ここで私たちがしている$n:qことはこれらのことです。shsh$nsh

このコードは$preamble最もトリッキーな部分です。すべてのシェルはさまざまな引用規則を利用しているため、コードの特定の部分は1つのシェルでのみ解釈され(他のシェルはコメント化されています)、各シェルはそのシェルの、、、、$b変数$qのみを定義します。$n$x

host以下は、例でリモートユーザーのログインシェルによって解釈されるシェルコードです。

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

このコードは、サポートされているシェルで解釈されたときに同じコマンドを実行します。

答え2

長すぎます。

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

詳細な解決策についてはコメントを読んで確認してくださいもう一つの答え

説明する

まあ、私の解決策はシェルではない場合は動作しませんbash。しかし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"'

関連情報