Question
Execute a Command and Capture Output in C++ with POSIX popen()
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.
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:
popen()starts a shell that runsprintf 'red\nblue\n'.- The command writes
red, a newline,blue, and a newline to standard output. popen()returns aFILE*whose input comes from that output.- The first
fgets()call reads"red\n"and appends it tooutput. - The second
fgets()call reads"blue\n"and appends it. - A later
fgets()call reaches end-of-file and returnsnullptr, ending the loop. pclose()closes the pipe, waits for the command if necessary, and returns its termination status.- 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 HEADto 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.
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 returnnullptr. - 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
| Approach | Captures stdout? | Uses a shell? | Best use |
|---|---|---|---|
system() | No | Yes | Run a simple command when only completion status matters |
popen(command, "r") | Yes | Yes | Read textual stdout from a trusted command |
popen(command, "w") | No; writes to child stdin | Yes | Send data to a command's standard input |
fork() + exec() + pipes | Yes, including separate stderr if implemented | No, unless you choose one | Robust POSIX process control with separate arguments |
| A library API | Usually not applicable | No | Prefer 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.
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 returnsnullptr.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 intocommand.- 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.
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.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.