是否有与 dash 等效的管道故障?

是否有与 dash 等效的管道故障?

我有一个 perl 脚本,它运行一个system()调用来执行 shell 命令,我想运行多个命令并在它们之间传输数据。类似于(在 Perl 中):

system("command1 | command2");

Perl 的system()用途/bin/sh,并且由于它们运行在 Ubuntu Server 系统上,/bin/sh因此是dash.与许多其他 shell 一样,dash 中管道的退出值是最右侧命令的退出值。这意味着类似的事情将会返回0(成功):

system("false | true")

如果我使用的系统/bin/shbash,我可以通过添加pipefail选项轻松修复它:

system("set -o pipefail; false | true");

事实上,这在我本地的 Arch 系统上按预期工作,其/bin/sh要点是bash

$ perl -le '$status = system("false | true"); print $status>>8'
0
$ perl -le '$status = system("set -o pipefail; false | true"); print $status>>8'
1

(是的,我知道这看起来很奇怪,但如果你不了解 Perl,请相信我的话:我们需要移动 8 才能获得实际的退出状态)

然而,由于它应该运行的机器有dash,所以同样的事情会失败:

$ perl -le '$status = system("set -o pipefail; false | true"); print $status>>8'
sh: 1: set: Illegal option -o pipefail
2

使用set -e也不是解决方案:

$ perl -le '$status = system("set -e; false | true"); print $status>>8'
0

我可以想到一些 Perl 解决方法,例如不使用管道或使用模块IPC::System::Simple,但我希望可能有一种更简单的方法可以直接在 dash 中执行此操作。

那么,是否有一些技巧、技巧或选项可以让我知道dash一组管道命令的退出状态仅应为0(成功)全部命令有效,并且如果其中任何一个命令以?set -o pipefail中的方式失败,则应为非 0。bash

答案1

我不知道在 Dash 中这样做,但你可以/bin/sh完全绕过。

Perlsystem()尝试猜测程序员想要什么,并且sh -c仅在只有一个参数且该参数包含 shell 元字符时才使用。否则,它将直接运行给定的命令,并且您可以指定确切的参数(包括进入shellargv[0]$0shell 中的命令名称,通过“间接对象”传递)。

文档说

请注意,参数处理取决于参数的数量。如果 LIST 中有多个参数,或者 LIST 是一个具有多个值的数组,则使用列表其余部分给出的参数启动列表第一个元素给出的程序。

如果只有一个标量参数,则检查该参数是否有 shell 元字符,如果有,则将整个参数传递到系统的命令 shell 进行解析(这是/bin/sh -c在 Unix 平台上,但在其他平台上有所不同)。

这两个会做出相同的execve()调用:

system{"/bin/sh"} ("sh", "-c", 'false |true; echo $?')
system('false |true; echo $?')

即这个,如下所示strace

execve("/bin/sh", ["sh", "-c", "false |true; echo $?"], [/* 39 vars */]) = 0

system("sh", "-c", '...')类似,但会查找$PATHfor sh,并且 system("/bin/sh", "-c", '...')会略有不同,因为它会传入/bin/sh启动argv[0]的 shell 而不是只是sh。没有太大区别。)

因此,我们可以在 Perl 中运行它:

system("bash", "-o", "pipefail", "-c", 'false |true; echo $?');
# or a bit shorter
system(qw/bash -o pipefail -c/, 'false |true; echo $?');

我们避免了往返shBash 命令行的关联双引号。

答案2

这是一个非常丑陋但实用的解决方案:

system(q{
  exec 3>&1; \
  status1="$(((false 2>&1 1>&3 3>&- 4>&-; echo $? >&4) \
    | true 1>&2 3>&- 4>&-) 4>&1)"; \
  status2=$? \
  [ $status2 != 0 ] && exit $status2; \
  exit $status1
});

如果您希望将它们全部放在一行上,则可以删除这些反斜杠、换行符和缩进。这是改编自Csh 编程被认为是有害的论文,§ 1d“更精细的组合”[Posix shell 可以做而 Csh 不能做的事情]。

这将打开文件描述符 3 以临时存储 stdout,然后运行第一个命令并重新确定其输出的位置:FD4 和 FD3 关闭,FD1(标准输出)进入 FD3,FD2(标准错误)进入 FD1。然后,在运行第二个命令之前,它将退出代码放入 FD4。第二个命令关闭其 FD4 和 FD3,然后将其标准输出放入 FD2(我相信它的 stderr 实际上会打印,这可能包括 stdout,但我可能没有正确读取魔法)。打印输出后,可以安全地将 FD4 返回到 stdout,然后这就是存储在$status1.由于第二个命令的状态填充为 ,因此分配$status2更加简单$?。现在我们只需要实施即可pipefail。如果$status2不是干净退出 0,则以其退出。否则,无论$status1存储什么都退出。

添加新的文件描述符(5、6 等)和$status更多管道的变量。

这为您提供了 Posix 合规性和可移植性,但是您最好在 perl 中更本地地执行此操作,或者使用阿尼克的建议并调用 bash (假设这是一个选项),相比之下这确实看起来很优雅:

system("bash", "-o", "pipefail", "-c", 'false |true; echo $?')

(为了在 Perl 中更安全地实现而提出的论点,请参阅伊尔卡丘的回答有关该调用的详细信息。)

相关内容