Search code examples
perlexecstdoutstdinstderr

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


I am attempting to use Perl5 to fork() a child process. The child process should exec() another program, redirecting its STDIN to a named pipe, and STDOUT and STDERR to log files. The parent process continues running in a loop, using waitpid and checking $? to restart the child in case it dies with non-zero exit status.

Perl documentation for the exec() function says:

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";

This sounds pretty cool, and I'd like to run my external program without an intermediary shell, as shown in these examples. Unfortunately, I am unable to combine this with output redirection (as in /bin/foo > /tmp/stdout).

In other words, this does not work:

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

So, my question is: how do I redirect the STD* files for my sub-command, without using the shell?


Solution

  • Redirection via < and > is a shell feature, which is why it does not work in this usage. You are essentially calling /bin/ls and passing >/tmp/stdout as just another argument, which is easily visible when replacing the command by echo:

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

    prints:

    /etc >/tmp/stdout
    

    Normally, your shell (/bin/sh) would have parsed the command, spotted the redirection attempts, opened the proper files, and also pruned the argument list going in to /bin/echo.

    However - A program started with exec() (or system()) will inherit the STDIN, STDOUT and STDERR files of its calling process. So, the proper way to handle this is to

    • close each special filehandle,
    • re-open them, pointing at your desired logfile, and
    • finally call exec() to start the program.

    Rewriting your example code above, this works fine:

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

    ...or, using the indirect-object syntax recommended by perldoc:

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

    (in fact, according to the documentation, this final syntax is the only reliable way to avoid instantiating a shell in Windows.)