リモートユーザーのログインシェルを知らずにssh経由で任意の単純なコマンドを実行するにはどうすればよいですか?


26

ssh を実行すると、迷惑な機能があります:

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

cmd引数をオンhostにしてそれを実行する代わりに、それとcmd引数をスペースで連結しhost、結果の文字列を解釈するためにシェルを実行します(呼び出したのsshではなく、呼び出した理由ですsexec)。

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

それを回避する方法はありますか?

私は記憶された引数のリストのようなコマンドがあるとbash、アレイを、非ヌルのバイトの任意の配列を含むことができるそれぞれが、それが実行されたためにどのような方法があるhostようにuserかかわらず、そのログインシェルの一貫した方法でuserオンhost(これは主要なUnixシェルファミリの1つであると仮定します:Bourne、csh、rc / es、fish)?

私ができるはずのもう1つの合理的な仮定は、Bourne互換のshコマンドが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[@]}")"

リモートユーザーのログインシェルがローカルシェルと同じである(またはprintf %qローカルシェルが生成するのと同じ方法で引用を理解する)場合にのみ機能し、同じロケールで実行されます。


3
場合はcmd引数がした/bin/sh -c我々は、我々はすべてのケースの99%でPOSIXシェルで終わるだろうではないでしょうか?もちろん、この方法で特殊文字をエスケープするのは少し苦痛ですが、最初の問題は解決しますか?
バナンイン

@Bananguin、いいえを実行する場合は、リモートユーザーのログインシェルがそのコマンドラインを解釈するssh host sh -c 'some cmd'と同じを実行ssh host 'sh -c some cmd'しますsh -c some cmd。そのシェルで引数shと一緒に呼び出されるように、そのシェルの正しい構文でコマンドを記述する必要があります(そして、それがどれなのかわかりません)。-csome cmd
ステファンシャゼル

1
@Otheusは、はい、sh -c 'some cmd'およびsome cmdコマンドラインはすべてのそれらのシェルで同じに解釈することが起こります。echo \'リモートホストでBourneコマンドラインを実行したい場合はどうすればよいですか?echo command-string | ssh ... /bin/sh私は私の答えで与えた解決策の1つですが、それはそのリモートコマンドの標準入力にデータを供給することができないことを意味します。
ステファンシャゼル

1
より長続きするソリューションのように聞こえますが、sshのrexecプラグイン、ftpプラグインです。
オテウス

1
@myrdd、そうではありません。シェルコマンドラインで引数を区切るには、スペースまたはタブが必要です。cmdisの場合cmd=(echo "foo bar")、渡されるシェルコマンドラインは、ssh'' echo '' foo bar ' . The *first* space (the one before echo ) is superflous, but doen't harm. The other one (the ones before ' foo bar ' ) is needed. With '%q ' , we'd pass a ' echo''foo bar'`コマンドラインのようになります。
ステファンシャゼル

回答:


19

の実装にsshは、シェルを使用せずにクライアントからサーバーにコマンドを渡すネイティブな方法はないと思います。

これで、特定のインタープリターのみを実行するようにリモートシェルに指示できれば(sh期待される構文がわかっているのような)、別の手段で実行するコードを与えることができます。

他の手段は、たとえば標準入力または環境変数です

どちらも使用できない場合は、以下のハッキーな3番目のソリューションを提案します。

stdinを使用する

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

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

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

そのxargs -0 env --コマンドラインは、これらすべてのシェルファミリで同じように解釈されます。xargsstdinの引数のヌル区切りリストを読み取り、それらを引数としてに渡しますenv。これは、最初の引数(コマンド名)に=文字が含まれていないことを前提としています。

またはshsh引用構文を使用して各要素を引用した後、リモートホストで使用できます。

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

環境変数を使用する

ここで、クライアントからリモートコマンドの標準入力にデータを供給する必要がある場合、上記のソリューションは機能しません。

sshただし、一部のサーバー展開では、クライアントからサーバーへの任意の環境変数の受け渡しが許可されます。たとえば、Debianベースのシステムでの多くのopensshデプロイメントでは、名前がで始まる変数を渡すことができますLC_

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

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

すべてのシェルファミリと互換性のあるコマンドラインを構築する

