Zitiert in den Konstrukten ssh $ host $ FOO und ssh $ host vom Typ „sudo su user -c $ FOO“

30

Ich gebe oft komplexe Befehle über ssh aus. Diese Befehle beinhalten die Weiterleitung an awk oder perl einzeilig und enthalten daher einfache Anführungszeichen und $. Ich habe weder eine feste Regel gefunden, um das Zitat richtig zu machen, noch eine gute Referenz dafür gefunden. Betrachten Sie zum Beispiel Folgendes:

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

(Beachten Sie die zusätzlichen Anführungszeichen in der awk-Anweisung.)

Aber wie bekomme ich das zum arbeiten, zB ssh $host "sudo su user -c '$CMD'"? Gibt es ein allgemeines Rezept für die Verwaltung von Angeboten in solchen Szenarien?

Leo Alekseyev
quelle

Antworten:

35

Der Umgang mit mehreren Ebenen des Zitierens (wirklich mehrere Ebenen des Parsens / Interpretierens) kann kompliziert werden. Es hilft, ein paar Dinge zu beachten:

  • Jede „Zitierstufe“ kann möglicherweise eine andere Sprache beinhalten.
  • Angebotsregeln variieren je nach Sprache.
  • Wenn Sie mit mehr als einer oder zwei verschachtelten Ebenen arbeiten, ist es normalerweise am einfachsten, „von unten nach oben“ zu arbeiten (dh von innen nach außen).

Ebenen des Zitierens

Sehen wir uns Ihre Beispielbefehle an.

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

Ihr erster Beispielbefehl (oben) verwendet vier Sprachen: Ihre Shell, den regulären Ausdruck in pgrep , den regulären Ausdruck in grep (der sich möglicherweise von der regulären Ausdruckssprache in pgrep unterscheidet ) und awk . Es gibt zwei Interpretationsebenen: die Shell und eine Ebene nach der Shell für jeden der beteiligten Befehle. Es gibt nur eine explizite Quote-Ebene (Shell-Quote in awk ).

ssh host 

Als nächstes haben Sie eine Stufe von ssh hinzugefügt . Dies ist praktisch eine andere Shell-Ebene: ssh interpretiert den Befehl nicht selbst, sondern übergibt ihn an eine Shell auf der Remote-Seite (über (z. B.) sh -c …), und diese Shell interpretiert den String.

ssh host "sudo su user -c …"

Dann haben Sie gefragt, ob Sie eine weitere Shell-Ebene in der Mitte hinzufügen möchten , indem Sie su verwenden (über sudo , das die Befehlsargumente nicht interpretiert, sodass wir sie ignorieren können). An diesem Punkt gibt es drei Ebenen der Verschachtelung ( awk → shell, shell → shell ( ssh ), shell → shell ( su user -c ) Ihre Muscheln sind Bourne-kompatibel (z. B. sh , ash , dash , ksh , bash , zsh usw.). Eine andere Art von Muschel ( fish , rc)usw.) erfordern möglicherweise eine andere Syntax, die Methode gilt jedoch weiterhin.

Prost

  1. Formulieren Sie die Zeichenfolge, die Sie auf der innersten Ebene darstellen möchten.
  2. Wählen Sie einen Zitiermechanismus aus dem Zitierrepertoire der nächsthöheren Sprache.
  3. Zitieren Sie die gewünschte Zeichenfolge gemäß dem von Ihnen ausgewählten Zitiermechanismus.
    • Es gibt oft viele Variationen, wie man welchen Zitiermechanismus anwendet. Das von Hand zu machen, ist normalerweise eine Frage der Übung und Erfahrung. Wenn Sie programmgesteuert vorgehen, ist es in der Regel am besten, die am einfachsten zu behebende Option auszuwählen (in der Regel die "wörtlichste" (die wenigsten Fluchtwege)).
  4. Verwenden Sie optional die resultierende Zeichenfolge in Anführungszeichen mit zusätzlichem Code.
  5. Wenn Sie Ihr gewünschtes Zitat / Interpretationsniveau noch nicht erreicht haben, nehmen Sie den resultierenden zitierten String (plus eventuell hinzugefügten Code) und verwenden Sie ihn als Start-String in Schritt 2.

