![如何通过 ssh 执行复杂的命令行?](https://linux22.com/image/1618563/%E5%A6%82%E4%BD%95%E9%80%9A%E8%BF%87%20ssh%20%E6%89%A7%E8%A1%8C%E5%A4%8D%E6%9D%82%E7%9A%84%E5%91%BD%E4%BB%A4%E8%A1%8C%EF%BC%9F.png)
我有时想获取在远程主机上运行的 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
并且将获取,。它将打印。a
b
echo a b
echo
a
b
a b
然后是这个:
ssh user@server 'echo a b'
ssh
获取user@server
,echo a b
。远程命令将是echo a b
,并且echo
将获取a
,b
。它将打印a b
。
最后是这个:
ssh user@server 'echo "a b"'
ssh
获取user@server
,echo "a b"
。远程命令将是echo "a b"
并且echo
将获取a b
。它将打印a b
。
结论是你应该在本地 shell 的上下文中引用分别地在远程 shell 的上下文中。请记住,当涉及到通过 shell 扩展内容时,外部引号很重要。
解决方案
把所有这些信息放在一起(仍然假设你想保护一切被本地 shell 扩展/解释),我建议如下:
- 引用或者逃避。
- 更喜欢引用避免转义,因为一对引号可以保护许多字符,而单个引号
\
只能保护一个字符。您很可能需要多个反斜杠来保护所有内容;通常只用一对引号就可以实现相同的结果。 - 更喜欢单引号(
'
),它们可以保护除 之外的所有内容'
。另一方面,双引号 ("
) 可以保护'
但不能保护$
nor"
(\
有时也不能,!
在 Bash 中有时也不能),除非$
和 等也被转义(即转义和引用,即在双引号内转义;除外!
即 麻烦的)。 - 更喜欢以单身的的論點
ssh
。
这导致了以下步骤:
- 准备您想要在远程端运行的逐字命令。
'
用'"'"'
或替换每个'\''
(您可以为每个 独立选择'
)。- 用单引号括住整个结果字符串。
- 加
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 中通过按键执行此操作的方法。针对您的情况调整的可能解决方案是:
在您的本地 shell 中定义以下自定义函数和绑定:
_prepend_ssh() { READLINE_LINE="ssh ${READLINE_LINE@Q}"; READLINE_POINT=4; } bind -x '"\C-x\C-h":_prepend_ssh'
仍在本地 shell 中键入(或粘贴)要在远程 shell 中运行的命令;不要执行。该命令应该是确切地正如您希望它位于远程 shell 中一样。
按Ctrl+ x, Ctrl+ h。本地 shell 将负责引用。它还会
ssh
在前面添加并将光标放在后面。添加(输入)缺少的参数(例如
-t user@server
)。为了方便起见,光标已经位于正确的位置来执行此操作。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 的远程解释器(例如
bash
或zsh
或python
)。
缺点:
- 您应该明确指定一个远程解释器,否则默认登录shell 将被生成,当天的消息可能会被打印出来。您仍然可以通过指定正确的引号将默认 shell 用作非登录 shell
exec "$SHELL"
(该行将类似于ssh … 'exec "$SHELL"' <<'EOF'
)。 - 标准输入
ssh
不是终端,因此您无法使用-t
(这就是我没有使用您的原始命令作为示例的原因)。 - 命令通过其标准输入到达远程解释器(
bash
在示例中)。可能出现的问题:- 子进程(或内置进程)将使用相同的标准输入。如果其中任何一个从其标准输入读取,那么它将读取相同的流,可能它将读取发往解释器的下一个命令。这种行为可以被抑制,甚至可以创造性地(滥用)使用,但我不会详细说明。
- 您不能轻易使用此通道来传输其他任何东西。