我有一个 perl 脚本,它运行一个system()
调用来执行 shell 命令,我想运行多个命令并在它们之间传输数据。类似于(在 Perl 中):
system("command1 | command2");
Perl 的system()
用途/bin/sh
,并且由于它们运行在 Ubuntu Server 系统上,/bin/sh
因此是dash
.与许多其他 shell 一样,dash 中管道的退出值是最右侧命令的退出值。这意味着类似的事情将会返回0
(成功):
system("false | true")
如果我使用的系统/bin/sh
是bash
,我可以通过添加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]
或$0
shell 中的命令名称,通过“间接对象”传递)。
文档说:
请注意,参数处理取决于参数的数量。如果 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", '...')
类似,但会查找$PATH
for 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 $?');
我们避免了往返sh
,和Bash 命令行的关联双引号。
答案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 中更安全地实现而提出的论点,请参阅伊尔卡丘的回答有关该调用的详细信息。)