Baeldung Pro – Linux – NPI EA (cat = Baeldung on Linux)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

Partner – Orkes – NPI EA (tag=Kubernetes)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

1. Introduction

Interfacing with the command line from a C++ program in Linux can open up numerous possibilities, such as automating tasks or interacting with system utilities.

In this tutorial, we’ll explore how to execute shell commands from C++ in Linux.

To begin with, we’ll cover the system() function for executing Linux commands. After that, we’ll discuss the gnome-terminal command and two ways to use it. Subsequently, we’ll look at the exec family to call Linux commands from a C++ program. Lastly, we’ll discuss a pure C++ approach that utilizes boost.process to execute the command line code.

2. Using the system() Function

The system() function from the <cstdlib> library is perhaps the simplest way to execute shell commands in C++. To elaborate, it spawns a new shell process, executes the supplied command, and returns control to the C++ program.

2.1. Understanding system()

The system() function accepts a string containing a shell command, executes it, and returns control to the calling C++ program once the command finishes. It uses the default system shell to run the command. For Unix-based systems, it commonly uses /bin/sh.

2.2. Example

For example, to use the system() function to list files in the current directory, we can supply the ls command:

$ cat system.cpp
#include <cstdlib>
#include <iostream>
int main() {
    std::cout << "Listing files:\n";
    system("ls");
    std::cout << "\nCreating a new directory 'testdir'\n";
    system("mkdir testdir");
    std::cout << "\nListing files after creating 'testdir':\n";
    system("ls");
    return 0;
}

To execute the program, we compile it using the gcc compiler, and execute the resulting binary:

$ gcc system.cpp system
$ ./system

The output first lists the files. After that, the program creates a new directory called testdir, and lists the files again:

Listing files:
file1.txt file2.cpp docs
Creating a new directory 'testdir'
Listing files after creating 'testdir':
file1.txt file2.cpp docs testdir

Thus, we employ shell commands from within the C++ program.

2.3. Advantages

This method is usually the easiest to implement and doesn’t require extensive error handling. In addition, the system() function is available across different platforms, making it useful in programs that need to interact with the shell on both Unix-based systems and Windows.

2.4. Disadvantages

Despite its simplicity, system() has some downsides.

Firstly, each call to system() spawns a new shell process. Any changes made, like changing the current directory with cd, don’t persist after the command finishes. If we need to perform multiple commands in the same shell environment, we must chain them together in a single string or set up the environment anew each time.

Furthermore, if user input is passed directly to the system(), it can expose the program to command injection attacks. For example, if we allow users to input a filename or command, that malicious input could execute arbitrary commands.

3. Opening a New Terminal and Executing Commands

Alternatively, we might want to open a new terminal window and execute specific commands. For this purpose, we use the gnome-terminal command.

3.1. Basic Usage

The general syntax for executing commands via gnome-terminal consists of consecutive commands:

$ gnome-terminal -x sh -c 'command1; command2; exec bash'

In this approach, we replace command1 and command2 with the specific commands we wish to run. This solution keeps the terminal open after executing the commands by using exec bash.

3.2. Using gnome-terminal — bash -c

One approach to opening a new terminal and executing commands is using the gnome-terminal command with the — bash -c option. This enables us to pass a string of commands that the terminal should run when it starts.

For instance, to open a new terminal and execute code from a C++ program, we use the gnome-terminal command within a system() call:

$ cat gnome.cpp
#include <cstdlib>
#include <iostream>
int main() {
    system("gnome-terminal -- bash -c \"cd ~ && ls; exec bash\"");
    return 0;
}

The output consists of the commands that we executed:

total 0
drwxrwxrwt 27 root wheel 864 24 Dec 15:27 .
drwxr-xr-x 6 root wheel 192 19 Dec 09:40 ..
-rw-r--r-- 1 sidrah wheel 0 20 Dec 12:46 .fctshutdown.lck
…

Let’s look at the code explanation:

  • gnome-terminal — bash -c tells the system to open a new instance of gnome-terminal and execute a command within it
  • cd ~ && ls first changes the directory to the home directory ~ and then lists the contents of that directory using the ls command
  • exec bash replaces the running process with a new Bash shell

The exec bash command is essential to keep the terminal window open after the initial commands finish executing. Without it, the terminal window would immediately close. To conclude, this method is useful when we need to execute multiple commands in a specific order and then leave the terminal open for further use or interaction.

3.3. Using -x sh -c

