C++ - C++ Shell - Programming Languages

The C++ Shell: A Developer’s Guide

Whether you want to print a list of files on your computer or use the output from another process in your code, you’ll eventually need to interact with your operating system from within a C++ application. 

In this article, we’ll discuss the most common ways to perform process management in C++ by use of the Unix shell.

What Is a Unix Shell?

As an aspiring C++ developer, you’ve likely heard about the “Big Divide” in the world of operating systems. On the one hand, we have the various Windows versions, which still constitute the majority of personal computer operating systems.

On the other, we have the family of Unix-like operating systems based on the POSIX standard. This standard is distinguished by elements like portability, modularity and the concept that everything in a system can be treated as a file.

Both macOS and GNU/Linux variants (like Ubuntu) belong to this group. In these systems, a user can interact with the kernel using a command-line interface that’s frequently called the shell. Unix-like systems can include a variety of shells like sh, bash, csh, fish and zsh. Some but not all of these shells are POSIX-compliant — that is, they follow the POSIX standard.

By using a shell, Mac and Linux users can start new processes on their machines with the programs they want to run.

C & C++ and the Unix Family

The C language family enjoys a close relationship with Unix. In fact, C was written for the purpose of creating a portable operating system.

Before Unix, operating systems were written in assembly language and closely tied to the computer‘s hardware. Thus, each architecture had to have its own custom system. Unix, by contrast, was designed as a ready-made operating system.

While UNIX systems can be proprietary, the GNU/Linux systems are open-source and freely available to anyone. The C++ language was created as an extension of C. So while Unix systems and C++ are not directly related, they definitely belong to the same extended family.

What Is a Unix Process?

A process, also known as a task, is an instance of a running program. Starting your machine triggers the first process, known as init. All other processes are created by “forking” —  a process used to create an identical copy. The copied process is known as the “child” or “clone,” and the original process is called the “parent.”

In the Unix context —  when you launch a process through a shell — the forking mechanism creates a copy of the shell process and then starts the program that you requested to run.

Every process in a Unix-like system is identified by a unique number: its process ID or “PID.” The lifecycle of a process consists of three phases: creation, runtime and termination. A process terminates either upon task completion or because it was aborted by the operating system, for example, because the process tried to access an area of memory that it doesn’t have access to or because it tried to execute an invalid instruction.

Process Management in C++

Now, let’s say we want to print a list of files in a directory to the console. 

We could do so by using the exec group of system calls, which we can import by including the library unistd in the header. An exec call allows your program to reach into your system’s shell and run a command.

In our example, we’ll use the function execvp(). We’ll give it the name of the function (and its parameters) that we want to pass to the shell. Let’s look at a minimal working example without error checking:

#include <string>
#include <unistd.h>

int main() {

  char* args[2] ;

  // the POSIX command ls lists the content of the current directory
  std::string cmd = "ls" ;

  // execvp expects a C-style string rather than a C++ string, so
  // we cast the command name to a C string
  args[0] = (char*)cmd.c_str() ;

  // NULL tells execvp that the list of arguments ends
  args[1] = NULL ;

  execvp( args[0], args ) ;

  return 0;
}

// output:
// example1 example2 example3 ls.cpp

Keep in mind that the system call invoked by exec calls will replace our current process. Upon completion of a process initiated with execvp(), the entire program terminates.

If we don’t want this to happen, we must first fork() our process. This will create a child task within which to make our exec call.

If we prefer not to fork manually, we can use popen(). This function will use fork() automatically to create a new process connected to its parent by a pipe, enabling us to send data from the child process to the parent.

The return value of  popen() is a stream that we can read from or write to. The direction of the pipe must be specified in the function’s arguments.

Let’s look at popen() in action, again using our example of listing a directory’s contents:

#include <cstdio>
#include <iostream>
#include <memory>
#include <string>
#include <array>

std::string exec(const char* cmd) {

    // we use std::array rather than a C-style array to get all the
    // C++ array convenience functions
    std::array<char, 128> buffer;
    std::string result;

    // popen() receives the command and parameter "r" for read,
    // since we want to read from a stream.
    // by using unique_ptr, the pipe object is automatically cleaned
    // from memory once we've read all the data from the pipe.
    std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd, "r"), pclose);
   
    while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
        result += buffer.data();
    }
    return result;
}

int main() {
        std::string result;
        result = exec( "ls" );
        std::cout << result ;
        return 0;
}

// output:
// example1 example2 example3 ls.cpp

(See this answer on Stack Overflow for the original implementation.)

Process Management in Windows

Process management in C++ is not system-agnostic. When we interact with the shell in our code, we have to use commands that our operating system will recognize. For example, if we were to work in Windows, we would have to use CreateProcess instead of fork().

However, with the 2016 introduction of the Windows Subsystem for Linux, Microsoft gives the option of invoking a Unix shell inside Windows. In this emulated Linux environment, we can use the POSIX commands we saw above.

When To Use a Shell in Your C++ Programs

Dependence on a specific shell command dramatically impacts your project’s portability. Commands might behave differently depending on the machine’s default shell, and you cannot assume that the programs you want to launch through a shell will be available for every user. We recommend generally avoiding the inclusion of system commands in your C++ code.

Instead of relying on a shell, it’s better to use C++ libraries and operating-system APIs to accomplish the job. For example, instead of reaching out into a shell to run an ls command, you can use functions from the std::filesystem namespace (available since C++17). The advantage of using native functions is in getting output that’s more interoperable with the rest of your C++ code. Moreover, you need not worry about the specific operating systems that your code will run on — the filesystem library takes care of that for you.

Become a C++ Developer

In this article, we went over how to invoke system calls within a C++ program. We also learned POSIX commands fork(), execvp() and popen() and saw both the potential and limitations of their use in code.

Are you looking to take your C++ game to the next level? Enroll in our expert-taught C++ Nanodegree program.

Start Learning