Question

Execute a Command and Capture Output in C++ with POSIX popen()

cppprocessposixsystemreturn-value

Question

How can I execute a command from a C++ program on a POSIX system and store the command's output in a std::string?

system() runs a command but returns its status rather than the text written by the command. For example, I want an approach conceptually like this:

std::string result = system("./some_command");

How can I run a command and retrieve its standard output?

Short Answer

You will learn why system() cannot capture command output, how POSIX popen() creates a readable pipe to a command, and how to read that pipe into a C++ std::string. You will also learn important limitations involving error output, exit statuses, and shell-injection risks.

Concept

system() and popen() both start commands through a POSIX shell, but they serve different purposes:

  • system(command) starts the command and waits for it to finish. Its return value represents termination status, not the command's printed output.
  • popen(command, "r") starts the command and gives your program a file stream connected to the command's standard output (stdout). Your C++ code can read from that stream.

This is useful whenever another program already performs work you need, such as obtaining a system value, calling a command-line utility, or running an existing script.

popen() is a POSIX API, so it is available on Linux, macOS, and many Unix-like systems. It is not part of standard ISO C++.

A key security detail: popen() executes its command through a shell. Do not build its command string from untrusted input. Characters such as ;, &, |, $, and backticks can change what the shell executes.

Mental Model

Think of a command-line program as a person writing messages onto a conveyor belt.

  • With system(), you tell the person to work, wait until they finish, and receive only a completion report.
  • With popen(..., "r"), you place a collection bucket under the conveyor belt. Everything the program writes to standard output arrives in your bucket, and your C++ program reads it piece by piece.

The command's standard error is a separate conveyor belt, so it does not enter the same bucket unless you explicitly redirect it.

Take Quiz

Syntax and Examples

The basic POSIX pattern is:

FILE* pipe = popen("command", "r");
// Read text from pipe.
int status = pclose(pipe);

Use mode "r" when your program needs to read the command's standard output.

#include <array>
#include <cstdio>
#include <stdexcept>
#include <string>

std::string runCommand(const std::string& command) {
    std::array<char, 256> buffer{};
    std::string output;

    FILE* pipe = popen(command.c_str(), "r");
    if (pipe == nullptr) {
        throw std::runtime_error("popen() failed");
    }

    while (fgets(buffer.data(), static_cast<int>(buffer.size()), pipe) != nullptr) {
        output += buffer.data();
    }

    pclose(pipe);
    return output;
}

int main() {
    const std::string result = runCommand("printf 'Hello from a command\\n'");
    std::printf("Captured output: %s", result.c_str());
}

fgets() reads up to one line at a time, or until the buffer is full. Repeating it in a loop ensures that output larger than the buffer is fully collected.

If ./some_command writes done followed by a newline, use:

std::string result = runCommand("./some_command");

Then result contains "done\n".

Step by Step Execution

Consider this call:

std::string result = runCommand("printf 'red\\nblue\\n'");

Inside runCommand:

  1. popen() starts a shell that runs printf 'red\nblue\n'.
  2. The command writes red, a newline, blue, and a newline to standard output.
  3. popen() returns a FILE* whose input comes from that output.
  4. The first fgets() call reads "red\n" and appends it to output.
  5. The second fgets() call reads "blue\n" and appends it.
  6. A later fgets() call reaches end-of-file and returns nullptr, ending the loop.
  7. pclose() closes the pipe, waits for the command if necessary, and returns its termination status.
  8. The function returns "red\nblue\n".

The newline characters are part of the captured output. Remove trailing whitespace only if your application specifically requires it.

Real World Use Cases

Typical uses include:

  • Developer tools: call git rev-parse --short HEAD to display a build revision.
  • System utilities: read a machine value from a trusted local utility or script.
  • Automation: invoke an existing report generator and save its textual report.
  • Data conversion: run a controlled command-line converter and consume its output.
  • Monitoring scripts: collect output from an internal health-check command.

For application features involving user-provided data, prefer a library API or direct process execution with argument arrays instead of forming shell commands.

Take Quiz

Real Codebase Usage

In production code, command execution is usually wrapped in a small function rather than scattered throughout the codebase.

Useful practices include:

  • Check startup failure: popen() can return nullptr.
  • Always call pclose(): it releases the pipe and obtains the child process status.
  • Keep commands fixed or trusted: use constant commands, or carefully validate inputs before any shell use.
  • Handle failures explicitly: distinguish "could not start" from "started but exited with an error."
  • Capture output deliberately: use stdout for normal results; treat stderr as diagnostics.

To inspect the exit code on POSIX systems:

#include <array>
#include <cstdio>
#include <stdexcept>
#include <string>
#include <sys/wait.h>

struct CommandResult {
    std::string output;
    int exitCode;
};

