web-dev-qa-db-ja.com

ssh $ Host $ FOOとssh $ Host "Sudo su user -c $ FOO"型構成の引用

Sshを介して複雑なコマンドを発行することがよくあります。これらのコマンドには、awkまたはPerlの1行へのパイプが含まれるため、結果として単一引用符と$が含まれます。私は引用を適切に行うための厳格な規則を理解することも、そのための適切なリファレンスを見つけることもできませんでした。たとえば、次のことを考慮してください。

# what I'd run locally:
CMD='pgrep -fl Java | grep -i datanode | awk '{print $1}'
# this works with ssh $Host "$CMD":
CMD='pgrep -fl Java | grep -i datanode | awk '"'"'{print $1}'"'"

(awkステートメントの余分な引用符に注意してください。)

しかし、これをどのように機能させるのですか? ssh $Host "Sudo su user -c '$CMD'"?そのようなシナリオで見積もりを管理するための一般的なレシピはありますか?..

31
Leo Alekseyev

複数のレベルの引用(実際には、複数のレベルの解析/解釈)を処理すると、複雑になる可能性があります。それはいくつかのことを覚えておくのに役立ちます:

  • 各「引用のレベル」には、異なる言語が含まれる可能性があります。
  • 引用規則は言語によって異なります。
  • 1つまたは2つ以上のネストされたレベルを処理する場合、通常、「下から上」(つまり、最も内側から最も外側)で作業するのが最も簡単です。

引用のレベル

コマンド例を見てみましょう。

pgrep -fl Java | grep -i datanode | awk '{print $1}'

最初のコマンド例(上記)は4つの言語を使用しています:シェル、pgrepの正規表現、grepの正規表現(これはpgrep)およびawkが正規表現言語と異なる。関連する解釈には2つのレベルがあります。シェルと、関連するコマンドごとにシェルの後に1レベルあります。引用の明示的なレベルは1つだけです(awkへのシェル引用)。

ssh Host …

次に、sshのレベルを上に追加しました。これは事実上別のシェルレベルです:sshはコマンド自体を解釈せず、リモートエンドのシェルに(たとえばsh -c …を介して)渡しますシェルは文字列を解釈します。

ssh Host "Sudo su user -c …"

