在 exec()... 中重定向 STDOUT 和 STDERR 没有 shell

Redirect STDOUT and STDERR in exec()... without shell

我正在尝试使用 Perl5 fork() 子进程。子进程应该 exec() 另一个程序,将其 STDIN 重定向到命名管道,并 STDOUTSTDERR 到日志文件。父进程在循环中继续 运行ning,使用 waitpid 并检查 $? 以重新启动子进程,以防它死于非零退出状态。

exec() 函数的 Perl 文档说:

If there is more than one argument in LIST, this calls execvp(3) with the arguments in LIST. If there is only one element in LIST, the argument is checked for shell metacharacters, and if there are any, the entire argument is passed to the system's command shell for parsing (this is /bin/sh -c on Unix platforms, but varies on other platforms). If there are no shell metacharacters in the argument, it is split into words and passed directly to execvp, which is more efficient. Examples:

exec '/bin/echo', 'Your arguments are: ', @ARGV;
exec "sort $outfile | uniq";

这听起来很酷,我想 运行 我的外部程序没有中介 shell,如这些示例所示。不幸的是,我无法将它与输出重定向结合起来(如 /bin/foo > /tmp/stdout)。

换句话说,这是行不通的:

exec ( '/bin/ls', '/etc', '>/tmp/stdout' );

所以,我的问题是:如何在不使用 shell 的情况下为我的子命令重定向 STD* 文件?

通过 <> 的重定向是一个 shell 功能,这就是它在这种用法中不起作用的原因。您实际上是在调用 /bin/ls 并将 >/tmp/stdout 作为另一个参数传递,当将命令替换为 echo:

时很容易看到
exec ('/bin/echo', '/etc', '>/tmp/stdout');

打印:

/etc >/tmp/stdout

通常,您的 shell (/bin/sh) 会解析命令,发现重定向尝试,打开正确的文件,并修剪进入 /bin/echo 的参数列表.

然而—— 以exec()(或system())开始的程序将继承其调用进程的STDINSTDOUTSTDERR文件。所以,正确的处理方式是

  • 关闭每个特殊文件句柄,
  • 重新打开它们,指向您想要的日志文件,然后
  • 终于调用exec()启动程序了。

重写上面的示例代码,效果很好:

close STDOUT;
open (STDOUT, '>', '/tmp/stdout');
exec ('/bin/ls', '/etc');

...或者,使用 perldoc 推荐的间接宾语语法:

close STDOUT;
open (STDOUT, '>', '/tmp/stdout');
exec { '/bin/ls' } ('ls', '/etc');

(事实上,根据文档,此最终语法是避免在 Windows 中实例化 shell 的唯一可靠方法。)

下面的 shell 命令告诉 shell 使用 /etc 启动 /bin/ls 作为参数并重定向其 STDOUT。

/bin/ls /etc >/tmp/stdout

另一方面,以下 Perl 语句告诉 Perl 将当前程序替换为 /bin/ls,参数为 /etc>/tmp/stdout

exec('/bin/ls', '/etc', '>/tmp/stdout');

您根本没有告诉 Perl 重定向 STDOUT!请记住 exec 不会启动新进程,因此如果您更改子进程的 fd 1,它将影响同一进程中的 ls 运行。

但不是只解决这个问题(就像 Greg Kennedy 所做的那样),而是完整地保留您的其他问题(例如,将无法启动 ls 误报为 ls 的错误),我'我会告诉你如何修复它们:

use IPC::Open3 qw( open3 );

my $stdout = '';
{
   # open3 will close the handle used as the child's STDIN.
   # open3 has issues with lexical file handles.
   open(local *CHILD_STDIN, '<', '/dev/null') or die $!;

   my $pid = open3('<&CHILD_STDIN', local *CHILD_STDOUT, '>&STDERR',
      '/bin/ls', '/etc');

   while (my $line = <CHILD_STDOUT>) {
      $stdout .= $line;
   }

   waitpid($pid, 0);
}

虽然这为您节省了一百行代码,但open3 is a still quite low-level. (You'll run into problems if you have to deal with two pipes.) I instead recommend IPC::Run3 (simpler) or IPC::Run(更灵活)。

use IPC::Run3 qw( run3 );
run3([ '/bin/ls', '/etc' ], \undef, \my $stdout);

use IPC::Run qw( run );
run([ '/bin/ls', '/etc' ], \undef, \my $stdout);