Search code examples
clinuxfork

STDERR output is not being captured when running invalid command


I am trying to catch the output from running the command /bin/lsx -lah /.

The output should be: bash: /bin/lsx: no such file or directory

However, I am just not getting anything in the printf().

This is the code

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define die(e) do { fprintf(stderr, "%s\n", e); exit(EXIT_FAILURE); } while (0);

int main() {
  int link[2];
  pid_t pid;
  char foo[4096];

  if (pipe(link)==-1)
    die("pipe");

  if ((pid = fork()) == -1)
    die("fork");

  if(pid == 0) {

    dup2 (link[1], STDOUT_FILENO);
    close(link[0]);
    close(link[1]);
    execl("/bin/lsx", "ls", "-1", (char *)0);
    die("execl");

  } else {

    close(link[1]);
    int nbytes = read(link[0], foo, sizeof(foo));
    printf("Output: (%.*s)\n", nbytes, foo);
    wait(NULL);

  }
  return 0;
}

I am just wondering why the output is not being captured and printed in the bottom printf().


Solution

  • As John pointed out, you are only capturing stdout, not stderr, and well-formed programs usually send error messages to stderr (the die macro you have shown is an example of this).

    A quick solution is to redirect the child processes' stderr to its stdout with another call to dup2

    dup2(link[1], STDOUT_FILENO);
    dup2(STDOUT_FILENO, STDERR_FILENO);
    

    which will redirect both output streams to the single pipe.

    Alternatively, capture both streams with two pipes to retain their separation as they propagate.

    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <unistd.h>
    
    #define die(e) do { fprintf(stderr, "%s\n", e); exit(EXIT_FAILURE); } while (0);
    
    ssize_t push_through(const char *prefix,
            FILE *out, int in,
            char *buf, size_t bufsz)
    {
        ssize_t bytes = read(in, buf, bufsz);
    
        if (bytes > 0)
            fprintf(out, "%s: (%.*s)\n", prefix, (int) bytes, buf);
    
        return bytes;
    }
    
    int main(void)
    {
        pid_t pid;
        int stdout_pipe[2];
        int stderr_pipe[2];
        char buffer[4096];
    
        if (pipe(stdout_pipe) == -1 || pipe(stderr_pipe) == -1)
            die("pipe");
    
        if ((pid = fork()) == -1)
            die("fork");
    
        if (pid == 0) {
            close(stdout_pipe[0]);
            close(stderr_pipe[0]);
    
            dup2(stdout_pipe[1], STDOUT_FILENO);
            dup2(stderr_pipe[1], STDERR_FILENO);
    
            close(stdout_pipe[1]);
            close(stderr_pipe[1]);
    
            execl("/bin/lsx", "ls", "-1", (char *) NULL);
            perror("execl failed because");
            die("execl");
        } else {
            ssize_t outbytes, errbytes;
    
            close(stdout_pipe[1]);
            close(stderr_pipe[1]);
    
            do {
                outbytes = push_through("out", stdout, stdout_pipe[0],
                        buffer, sizeof buffer);
                errbytes = push_through("err", stderr, stderr_pipe[0],
                        buffer, sizeof buffer);
            } while (outbytes > 0 || errbytes > 0);
    
            wait(NULL);
        }
    }
    

    In the event that execl fails, it might be useful to call perror to print a more detailed error message.

    Note that expecting the error

    bash: /bin/lsx: no such file or directory
    

    is possibly misguided, as exec* functions only defer to a shell under certain conditions.

    The only mention of a shell in man 3 exec is with regards to execlp, execvp, and execvpe:

    If the header of a file isn't recognized (the attempted execve(2) failed with the error ENOEXEC), these functions will execute the shell (/bin/sh) with the path of the file as its first argument. (If this attempt fails, no further searching is done.)