ssh $ host $ FOOおよびssh $ host“ sudo su user -c $ FOO”タイプの構造で引用


30

私はしばしば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'"ますか?このようなシナリオで見積もりを管理するための一般的なレシピはありますか?..

回答:


35

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

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

引用のレベル

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

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

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

ssh host 

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

ssh host "sudo su user -c …"

次に、suを使用して中間に別のシェルレベルを追加することを求めました(コマンド引数を解釈しないsudoを使用して、無視できます)。この時点で、3つのレベルのネストが行われています(awk →シェル、シェル→シェル(ssh)、シェル→シェル(su user -c)。シェルはBourne互換です(shashdashkshbashzshなど)。他の種類のシェル(fishrcなど)異なる構文が必要になる場合がありますが、この方法は引き続き適用されます。

一気飲み

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

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

ここで心に留めておくべきことは、各言語(引用レベル)が同じ引用文字に対してわずかに異なるセマンティクス(または大幅に異なるセマンティクス)を与える場合があるということです。

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

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

あなたの例

例の最も内側のレベルは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}'

次に、susudoを介してこれを実行したいと考えました。

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

5
素晴らしい説明!
ジル「SO-悪であるのをやめる」

7

一般的な解決策に関する明確で詳細な説明については、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}'

2

クリス・ジョンソンは非常によく説明し、あなたはここで引用された間接のいくつかのレベルを持っています。パイプラインをとして実行するようにリモートに指示するよう指示する必要があることを介してshell、リモートに指示するようローカルに指示します。そのようなコマンドは多くの退屈な作業を必要とします。shellsshsudosushellpgrep -fl java | grep -i datanode | awk '{print $1}'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>

多くのための:

スラッシュ置換の引用符の種類が異なるパイピングパスに対する私の(おそらく長すぎる)答えをチェックしてください。

-マイク


これはおもしろそうに見えますが、うまくいきません。最小限の作業例を投稿していただけますか?
ジョンローレンスアスプデン

この例では、stdinへの複数のリダイレクトを使用するため、zshが必要だと思います。他のBourneのようなシェルでは、2番目のシェル<<が最初のシェルを単純に置き換えます。どこかで「zshのみ」と言うべきでしょうか、それとも何かを見逃しましたか?(巧妙なトリックですが、部分的にローカル展開の対象となるヒアドキュメントを作成するため)

bash互換バージョンは次のとおり
422489

0

さらに二重引用符を使用してはどうですか?

次に、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)\""

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.