Search code examples
c++linuxc++11unixposix

How to combine stderr and stdout to single C++ string?


I can grab stdout and stderr separately using fork, execvp, pipe, etc. functions and put them into two separate C++ strings. How can I use this family of functions to combine both stdout and stderr into a single combined string like the shell would as if I am redirecting like "2>&1"? The example below just captures stdout:

#include <sys/wait.h>
#include <unistd.h>
#include <string>
#include <vector>

std::string qx(const std::vector<std::string>& args) {
  int stdout_fds[2];
  pipe(stdout_fds);

  int stderr_fds[2];
  pipe(stderr_fds);

  const pid_t pid = fork();
  if (!pid) {
    close(stdout_fds[0]);
    dup2(stdout_fds[1], 1);
    close(stdout_fds[1]);

    close(stderr_fds[0]);
    dup2(stderr_fds[1], 2);
    close(stderr_fds[1]);

    std::vector<char*> vc(args.size() + 1, NULL);
    for (size_t i = 0; i < args.size(); ++i) {
      vc[i] = const_cast<char*>(args[i].c_str());
    }

    execvp(vc[0], &vc[0]);
    exit(0);
  }

  close(stdout_fds[1]);

  std::string out;
  const int buf_size = 4096;
  char buffer[buf_size];
  do {
    const ssize_t r = read(stdout_fds[0], buffer, buf_size);
    if (r > 0) {
      out.append(buffer, r);
    }
  } while (errno == EAGAIN || errno == EINTR);

  close(stdout_fds[0]);

  close(stderr_fds[1]);
  close(stderr_fds[0]);

  int r, status;
  do {
    r = waitpid(pid, &status, 0);
  } while (r == -1 && errno == EINTR);

  return out;
}

int main() {
  qx({"openssl", "hjas"});
  qx({"openssl", "dkjsah"});
  qx({"uname"});
  qx({"uname"});
}

Solution

  • Just use one pipe to collect both:

    std::string qx(const std::vector<std::string>& args) {
      int output[2];
      pipe(output);
    
      const pid_t pid = fork();
      if (!pid) {
        // collect both stdout and stderr to the one pipe
        close(output[0]);
        dup2(output[1], STDOUT_FILENO);
        dup2(output[1], STDERR_FILENO);
        close(output[1]);
    
        std::vector<char*> vc(args.size() + 1, NULL);
        for (size_t i = 0; i < args.size(); ++i) {
          vc[i] = const_cast<char*>(args[i].c_str());
        }
    
        execvp(vc[0], &vc[0]);
        // if execvp() fails, we do *not* want to call exit()
        // since that can call exit handlers and flush buffers
        // copied from the parent process
        _exit(0);
      }
    
      close(output[1]);
    
      std::string out;
      const int buf_size = 4096;
      char buffer[buf_size];
      do {
        errno = 0;
        const ssize_t r = read(output[0], buffer, buf_size);
        if (r > 0) {
          out.append(buffer, r);
        }
      } while (errno == EAGAIN || errno == EINTR);
    

    Note that your original code doesn't collect the child's stderr output. Should the child write enough to stderr that the pipe fills up, the child process might block.