Zitiersemantik Vary

Hierbei ist zu beachten, dass jede Sprache (Zitatstufe) ein und demselben Zitatzeichen eine leicht unterschiedliche Semantik (oder sogar eine drastisch unterschiedliche Semantik) geben kann.

Die meisten Sprachen haben einen "wörtlichen" Zitiermechanismus, aber sie unterscheiden sich genau darin, wie wörtlich sie sind. Das einfache Anführungszeichen von Bourne-ähnlichen Shells ist tatsächlich wörtlich (was bedeutet, dass Sie es nicht verwenden können, um ein einfaches Anführungszeichen selbst zu zitieren). Andere Sprachen (Perl, Ruby) sind insofern weniger wörtlich, als sie einige Backslash-Sequenzen innerhalb von Regionen in einfachen Anführungszeichen nicht wörtlich interpretieren (insbesondere \\und \'führen zu \und ', aber andere Backslash-Sequenzen sind tatsächlich wörtlich).

Sie müssen die Dokumentation für jede Ihrer Sprachen lesen, um die Angebotsregeln und die allgemeine Syntax zu verstehen.

Dein Beispiel

Die innerste Ebene Ihres Beispiels ist ein awk- Programm.

{print $1}

Sie werden dies in eine Shell-Befehlszeile einbetten:

pgrep -fl java | grep -i datanode | awk 

Wir müssen (zumindest) den Raum und $das AWK- Programm schützen . Die naheliegende Wahl ist die Verwendung von einfachen Anführungszeichen in der Shell um das gesamte Programm.

  • '{print $1}'

Es gibt jedoch noch andere Möglichkeiten:

  • {print\ \$1} direkt dem Raum entkommen und $
  • {print' $'1} einfaches Anführungszeichen nur der Raum und $
  • "{print \$1}" das Ganze doppelt zitieren und dem entkommen $
  • {print" $"1}Nur das Leerzeichen in doppelte Anführungszeichen setzen und $
    Dies kann die Regeln ein wenig verbiegen (das Entkommen $am Ende einer Zeichenfolge in doppelten Anführungszeichen ist wörtlich), aber es scheint in den meisten Shells zu funktionieren.

Wenn das Programm ein Komma zwischen den offenen und geschlossenen geschweiften Klammern verwenden würde, müssten wir auch entweder das Komma oder die geschweiften Klammern in Anführungszeichen setzen oder streichen, um eine „Klammererweiterung“ in einigen Shells zu vermeiden.

Wir wählen es aus '{print $1}'und binden es in den Rest des Shell-Codes ein:

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

Als nächstes wollten Sie dies über su und sudo ausführen .

sudo su user -c 

su user -c …ist genau wie some-shell -c …(außer, dass es unter einer anderen UID läuft), also fügt su nur eine weitere Shell-Ebene hinzu. sudo interpretiert seine Argumente nicht und fügt daher keine Anführungszeichen hinzu.

Wir benötigen eine andere Shell-Ebene für unsere Befehlszeichenfolge. Wir können wieder einfache Anführungszeichen auswählen, müssen aber den vorhandenen einfachen Anführungszeichen eine besondere Behandlung geben. Der übliche Weg sieht so aus:

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

Hier gibt es vier Zeichenfolgen, die von der Shell interpretiert und verkettet werden: die erste Zeichenfolge in Anführungszeichen ( pgrep … awk), ein einfaches Anführungszeichen mit Escapezeichen, das Programm awk mit Anführungszeichen und ein weiteres einfaches Anführungszeichen mit Escapezeichen.