CommandResult runCommandChecked(const std::string& command) {
    std::array<char, 256> buffer{};
    std::string output;

    FILE* pipe = popen(command.c_str(), "r");
    if (pipe == nullptr) {
        throw std::runtime_error("Could not start command");
    }

    while (fgets(buffer.data(), static_cast<int>(buffer.size()), pipe) != nullptr) {
        output += buffer.data();
    }

    const int status = pclose(pipe);
    if (status == -1) {
        throw std::runtime_error("pclose() failed");
    }

    if (!WIFEXITED(status)) {
        throw std::runtime_error("Command did not exit normally");
    }

    return {output, WEXITSTATUS(status)};
}

A command can produce valid-looking output and still fail, so checking the exit code matters when correctness is important.

Common Mistakes

Expecting system() to return text

This does not capture the command's output:

std::string result = system("./some_command"); // Incorrect

system() returns an integer status. Use popen() when you need standard output.

Reading only once

char buffer[256];
fgets(buffer, sizeof(buffer), pipe);

This reads only the first chunk. Commands can output more than one line or more than 255 characters. Read in a loop until fgets() returns nullptr.

Forgetting pclose()

FILE* pipe = popen("./some_command", "r");
// Read output...
// Missing pclose(pipe)

Use pclose(), not fclose(). It closes the pipe and waits for the command process.

Assuming stderr is captured

popen(command, "r") captures stdout only. If a command prints errors to stderr, output may be empty even though the command failed.

For a fixed, trusted command, a shell redirect can merge stderr into stdout:

std::string output = runCommand("./some_command 2>&1");

Do not append 2>&1 to an untrusted command string.

Building commands from untrusted input

std::string command = "grep " + userInput + " file.txt";
runCommand(command); // Dangerous

A malicious value can be interpreted by the shell as additional commands. Prefer a process API that accepts an executable and separate arguments, or avoid executing external commands entirely.

Treating pclose() as a direct exit code

The return value is a wait status, not always the exit code itself. Use WIFEXITED(status) and WEXITSTATUS(status) from <sys/wait.h>.

Comparisons

ApproachCaptures stdout?Uses a shell?Best use
system()NoYesRun a simple command when only completion status matters
popen(command, "r")YesYesRead textual stdout from a trusted command
popen(command, "w")No; writes to child stdinYesSend data to a command's standard input
fork() + exec() + pipesYes, including separate stderr if implementedNo, unless you choose oneRobust POSIX process control with separate arguments
A library APIUsually not applicableNoPrefer when the needed capability has a direct C++/library interface

popen() is concise and convenient, but fork()/exec() with pipes offers more control. In particular, exec() can receive an argument array, which avoids shell parsing and is safer for variable input.

Take Quiz

Cheat Sheet

#include <array>
#include <cstdio>
#include <string>

std::string runCommand(const std::string& command) {
    std::array<char, 256> buffer{};
    std::string output;

    FILE* pipe = popen(command.c_str(), "r");
    if (pipe == nullptr) {
        return {};
    }

    while (fgets(buffer.data(), static_cast<int>(buffer.size()), pipe) != nullptr) {
        output += buffer.data();
    }

    pclose(pipe);
    return output;
}
  • popen(command, "r"): run a command and read its stdout.
  • fgets(...): read output repeatedly until it returns nullptr.
  • pclose(pipe): close the pipe and collect process status.
  • Output commonly includes trailing newlines.
  • stdout and stderr are separate; popen(..., "r") captures stdout only.
  • popen() runs a shell command: never concatenate untrusted input into command.
  • POSIX only; not portable standard C++.

FAQ

Does popen() return the command output directly?

No. It returns a FILE* stream. Read from that stream with fgets(), fread(), or another C stream-reading function.

What is the difference between system() and popen() in C++?

system() runs a command and returns a status value. popen() creates a pipe so that you can read the command's standard output or write to its standard input.

Does popen() capture standard error?

No. In read mode, it captures standard output only. For trusted fixed commands, 2>&1 can combine stderr with stdout, but this relies on shell redirection.

Why must I use pclose() instead of fclose()?

pclose() closes the stream and waits for the command process, returning its termination status. fclose() does not provide the required process-handling behavior.

Can I use popen() on Windows?

popen() is a POSIX interface. Some Windows toolchains provide compatible functions such as _popen(), but the code and command shell behavior are not fully portable. Use a platform abstraction when Windows support is required.

Is it safe to pass user input to popen()?

Not by direct string concatenation. Since a shell interprets the command, untrusted text can cause command injection. Prefer APIs that run an executable with separate arguments.

How do I know whether the command succeeded?

Check the result of pclose(). On POSIX, use WIFEXITED(status) and WEXITSTATUS(status) from <sys/wait.h> to inspect a normal process exit code.

Take Quiz

Mini Project

Description

Build a small POSIX command runner that executes a trusted command, collects its standard output, and reports whether it completed successfully. This mirrors utility code used in build tools and internal automation scripts.

Goal

Create a program that runs printf, displays its captured output, and prints the command's exit code.

Requirements

Use popen() in read mode to execute a fixed command. Read all output into a std::string. Call pclose() after reading. Check whether the process exited normally. Display the captured text and exit code.

Take Quiz

Keep learning

Related questions

View more