This is a text-only version of the following page on https://raymii.org: --- Title : Execute a command and get both output and exit status in C++ (Windows & Linux) Author : Remy van Elst Date : 07-06-2021 URL : https://raymii.org/s/articles/Execute_a_command_and_get_both_output_and_exit_code.html Format : Markdown/HTML --- Recently I had to parse some command line output inside a C++ program. Executing a command and getting just the exit status is easy using `std::system`, but also getting output is a bit harder and OS specific. By using `popen`, a POSIX `C` function we can get both the exit status as well as the output of a given command. On Windows I'm using `_popen`, so the code should be cross platform, except for the exit status on Windows is alway 0, that concept does not exist there. This article starts off with a stack overflow example to get just the output of a command and builds on that to a safer version (null-byte handling) that returns both the exit status as well as the command output. It also involves a lot of detail on `fread` vs `fgets` and how to handle binary data. <p class="ad"> <b>Recently I removed all Google Ads from this site due to their invasive tracking, as well as Google Analytics. Please, if you found this content useful, consider a small donation using any of the options below:</b><br><br> <a href="https://leafnode.nl">I'm developing an open source monitoring app called Leaf Node Monitoring, for windows, linux & android. Go check it out!</a><br><br> <a href="https://github.com/sponsors/RaymiiOrg/">Consider sponsoring me on Github. It means the world to me if you show your appreciation and you'll help pay the server costs.</a><br><br> <a href="https://www.digitalocean.com/?refcode=7435ae6b8212">You can also sponsor me by getting a Digital Ocean VPS. With this referral link you'll get $100 credit for 60 days. </a><br><br> </p> The complete code example with usage examples can be found on [github here][2] or at the bottom of this page. A working example is compiled [on github actions][11] for different platforms (windows & linux). Normally I would advise against parsing command line output. It is error-prone, you're dependent on the language selected by the user, different versions might have different flags (`OS X` vs `Linux`) and much more. If you have the option to use a native library, you should use that. An example could be parsing `curl` output to get some data from an API. There are probably a metric ton of `http` libraries available for your favorite programming language to use instead of parsing the `curl` or `wget` or `fetch` output. In my case I have to use an old program to parse a closed-source file to get some binary output. This is a temporary situation, a native parsing library is also under development. The binary is under my control as well as the system settings, language, other tools and such, so for this specific use case the solution to parse command line output was acceptable for the time being. Do note that in this post I'll interchange the term nullbyte, null character, null termination and null-terminated. They all mean the same, [the null-byte character][6] used to end a C string (`\0`, or `^@`, `U+0000` or `0x00`, you get the gist). If you need more features, more cross-platform or async execution, [boost.Process][12] is a great alternative. I however can't use boost on the environment this code is going to run due to compiler and size constraints. ### The stackoverflow example using fgets On [stackoverflow][1] the example given is a good base to build on, however, to get the exit code and the output, it must be modified. Because we want to also grab the exit code, we cannot use the example which uses the `std::unique_ptr`. Which in itself is a great example of using a `unique_ptr` with a custom deleter (`cmd` is a `const char*` with the command to execute: std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd, "r"), pclose); The code is copied below: std::string exec(const char* cmd) { char buffer[128]; std::string result = ""; FILE* pipe = popen(cmd, "r"); if (!pipe) throw std::runtime_error("popen() failed!"); try { while (fgets(buffer, sizeof buffer, pipe) != NULL) result += buffer; } } catch (...) { pclose(pipe); throw; } pclose(pipe); return result; } This example does what it states, but with a few gotchas. It uses a `FILE*` (pointer), `char` buffer allocation and manually closing the `FILE*` when something goes wrong (`catch`). The `unique_ptr` example is more modern, due to not having to handle the exception and using a `std::array<char, 128>` instead of a C-style `char*` buffer. Exception throwing is a whole other issue, but let's not get into that today. What we are going to get into today is C style code to read from `FILE*` and how binary data is handled. The stackoverflow example is probably just fine if you only need textual output into a `std::string`. My usecase however was a bit more complex, as you'll find out while reading the rest of this artice. ### fread vs fgets Code style aside, my biggest issue was that using `fgets` in this way combined with adding a `const char*` to a `std::string` stops when it encounters a `nullyte` (`\0`). For regular string output that often is not an issue, most commands just output a few strings and call it a day. My output returns a binary blob, which might include nullbytes. `fread` reads an amount of bytes and returns how much it has read successfully, which we can use when adding the output to our `std::string` including the nullbytes. The example above does `result += buffer`, adding a `const char*` to a `std::string`, in this case according to [cppreference on operator+= on std::string][4]: `Appends the null-terminated character string pointed to by s.` The problem therein lies that the characters after the nullbyte should be added as well in my case. `fgets` does not give back the amount of data it read. Using `fgets` and a buffer of 128, if I have a nullbyte at 10 and a newline at 40, then the first 10 bytes plus what is after 40 bytes will be returned. Effectively we're loosing everything in between the nullbyte and the newline, or until the end of the buffer (128) if there is no newline in between. `fread` does return the amount of bytes it has read. Combining that with a constructor of `std::string` that takes a `const char*` and a `size_t` we can force the entire contents inside the string. This is safe, since a `std::string` knows its size, it does not rely on a null-termination character. However, other code that uses `const char*` will not be able to work with these nullbytes, keep that in mind. [This stackoverflow post][5] was very helpful for me to understand `fread`, as well as help from a co-worker that dreams in `C`, he explained a lot of the inner workings. And if, after all of this, you're wondering why I'm shoehorning binary data inside a `std::string`, great question. I'll probably go into that another time since that would require a longer post than this entire article. ### Command execution including output and exit code My code checks the exit status of the executed binary (for error handling) and uses the data returned for further processing. To keep this all in one handy dandy place, lets start with defining a struct to hold that data. It will hold the result of a `command`, so the name `CommandResult` sounds descriptive enough. Below you'll find the struct code, including an equality operator as well as a stream output operator. struct CommandResult { std::string output; int exitstatus; friend std::ostream &operator<<(std::ostream &os, const CommandResult &result) { os << "command exitstatus: " << result.exitstatus << " output: " << result.output; return os; } bool operator==(const CommandResult &rhs) const { return output == rhs.output && exitstatus == rhs.exitstatus; } bool operator!=(const CommandResult &rhs) const { return !(rhs == *this); } }; The meat and potatoes of the struct are of course the `output` and `exitstatus`. I'm using an `int` for the exit status [because of reasons][7]. The next part is the `Command` class itself, here is that code: class Command { public: /** * Execute system command and get STDOUT result. * Like system() but gives back exit status and stdout. * @param command system command to execute * @return CommandResult containing STDOUT (not stderr) output & exitstatus * of command. Empty if command failed (or has no output). If you want stderr, * use shell redirection (2&>1). */ static CommandResult exec(const std::string &command) { int exitcode = 255; std::array<char, 1048576> buffer {}; std::string result; #ifdef _WIN32 #define popen _popen #define pclose _pclose #define WEXITSTATUS #endif FILE *pipe = popen(command.c_str(), "r"); if (pipe == nullptr) { throw std::runtime_error("popen() failed!"); } try { std::size_t bytesread; while ((bytesread = fread(buffer.data(), sizeof(buffer.at(0)), sizeof(buffer), pipe)) != 0) { result += std::string(buffer.data(), bytesread); } } catch (...) { pclose(pipe); throw; } exitcode = WEXITSTATUS(pclose(pipe)); return CommandResult{result, exitcode}; } } The `fread` command will run until there are no more bytes returned from the command output. I know the kind of output I'm working with so my buffer is 1 MiB, which is probably too large for your data. In my case I benchmarked it and between 10KiB and 1 MiB was the fastest on the target architecture. 128 or 8192 is just fine as well probably, but you should benchmark that for yourself. A rather simple test is to output some enormous file with `cat` and take the execution time plus cpu and memory usage. Don't print the result, just look at those three things and choose what ratio is acceptable for you. Why not also initialize the `std::string` with 1 MiB of characters? `std::strings` cannot be allocated for a given size at construction, other than by filling them or afterwards calling `.reserve()`, my benchmarks did not show any meaningful speed or performance boost by doing either. Using the above code is easy. Since it's a static function, you don't need a class instance to use it. Here is an example: std::cout << Command::exec("echo 'Hello you absolute legends!'") << std::endl; Which results in: command exitstatus: 0 output: Hello you absolute legends! Since we're going through a shell, redirection works as well. Redirecting `stdout` to `stderr` results in no output, just an exit status: std::cout << Command::exec("echo 'Hello you absolute legends!' 1>&2") << std::endl; The output is on `stderr` in my shell however, which is expected: ![stderr][8] If you do need to capture `stderr` then you redirect output the other way around, like so: std::cout << Command::exec("/bin/bash --invalid 2>&1") << std::endl; Pipes work as well as in your shell, but do note that this is all using `sh` and you have no control over environment variables or the default shell. Read more on the [POSIX page on `popen`][10] to find out why that is. #### A note on Windows Here is an example for Windows, where we [must use `_popen` and `_pclose`][9]: std::cout << "Windows example:" << std::endl; std::cout << Command::exec("dir * /on /p") << std::endl; The exit code will always be zero since that concept does not translate to windows. There is `%ErrorLevel%`, but that is only an environment variable for console applications, not the actual exit status. The microsoft page also notes that `_popen` will not work with GUI applications, just console programs. If you need that, use [Boost.process][12] or `system`. ### Nullbytes in output example: In the example code on github you'll also see a `execFgets` function, I've left that in there to show the difference in nullbyte handling. For reference I'll show an example here as well. The relevant part of command using `fgets`: while (std::fgets(buffer.data(), buffer.size(), pipe) != nullptr) result += buffer.data(); The part using `fread`: std::size_t bytesread; while ((bytesread = fread(buffer.data(), sizeof(buffer.at(0)), sizeof(buffer), pipe)) != 0) result += std::string(buffer.data(), bytesread); The test command, including a clang-tidy warning exclusion (`// NOLINT`): int main() { using namespace raymii; std::string expectedOutput("test\000abc\n", 9); //NOLINT commandResult nullbyteCommand = command::exec("/usr/bin/printf 'test\\000abc\\n'"); // NOLINT(bugprone-string-literal-with-embedded-nul) commandResult fgetsNullbyteCommand = command::execFgets("/usr/bin/printf 'test\\000abc\\n'"); // NOLINT(bugprone-string-literal-with-embedded-nul) std::cout << "Expected output: " << expectedOutput << std::endl; std::cout << "Output using fread: " << nullbyteCommand << std::endl; std::cout << "Output using fgets: " << fgetsNullbyteCommand << std::endl; return 0; } Output: Expected output: test\0abc A command with nullbytes using fread: exitstatus: 0 output: test\0abc A command with nullbytes using fgets: exitstatus: 0 output: test The nullbyte character is substituted with `\0` in the above output. Here is a screenshot showing how it looks in my terminal: ![screenshot][3] Once again do note that this is safe to use with `std::strings`, methods that take a `string_view` or a `const char*` probably will not react very well to nullbytes. For my use case this is safe, your milage may vary. Try playing with the `buffer` size and then looking at the output. If you set it to 4, the output with `fgets` is `testbc`. Funny right? I like such things. ### Complete code Below you can find the header file `command.h`. It is also on [my github][2]. If you want usage examples you can find them in the github project `main.cpp` file. #command.h #ifndef COMMAND_H #define COMMAND_H // Copyright (C) 2021 Remy van Elst // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. #include <array> #include <ostream> #include <string> #ifdef _WIN32 #include <stdio.h> #endif namespace raymii { struct CommandResult { std::string output; int exitstatus; friend std::ostream &operator<<(std::ostream &os, const CommandResult &result) { os << "command exitstatus: " << result.exitstatus << " output: " << result.output; return os; } bool operator==(const CommandResult &rhs) const { return output == rhs.output && exitstatus == rhs.exitstatus; } bool operator!=(const CommandResult &rhs) const { return !(rhs == *this); } }; class Command { public: /** * Execute system command and get STDOUT result. * Regular system() only gives back exit status, this gives back output as well. * @param command system command to execute * @return commandResult containing STDOUT (not stderr) output & exitstatus * of command. Empty if command failed (or has no output). If you want stderr, * use shell redirection (2&>1). */ static CommandResult exec(const std::string &command) { int exitcode = 0; std::array<char, 1048576> buffer {}; std::string result; #ifdef _WIN32 #define popen _popen #define pclose _pclose #define WEXITSTATUS #endif FILE *pipe = popen(command.c_str(), "r"); if (pipe == nullptr) { throw std::runtime_error("popen() failed!"); } try { std::size_t bytesread; while ((bytesread = std::fread(buffer.data(), sizeof(buffer.at(0)), sizeof(buffer), pipe)) != 0) { result += std::string(buffer.data(), bytesread); } } catch (...) { pclose(pipe); throw; } exitcode = WEXITSTATUS(pclose(pipe)); return CommandResult{result, exitcode}; } }; }// namespace raymii #endif//COMMAND_H [1]: http://web.archive.org/web/20210607112805/https://stackoverflow.com/questions/478898/how-do-i-execute-a-command-and-get-the-output-of-the-command-within-c-using-po/478960 [2]: https://github.com/RaymiiOrg/cpp-command-output [3]: /s/inc/img/nullbytes.png [4]: https://en.cppreference.com/w/cpp/string/basic_string [5]: http://web.archive.org/web/20210607145050/https://cs50.stackexchange.com/questions/21350/does-fread-really-read-from-the-disc-the-size-and-the-number-of-times-as-it-is [6]: https://en.wikipedia.org/wiki/Null_character [7]: http://web.archive.org/web/20210607185349/https://unix.stackexchange.com/questions/418784/what-is-the-min-and-max-values-of-exit-codes-in-linux [8]: /s/inc/img/stderr.png [9]: http://web.archive.org/web/20210607191758/https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/popen-wpopen?view=msvc-160 [10]: http://web.archive.org/web/20210607193329/https://pubs.opengroup.org/onlinepubs/9699919799/functions/popen.html [11]: https://github.com/RaymiiOrg/cpp-command-output/actions [12]: https://www.boost.org/doc/libs/1_76_0/doc/html/process.html --- License: All the text on this website is free as in freedom unless stated otherwise. This means you can use it in any way you want, you can copy it, change it the way you like and republish it, as long as you release the (modified) content under the same license to give others the same freedoms you've got and place my name and a link to this site with the article as source. This site uses Google Analytics for statistics and Google Adwords for advertisements. You are tracked and Google knows everything about you. Use an adblocker like ublock-origin if you don't want it. All the code on this website is licensed under the GNU GPL v3 license unless already licensed under a license which does not allows this form of licensing or if another license is stated on that page / in that software: This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. Just to be clear, the information on this website is for meant for educational purposes and you use it at your own risk. I do not take responsibility if you screw something up. Use common sense, do not 'rm -rf /' as root for example. If you have any questions then do not hesitate to contact me. See https://raymii.org/s/static/About.html for details.