I’m finding STDERR redirection within a backticks call can be lost if the command fails to execute. I’m stumped by the behavior I am seeing.
$ perl -e 'use strict; use warnings; my $out=`DNE`; print $out' Can't exec "DNE": No such file or directory at -e line 1. Use of uninitialized value in print at -e line 1. $ perl -e 'use strict; use warnings; my $out=`DNE 2>&1`; print $out' Use of uninitialized value in print at -e line 1. $ perl -e 'use strict; use warnings; my $out=`echo 123; DNE 2>&1`; print $out' 123 sh: DNE: command not found
Is my syntax incorrect?
I'm using Perl 5.8.5 on Linux.
The regular output is sent to Standard Out (STDOUT) and the error messages are sent to Standard Error (STDERR). When you redirect console output using the > symbol, you are only redirecting STDOUT. In order to redirect STDERR, you have to specify 2> for the redirection symbol.
Redirecting stdout and stderr to a file: The I/O streams can be redirected by putting the n> operator in use, where n is the file descriptor number. For redirecting stdout, we use “1>” and for stderr, “2>” is added as an operator.
Your syntax is correct, but in one case perl is dropping the error message.
In general, consider testing during initialization that your system has the command you want, and fail early if it is missing.
my $foopath = "/usr/bin/foo";
die "$0: $foopath is not executable" unless -x $foopath;
# later ...
my $output = `$foopath 2>&1`;
die "$0: $foopath exited $?" if $?;
To fully understand the differences in output, it is necessary to understand details of Unix system programming. Read on.
Consider a simple perl invocation.
perl -e 'print "hi\n"; warn "bye\n"'
Its output is
hi bye
Note that the output of print goes to STDOUT, the standard output, and warn writes to STDERR, the standard error. When run from a terminal, both appear on the terminal, but we can send them to different places. For example
$ perl -e 'print "hi\n"; warn "bye\n"' >/dev/null bye
The null device or /dev/null discards any output sent to it, and so in the command above, “hi” disappears. The command above is shorthand for
$ perl -e 'print "hi\n"; warn "bye\n"' 1>/dev/null bye
That is, 1 is the file descriptor for STDOUT. To throw away “bye” instead, run
$ perl -e 'print "hi\n"; warn "bye\n"' 2>/dev/null hi
As you can see, 2 is the file descriptor for STDERR. (For completeness, the file descriptor for STDIN is 0.)
In the Bourne shell and its derivatives, we can also merge STDOUT and STDERR with 2>&1. Read it as “make file descriptor 2’s output go to the same place as file descriptor 1’s.”
$ perl -e 'print "hi\n"; warn "bye\n"' 2>&1 hi bye
Terminal output does not highlight the distinction, but an extra redirection shows what’s happening. We can discard both by running
$ perl -e 'print "hi\n"; warn "bye\n"' >/dev/null 2>&1
Order matters with this family of shells that processes redirections in left-to-right order, so transposing the two yields
$ perl -e 'print "hi\n"; warn "bye\n"' 2>&1 >/dev/null bye
This may be surprising at first. The shell first processes 2>&1 which means send STDERR to the same destination as STDOUT—which it already is: the terminal! Then it processes >/dev/null and redirects STDOUT to the null device.
This duplication of file descriptors happens by calling dup2, which is usually a wrapper for fcntl.
Now say we want to add a prefix to each line of the output of our command.
$ perl -e 'print "hi\n"; warn "bye\n"' | sed -e 's/^/got: /' bye got: hi
The order is different, but remember that STDERR and STDOUT are different streams. Notice also that only “hi” got a prefix. To get both lines, they both have to appear on STDOUT.
$ perl -e 'print "hi\n"; warn "bye\n"' 2>&1 | sed -e 's/^/got: /' got: bye got: hi
To construct a pipeline, the shell creates child processes with fork, performs redirections with dup2, and starts each stage of the pipeline with calls to exec in the appropriate processes. For the pipeline above, the process is similar to
fork a process to run sed
sed with waitpid
pipe to feed input to perl
fork a process to run perl
dup2 to make STDIN read from the read end of the pipeexec the sed commandSTDIN
dup2 to send STDOUT to the write end of the pipe from step 3dup2 to send STDERR to STDOUT’s destinationexec the perl commandexit
perl’s exit status with waitpid
exit
$? with the return value from waitpid
Note that the child processes are created in right-to-left order. This is because shells in the Bourne family define the exit status of a pipeline to be the exit status of the last process.
You can build the above pipeline in Perl with the code below.
#! /usr/bin/env perl
use strict;
use warnings;
my $pid = open my $fh, "-|";
die "$0: fork: $!" unless defined $pid;
if ($pid) {
while (<$fh>) {
s/^/got: /;
print;
}
}
else {
open STDERR, ">&=", \*STDOUT or print "$0: dup: $!";
exec "perl", "-e", q[print "hi\n"; warn "bye\n"]
or die "$0: exec: $!";
}
The first call to open does a lot of work for us, as noted in the perlfunc documentation on open:
For three or more arguments if MODE is
"|-", the filename is interpreted as a command to which output is to be piped, and if MODE is"-|", the filename is interpreted as a command that pipes output to us. In the two-argument (and one-argument) form, one should replace dash ("-") with the command. See Usingopenfor IPC in perlipc for more examples of this.
Its output is
$ ./simple-pipeline got: bye got: hi
The code above hardcodes the duplication of STDOUT, which we can see below.
$ ./simple-pipeline >/dev/null
To capture the output of another command, perl sets up the same machinery, which you can see in pp_backtick (in pp_sys.c), which calls Perl_my_popen (in util.c) to create a child process and set up the plumbing (fork, pipe, dup2). The child does some plumbing and calls Perl_do_exec3 (in doio.c) to start the command whose output we want. There we notice a relevant comment:
/* handle the 2>&1 construct at the end */
The implementation recognizes the sequence 2>&1, duplicates STDOUT, and removes the redirection from the command to be passed to the shell.
if (*s == '>' && s[1] == '&' && s[2] == '1'
&& s > cmd + 1 && s[-1] == '2' && isSPACE(s[-2])
&& (!s[3] || isSPACE(s[3])))
{
const char *t = s + 3;
while (*t && isSPACE(*t))
++t;
if (!*t && (PerlLIO_dup2(1,2) != -1)) {
s[-2] = '\0';
break;
}
}
Later we see
PerlProc_execl(PL_sh_path, "sh", "-c", cmd, (char *)NULL);
PERL_FPU_POST_EXEC
S_exec_failed(aTHX_ PL_sh_path, fd, do_report);
Inside S_exec_failed, we find
if (ckWARN(WARN_EXEC))
Perl_warner(aTHX_ packWARN(WARN_EXEC), "Can't exec \"%s\": %s",
cmd, Strerror(e));
That is one of the warnings you asked about in your question.
Let’s walk through the details of how perl processes the commands from your question.
$ perl -e 'use strict; use warnings; my $out=`DNE`; print $out' Can't exec "DNE": No such file or directory at -e line 1. Use of uninitialized value in print at -e line 1.
No surprises here.
A subtle detail is important to understand. The code above that handles 2>&1 in-house runs only when a condition is true of the command to be executed:
if (*s != ' ' && !isALPHA(*s) &&
strchr("$&*(){}[]'\";\\|?<>~`\n",*s)) {
This is an optimization. If the command in backticks contains the above shell metacharacters, then perl has to hand it off to the shell. But if no shell metacharacters are present, perl can exec the command directly—saving the fork and shell startup costs.
The non-existent command DNE contains no shell metacharacters, so perl does all the work. The exec-category warning is generated because the command failed and you enabled the warnings pragma. The perlop documentation tells us that backticks or qx// returns undef in scalar context when the command fails, so that’s why you get the warning about printing the undefined value of $out.
$ perl -e 'use strict; use warnings; my $out=`DNE 2>&1`; print $out' Use of uninitialized value in print at -e line 1.
Where did the failed exec warning go?
Remember the basic steps of creating a child process that runs another command:
fork to create a nearly identical child process.dup2 to connect STDOUT to the write end of the pipe.exec to cause the newly created child to execute another program instead.To capture the output of another command, perl goes through these steps. In preparation to attempt running DNE 2>&1, perl forks a child and in the child process causes STDERR to be a duplicate of the STDOUT, but there is another side effect.
if (!*t && (PerlLIO_dup2(1,2) != -1)) {
s[-2] = '\0';
break;
}
If 2>&1 is at the end of the command and the dup2 succeeds, then perl writes a NUL byte just before the redirection. This has the effect of removing it from the command, e.g., DNE 2>&1 becomes DNE! Now, with no shell metacharacters in the command, perl in the child process thinks to itself, ‘Self, we can exec this command directly.’
The call to exec fails because DNE does not exist. The child still emits the failed exec warning on STDERR. It doesn’t go to the terminal because of the dup2 that pointed STDERR to the same place as STDOUT: the write end of the pipe back to the parent.
The parent process detects that the child exited abnormally, and ignores the contents of the pipe because the result of failed command-execution is documented to be undef.
$ perl -e 'use strict; use warnings; my $out=`echo 123; DNE 2>&1`; print $out' 123 sh: DNE: command not found
Here we see a different diagnostic of DNE not existing. The first shell metacharacter encountered is ;, so perl hands the command unchanged to the shell for execution. The echo completes normally, and then DNE fails in the shell, and the shell’s STDOUT and STDERR go back to the parent process. From perl’s perspective, the shell executed fine, so there is nothing to warn about.
When you enable the warnings pragma—a Very Good Practice!—this enables the exec warning category. To see the full list of these warnings, search the perldiag documentation for the string W exec.
Observe the difference.
$ perl -Mstrict -Mwarnings -e 'my $out=`DNE`; print $out' Can't exec "DNE": No such file or directory at -e line 1. Use of uninitialized value $out in print at -e line 1. $ perl -Mstrict -Mwarnings -M-warnings=exec -e 'my $out=`DNE`; print $out' Use of uninitialized value $out in print at -e line 1.
The latter invocation is equivalent to
use strict;
use warnings;
no warnings 'exec';
my $out = `DNE`;
print defined($out) ? $out : "command failed\n";
I like formatting my own error messages when something goes wrong with an exec, pipe open, and so on. This means I usually disable exec warnings, but it also means I have to be extra-careful to test return values.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With