Wie führe ich eine komplexe Befehlszeile über SSH aus?

Wie führe ich eine komplexe Befehlszeile über SSH aus?

Manchmal möchte ich die Umgebung eines Docker-Containers abrufen, der auf einem Remote-Host ausgeführt wird. Dazu melde ich mich beim Host an:

ssh [email protected]

und dann führe ich diesen Befehl aus:

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

Ich möchte dies jetzt mit einem Befehl erledigen (wenn ich es mehr als dreimal mache, möchte ich es automatisieren... :-)).

Das funktioniert:

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

aber wenn ich das mache

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

Ich bekomme

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

Weiß jemand, wie ich das zum Laufen bekomme?

Antwort1

Allgemeine Beobachtungen

Es gibt einige Schlüsselwörter in Bash, die die Analyse des ihnen folgenden Befehls beeinflussen, z. B. [[. Aber sshist nicht eines davon, sondern ein normaler Befehl. Das bedeutet:

  • Die gesamte ssh …Zeile wird normalerweise von IhremlokalShell; Zeichen wie |, ;, *, ", $oder Leerzeichen haben für die Shell eine Bedeutung, sie gelangen nicht zu ssh, es sei denn, Sie setzen sie in Anführungszeichen oder maskieren sie (mit wenigen Ausnahmen, z. B. $ist „sole“ als separates Wort nichts Besonderes). Dies ist die erste Ebene der Analyse und Interpretation.

  • sshWas auch immer die Argumente (oder ein anderer regulärer Befehl) erreichen , nachdem die Shell ihre Arbeit erledigt hat, es sind nur Argumente, Zeichenfolgen. Es ist jetzt die Aufgabe des Tools, sie zu interpretieren. Dies ist die zweite Ebene.

In diesem Fall sshwerden einige (null, eins oder mehrere) seiner Befehlszeilenargumente als Befehl interpretiert, der auf der Serverseite ausgeführt werden soll. Im Allgemeinen sshist es möglich, einen Befehl aus mehreren Argumenten zu erstellen. Der Effekt ist, als ob Sie auf dem Server etwas wie das Folgende aufrufen würden:

"$SHELL" -c "$command_line_built_by_ssh"

(Ich behaupte nicht, es istgenauso, aber es ist sicherlich nah genug dran, um zu verstehen, was passiert. Ich habe es so geschrieben, als ob es in einer Shell aufgerufen würde, also sieht es vertraut aus; aber in Wirklichkeit gibt es noch keine Shell. Und es gibt keine $command_line…Variable, ich verwende diesen Namen nur, um für den Zweck dieser Antwort auf eine Zeichenfolge zu verweisen.)

Dann $SHELLführt der Server die Analyse $command_line…selbst durch. Dies ist die dritte Ebene.


Spezifische Beobachtungen

Der fehlgeschlagene Befehl

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

fehlgeschlagen, da 123.456.789.10es sich nicht um eine gültige IP-Adresse handelt.

OK, ich verstehe, 123.456.789.10dass es sich um einen Platzhalter handelt, aber trotzdem ist es ungültig. :)

Der Befehl ist fehlgeschlagen, da er sudo docker ps | grep mycontainername | awk '{print $1;}'lokal ausgeführt wurde. Die Ausgabe war wahrscheinlich leer. Dann $command_line_built_by_sshwar es weit von dem entfernt, was Sie wollten.

Hinweis: Lokal ssh … | grep mycontainernameausführen grep(Sie sind sich dessen möglicherweise bewusst, müssen es aber nicht).


Diskussion

Um die Kontrolle darüber zu haben, was die Remote-Shell als Ergebnis erhält $command_line_built_by_ssh, müssen Sie die Analyse und Interpretation, die vorher stattfindet, verstehen, vorhersagen und steuern. Sie müssen Ihren lokalen Befehl so gestalten, dassnachdie lokale Shell und sshverarbeiten Sie es, es wird genau das, was $command_line…Sie auf der Remote-Seite ausführen möchten.

Es kann ziemlich kompliziert werden, wenn Sie tatsächlich möchten, dass Ihre lokale Shell etwas erweitert oder ersetzt, bevor das Ergebnis zu gelangt ssh. Ihr Fall ist einfacher, da Sie bereits die wörtliche Zeichenfolge haben, die Sie als haben möchten $command_line_built_by_ssh. Die Zeichenfolge lautet:

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

Anmerkungen:

  • Ich habe Befehlssubstitution in Form von verwendet $(), nicht in Form von Backticks. Es gibtGründe für die Bevorzugung$().
  • Ich weiß dockeres überhaupt nicht, ich kann nicht sagen, ob Ihr $(…)in doppelte Anführungszeichen gesetzt werden sollte. Im AllgemeinenNichtzitieren ist fast immer schlecht. Fragen Sie sich, was passiert, wenn die Ersetzung mehrere Wörter zurückgibt (d. h. mehrere Zeilen enthalten awk). Dies ist ein anderes Problem (falls es in diesem Fall überhaupt ein Problem war) und ich werde in dieser Antwort nicht darauf eingehen.

Um zu verhindern, dass alles von der lokalen Shell erweitert/interpretiert wird, müssen Sie alle Zeichen, die eine Erweiterung auslösen oder interpretiert werden können, ordnungsgemäß in Anführungszeichen setzen oder maskieren (mit ). Setzen Sie in diesem Fall , , , , , , , und (möglicherweise oder optional) \Leerzeichen in Anführungszeichen.$()|;{}'

Ich habe „vielleicht oder optional Anführungszeichen oder Escape-Leerzeichen“ gesagt, weil es so ssh … some commandfunktioniert. Wenn es zwei oder mehr Argumente findet, die es als Code interpretiert, der auf dem Server ausgeführt werden soll, wird es sie verketten und einzelne Leerzeichen dazwischen einfügen. So ist es $command_line_built_by_sshaufgebaut. Wenn Sie in dem, was wie Code für die Remote-Shell aussieht, weder Anführungszeichen noch Escape-Leerzeichen setzen, dann wird die lokale Shell beim Trennen von Wörtern Leerzeichen (und Tabulatoren) verbrauchen und dann sshLeerzeichen hinzufügen. Das Ergebnis ist möglicherweise nicht genau das, was Sie wollen, wenn Tabulatoren oder mehrere aufeinanderfolgende Leerzeichen vorhanden sind. Beispiel:

ssh user@server echo a     b

ssherhält user@server, echo, a, b. Der Remote-Befehl lautet echo a bund echoes wird erhalten a, b. Es wird gedruckt a b.

Dann das:

ssh user@server 'echo a     b'

ssherhält user@server, echo a b. Der Remote-Befehl lautet echo a bund echoes wird erhalten a, b. Es wird gedruckt a b.

Und zum Schluss noch dies:

ssh user@server 'echo "a     b"'

ssherhält user@server, echo "a b". Der Remote-Befehl lautet echo "a b"und echoes wird erhalten a b. Es wird gedruckt a b.

Die Schlussfolgerung ist, dass Sie im Kontext der lokalen Shell zitieren sollten undseparatim Kontext der Remote-Shell. Denken Sie daran, dass, wenn es darum geht, Dinge durch eine Shell zu erweitern,Die äußeren Anführungszeichen sind wichtig.


Lösung

Wenn man all diese Informationen zusammenträgt (und immer noch davon ausgeht, dass man schützen willallesvon der lokalen Shell erweitert/interpretiert werden), rate ich zu Folgendem:

  • Zitieren oder entkommen.
  • Bevorzugenzitierengegenüber Escapezeichen, da ein einzelnes Anführungszeichenpaar viele Zeichen schützen kann, während ein einzelnes \nur ein Zeichen schützt. Sie werden höchstwahrscheinlich viele Backslashs benötigen, um alles zu schützen; oft kann dasselbe Ergebnis mit nur einem Anführungszeichenpaar erreicht werden.
  • BevorzugenEinzelzitate( '), können sie alles außer schützen '. Auf der anderen Seite können doppelte Anführungszeichen ( ") schützen ', aber nicht $nor "( \manchmal nor, !manchmal nor in Bash), unless $und solche werden ebenfalls maskiert (d. h. maskiertUndzitiert, d. h. in Anführungszeichen eingeschlossen; außer! welches ist lästig).
  • Geben Sie Befehle lieber alseinzelArgument für ssh.

Daraus ergibt sich folgendes Vorgehen:

  1. Bereiten Sie einen wörtlichen Befehl vor, den Sie auf der Remote-Seite ausführen möchten.
  2. Ersetzen Sie jedes 'durch '"'"'oder durch '\''(Sie können für jedes unabhängig wählen ').
  3. Umfassen Sie die gesamte resultierende Zeichenfolge mit einfachen Anführungszeichen.
  4. ssh …Vorne hinzufügen .

Ihr wörtlicher Befehl lautet:

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

Das Verfahren führt zu:

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

Beachten Sie, dass das Verfahren möglicherweise nicht zur kürzestmöglichen Zeichenfolge führt. Mit etwas Einsicht kann man die Zeichenfolge manchmal „optimieren“. Aber das Verfahren ist ziemlicheinfachund absolut zuverlässig. Wenn Sie nur wissen, dass Sie alles vor der Erweiterung/Interpretation durch die lokale Shell schützen möchten, erfordert das Verfahren selbst keinerlei weitere Einsicht.


Automatisierung

Tatsächlich kann der Vorgang automatisiert werden. Es gibt Tools, die Anführungszeichen zu einem String hinzufügen und vorhandene Anführungszeichen korrekt beibehalten können. Bash selbst ist ein solches Tool.Diese Antwort von mirbietet eine Möglichkeit, dies mit einem Tastendruck in Bash zu tun. Eine mögliche, auf Ihren Fall angepasste Lösung ist:

  1. Definieren Sie in Ihrer lokalen Shell die folgende benutzerdefinierte Funktion und Bindung:

     _prepend_ssh() { READLINE_LINE="ssh  ${READLINE_LINE@Q}"; READLINE_POINT=4; }
     bind -x '"\C-x\C-h":_prepend_ssh'
    
  2. Geben Sie in Ihrer lokalen Shell den Befehl ein (oder fügen Sie ihn ein), den Sie in der Remote-Shell ausführen möchten; führen Sie ihn nicht aus. Der Befehl sollte lauten:genauwie Sie es in der Remote-Shell haben möchten.

  3. Drücken Sie Ctrl+ x, Ctrl+ h. Die lokale Shell übernimmt die Anführungszeichen. Sie fügt auch sshvorn ein und platziert den Cursor direkt dahinter.

  4. Fügen Sie fehlende Argumente hinzu (z. B. -t user@server). Der Cursor steht hierfür bereits an der richtigen Stelle.

  5. Enter


Alternativen

Es gibt noch eine andere Möglichkeit, Befehle wörtlich an eine Remote-Shell zu übergeben. In einigen Fällen können SieRohrsie über ssh. Nehmen wir an, die Remote-Befehlszeile sollte lauten:

echo "$PATH"; date

Wir könnten wie oben vorgehen, einfache Anführungszeichen hinzufügen und es lokal wie folgt ausführen:

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

Das Beispiel ist einfach, aber im Allgemeinen ist das Hinzufügen von Anführungszeichen nicht immer so einfach. Alternativ können wir den Befehl wie folgt weiterleiten (das erste echoder Einfachheit halber;printfist besser):

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

was immer noch diese einfachen Anführungszeichen erfordert. Aber wenn Sie den/die Befehl(e) in einer Datei haben, dann:

<file ssh user@server bash

Oder sogar ohne Datei (hier Dokument):

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

(Beachten Sie, dass die Anführungszeichen eine lokale Erweiterung <<'EOF'verhindern .)$PATH

Vorteile:

  • Sie können problemlos mehrzeilige Befehle/Snippets/Skripte übergeben (ich habe die Aufteilung echo … ; datenur zur Veranschaulichung vorgenommen).
  • Keine zusätzliche Anführungszeichenebene erforderlich.
  • Sie können explizit einen Remote-Interpreter auswählen, der keine Shell sein muss (z. B. bashoder zsh, oder python).

Nachteile:

  • Sie sollten explizit einen Remote-Interpreter angeben, sonst wird der StandardAnmeldungShell wird gestartet, Nachricht des Tages möglicherweise gedruckt. Sie können die Standard-Shell weiterhin als Nicht-Login-Shell verwenden, indem Sie sie in Anführungszeichen setzen exec "$SHELL"(die Zeile sieht dann so aus ssh … 'exec "$SHELL"' <<'EOF').
  • Die Standardeingabe von sshist kein Terminal, Sie können sie also nicht verwenden -t(aus diesem Grund habe ich Ihren ursprünglichen Befehl nicht als Beispiel verwendet).
  • Die Befehle gelangen zum Remote-Interpreter ( bashim Beispiel) über dessen Standardeingabe. Mögliche Probleme:
    • Untergeordnete Prozesse (oder integrierte Prozesse) verwenden dieselbe Standardeingabe. Wenn einer von ihnen von seiner Standardeingabe liest, liest er denselben Datenstrom, möglicherweise liest er die nächsten Befehle, die für den Interpreter bestimmt sind. Dieses Verhalten kann unterdrückt oder sogar kreativ (missbraucht) werden, ich werde jedoch nicht näher darauf eingehen.
    • Sie können diesen Kanal nicht einfach zum Weiterleiten anderer Dinge verwenden.

verwandte Informationen