Execute a Shell Command in C

Write to stdin, read from stdout and stderr.

Say we want to execute a shell command, write to its standard input pipe, and capture the output of its standard output and standard error pipes.

This is surprisingly tricky to set up. You can find a working demo on Github that includes all the code below.

Headers

First, we need these headers:

// C standard library.
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// POSIX.
#include <unistd.h>
#include <sys/wait.h>

Helper Functions

Next, we need a helper function for writing to a file descriptor:

// Writes the null-terminated string [input] to the file descriptor [fd].
bool write_to_fd(int fd, const char* input) {
    size_t index = 0;
    size_t length = strlen(input);

    while (index < length) {
        size_t num_bytes_to_write = length - index;
        ssize_t count = write(fd, &input[index], num_bytes_to_write);
        if (count < 0) {
            if (errno == EINTR) {
                continue;
            } else {
                return false;
            }
        }
        index += count;
    }

    return true;
}

And a helper function for reading from a file descriptor:

// Reads from the file descriptor [fd].
bool read_from_fd(int fd) {
    uint8_t buf[1024];

    while (true) {
        ssize_t count = read(fd, buf, 1024);
        if (count < 0) {
            if (errno == EINTR) {
                continue;
            } else {
                return false;
            }
        } else if (count == 0) {
            break;
        } else {
            printf("%.*s", (int)count, buf);
        }
    }

    return true;
}

Execute Command

This example uses the execl() function (manpage) to execute a shell command — exactly like the standard library's system() function (manpage) — but it can easily be adapted to execute any kind of command.

// Executes the null-terminated string [cmd] as a shell command.
// Writes the null-terminated string [input] to the command's stdin.
void exec_shell_cmd(const char* cmd, const char* input) {
    int child_stdin_pipe[2];
    if (pipe(child_stdin_pipe) == -1) {
        perror("failed to create child's stdin pipe");
        exit(1);
    }

    int child_stdout_pipe[2];
    if (pipe(child_stdout_pipe) == -1) {
        close(child_stdin_pipe[0]);
        close(child_stdin_pipe[1]);
        perror("failed to create child's stdout pipe");
        exit(1);
    }

    int child_stderr_pipe[2];
    if (pipe(child_stderr_pipe) == -1) {
        close(child_stdin_pipe[0]);
        close(child_stdin_pipe[1]);
        close(child_stdout_pipe[0]);
        close(child_stdout_pipe[1]);
        perror("failed to create child's stderr pipe");
        exit(1);
    }

    pid_t child_pid = fork();

    // If child_pid == 0, we're in the child.
    if (child_pid == 0) {
        close(child_stdin_pipe[1]); // close the write end of the input pipe
        while ((dup2(child_stdin_pipe[0], STDIN_FILENO) == -1) && (errno == EINTR)) {}
        close(child_stdin_pipe[0]);

        close(child_stdout_pipe[0]); // close the read end of the output pipe
        while ((dup2(child_stdout_pipe[1], STDOUT_FILENO) == -1) && (errno == EINTR)) {}
        close(child_stdout_pipe[1]);

        close(child_stderr_pipe[0]); // close the read end of the error pipe
        while ((dup2(child_stderr_pipe[1], STDERR_FILENO) == -1) && (errno == EINTR)) {}
        close(child_stderr_pipe[1]);

        execl("/bin/sh", "sh", "-c", cmd, (char*)NULL);
        perror("returned from execl() in child");
        exit(1);
    }

    // If child_pid > 0, we're in the parent.
    else if (child_pid > 0) {
        close(child_stdin_pipe[0]);  // close the read end of the input pipe
        close(child_stdout_pipe[1]); // close the write end of the output pipe
        close(child_stderr_pipe[1]); // close the write end of the error pipe

        // If [input] is not NULL, write it to the child's stdin pipe.
        if (input) {
            if (!write_to_fd(child_stdin_pipe[1], input)) {
                close(child_stdin_pipe[1]);
                close(child_stdout_pipe[0]);
                close(child_stderr_pipe[0]);
                perror("error while writing to child's stdin pipe");
                exit(1);
            }
        }
        close(child_stdin_pipe[1]);

        // Read from the child's stdout pipe.
        printf("[CHILD STDOUT]: \n");
        if (!read_from_fd(child_stdout_pipe[0])) {
            close(child_stdout_pipe[0]);
            close(child_stderr_pipe[0]);
            perror("error while reading from child's stdout pipe");
            exit(1);
        }
        close(child_stdout_pipe[0]);

        // Read from the child's stderr pipe.
        printf("[CHILD STDERR]: \n");
        if (!read_from_fd(child_stderr_pipe[0])) {
            close(child_stderr_pipe[0]);
            perror("error while reading from child's stderr pipe");
            exit(1);
        }
        close(child_stderr_pipe[0]);

        // Wait for the child to exit.
        int status;
        do {
            waitpid(child_pid, &status, WUNTRACED);
        } while (!WIFEXITED(status) && !WIFSIGNALED(status));
        printf("[CHILD EXIT CODE]: %d\n", status);
    }

    // If child_pid < 0, the attempt to fork() failed.
    else {
        perror("fork() failed");
        exit(1);
    }
}

There's more clean-up code here than is strictly necessary — we don't really need to close all the hanging pipes in the error-handling code before calling exit(), but we would need to do this if we were handling the errors in a more sophisticated way in a long-running process.

Run Demo

Some sample calls to see the function in action:

int main(int argc, char** argv) {
    printf("---\n");
    exec_shell_cmd("ls", NULL);

    printf("---\n");
    exec_shell_cmd("cat", "foo bar\n");

    printf("---\n");
    exec_shell_cmd("echo $HOME", NULL);

    printf("---\n");
    return 0;
}