Another method to execute command line code from a C++ program is to use the -x sh -c option of gnome-terminal. This option opens a new terminal and executes a specified command. Notably, this approach is slightly more concise as compared to the –bash approach.

Let’s modify the same example gnome.cpp to use -x sh -c:

$ cat gnome.cpp
#include <cstdlib>
#include <iostream>
int main() {
    system("gnome-terminal -x sh -c 'cd /tmp ; ls -la ; exec bash'");
    return 0;
}

In the above code, we tell gnome-terminal to start a new shell session using sh -c and execute the command passed to it. After that, the cd /tmp ; ls -la commands are executed sequentially. cd /tmp changes the directory to /tmp, while ls -la lists the files in the /tmp directory in long format with detailed information.

This solution is particularly useful for simple tasks where complex command chaining isn’t required. With both approaches, we can pass additional options to customize the new terminal window. In addition, if any commands fail, the terminal shows an error message. This can be helpful for debugging scripts or automating tasks.

4. Using exec Family of Functions

Another approach to executing Linux commands in a C++ program uses the exec family of functions. These functions replace the current process image with a new one and execute the specified command.

4.1. Members of the exec Family

There are different types of exec functions:

  • execl() executes a command with a list of arguments
  • execv() executes a command with an array of arguments
  • execle() and execve() execute a command with custom environment variables
  • execlp() and execvp() search the PATH environment variable for the command

Notably, Unlike system(), these functions do not spawn a new shell.

4.2. Example

For example, to execute the ls command from a C++ program, we use the excel command:

$ cat execExample.cpp
#include <unistd.h>
#include <iostream>
int main() {
    std::cout << "Executing the 'ls' command:\n";
    execlp("ls", "ls", "-l", NULL);
    perror("execlp failed");
    return 1;
}

In the above code, unistd.h is a header file provided by POSIX-compliant operating systems and not part of the C or C++ standard libraries. It provides various system-level functions, such as file operations, process control, and other OS-specific features. Moreover, the first parameter in execlp specifies the command to be executed. Subsequently, the other parameters are arguments for the command. In addition, the last parameter must be NULL to terminate the argument list. If the command executes successfully, the process image is replaced, and the code after execlp doesn’t execute.

4.3. Uses and Limitations of exec

The exec functions offer more control than the system() function. Notably, these functions avoid spawning a new shell, which reduces overhead. They are also safer if user input is validated and arguments are properly sanitized.

However, exec functions do have some limitations. First, they replace the current process, so it cannot return to the original program after execution. Additionally, error handling is more complex since the command replaces the process.

Notably, the exec family of functions is suitable for scenarios where a new shell isn’t required. Furthermore, we use it when we need greater efficiency or control over the execution. Hence, this method provides an efficient and secure alternative to system(), especially for executing a single command or script without needing a full shell environment.

5. Using boost.process

Another modern C++ approach to execute Linux commands is boost.process. Unlike the exec*() functions, which depend on OS-specific headers like unistd.h, boost.process is a pure C++ solution, ensuring portability and better error handling.

To start using boost.process, we first install the boost library:

$ sudo apt-get install libboost-all-dev

Now, to execute the ls command from the C++ program, we include the relative libraries and execute the boost.process command:

$ cat main.cpp
#include <boost/process.hpp>
#include <iostream>
int main() {
    try {
        boost::process::system("ls");
    } catch (const boost::process::process_error& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
    return 0;
}

In the above code, we use boost::process::system to execute the terminal commands. In addition, this enables us to capture the output of the executed commands. Furthermore, we can pass arguments to the commands too.

Notably, when compiling the program, we tell the compiler where to find the boost header files:

$ g++ -o my_program main.cpp -I/usr/local/include -L/usr/local/lib -lboost_system -lboost_filesystem -lboost_process

Let’s understand this command:

  • -I/usr/local/include tells the compiler where to find Boost headers
  • -L/usr/local/lib tells the linker where to find Boost libraries
  • -lboost_system, -lboost_filesystem, -lboost_process link against the required Boost libraries

Once we execute the program, we can see the command output:

$ ./my_program
Applications		Documents		Library			Pictures
Downloads		Movies			Public
Desktop			Google Drive		Music

Thus, boost.process is platform-independent and doesn’t rely on system-specific headers, making it suitable for cross-platform development.

6. Conclusion

In this article, we covered how to execute command line code from C++ in Linux.

To begin with, we covered the system() command. Moreover, we also discussed the gnome-terminal and exec family system calls. Lastly, we covered the boost.process approach.