The next task that I recommend new coders try after they learn how to write Hello World is how to pass arguments to an application on the command line and read those arguments in. Back in the day I’d write it all myself, because, well, there was about all there was. This time I’m using Boost’s Program Options library to parse and handle command line input. This is in the same vein to Python’s argparse
, at least with regards to using another package.
#include <exception>#include <iostream>#include <boost/program_options.hpp>namespace po = boost::program_options;int main(int argc, char *argv[]) {po::options_description options("Options");options.add_options()("help", "Help using application.")("compression", po::value<int>(), "Set compression level to integer value");try {po::variables_map varmap;po::store(po::parse_command_line(argc, argv, options), varmap);po::notify(varmap);if (varmap.empty() or varmap.count("help")) {std::cout << options << std::endl;}else if (varmap.count("compression")) {std::cout << "Compression level set to "<< varmap["compression"].as<int>() << std::endl;}else {std::cout << "Compression level was not set.\n";return -1;}}catch (std::exception &ex) {std::cerr << "Error: " << ex.what() << std::endl;std::cout << std::endl << options << std::endl;return -1;}return 0;}
The example above started with an example provided on the Boost Program Options Getting Started page ( https://www.boost.org/doc/libs/1_80_0/doc/html/program_options/tutorial.html#id-1.3.30.4.3 ). The first code example in the tutorial won’t compile. In order to compile that code you need to include three libraries as shown in the first three lines of the listing above. I will make an editorial comment about the code examples in the Boost tutorial, and say that they’re all bad. Not only will they not compile, they won’t run very well either. I’ll get to that running poorly in a moment.
The bulk of the original tutorial logic is between lines 5 to 30. Everything else I had to add in order to produce an application that would behave reasonably, especially when erroneous input is given to the application.
The try/catch block was added in order to handle exceptions that the Boost library throws when it has an error. That isn’t documented anywhere that I can find for this library. While handling exceptions is really no big deal, and is considered the Python Way (using try/except) within Python, at least you should tell someone what will happen if your library reaches a point where an error occurs. I discovered this behavior when I successfully compiled the initial version without any sort of try/catch logic. Every time I gave it bad switches, or nothing at all, it would throw an exception and core dump. Yeah, that’s gonna work just fine. Not.
The rest of the code is the try/catch block, with some logic in the catch section to tell you what the error is and then prints out help. That’s what should happen.
If you just type the name of the application, line 20 will check to see if any switches were passed to the application. If none were, or if you entered --help
, then you get help. The original code would not check for lack of any options and throw an exception and core dump. By the way, the library will only accept an option if it is preceded by --
. Anything else, such as one dash, is treated as an invalid input.
The only problem with the listing is the last else statement, lines 27 to 30. With the logic as it currently exists, this last statement is no longer executed and should be removed. I left it in place because it’s part of the original Boost tutorial.
Setting up the Boost Build Environment
There are two ways to set up the Boost build environment. The first is to download the source tarball, unpack that into a work area, build it, then install it. The directions on the Boost website ( https://www.boost.org ) are clear and complete on this. I’ve done it myself just to see what it’s like and if it provides any advantage over installing from the Ubuntu repos. That depends. The current Boost release is version 1.80, released this November. The version installed from the repo is 1.74, released in August 2020, over two years ago. If you want the latest release with all its bug fixes and newer features, then yes, build Boost from the source and then build your application(s) on top of that. For the most part start with the repo and then build Boost from latest if you find a problem or you want a feature in version 1.80 that’s not in version 1.74.
The second way is to install from the repo. Specifically, sudo apt install libboost-all-dev
will install all header and library files. The header files are installed in /usr/include/boost/
, and the library files are installed in /usr/lib/x86_64-linux-gnu/
.
Building the Boost Example
I used to be a make file nerd back in the day because make was all you had, and so you learned to use it. One of the primary reasons for make back in the day was because build systems were so slow compared to today’s systems. Make checked dependencies and only rebuilt those portions of a project that had changed, then rebuilt those changed source files and then relinked the full application binary. That’s not such a problem today, especially when my $300 2 GHz 8 core AMD Ryzen 5 blows the doors off those SUN Microsystems build servers I used to use. And Linux Mint is so much better than Solaris.
I’ve since fallen away from make, especially when all I want to do is build some simple applications. These days I’ll use Bash or Python, or if it gets complicated, CMake. If I write in Go or Rust, those two languages have a complete toolset that will build my application for me. For building this application I wrote a simple Bash script.
#!/bin/env bashg++ -std=c++20 -c cli.cppg++ -static -o cli cli.o -l boost_program_options
Line 1 is the standard shebang. Line two invokes the GNU C++ compiler to compile the application using the C++20 standard rules. I set the standard to C++20 because that’s what I’m brushing up on at the moment. If you’re running on a Linux distribution that has GNU C++ version 11 or later, then you’re all set. Linux Mint 21 has version 11.3.0. Line 3 is the linker line. Note the -static command line switch, and also note that I’ve had to select the specific library to link against with the -l boost_program_options command line switch. I’m building a static application because that is how Go and Rust build binaries. I heartily approve of static linking so that you can give your customers a single binary that will run just about anywhere. Having your application depend upon whether they have every supporting library installed is a royal pain. That may be the way things were done twenty or more years ago, but not today.
Running the Application
Here are a few runs of the application.
~ ./cli Options: --helpHelp using application. --compression arg Set compression level to integer value
Simply running the application without any input produces help. Just as you would want it to do.
~ ./cli --helpOptions: --helpHelp using application. --compression arg Set compression level to integer value
Running the application for explicit help.
~ ./cli --compressionError: the required argument for option '--compression' is missingOptions: --helpHelp using application. --compression arg Set compression level to integer value
We passed a proper option, but the argument is missing, thus the error. And once again we print out the help.
~ ./cli --compression 10Compression level set to 10
Now we have a successful invocation. But what happens if we try to pass a float argument? Remember, help says to pass an integer argument.
~ ./cli --compression 10.1Error: the argument ('10.1') for option '--compression' is invalidOptions: --helpHelp using application. --compression arg Set compression level to integer value
We get an error and another dose of help. What about a totally different option?
~ ./cli --fooError: unrecognised option '--foo'Options: --helpHelp using application. --compression arg Set compression level to integer value
Which is what we would expect. Before you ask, it will bale on on the first unrecognized option. And I know that “unrecognised” is misspelled in the error message. But that error message is part of the package, nothing that I can easily fix. There’s also one other feature. If I pass --comp
or even --c
, the code will recognize it because it’s still a partial match and it’s unique.
Some Final Thoughts
I choose to dig deeper into Boost because of the general good chatter around the library set. And the first few forays into Boost were very easy. Those first few forays used header-only libraries that didn’t require linking. This is the first instance where I had to link. To be honest, this experience has been a bit rough. If I’d had experience with this first, I might not have gone any farther. But it does show that quality in Boost is uneven. Not just in the tutorials, but in the implementation. For example consider the penchant to throw exceptions all over the place. Boost has two exception libraries, a regular Exception and another called LEAF. If you go to the Exception documentation they tell you that if you’re past a certain Boost version then you should use LEAF. I looked at LEAF and left in confusion. I tried Boost’s Exception and discovered that Boost’s Exception printed out that it was using regular C++ exception and that maybe the application should use the more advance Boost Exception. Really?
I’m going to continue to survey Boost as well as move on my own into C++ 20 style C++.