複雑なコマンドラインを ssh 経由で実行するにはどうすればよいですか?

複雑なコマンドラインを ssh 経由で実行するにはどうすればよいですか?

リモート ホストで実行されている Docker コンテナの env を取得したい場合があります。これを行うには、ホストにログインします。

ssh [email protected]

そして、次のコマンドを実行します:

sudo docker exec -it `sudo docker ps | grep mycontainername | awk '{print $1;}'` env

これを 1 つのコマンドで実行したいと思います (3 回以上実行して自動化したいです... :-) )。

これは機能します:

 ssh -t [email protected] sudo docker ps | grep mycontainername

しかし、私がこれをすると

ssh -t [email protected] sudo docker exec -it `sudo docker ps | grep mycontainername | awk '{print $1;}'` env

私は

"docker exec" requires at least 2 arguments.
See 'docker exec --help'.

Usage:  docker exec [OPTIONS] CONTAINER COMMAND [ARG...]

Run a command in a running container
Connection to 123.456.789.10 closed.

これを実行する方法を知っている人はいますか?

答え1

一般的な観察

Bash には、その後に続くものの解析に影響するキーワードがいくつかあります。たとえば、 です[[。ただし、sshはそれらの 1 つではなく、通常のコマンドです。つまり、次のようになります。

  • 行全体ssh …は通常、地元シェル; |、、、、またはスペースなど;の文字はシェルにとって意味を持ちますが、引用符で囲むかエスケープしない限り、 には到達しません (いくつかの例外があります。たとえば、sole は独立した単語として特別ではありません)。これが解析と解釈の最初のレベルです。*"$ssh$

  • シェルが処理を終えた後に渡される引数ssh(または他の通常のコマンド) は、単なる引数、つまり文字列です。これを解釈するのはツールの役割です。これが第 2 レベルです。

コマンドライン引数のいくつかssh(0、1、またはそれ以上) は、サーバー側で実行されるコマンドとして解釈されます。一般的に、ssh複数の引数からコマンドを構築できます。その効果は、サーバー上で次のようなものを呼び出した場合と同じです。

"$SHELL" -c "$command_line_built_by_ssh"

(私はそれがその通りこんな感じですが、何が起こるか理解するには十分近いです。シェルで呼び出されるかのように書いたので、見覚えがあるように見えますが、実際にはまだシェルはありません。また、$command_line…変数としてはありません。この回答の目的で、この名前を使用して文字列を参照しているだけです。

その後、$SHELLサーバー側で$command_line…独自に解析します。これが第 3 レベルです。


具体的な観察

失敗したコマンド

ssh -t [email protected] sudo docker exec -it `sudo docker ps | grep mycontainername | awk '{print $1;}'` env

123.456.789.10有効な IP アドレスではないため失敗しました。

123.456.789.10わかりました。プレースホルダーであることは理解していますが、それでも有効ではありません。 :)

コマンドはsudo docker ps | grep mycontainername | awk '{print $1;}'ローカルで実行されたため失敗しました。出力はおそらく空です。これは、$command_line_built_by_ssh期待していたものとはかけ離れています。

ssh … | grep mycontainernameローカルで実行されることに注意してくださいgrep(これに気付いている場合と気付いていない場合があります)。


議論

リモートシェルが何を取得するかを制御するには$command_line_built_by_ssh、その前に行われる解析と解釈を理解し、予測し、計画する必要があります。ローカルコマンドを作成する必要があります。ローカルシェルで実行してsshダイジェストすると、$command_line…リモート側で実行したい内容とまったく同じになります。

結果が になる前に、実際にローカル シェルで何かを展開または置換したい場合は、かなり複雑になる可能性がありますssh。 必要な逐語的な文字列が として既にあるため、ケースはより単純になります$command_line_built_by_ssh。文字列は次のとおりです。

sudo docker exec -it $(sudo docker ps | grep mycontainername | awk '{print $1;}') env

ノート:

  • 私はバッククォートではなく、 の形式でコマンド置換を使用しました$()好む理由$()
  • 全く分かりません。二重引用符で囲む必要があるdockerかどうかは分かりません。一般的には$(…)引用しないことはほとんどの場合悪い置換によって複数の単語が返された場合(つまり、複数の行が と入力された場合awk)に何が起こるかを考えてみましょう。これは別の問題であり(この場合、問題になるとしても)、この回答では取り上げません。

ローカル シェルによってすべてが展開/解釈されるのを防ぐには、展開をトリガーしたり解釈されたりする可能性のあるすべての文字を適切に引用符で囲むか、エスケープ ( を使用) する必要があります\。この場合$、、、、、、、、、および(場合によってはオプションで) スペースを引用符で(囲みます。)|;{}'

「おそらく、またはオプションでスペースを引用符で囲むかエスケープする」と言ったのは、どのようssh … some commandに動作するかによるものです。サーバー上で実行されるコードとして解釈される引数が 2 つ以上見つかった場合、それらを連結し、間に 1 つのスペースを追加します。これが の$command_line_built_by_ssh構築方法です。リモート シェルのコードと思われる部分でスペースを引用符で囲んだりエスケープしたりしない場合、ローカル シェルは単語を分割する際にスペース (およびタブ) を消費し、その後sshスペースを追加します。タブや連続する複数のスペースがある場合、結果は期待どおりにならない可能性があります。例:

ssh user@server echo a     b

sshuser@server、、、echoを取得しますabリモート コマンドは となりecho a bechoを取得しますabが出力されますa b

それからこれ:

ssh user@server 'echo a     b'

sshを取得しますuser@serverecho a bリモート コマンドは となりecho a bechoを取得しますabが出力されますa b

そして最後にこれ:

ssh user@server 'echo "a     b"'

sshを取得しますuser@serverecho "a b"リモート コマンドは となりecho "a b"echoが取得されますa b。 が印刷されますa b

結論としては、ローカルシェルのコンテキストで引用する必要があり、別々にリモートシェルのコンテキストでは、シェルによる展開に関しては、外側の引用符は重要


解決

これらの情報をすべてまとめると(そして、保護したいと仮定すると)、すべてローカルシェルによって展開/解釈されないようにするには、次のようにアドバイスします。

  • 引用するか、逃げるか。
  • 好む引用1 組の引用符では多くの文字を保護できますが、1 組の引用符では\1 文字しか保護できないため、エスケープは多用されがちです。すべてを保護するには、おそらく多くのバックスラッシュが必要になりますが、多くの場合、1 組の引用符だけで同じ結果を得ることができます。
  • 好むシングルクォート( ') は、 以外のすべてを保護できます'。一方、二重引用符 ( ") は、も保護できます'が、 は保護できません( Bash では も保護できない場合もありますが、も保護できない場合もあります)。また、 もエスケープされます (つまり、$"\!$そして引用符で囲まれている、つまり二重引用符で囲まれている。ただし! それは 面倒な)。
  • コマンドを次のように提供することを推奨しますシングルへの議論ssh

これにより、次の手順が実行されます。

  1. リモート側で実行する逐語的なコマンドを準備します。
  2. すべての''"'"'または に置き換えます'\''( ごとに個別に選択できます')。
  3. 結果の文字列全体を一重引用符で囲みます。
  4. ssh …前に追加します。

逐語的なコマンドは次のとおりです。

sudo docker exec -it $(sudo docker ps | grep mycontainername | awk '{print $1;}') env

この手順の結果は次のようになります。

ssh -t user@server 'sudo docker exec -it $(sudo docker ps | grep mycontainername | awk '\''{print $1;}'\'') env'
# single-quoted     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^    ^^^^^^^^^^^    ^^^^^
# escaped                                                                                ^              ^

この手順は、最短の文字列につながる場合とそうでない場合があることに注意してください。洞察力があれば、文字列を「最適化」できる場合があります。しかし、この手順は非常に単純そして完全に信頼できます。ローカル シェルによってすべてが展開/解釈されないように保護する必要があることがわかっている場合、手順自体にはそれ以上の洞察はまったく必要ありません。


オートメーション

実際、この手順は自動化できます。文字列に引用符を追加し、既存の引用符を適切に保持できるツールがあります。Bash 自体がそのようなツールです。私のこの答えBash でキーストロークを使用してこれを行う方法を提供します。あなたのケースに合わせて調整可能な解決策は次のとおりです。

  1. ローカル シェルで、次のカスタム関数とバインディングを定義します。

     _prepend_ssh() { READLINE_LINE="ssh  ${READLINE_LINE@Q}"; READLINE_POINT=4; }
     bind -x '"\C-x\C-h":_prepend_ssh'
    
  2. ローカルシェルで、リモートシェルで実行したいコマンドを入力(または貼り付け)します。実行しないでください。コマンドは次のようになります。その通りリモート シェルで希望どおりに実行します。

  3. Ctrl+ xCtrl+を押しますh。ローカル シェルが引用符の処理を行います。また、ssh先頭に追加し、その直後にカーソルを配置します。

  4. 不足している引数を追加(入力)します(例:-t user@server)。便宜上、カーソルはすでにこれを行うための適切な位置にあります。

  5. Enter


代替案

リモートシェルにコマンドをそのまま渡す別の方法があります。場合によっては、パイプを介して実行しますssh。リモート コマンド ラインは次のようになると仮定します。

echo "$PATH"; date

上記のように進めて、一重引用符を追加し、次のようにローカルで実行できます。

ssh user@server 'echo "$PATH"; date'

この例は単純ですが、一般的に引用符を追加するのは必ずしも簡単ではありません。代わりに、次のようにコマンドをパイプすることもできます (最初の例はecho単純化のため)。printf優れている):

echo 'echo "$PATH"; date' | ssh user@server bash

それでも、これらの一重引用符が必要です。ただし、コマンドがファイル内にある場合は、次のようになります。

<file ssh user@server bash

あるいはファイルがなくても(ここに文書):

ssh user@server bash <<'EOF'
echo "$PATH"
date
EOF

(引用符がローカルで展開されないように注意してください<<'EOF'$PATH)

利点:

  • 複数行のコマンド/スニペット/スクリプトを簡単に渡すことができます (echo … ; dateこれを示すために分割しました)。
  • 追加の引用は必要ありません。
  • シェルである必要のないリモート インタープリターを明示的に選択できます (例:bashまたはzsh、またはpython)。

デメリット:

  • リモートインタープリタを明示的に指定する必要があります。そうでない場合は、デフォルトログインシェルが生成され、その日のメッセージが印刷されるかもしれません。 を適切に引用符で囲んで指定することで、デフォルトのシェルを非ログイン シェルとして使用することもできますexec "$SHELL"(行は次のようになりますssh … 'exec "$SHELL"' <<'EOF')。
  • の標準入力はssh端末ではないため、使用できません-t(これが、元のコマンドを例として使用しなかった理由です)。
  • コマンドはbash標準入力を介してリモートインタープリター(例では)に送信されます。考えられる問題:
    • 子プロセス (または組み込み) は同じ stdin を使用します。いずれかが stdin から読み取る場合、同じストリームが読み取られ、インタープリタ宛ての次のコマンドが読み取られる可能性があります。この動作は抑制することも、創造的に (悪用) することもできますが、ここでは詳しく説明しません。
    • このチャネルを使用して他のものをパイプすることは簡単にはできません。

関連情報