上記のオプションがどれも受け入れられない場合(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 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、リモートログインシェルがある、あなたは、その引数が無効な文字が含まれているコマンドを渡すことはできませんが、それはの制限だyashあなたはとにかく回避することができません。
  • cshやbashなどの一部のシェルは、ssh経由で起動されると、いくつかのスタートアップファイルを読み取ります。これらはプリアンブルがまだ機能するように動作を劇的に変更しないと仮定します。
  • のほかにsh、リモートシステムにprintfコマンドがあることも想定しています。

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

  • Bourne:特別な文字'...'を含まない強力な引用符です。バックスラッシュでエスケープできる"..."弱い引用符"です。
  • csh"内部でエスケープできないことを除き、Bourneと同じ"..."です。また、改行文字の前にバックスラッシュを入力する必要があります。そして!、一重引用符の内側でも問題を引き起こします。
  • rc。唯一の引用符は'...'(強い)です。単一引用符内の単一引用符は''(など'...''...')として入力されます。二重引用符またはバックスラッシュは特別ではありません。
  • es。rcと同じですが、引用符の外側でバックスラッシュを使用すると単一引用符をエスケープできます。
  • fish:バックスラッシュが'内部でエスケープすることを除き、Bourneと同じ'...'です。

これらすべての制約があるため、コマンドライン引数を確実に引用できないため、すべてのシェルで機能することがわかります。

次のように単一引用符を使用します。

'foo' 'bar'

以下を除くすべてで動作します:

'echo' 'It'\''s'

では動作しませんrc

'echo' 'foo
bar'

では動作しませんcsh

'echo' 'foo\'

では動作しませんfish

我々は、バックスラッシュのように、変数にそれらの問題の文字を格納するために管理している場合しかし、私たちは周りのほとんどのこれらの問題の仕事をすることができるはず$b、で単一引用符$qでは、改行$n(および!$xシェル独立した方法で、cshの履歴展開のため)。

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

すべてのシェルで動作します。cshただし、それでも改行では機能しません。$n改行が含まれている場合は、改行に展開するためにそれをcsh記述する必要があり$n:q、他のシェルでは機能しません。したがって、代わりにここで行うことはshshそれらを呼び出して展開すること$nです。これは、リモートログインシェル用との2つのレベルの引用を行う必要があることも意味しますsh

$preambleそのコードではトリッキーな部分です。それはちょうどそれらを定義し、それぞれが(それは他人のためにコメントアウトている間)のシェルの一つだけによって解釈コードのいくつかのセクションを持っているすべてのシェルでは、様々な異なる引用規則を使用しています$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

そのコードは、サポートされているシェルのいずれかによって解釈されると、同じコマンドを実行することになります。


1
SSHプロトコル(RFC 4254§6.5)は、リモートコマンドを文字列として定義します。その文字列の解釈方法を決定するのはサーバー次第です。Unixシステムでは、通常の解釈では、ユーザーのログインシェルに文字列を渡します。制限されたアカウントの場合、これはrsshやrushのようなもので、任意のコマンドを受け入れません。アカウントまたはキーに強制コマンドがあり、クライアントから送信されたコマンド文字列が無視されることもあります。
ジル 'SO-悪であるのをやめる'

1
@ Gilles、RFCリファレンスに感謝します。はい、このQ&Aの前提は、リモートユーザーのログインシェルが(実行したいリモートコマンドを実行できるように)使用可能であり、POSIXシステムのメインシェルファミリの1つであることです。制限付きシェルや非シェル、強制コマンド、またはとにかくそのリモートコマンドを実行できないものには興味がありません。
ステファンシャゼル

1
いくつかの一般的なシェル間の構文の主な違いに関する有用なリファレンスは、Hyperpolyglotにあります。
lcd047

0

tl; dr

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

1
これは、リモートユーザーのログインシェルが(bashのprintf%qがbash形式で引用しているように)bash bashであり、リモートマシンで使用できることを前提としています。また、空白やワイルドカードの問題を引き起こす引用符の欠落に関する問題もいくつかあります。
ステファンシャゼラス

@StéphaneChazelas確かに、私の解決策はおそらくbashシェルのみを対象としています。しかし、うまくいけば、人々はそれが役に立つと思うでしょう。私は他の問題にも対処しようとしました。そのbashこと以外に私が見逃しているものがあれば、私に教えてください。
x-yuri

1
質問(ssh_run user@host "${cmd[@]}")のサンプルコマンドではまだ機能しないことに注意してください。まだ引用符が欠落しています。
ステファンシャゼル

1
それは良いです。bashの出力はprintf %q別のロケールで使用するのは安全ではないことに注意してください(また、非常にバグがあります。たとえば、BIG5文字セットを使用するロケールではεα`!(4.3.48)として引用符を付けます)。そのためには、最善の方法は、すべてを引用し、単一引用符で囲むことshquote()です。
ステファンシャゼラス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.