次に、suを使用してSudoを使用して、途中に別のシェルレベルを追加することについて質問しました。これは、コマンド引数を解釈しません。無視できます)。この時点で、(awk→シェル、シェル→シェル(ssh)、シェル→シェル(su user -c)なので、「下から上」のアプローチを使用することをお勧めします。シェルはBourne互換であると想定します(たとえば shashdashkshbashzshなど)。他の種類のシェル(fishrcなど)は異なる構文を必要とする場合がありますが、メソッドは引き続き適用されます。

一気飲み

  1. 最も内側のレベルで表現する文字列を作成します。
  2. 次に高い言語の引用レパートリーから引用メカニズムを選択します。
  3. 選択した引用メカニズムに従って、目的の文字列を引用します。
    • 多くの場合、どの引用メカニズムを適用するかについては多くのバリエーションがあります。手作業で行うことは、通常、実践と経験の問題です。プログラムでそれを行う場合、通常は最も簡単に正しく選択することをお勧めします(通常は「ほとんどのリテラル」(最小限のエスケープ))。
  4. オプションで、結果の引用符付き文字列を追加のコードとともに使用します。
  5. 希望する引用/解釈レベルにまだ達していない場合は、結果の引用文字列(および追加されたコード)を取得し、それをステップ2の開始文字列として使用します。

引用セマンティクスは異なります

ここで覚えておくべきことは、各言語(引用レベル)が同じ引用文字にわずかに異なるセマンティクス(または大幅に異なるセマンティクス)を与える可能性があることです。

ほとんどの言語には「リテラル」引用メカニズムがありますが、それらは正確にどのようにリテラルかによって異なります。 Bourneのようなシェルの単一引用符は、実際にはリテラルです(つまり、それを使用して単一引用符自体を引用することはできません)。他の言語(Perl、Ruby)は、一重引用符で囲まれた領域内のsomeバックスラッシュシーケンスを非文字通りに解釈するという点で、あまりリテラルではありません(具体的には、\\および\'\および'ですが、他のバックスラッシュシーケンスは実際にはリテラルです)。

引用規則と全体的な構文を理解するには、各言語のドキュメントを読む必要があります。

あなたの例

あなたの例の最も内側のレベルはawkプログラムです。

{print $1}

これをシェルのコマンドラインに埋め込みます:

pgrep -fl Java | grep -i datanode | awk …

(少なくとも)awkプログラムのスペースと$を保護する必要があります。明白な選択は、プログラム全体の周りにシェルで単一引用符を使用することです。

  • '{print $1}'

ただし、他の選択肢もあります。

  • {print\ \$1}はスペースを直接エスケープし、$
  • {print' $'1}スペースのみの単一引用符と$
  • "{print \$1}"全体を二重引用符で囲み、$をエスケープします
  • {print" $"1}スペースのみを二重引用符で囲み、$
    これは規則を少し曲げている可能性があります(二重引用符で囲まれた文字列の末尾のエスケープされていない$はリテラルです)が、ほとんどのシェルで機能するようです。

プログラムが開始中括弧と終了中括弧の間にコンマを使用した場合、一部のシェルでの「中括弧の展開」を回避するために、コンマまたは中括弧のいずれかを引用またはエスケープする必要もあります。

'{print $1}'を選択し、それをシェルの残りの「コード」に埋め込みます。

pgrep -fl Java | grep -i datanode | awk '{print $1}'

次に、これをsuおよびSudoで実行したいとします。

Sudo su user -c …

su user -c …some-Shell -c …とまったく同じです(他のUIDで実行することを除く)。したがって、suは、別のシェルレベルを追加するだけです。 Sudoは引数を解釈しないため、引用レベルは追加されません。

コマンド文字列には別のシェルレベルが必要です。もう一度単一引用符を選択できますが、既存の単一引用符に特別な処理を与える必要があります。通常の方法は次のようになります。

'pgrep -fl Java | grep -i datanode | awk '\''{print $1}'\'

ここでは、シェルが解釈して連結する4つの文字列があります。最初の単一引用符付き文字列(pgrep … awk)、エスケープされた単一引用符、単一引用符付きawkプログラム、別のエスケープされた単一引用符。

もちろん、多くの選択肢があります。

  • pgrep\ -fl\ Java\ \|\ grep\ -i\ datanode\ \|\ awk\ \'{print\ \$1}重要なものすべてをエスケープする
  • pgrep\ -fl\ Java\|grep\ -i\ datanode\|awk\ \'{print\$1}と同じですが、余分な空白文字はありません(awkプログラムでも!)
  • "pgrep -fl Java | grep -i datanode | awk '{print \$1}'"全体を二重引用符で囲み、$をエスケープします
  • 'pgrep -fl Java | grep -i datanode | awk '"'"'{print \$1}'"'"バリエーション。エスケープ(1文字)ではなく二重引用符(2文字)を使用するため、通常の方法より少し長くなります。

最初のレベルで異なる引用を使用すると、このレベルで他のバリエーションが可能になります。

  • 'pgrep -fl Java | grep -i datanode | awk "{print \$1}"'
  • 'pgrep -fl Java | grep -i datanode | awk {print\ \$1}'

Sudo/ * su *コマンドラインに最初のバリエーションを埋め込むと、次のようになります。

Sudo su user -c 'pgrep -fl Java | grep -i datanode | awk '\''{print $1}'\'

同じ文字列を他の単一のシェルレベルコンテキストで使用できます(例:ssh Host …)。

次に、sshのレベルを上に追加しました。これは事実上別のシェルレベルです:sshはコマンド自体を解釈しませんが、リモートエンドのシェルに((たとえば)sh -c …を介して)渡しますそのシェルは文字列を解釈します。

ssh Host …

プロセスは同じです:文字列を取り、引用メソッドを選び、それを使用し、埋め込みます。

もう一度一重引用符を使用する:

'Sudo su user -c '\''pgrep -fl Java | grep -i datanode | awk '\'\\\'\''{print $1}'\'\\\'

これで、解釈および連結される11個の文字列があります:'Sudo su user -c '、エスケープされた一重引用符、'pgrep … awk '、エスケープされた一重引用符、エスケープされた円記号、2つのエスケープされた一重引用符、一重引用符awk プログラム、エスケープされた単一引用符、エスケープされた円記号、および最後のエスケープされた単一引用符。

最終的なフォームは次のようになります。

ssh Host 'Sudo su user -c '\''pgrep -fl Java | grep -i datanode | awk '\'\\\'\''{print $1}'\'\\\'

これは手で入力するのが少し扱いに​​くいですが、シェルの単一引用の文字通りの性質により、わずかなバリエーションを簡単に自動化できます。

#!/bin/sh

sq() { # single quote for Bourne Shell evaluation
    # Change ' to '\'' and wrap in single quotes.
    # If original starts/ends with a single quote, creates useless
    # (but harmless) '' at beginning/end of result.
    printf '%s\n' "$*" | sed -e "s/'/'\\\\''/g" -e 1s/^/\'/ -e \$s/\$/\'/
}

# Some shells (ksh, bash, zsh) can do something similar with %q, but
# the result may not be compatible with other shells (ksh uses $'...',
# but dash does not recognize it).
#
# sq() { printf %q "$*"; }

ap='{print $1}'
s1="pgrep -fl Java | grep -i datanode | awk $(sq "$ap")"
s2="Sudo su user -c $(sq "$s1")"

ssh Host "$(sq "$s2")"
35
Chris Johnsen

一般的な解決策の明確で詳細な説明については、 Chris Johnsenの回答 を参照してください。いくつかの一般的な状況で役立ついくつかの追加のヒントを提供します。

単一引用符は、単一引用符以外のすべてをエスケープします。したがって、変数の値に一重引用符が含まれていないことがわかっている場合は、シェルスクリプトで一重引用符の間を安全に補間できます。

su -c "grep '$pattern' /root/file"  # assuming there is no ' in $pattern

ローカルシェルがksh93またはzshの場合は、変数の単一引用符を'\''に書き換えることで対処できます。 (bashにも${foo//pattern/replacement}構文がありますが、単一引用符の処理は私には意味がありません。)

su -c "grep '${pattern//'/'\''}' /root/file"  # if the outer Shell is zsh
su -c "grep '${pattern//\'/\'\\\'\'}' /root/file"  # if the outer Shell is ksh93

ネストされた引用を処理する必要がないようにする別のヒントは、環境変数を介して文字列をできるだけ渡すことです。 SshとSudoはほとんどの環境変数を削除する傾向がありますが、通常はLC_*を通過させるように構成されています。これらは通常、使いやすさにとって非常に重要であり(ロケール情報が含まれているため)、セキュリティ上重要であるとは考えられないためです。

LC_CMD='what you would use locally' ssh $Host 'Sudo su user -c "$LC_CMD"'

ここでは、LC_CMDにはシェルスニペットが含まれているため、文字通り、最も内側のシェルに提供する必要があります。したがって、変数はすぐ上のシェルによって展開されます。最も内側の1つのシェルは"$LC_CMD"を参照し、最も内側のシェルはコマンドを参照します。

同様の方法は、データをテキスト処理ユーティリティに渡すのに役立ちます。シェル補間を使用する場合、ユーティリティは変数の値をコマンドとして扱います。変数にsed "s/$pattern/$replacement/"が含まれている場合、/は機能しません。したがって、awk(sedではない)とその-vオプションまたはENVIRON配列を使用して、シェルからデータを渡します(ENVIRONを使用する場合は、変数をエクスポートしてください) 。

awk -vpattern="$pattern" replacement="$replacement" '{gsub(pattern,replacement); print}'

Chris Johnsonが非常によく説明している のように、引用符で囲まれた間接参照のレベルがいくつかあります。 ShellShellにリモートsshにパイプラインpgrep -fl Java | grep -i datanode | awk '{print $1}'を__some_variableとして実行するように指示するようにSudoに指示する必要があることをsuを介してリモートShellに指示するようにローカルuserに指示しますこの種のコマンドでは、面倒な\'"quote quoting"\'がたくさん必要になります。

私の助言を受け入れるなら、あなたはナンセンスのすべてを放棄し、実行します:

% ssh $Host <<REM=LOC_EXPANSION <<'REMOTE_CMD' |
> localhost_data='$(commands run on localhost at runtime)' #quotes don't affect expansion
> more_localhost_data="$(values set at heredoc expansion)" #remote Shell will receive m_h_d="result"
> REM=LOC_EXPANSION
> commands typed exactly as if located at 
> the remote terminal including variable 
> "${{more_,}localhost_data}" operations
> 'quotes and' \all possibly even 
> a\wk <<'REMOTELY_INVOKED_HEREDOC' |
> {as is often useful with $awk
> so long as the terminator for}
> REMOTELY_INVOKED_HEREDOC
> differs from that of REM=LOC_EXPANSION and
> REMOTE_CMD
> and here you can | pipeline operate on |\
> any output | received from | ssh as |\
> run above | in your local | terminal |\
> however | tee > ./you_wish.result
<desired output>

詳細:

スラッシュ置換のためのさまざまなタイプの引用符のあるパスのパイピング への私の(おそらく長すぎる)回答を確認してください。ここで、それが機能する理由の背後にある理論のいくつかについて説明します。

-マイク

2
mikeserv

より多くの二重引用符を使用するのはどうですか?

次に、あなたの_ssh $Host $CMD_はこれでうまく機能するはずです:

_CMD="pgrep -fl Java | grep -i datanode | awk '{print $1}'"_

次に、より複雑なもの、_ssh $Host "Sudo su user -c \"$CMD\""_について説明します。 CMDのセンシティブ文字をエスケープするだけでよいのではないかと思います:_$_、_\_および_"_。だから私はこれがうまくいくかどうか試してみます:_echo $CMD | sed -e 's/[$\\"]/\\\1/g'_。

これで問題がなければ、echo + sedをシェル関数にラップし、ssh $Host "Sudo su user -c \"$(escape_my_var $CMD)\""を使用することをお勧めします。

0
alex