通过 ssh 执行的 bash 脚本返回不正确的退出代码 0

通过 ssh 执行的 bash 脚本返回不正确的退出代码 0

我正在尝试自动化一个涉及通过 ssh 在各种机器上运行脚本的过程。捕获输出和返回代码(对于错误检测)至关重要。

显式设置退出代码按预期工作:

~$ ssh host exit 5 && echo OK || echo FAIL
FAIL

然而,如果有一个 shell 脚本发出不干净的退出信号,ssh 总是返回 0(通过字符串执行模拟的脚本):

~$ ssh host sh -c 'exit 5' && echo OK || echo FAIL
OK

在交互式 shell 中的主机上运行完全相同的脚本效果很好:

~$ sh -c 'exit 5' && echo OK || echo FAIL
FAIL

我很困惑为什么会发生这种情况。我如何告诉 ssh 传播 bash 的返回码?我可能不会更改远程脚本。

我正在使用公钥身份验证,私钥已解锁 - 无需用户交互。所有系统都是Ubuntu 18.04。应用程序版本有:

  • OpenSSH_7.6p1 Ubuntu-4ubuntu0.1, OpenSSL 1.0.2n 7 Dec 2017
  • GNU bash, Version 4.4.19(1)-release (x86_64-pc-linux-gnu)

注意:这个问题与这些看似相似的问题不同:

答案1

如中所述答案你已经有了,遥控器sh没有执行exit 5。只是exit

$ ssh test sh -x -c 'exit 5'; echo $?
+ exit
0

例如,解释了这里发生的事情这个答案

ssh执行远程 shell 并传递细绳对它,不是参数列表。

当我们执行ssh host sh -c 'exit 5'

  1. 本地 shell 删除单引号 (报价删除);
  2. 客户ssh端获取参数hostsh-cexit 5。它将它们连接成一个字符串并将其发送到远程主机;
  3. 在远程主机上,ssh调用 shell 并向其传递字符串sh -c exit 5
  4. 远程 shell 调用sh并向其传递-c选项,exit如下所示命令串,并且5作为命令名称

请注意,如果我们在 后面添加单词exit 5,它们只会作为进一步的参数传递sh- 不会出现与它们不被 shell 识别相关的错误:

$ ssh test sh -x -c 'exit 5' a b c; echo $?
+ exit
0

strace确认5不是此处给出的命令字符串的一部分sh;这是一个论点:

$ ssh test strace -e execve sh -c 'exit 5'; echo $?
execve("/usr/bin/sh", ["sh", "-c", "exit", "5"], 0x7ffc0d744c38 /* 14 vars */) = 0
+++ exited with 0 +++
0

为了按sh -c 'command'预期在远程主机上执行,我们还必须确保正确地向其发送引号:

$ ssh test "sh -x -c 'exit 5'"; echo $?
+ exit 5
5

为了明确引用整个远程命令与我们当前的问题无关,我们可以这样写:

$ ssh test sh -x -c "'exit 5'"; echo $?
+ exit 5
5

用反斜杠转义内部引号,而不是引用两次,也可以。


关于该命令的注释ssh host sh -c ':; exit 5'(来自对您的问题的评论)。它的作用是:

$ ssh test sh -x -c ':; exit 5'; echo $?
+ :
5

也就是说,exit 5是由外壳执行的,而不是由sh.再次,sh使用所需的代码退出:

$ ssh test sh -x -c "':; exit 5'"; echo $?
+ :
+ exit 5
5

答案2

我可以使用您使用的命令复制此问题,并且可以通过将远程命令用引号括起来来解决它。这是我的测试用例:

#!/bin/bash -x

echo 'Unquoted Test:'
ssh evil sh -x -c exit 5 && echo OK || echo FAIL

echo 'Quoted Test 1:'
ssh evil sh -x -c 'exit 5' && echo OK || echo FAIL

echo 'Quoted Test 2:'
ssh evil 'sh -x -c "exit 5"' && echo OK || echo FAIL

结果如下:

bash-[540]$ bash -x test.sh
+ echo 'Unquoted Test:'
Unquoted Test:
+ ssh evil sh -x -c exit 5
+ exit
+ echo OK
OK
+ echo 'Quoted Test 1:'
Quoted Test 1:
+ ssh evil sh -x -c 'exit 5'
+ exit
+ echo OK
OK
+ echo 'Quoted Test 2:'
Quoted Test 2:
+ ssh evil 'sh -x -c "exit 5"'
+ exit 5
+ echo FAIL
FAIL

在第一次测试和第二次测试中,似乎没有像我们期望的那样5被传递。exit它似乎正在消失。它不会exitsh不会抱怨5: command not found,也ssh不会抱怨。

在第三个测试中,exit 5在远程主机上运行的较大命令中引用了,与第二个测试中相同。这确保了 被5传递到,并且两者都作为的选项exit执行。第二个和第三个测试之间的区别在于,整个命令和参数集被发送到作为单个命令参数引用的远程主机。-cshssh

答案3

其他答案很好地回答了这个问题,而不是给出的例子。我的实际应用程序更加复杂,涉及一系列脚本和子流程。这是我要执行的简化示例脚本:

#!/bin/bash
sub-process-that-fails
# store and echo returncode for debug purposes
rc=$?
echo $rc
exit $rc

为了确保远程执行的 shell 实际上是 bash 而不是 dash(正如@JeffSchaller 所指出的),我尝试像这样调用脚本:

~$ ssh -t -t host /bin/bash -x /srv/scripts/run.sh ; echo $?

这导致了这个奇怪的输出:

+ sub-process-that-fails
+ rc=5
+ echo 5
5
+ exit 5
0

经过几个小时的探索,我trap 'kill 0' EXIT注意到.bashrc.这样做是为了在 bash 被终止时终止所有子进程。 bash 的跟踪似乎没有显示此陷阱的执行情况。我将陷阱移至包装脚本中。现在我可以看到实际执行的内容:

+ trap 'kill 0' EXIT
+ sub-process-that-fails
+ rc=5
5
+ echo 5
+ exit 5
+ kill 0
0

远程 shell 以最后一个命令的退出代码退出。是的kill 0,并且以 0 退出。

相关内容