Es gibt natürlich viele Alternativen:

  • pgrep\ -fl\ java\ \|\ grep\ -i\ datanode\ \|\ awk\ \'{print\ \$1} entkomme allem wichtigen
  • pgrep\ -fl\ java\|grep\ -i\ datanode\|awk\ \'{print\$1}das gleiche, aber ohne überflüssiges Leerzeichen (auch im awk- Programm!)
  • "pgrep -fl java | grep -i datanode | awk '{print \$1}'" doppelte Anführungszeichen das Ganze, entkomme dem $
  • 'pgrep -fl java | grep -i datanode | awk '"'"'{print \$1}'"'"Ihre Variation; etwas länger als üblich, da doppelte Anführungszeichen (zwei Zeichen) anstelle von Escapezeichen (ein Zeichen) verwendet werden

Die Verwendung anderer Anführungszeichen in der ersten Ebene ermöglicht andere Variationen auf dieser Ebene:

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

Wenn Sie die erste Variante in die sudo / * su * -Befehlszeile einbetten, geben Sie Folgendes ein:

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

Sie können dieselbe Zeichenfolge auch in anderen Kontexten auf Shell-Ebene verwenden (z ssh host …. B. ).

Als nächstes haben Sie eine Stufe von ssh hinzugefügt . Dies ist praktisch eine andere Shell-Ebene: ssh interpretiert den Befehl selbst nicht, übergibt ihn jedoch einer Shell auf der Remote-Seite (über (z. B.) sh -c …), und diese Shell interpretiert den String.

ssh host 

Der Prozess ist der gleiche: Nehmen Sie die Zeichenfolge, wählen Sie eine Anführungszeichenmethode aus, verwenden Sie sie, betten Sie sie ein.

Wieder einfache Anführungszeichen verwenden:

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

Jetzt gibt es elf Zeichenfolgen, die interpretiert und verkettet werden: 'sudo su user -c 'Escape-Anführungszeichen, 'pgrep … awk 'Escape-Anführungszeichen, Escape-Backslash, Zwei Escape-Anführungszeichen, das Programm awk mit einfachen Anführungszeichen, Escape- Anführungszeichen, Escape-Backslash und End-Escape-Anführungszeichen .

Das endgültige Formular sieht folgendermaßen aus:

ssh host 'sudo su user -c '\''pgrep -fl java | grep -i datanode | awk '\'\\\'\''{print $1}'\'\\\'

Es ist etwas unhandlich, dies von Hand zu tippen, aber die wörtliche Art der einfachen Anführungszeichen in der Shell macht es einfach, eine geringfügige Variation zu automatisieren:

#!/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")"
Chris Johnsen
quelle
5
Tolle Erklärung!
Gilles 'SO - hör auf böse zu sein'
7

In der Antwort von Chris Johnsen finden Sie eine klare, ausführliche Erklärung mit einer allgemeinen Lösung. Ich werde ein paar zusätzliche Tipps geben, die unter bestimmten Umständen hilfreich sind.

Einfache Anführungszeichen entziehen sich allem außer einem einfachen Anführungszeichen. Wenn Sie also wissen, dass der Wert einer Variablen kein einzelnes Anführungszeichen enthält, können Sie ihn sicher zwischen einzelnen Anführungszeichen in einem Shell-Skript interpolieren.

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

