There are times when it's useful or necessary to use an external command as part of a program, and full control of the command's stdin, stdout, and stderr are required, rendering popen() insufficient. This level of control is useful for, among other things, writing a GUI frontend for a console application.
I recently encountered just such an occasion, and wrote a function called popen3() (the 3 indicates that full control is given over all three of the program's standard file descriptors). I also remembered writing a similar implementation several years ago, and dug through my old backups to find it. To spare anyone else the burden of implementing popen3() from scratch, I've uploaded both implementations as GitHub Gists. They are released into the public domain, though attribution is appreciated.
/*
* This implementation of popen3() was created from scratch in June of 2011. It
* is less likely to leak file descriptors if an error occurs than the 2007
* version and has been tested under valgrind. It also differs from the 2007
* version in its behavior if one of the file descriptor parameters is NULL.
* Instead of closing the corresponding stream, it is left unmodified (typically
* sharing the same terminal as the parent process). It also lacks the
* non-blocking option present in the 2007 version.
*
* No warranty of correctness, safety, performance, security, or usability is
* given. This implementation is released into the public domain/CC0, but if used
* in an open source application, attribution would be appreciated.
*
* Mike Bourgeous
* https://github.com/mike-bourgeous
* http://www.mikebourgeous.com/
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
/*
* Sets the FD_CLOEXEC flag. Returns 0 on success, -1 on error.
*/
static int set_cloexec(int fd)
{
if(fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC) == -1) {
perror("Error setting FD_CLOEXEC flag");
return -1;
}
return 0;
}
/*
* Runs command in another process, with full remote interaction capabilities.
* Be aware that command is passed to sh -c, so shell expansion will occur.
* Writing to *writefd will write to the command's stdin. Reading from *readfd
* will read from the command's stdout. Reading from *errfd will read from the
* command's stderr. If NULL is passed for writefd, readfd, or errfd, then the
* command's stdin, stdout, or stderr will not be changed. Returns the child
* PID on success, -1 on error.
*/
pid_t popen3(char *command, int *writefd, int *readfd, int *errfd)
{
int in_pipe[2] = {-1, -1};
int out_pipe[2] = {-1, -1};
int err_pipe[2] = {-1, -1};
pid_t pid;
// 2011 implementation of popen3() by Mike Bourgeous
// https://gist.github.com/1022231
if(command == NULL) {
fprintf(stderr, "Cannot popen3() a NULL command.\n");
goto error;
}
if(writefd && pipe(in_pipe)) {
perror("Error creating pipe for stdin");
goto error;
}
if(readfd && pipe(out_pipe)) {
perror("Error creating pipe for stdout");
goto error;
}
if(errfd && pipe(err_pipe)) {
perror("Error creating pipe for stderr");
goto error;
}
pid = fork();
switch(pid) {
case -1:
// Error
perror("Error creating child process");
goto error;
case 0:
// Child
if(writefd) {
close(in_pipe[1]);
if(dup2(in_pipe[0], 0) == -1) {
perror("Error assigning stdin in child process");
exit(-1);
}
close(in_pipe[0]);
}
if(readfd) {
close(out_pipe[0]);
if(dup2(out_pipe[1], 1) == -1) {
perror("Error assigning stdout in child process");
exit(-1);
}
close(out_pipe[1]);
}
if(errfd) {
close(err_pipe[0]);
if(dup2(err_pipe[1], 2) == -1) {
perror("Error assigning stderr in child process");
exit(-1);
}
close(err_pipe[1]);
}
execl("/bin/sh", "/bin/sh", "-c", command, (char *)NULL);
perror("Error executing command in child process");
exit(-1);
default:
// Parent
break;
}
if(writefd) {
close(in_pipe[0]);
set_cloexec(in_pipe[1]);
*writefd = in_pipe[1];
}
if(readfd) {
close(out_pipe[1]);
set_cloexec(out_pipe[0]);
*readfd = out_pipe[0];
}
if(errfd) {
close(err_pipe[1]);
set_cloexec(out_pipe[0]);
*errfd = err_pipe[0];
}
return pid;
error:
if(in_pipe[0] >= 0) {
close(in_pipe[0]);
}
if(in_pipe[1] >= 0) {
close(in_pipe[1]);
}
if(out_pipe[0] >= 0) {
close(out_pipe[0]);
}
if(out_pipe[1] >= 0) {
close(out_pipe[1]);
}
if(err_pipe[0] >= 0) {
close(err_pipe[0]);
}
if(err_pipe[1] >= 0) {
close(err_pipe[1]);
}
return -1;
}
/*
* This implementation of popen3() was created in 2007 for an experimental
* mpg123 frontend and is based on a popen2() snippet found online. This
* implementation may behave in unexpected ways if stdin/stdout/stderr have
* been closed or modified. No warranty of its correctness, security, or
* usability is given. My modifications are released into the public domain,
* but if used in an open source application, attribution would be appreciated.
*
* Mike Bourgeous
* https://github.com/mike-bourgeous
* http://www.mikebourgeous.com/
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
/*
* popen3()
*
* Runs the specified command line using a shell (SECURITY WARNING: don't put
* user-provided input here, to avoid shell expansion), opening pipes for
* stdin, stdout, and stderr. If NULL is specified for a given fd, that stream
* will be discarded.
*
* It is recommended to set nonblock_in to zero, nonblock_outerr to nonzero.
*/
pid_t popen3(const char *command, int *fp_in, int *fp_out, int *fp_err, int nonblock_in, int nonblock_outerr) /* {{{ */
{
#define POPEN3_READ 0
#define POPEN3_WRITE 1
// Based on popen2() from http://snippets.dzone.com/posts/show/1134,
// where it was listed as freeware. Modifications by Mike Bourgeous.
// https://gist.github.com/1022231
int p_stdin[2], p_stdout[2], p_stderr[2];
pid_t pid;
/* Open the three pipes */
if(pipe(p_stdin) != 0 || pipe(p_stdout) != 0 || pipe(p_stderr) != 0) {
perror("popen3: unable to open pipes for I/O");
return -1;
}
/* Set the pipes to nonblocking IO if requested (a good idea for single-threaded apps) */
if(nonblock_in) {
fcntl(p_stdin[POPEN3_WRITE], F_SETFL, fcntl(p_stdin[POPEN3_WRITE], F_GETFL) | O_NONBLOCK);
}
if(nonblock_outerr) {
fcntl(p_stdout[POPEN3_READ], F_SETFL, fcntl(p_stdout[POPEN3_READ], F_GETFL) | O_NONBLOCK);
fcntl(p_stderr[POPEN3_READ], F_SETFL, fcntl(p_stderr[POPEN3_READ], F_GETFL) | O_NONBLOCK);
}
pid = fork();
if (pid < 0) {
/* An error occurred */
return pid;
} else if (pid == 0) {
/* This is the child process */
// Close the parent-side half of the pipes
close(p_stdin[POPEN3_WRITE]);
close(p_stderr[POPEN3_READ]);
close(p_stdout[POPEN3_READ]);
// Replace the real stdin/stdout/stderr with the pipes created above
dup2(p_stdin[POPEN3_READ], fileno(stdin));
dup2(p_stderr[POPEN3_WRITE], fileno(stderr)); // stderr is first in case err is redirected to out (i.e. with 2>&1)
dup2(p_stdout[POPEN3_WRITE], fileno(stdout));
execl("/bin/sh", "sh", "-c", command, (char *)NULL);
perror("popen3: execl failed");
exit(1);
}
/* This is the parent process */
if(fp_in == NULL) {
/* fp_in was null - the caller doesn't want to write to stdin */
close(p_stdin[POPEN3_WRITE]);
} else {
/* Give the caller the file number for stdin */
*fp_in = p_stdin[POPEN3_WRITE];
}
if(fp_out == NULL) {
/* fp_out was null - the caller doesn't want to read from stdout */
close(p_stdout[POPEN3_READ]);
} else {
/* Give the caller the file number for stdout */
*fp_out = p_stdout[POPEN3_READ];
}
if(fp_err == NULL) {
/* fp_err was null - the caller doesn't want to read from stderr */
close(p_stderr[POPEN3_READ]);
} else {
/* Give the caller the file number for stderr */
*fp_err = p_stderr[POPEN3_READ];
}
return pid;
#undef POPEN3_READ
#undef POPEN3_WRITE
} /* }}} */