如何通过 ssh 执行复杂的命令行?

kra*_*r65 4 linux ssh bash

我有时想获取在远程主机上运行的 docker 容器的环境。为此,我登录主机:

ssh kramer65@123.456.789.10
Run Code Online (Sandbox Code Playgroud)

然后我运行这个命令:

sudo docker exec -it `sudo docker ps | grep mycontainername | awk '{print $1;}'` env
Run Code Online (Sandbox Code Playgroud)

我现在想用一个命令来完成此操作(执行超过 3 次,我想自动化它..:-))。

这有效:

 ssh -t kramer65@123.456.789.10 sudo docker ps | grep mycontainername
Run Code Online (Sandbox Code Playgroud)

但当我这样做时

ssh -t kramer65@123.456.789.10 sudo docker exec -it `sudo docker ps | grep mycontainername | awk '{print $1;}'` env
Run Code Online (Sandbox Code Playgroud)

我明白了

"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.
Run Code Online (Sandbox Code Playgroud)

有谁知道我怎样才能让它运行?

Kam*_*ski 14

一般观察

\n

Bash 中有一些关键字会影响对其后面内容的解析,例如[[. 但这ssh不是其中之一,它是一个常规命令。这意味着:

\n
    \n
  • 整行通常由本地shellssh \xe2\x80\xa6解析;像、、、、或空格这样的字符对 shell 来说意味着一些东西,除非您引用或转义它们,否则它们不会到达。(除了少数例外,例如,sole作为单独的单词并不特殊)。这是解析和解释的第一层次。|;*"$ssh$

    \n
  • \n
  • sshshell 完成工作后,无论参数(或任何其他常规命令)得到什么,它们都只是参数、字符串。现在工具的工作就是解释它们。这是第二个层次。

    \n
  • \n
\n

如果ssh其某些(零个、一个或多个)命令行参数被解释为要在服务器端运行的命令。一般来说,ssh能够从许多参数构建命令。效果就好像您在服务器上调用了类似的东西:

\n
"$SHELL" -c "$command_line_built_by_ssh"\n
Run Code Online (Sandbox Code Playgroud)\n

(我并不是说它完全像这样,但它确实足够接近以理解发生的情况。我编写它就像在 shell 中调用它一样,所以它看起来很熟悉;但实际上有还没有 shell。并且没有$command_line\xe2\x80\xa6作为变量,我只是使用这个名称来引用某个字符串以达到这个答案的目的。)

\n

然后$SHELL在服务器上$command_line\xe2\x80\xa6自行解析。这是第三个层次。

\n
\n

具体观察

\n

失败的命令

\n
\n
ssh -t kramer65@123.456.789.10 sudo docker exec -it `sudo docker ps | grep mycontainername | awk \'{print $1;}\'` env\n
Run Code Online (Sandbox Code Playgroud)\n
\n

失败,因为123.456.789.10不是有效的 IP 地址。

\n

好的,我知道123.456.789.10这是一个占位符,但它仍然无效。:)

\n

该命令失败,因为它sudo docker ps | grep mycontainername | awk \'{print $1;}\'在本地执行。输出可能是空的。那么$command_line_built_by_ssh离你想要的还很远。

\n

注意在本地ssh \xe2\x80\xa6 | grep mycontainername运行grep(您可能会或可能不会意识到这一点)。

\n
\n

讨论

\n

为了控制远程 shell 将获得的内容$command_line_built_by_ssh,您需要理解、预测和策划之前发生的解析和解释。您需要制作本地命令,因此在本地 shell 并消化它之后,它就成为您想要在远程端执行的ssh确切命令。$command_line\xe2\x80\xa6

\n

如果您确实希望本地 shell 在结果到达之前扩展或替换任何内容,则可能会非常复杂ssh。您的情况更简单,因为您已经拥有所需的逐字字符串$command_line_built_by_ssh。该字符串是:

\n
sudo docker exec -it $(sudo docker ps | grep mycontainername | awk \'{print $1;}\') env\n
Run Code Online (Sandbox Code Playgroud)\n

笔记:

\n
    \n
  • 我以 的形式使用命令替换$(),而不是反引号。有理由选择$()
  • \n
  • docker我根本不知道,我不知道你是否$(\xe2\x80\xa6)应该用双引号引起来。一般来说,不引用几乎总是不好的。问问自己,当替换返回多个单词(即多行 Enter awk)时会发生什么。这是一个不同的问题(如果在这种情况下有问题的话),我不会在这个答案中解决它。
  • \n
\n

为了保护所有内容不被本地 shell 扩展/解释,您需要正确引用或转义(使用\\)所有可以触发扩展或可以解释的字符。在这种情况下$,引用()|;{}\'和(可能或可选)空格。

\n

我说“也许或可选地引用或转义空格”是因为它的ssh \xe2\x80\xa6 some command工作原理。如果它找到两个或多个参数并将其解释为要在服务器上运行的代码,它将连接它们,并在它们之间添加单个空格。就是这样$command_line_built_by_ssh建造的。如果您在看起来像远程 shell 的代码中既不引用也不转义空格,则本地 shell 在分割单词时将消耗空格(和制表符),然后ssh添加空格。如果存在制表符或多个连续空格,结果可能不完全是您想要的。例如:

\n
ssh user@server echo a     b\n
Run Code Online (Sandbox Code Playgroud)\n

ssh得到user@server, echo, a, b。远程命令将是,echo a b并且echo将得到a, b。它将打印a b

\n

然后这个:

\n
ssh user@server \'echo a     b\'\n
Run Code Online (Sandbox Code Playgroud)\n

ssh得到user@server, echo a b. 远程命令将是,echo a b并且echo将得到a, b。它将打印a b

\n

最后是这个:

\n
ssh user@server \'echo "a     b"\'\n
Run Code Online (Sandbox Code Playgroud)\n

ssh得到user@server, echo "a b". 远程命令将会是echo "a b"并且echo将会有 get a b。它将打印a b

\n

结论是您应该在本地 shell 上下文中引用,并在远程 shell 上下文中单独引用。请记住,当涉及到通过 shell 扩展内容时,外部引号很重要

\n
\n

解决方案

\n

将所有这些信息放在一起(并且仍然假设您希望保护所有内容不被本地 shell 扩展/解释),我建议如下:

\n
    \n
  • 引用或逃避。
  • \n
  • 优先使用引号而不是转义,因为一对引号可以保护多个字符,而一对\\引号只能保护一个字符。您很可能需要许多反斜杠来保护所有内容;通常只需一对引号即可达到相同的结果。
  • \n
  • 更喜欢单引号( \'),它们可以保护除 之外的所有内容\'。另一方面,双引号 ( ") 可以保护\'但不能保护$nor "(在 Bash 中\\有时也不能,有时也不能),除非和此类也被转义(即转义引用,即在双引号内转义;除非很麻烦)。!$!
  • \n
  • 更喜欢将命令作为单个参数提供给ssh.
  • \n
\n

这导致以下过程:

\n
    \n
  1. 准备要在远程端运行的逐字命令。
  2. \n
  3. 将 every 替换\'为 或\'"\'"\'with \'\\\'\'(您可以为每个 单独选择\')。
  4. \n
  5. 用单引号括住整个结果字符串。
  6. \n
  7. 添加ssh \xe2\x80\xa6在前面。
  8. \n
\n

您的逐字命令是:

\n
sudo docker exec -it $(sudo docker ps | grep mycontainername | awk \'{print $1;}\') env\n
Run Code Online (Sandbox Code Playgroud)\n

该程序的结果是:

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

请注意,该过程可能会也可能不会导致最短的字符串。凭借洞察力,有时可以“优化”字符串。但该过程非常简单且完全可靠。如果您知道要保护所有内容不被本地 shell 扩展/解释,则该过程本身根本不需要进一步了解。

\n
\n

自动化

\n

事实上,该过程可以自动化。有些工具可以向字符串添加引号并正确保留现有引号。Bash 本身就是这样的工具。我的这个答案提供了一种在 Bash 中通过击键来做到这一点的方法。根据您的情况调整的可能解决方案是:

\n
    \n
  1. 在本地 shell 中定义以下自定义函数和绑定:

    \n
     _prepend_ssh() { READLINE_LINE="ssh  ${READLINE_LINE@Q}"; READLINE_POINT=4; }\n bind -x \'"\\C-x\\C-h":_prepend_ssh\'\n
    Run Code Online (Sandbox Code Playgroud)\n
  2. \n
  3. 仍然在本地 shell 中键入(或粘贴)要在远程 shell 中运行的命令;不执行。该命令应该与您希望在远程 shell 中显示的命令完全相同。

    \n
  4. \n
  5. Ctrl+ xCtrl+ h。本地 shell 将负责引用。它还会ssh在前面添加并将光标放在后面。

    \n
  6. \n
  7. 添加(类型)缺少的参数(例如-t user@server)。为了您的方便,光标已经处于执行此操作的正确位置。

    \n
  8. \n
  9. Enter

    \n
  10. \n
\n
\n

备择方案

\n

还有另一种方法可以将逐字命令传递到远程 shell。在某些情况下,您可以通过管道它们ssh。让我们假设远程命令行应该是:

\n
echo "$PATH"; date\n
Run Code Online (Sandbox Code Playgroud)\n

我们可以像上面一样继续,添加单引号并在本地运行,如下所示:

\n
ssh user@server \'echo "$PATH"; date\'\n
Run Code Online (Sandbox Code Playgroud)\n

该示例很简单,但通常添加引号并不总是那么容易。或者,我们可以像这样通过管道传输命令(echo为了简单起见,第一个printf更好):

\n
echo \'echo "$PATH"; date\' | ssh user@server bash\n
Run Code Online (Sandbox Code Playgroud)\n

仍然需要这些单引号。但是如果文件中有命令,那么:

\n
<file ssh user@server bash\n
Run Code Online (Sandbox Code Playgroud)\n

或者甚至没有任何文件(此处为文档):

\n
ssh user@server bash <<\'EOF\'\necho "$PATH"\ndate\nEOF\n
Run Code Online (Sandbox Code Playgroud)\n

<<\'EOF\'(请注意防止$PATH在本地展开的引号。)

\n

优点:

\n
    \n
  • 您可以轻松地传递多行命令/片段/脚本(我分开echo \xe2\x80\xa6 ; date只是为了展示这一点)。
  • \n
  • 不需要额外的引用层。
  • \n
  • 您可以显式选择一个远程解释器,该解释器不必是 shell(例如bashzsh、 或python)。
  • \n
\n

缺点:

\n
    \n
  • 您应该显式指定远程解释器,否则将生成默认登录shell,可能会打印当天的消息。您仍然可以通过指定正确的引号来使用默认 shell 作为非登录 shell exec "$SHELL"(该行将类似于ssh \xe2\x80\xa6 \'exec "$SHELL"\' <<\'EOF\')。
  • \n
  • 标准输入ssh不是终端,因此您无法使用-t(这就是为什么我没有使用您的原始命令作为示例)。
  • \n
  • 命令bash通过其标准输入到达远程解释器(在示例中)。可能的问题:\n
      \n
    • 子进程(或内置进程)将使用相同的标准输入。如果它们中的任何一个从其标准输入读取,那么它将读取相同的流,可能它会读取发往解释器的下一个命令。这种行为可以被抑制,甚至可以创造性地(滥用)使用,但我不会详细说明。
    • \n
    • 您无法轻松地使用此通道来传输其他任何内容。
    • \n
    \n
  • \n
\n