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

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

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

ssh [email protected]

然后我运行这个命令:

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

我现在想用一个命令来执行此操作(执行超过 3 次并且我想使其自动化.. :-) )。

这有效:

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

但是当我这样做时

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

我明白了

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

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

答案1

一般观察

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

  • ssh …行通常由你的当地的shell;诸如|;*"$空格之类的字符对 shell 来说具有某种意义,除非您引用或转义它们,否则它们不会到达ssh(少数例外,例如 sole$作为单独的单词并不特殊)。这是解析和解释的第一级。

  • 在 shell 完成其工作后,无论参数(或任何其他常规命令)到达哪里ssh,它们都只是参数、字符串。现在工具的工作就是解释它们。这是第二层。

如果ssh某些(零个、一个或多个)命令行参数被解释为要在服务器端运行的命令。通常,ssh可以从许多参数构建命令。效果就像您在服务器上调用了类似这样的命令一样:

"$SHELL" -c "$command_line_built_by_ssh"

(我并不是说确切地像这样,但它肯定足够接近,可以理解发生了什么。我把它写得好像它是在 shell 中调用的,所以看起来很熟悉;但实际上还没有 shell。而且没有$command_line…变量,我只是用这个名字来引用一些字符串,以便回答这个问题。)

然后$SHELL服务器$command_line…自己解析。这是第三层。


具体观察

失败的命令

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

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

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

该命令失败,因为它sudo docker ps | grep mycontainername | awk '{print $1;}'在本地执行。输出可能为空。这$command_line_built_by_ssh与您想要的相差甚远。

注意在本地ssh … | grep mycontainername运行grep(您可能知道或可能不知道这一点)。


讨论

要控制远程 shell 将获取的内容$command_line_built_by_ssh,您需要了解、预测和策划之前发生的解析和解释。您需要精心设计本地命令,因此本地 shell 并消化它,它就变成您想要在远程端执行的ssh精确内容。$command_line…

如果你真的想让你的本地 shell 在结果到达 之前扩展或替换任何内容ssh,那么这可能会非常复杂。你的情况更简单,因为你已经有了你想要的逐字字符串$command_line_built_by_ssh。该字符串是:

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

笔记:

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

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

我说“可能或可选地引用或转义空格”是因为它的ssh … some command工作原理。如果它发现两个或多个参数被解释为要在服务器上运行的代码,它会将它们连接起来,并在它们之间添加单个空格。这就是它的$command_line_built_by_ssh构建方式。如果您在看起来像远程 shell 的代码中既不引用也不转义空格,那么本地 shell 将在拆分单词时使用空格(和制表符),然后ssh添加空格。如果有制表符或多个连续的空格,结果可能不是您想要的。例如:

ssh user@server echo a     b

ssh获取user@server,,,。远程命令将是echo并且将获取,。它将打印。abecho a bechoaba b

然后是这个:

ssh user@server 'echo a     b'

ssh获取user@serverecho a b。远程命令将是echo a b,并且echo将获取ab。它将打印a b

最后是这个:

ssh user@server 'echo "a     b"'

ssh获取user@serverecho "a b"。远程命令将是echo "a b"并且echo将获取a b。它将打印a b

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


解决方案

把所有这些信息放在一起(仍然假设你想保护一切被本地 shell 扩展/解释),我建议如下:

  • 引用或者逃避。
  • 更喜欢引用避免转义,因为一对引号可以保护许多字符,而单个引号\只能保护一个字符。您很可能需要多个反斜杠来保护所有内容;通常只用一对引号就可以实现相同的结果。
  • 更喜欢单引号( '),它们可以保护除 之外的所有内容'。另一方面,双引号 ( ") 可以保护'但不能保护$nor "\有时也不能,!在 Bash 中有时也不能),除非$和 等也被转义(即转义引用,即在双引号内转义;除外! 麻烦的)。
  • 更喜欢以单身的的論點ssh

这导致了以下步骤:

  1. 准备您想要在远程端运行的逐字命令。
  2. ''"'"'或替换每个'\''(您可以为每个 独立选择')。
  3. 用单引号括住整个结果字符串。
  4. ssh …在前面。

您的逐字命令是:

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

该程序的结果是:

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

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


自动化

事实上,这个过程可以自动化。有些工具可以将引号添加到字符串中并正确保留现有引号。Bash 本身就是这样的工具。我的这个答案提供了一种在 Bash 中通过按键执行此操作的方法。针对您的情况调整的可能解决方案是:

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

     _prepend_ssh() { READLINE_LINE="ssh  ${READLINE_LINE@Q}"; READLINE_POINT=4; }
     bind -x '"\C-x\C-h":_prepend_ssh'
    
  2. 仍在本地 shell 中键入(或粘贴)要在远程 shell 中运行的命令;不要执行。该命令应该是确切地正如您希望它位于远程 shell 中一样。

  3. Ctrl+ x, Ctrl+ h。本地 shell 将负责引用。它还会ssh在前面添加并将光标放在后面。

  4. 添加(输入)缺少的参数(例如-t user@server)。为了方便起见,光标已经位于正确的位置来执行此操作。

  5. Enter


备择方案

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

echo "$PATH"; date

我们可以像上面那样进行,添加单引号并像这样在本地运行:

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

这个例子很简单,但一般来说添加引号并不总是那么容易。或者,我们可以像这样管道命令(第一个命令echo是为了简单起见;printf更好):

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

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

<file ssh user@server bash

或者甚至没有任何文件(这里的文件):

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

(请注意,引号中的引号<<'EOF'会阻止$PATH其在本地扩展。)

优点:

  • 您可以轻松传递多行命令/片段/脚本(我拆分echo … ; date只是为了显示这一点)。
  • 无需额外的引用层。
  • 您可以明确选择一个不必是 shell 的远程解释器(例如bashzshpython)。

缺点:

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

相关内容