Wenn Ihre lokale Shell ksh93 oder zsh ist, können Sie mit einfachen Anführungszeichen in der Variablen fertig werden, indem Sie sie in neu schreiben '\''. (Obwohl bash auch das ${foo//pattern/replacement}Konstrukt hat, macht der Umgang mit einfachen Anführungszeichen für mich keinen Sinn.)

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

Ein weiterer Tipp, um verschachtelte Anführungszeichen zu vermeiden, besteht darin, Zeichenfolgen so oft wie möglich durch Umgebungsvariablen zu übergeben. Ssh und sudo neigen dazu, die meisten Umgebungsvariablen zu löschen, sind jedoch häufig so konfiguriert, dass sie durchgelassen werden LC_*, da diese normalerweise sehr wichtig für die Benutzerfreundlichkeit sind (sie enthalten Gebietsschemainformationen) und selten als sicherheitsrelevant eingestuft werden.

LC_CMD='what you would use locally' ssh $host 'sudo su user -c "$LC_CMD"'

Da hier LC_CMDein Shell-Snippet enthalten ist, muss es buchstäblich für die innerste Shell bereitgestellt werden. Daher wird die Variable durch die unmittelbar darüber liegende Shell erweitert. Die innerste Shell sieht "$LC_CMD", und die innerste Shell sieht die Befehle.

Eine ähnliche Methode ist nützlich, um Daten an ein Textverarbeitungsprogramm zu übergeben. Wenn Sie die Shell-Interpolation verwenden, wird der Wert der Variablen vom Dienstprogramm als Befehl behandelt, z. B. sed "s/$pattern/$replacement/"funktioniert dies nicht, wenn die Variablen enthalten /. Verwenden Sie daher awk (nicht sed) und entweder dessen -vOption oder das ENVIRONArray, um Daten von der Shell zu übergeben (wenn Sie dies durchlaufen ENVIRON, müssen Sie die Variablen exportieren).

awk -vpattern="$pattern" replacement="$replacement" '{gsub(pattern,replacement); print}'
Gilles 'SO - hör auf böse zu sein'
quelle
2

Wie Chris Johnson sehr gut beschreibt , gibt es hier einige Stufen der zitierten Indirektion. Sie anweisen , Ihre lokale shelldie Fernbedienung anzuweisen , shellüber sshdie sie anweisen sollte sudoanweisen , sudie Fernbedienung zu beauftragen , shellIhre Pipeline zu laufen pgrep -fl java | grep -i datanode | awk '{print $1}'wie user. Diese Art von Befehl erfordert viel Mühe \'"quote quoting"\'.

Wenn Sie meinen Rat befolgen, werden Sie auf den ganzen Unsinn verzichten und Folgendes tun:

% 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>

FÜR MEHR:

Überprüfen Sie meine (möglicherweise zu langwierige) Antwort auf Piping-Pfade mit verschiedenen Arten von Anführungszeichen für die Slash-Substitution, in der ich einige der Theorien erörtere, die dahinter stehen, warum dies funktioniert.

-Mike

U / min mikeserv
quelle
Das sieht interessant aus, aber ich kann es nicht zum Laufen bringen. Könnten Sie ein minimales Arbeitsbeispiel posten?
John Lawrence Aspden
Ich glaube, dieses Beispiel erfordert zsh, da es mehrere Umleitungen zu stdin verwendet. In anderen Bourne-ähnlichen Schalen <<ersetzt die zweite einfach die erste. Sollte irgendwo "zsh only" stehen oder habe ich etwas verpasst? (Cleverer Trick, um einen Heredoc zu haben, der teilweise der lokalen Erweiterung unterliegt)
Hier ist eine bash-kompatible Version: unix.stackexchange.com/questions/422489/…
dabest1
0

Wie wäre es mit mehr Anführungszeichen?

Dann ssh $host $CMDsolltest du mit diesem gut zurechtkommen:

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

Nun zum komplexeren, dem ssh $host "sudo su user -c \"$CMD\"". Ich denke , alles , was Sie tun müssen , ist empfindlich Escape - Zeichen in CMD: $, \und ". Also würde ich versuchen und sehen , ob das funktioniert: echo $CMD | sed -e 's/[$\\"]/\\\1/g'.

Wenn das in Ordnung aussieht, wickeln Sie das Echo + Sed in eine Shell-Funktion ein, und Sie können loslegen ssh $host "sudo su user -c \"$(escape_my_var $CMD)\"".

Alex
quelle