Executing Linux Commands from C++ with popen()

Building a SystemAnalyser class that runs shell commands, captures output, and displays results

Vivek Bhadra  |  C++17 Linux POSIX popen

Sometimes you need to invoke a Linux command from your C++ program and capture its output. Once you have the output, the next step might be to parse it and take some meaningful action based on the results. Fortunately, Linux provides a handy system function called popen, which makes this task much easier.

In this post we will take a deep dive into the popen function, exploring how it works and how we can leverage it to execute Linux commands directly from a C++ program. We will see how to capture the command output, process it, and print it to the console. By the end you will have a clear understanding of how popen can be used effectively to interact with the Linux shell from within a C++ application.

Object Modelling

Because this is C++ we will take an object-oriented approach and model the program around classes. We will call our program SystemAnalyser. The SystemAnalyser class will have the following responsibilities:

  1. Launch a Linux command
  2. Capture the output of the command
  3. Display the captured output on the console

These are fairly simple operations and we will implement them as straightforward class methods. Here is the initial class declaration:

#ifndef __SYSTEM_ANALYSER_H__
#define __SYSTEM_ANALYSER_H__
#include <string>
class SystemAnalyser
{
    public:
        SystemAnalyser();
        virtual void RunCommand(const char * command);
        virtual void StoreOutput(std::string& result);
        virtual void DisplayOutput();
        virtual ~SystemAnalyser();
};
#endif

This is our blueprint for SystemAnalyser based on what we know so far. The declaration lives in a .h file and the implementation will follow in a .cpp file. The skeleton .cpp looks like this at this stage:

#include "SystemAnalyser.h"
SystemAnalyser::SystemAnalyser()
{
}
void SystemAnalyser::RunCommand(const char * command)
{
}
void SystemAnalyser::StoreOutput(std::string& result)
{
}
void SystemAnalyser::DisplayOutput()
{
}
SystemAnalyser::~SystemAnalyser()
{
}

Now we write a short main.cpp to wire it together and confirm it compiles cleanly:

#include <iostream>
#include <memory>
#include "SystemAnalyser.h"
int main()
{
    std::cout << "Hello World" << std::endl;
    auto system = std::make_unique<SystemAnalyser>();
    system->RunCommand("ls -la");
    system->DisplayOutput();
    return 0;
}

Compiling the Skeleton

No Makefile yet. A direct command-line compile is enough at this stage:

~/system_analysier/SystemAnalyser$ g++ --std=c++17 main.cpp SystemAnalyser.cpp -o system
~/system_analysier/SystemAnalyser$ ./system
Hello World

The program compiles and runs. It prints Hello World and nothing else, which is expected since the methods are still empty. Now we extend RunCommand() to do the real work.

How popen() Works

We will use the Linux system function popen to launch our command. In a nutshell, popen() does three things:

  • Creates a pipe stream
  • Creates a new process via fork()
  • Invokes the Linux shell /bin/sh to execute the command
Unidirectional pipe: The pipe that popen() creates is unidirectional. You can either read from it or write to it, but not both at the same time. Think of it as a one-way stream of data. When we open it in read mode ("r"), the command’s stdout flows into the pipe and we read it from our C++ side.
popen pipe diagram showing data flow from shell command into C++ program
Data flow: the shell command writes to the pipe, the C++ program reads from it

Implementation

Here is the full implementation of RunCommand():

std::string SystemAnalyser::RunCommand(const char * command)
{
    std::array<char, 128> buffer;
    std::string result;
    std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(command, "r"), pclose);
    if (!pipe) {
        throw std::runtime_error("popen() failed!");
    }
    while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
        result += buffer.data();
    }
    std::cout << result << std::endl;
    return result;
}

The unique_ptr with a Custom Deleter

The most interesting line in the implementation is this one:

std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(command, "r"), pclose);

There are two distinct parts to understand here:

PartWhat it does
decltype(&pclose) Tells the compiler to use the type of pclose as the deleter type. decltype deduces the type of an expression at compile time without evaluating it.
pclose (second argument) Provides the actual deleter function. When the unique_ptr goes out of scope, the runtime calls pclose() automatically to close the pipe and free resources.
Why use unique_ptr here? The pipe descriptor returned by popen() is a raw FILE* pointer. Wrapping it in a unique_ptr with pclose as the deleter ensures the pipe is always closed when the function exits, whether normally or via an exception. No manual pclose() call required.

Validating the Pipe

if (!pipe) {
    throw std::runtime_error("popen() failed!");
}

We check that the pointer holds a non-zero value before proceeding. If popen() failed for any reason (shell not found, invalid command, resource limits), the pointer will be null and we throw immediately with a descriptive message.

Reading the Output with fgets()

while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
    result += buffer.data();
}

We read the pipe using the POSIX function fgets(). Here is what each argument does:

  • buffer.data() provides a raw pointer to the first element of the array, which is where fgets() writes the data it reads.
  • buffer.size() limits the read to 128 bytes per call.
  • pipe.get() returns the underlying raw FILE* from the unique_ptr, which is the actual pipe descriptor fgets() needs.

The while loop keeps calling fgets() and appending to result until the pipe is empty. When the command finishes and all output has been read, fgets() encounters EOF and returns nullptr, which exits the loop.

Storing and Displaying the Output

We add a private member string to the class to hold the captured output:

    private:
        std::string outputStore;

StoreOutput() assigns the result string to this member. Since it is called internally by RunCommand() it does not need to be public:

void SystemAnalyser::StoreOutput(std::string result)
{
    outputStore = result;
}

DisplayOutput() simply sends the stored string to cout:

void SystemAnalyser::DisplayOutput()
{
    std::cout << outputStore << std::endl;
}
OOP note: StoreOutput() is called from within RunCommand() and has no reason to be accessible externally. Moving it to the private section of the class is the right design choice. Exposing only what the caller genuinely needs to use keeps the public interface minimal and the class easier to reason about.

Final RunCommand() and StoreOutput()

With the return type of RunCommand() changed to void (no need to return the string since it is stored internally), here are the two functions in their final form:

void SystemAnalyser::RunCommand(const char * command)
{
    std::array<char, 128> buffer;
    std::string result;
    std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(command, "r"), pclose);
    if (!pipe) {
        throw std::runtime_error("popen() failed!");
    }
    while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
        result += buffer.data();
    }
    StoreOutput(result);
}
void SystemAnalyser::StoreOutput(std::string result)
{
    outputStore = result;
}

Sample Output

VBR09@mercury:~/system_analysier/SystemAnalyser$ ./system
Hello World
total 64
drwxr-xr-x 2 VBR09 tpl_sky_pdd_jira_user  4096 Dec 23 18:51 .
drwxr-xr-x 4 VBR09 tpl_sky_pdd_jira_user  4096 Dec 23 14:20 ..
-rw-r--r-- 1 VBR09 tpl_sky_pdd_jira_user   683 Dec 23 18:51 SystemAnalyser.cpp
-rw-r--r-- 1 VBR09 tpl_sky_pdd_jira_user   329 Dec 23 18:49 SystemAnalyser.h
-rw-r--r-- 1 VBR09 tpl_sky_pdd_jira_user   244 Dec 23 14:22 main.cpp
-rwxr-xr-x 1 VBR09 tpl_sky_pdd_jira_user 44608 Dec 23 18:51 system

The program runs ls -la via popen(), captures the directory listing into outputStore, and prints it to the console via DisplayOutput().

In the next post we will expand the functionalities and include an output parser, which will give us a closer look at the interesting parts of the captured output.

2 Comments

Leave a Reply to vivekbhadraCancel reply