By the time you are finished with this lesson, your program will be able to obtain the name of the tree file to be processed from the user via command line options. It will also be possible to obtain the program version and help using command line switches.
Create a new header file named strom.hpp containing the following code:
#pragma once
#include <iostream>
#include "tree_summary.hpp"
#include <boost/program_options.hpp>
namespace strom {
class Strom {
public:
Strom();
~Strom();
void clear();
void processCommandLineOptions(int argc, const char * argv[]);
void run();
private:
std::string _data_file_name;
std::string _tree_file_name;
TreeSummary::SharedPtr _tree_summary;
static std::string _program_name;
static unsigned _major_version;
static unsigned _minor_version;
};
// member function bodies go here
}
inline Strom::Strom() {
//std::cout << "Constructing a Strom" << std::endl;
clear();
}
The constructor contains an output statement that notifies us when a Strom
object is created. Note that this line is commented out. Constructor and destructor comments will be commented out from now on, but feel free to uncomment them if, for debugging purposes, you ever need to know when objects of a particular class are created or destroyed. Please do not comment out the clear
function call, however. That ensures that a just-constructed Strom
object is in a consistent state. The clear
function may be used to return a Strom
object to its just-constructed state if desired.
inline Strom::~Strom() {
//std::cout << "Destroying a Strom" << std::endl;
}
Our usual destructor function just notifies us (if its single line is uncommented) that a Strom
object has been destroyed.
inline void Strom::clear() {
_data_file_name = "";
_tree_file_name = "";
_tree_summary = nullptr;
}
The clear
function currently just sets each of the two non-static data members to the empty string.
inline void Strom::processCommandLineOptions(int argc, const char * argv[]) {
boost::program_options::variables_map vm;
boost::program_options::options_description desc("Allowed options");
desc.add_options()
("help,h", "produce help message")
("version,v", "show program version")
("datafile,d", boost::program_options::value(&_data_file_name), "name of a data file in NEXUS format")
("treefile,t", boost::program_options::value(&_tree_file_name)->required(), "name of a tree file in NEXUS format")
;
boost::program_options::store(boost::program_options::parse_command_line(argc, argv, desc), vm);
try {
const boost::program_options::parsed_options & parsed = boost::program_options::parse_config_file< char >("strom.conf", desc, false);
boost::program_options::store(parsed, vm);
}
catch(boost::program_options::reading_file & x) {
std::cout << "Note: configuration file (strom.conf) not found" << std::endl;
}
boost::program_options::notify(vm);
// If user specified --help on command line, output usage summary and quit
if (vm.count("help") > 0) {
std::cout << desc << "\n";
std::exit(1);
}
// If user specified --version on command line, output version and quit
if (vm.count("version") > 0) {
std::cout << boost::str(boost::format("This is %s version %d.%d") % _program_name % _major_version % _minor_version) << std::endl;
std::exit(1);
}
}
This function does the work of extracting information provided by the user on the command line.
This function begins by creating two objects defined by the Boost program_options
library (which is available because we included the boost/program_options.hpp header at the top of the strom.hpp file):
The desc
object stores information about the available program options. This object figures out whether what the user types is valid, and can provide a usage string that shows the user what options are available and what to type to specify each option.
The vm
object is what actually stores the information provided by the user.
Our function then adds options to the desc
object. Options that are stand-alone (e.g. “help” and “version”) are simply specified by providing the name of the option, a single-letter abbreviation (if desired), and a string description of the option. For example, we specify the “help” option by providing “help” as the name, “h” as the abbreviation, and “produce help message” as the description. If the user desired help, she or he could type either -h
or --help
on the command line when running the program and obtain string descriptions of each option along with what tokens to type in order to activate that option. The convention is that single letter abbreviations of program options are preceded by a single hyphen, while program option full names are preceded by two hyphens.
The treefile option is more complicated because it will be followed by a string (the tree file name). We will also go ahead and provide for specifying a data file name on the command line, even though our program cannot yet read data files. The code
boost::program_options::value(&_data_file_name)
provides the memory address (the &
symbol means “address of”) where the value provided by the user will be stored. The program options library can tell from the fact that the variable _data_file_name
is an object of type std::string
that it should expect the user to type a string at the command line for this option.
Note that the value
function returns an object that has associated member functions that may be called. In this case, we can call the member function required()
for the treefile
option. This has the effect of ensuring that the user supplies a value for this option. If this option is not provided, the program will abort and tell the user that a value must be supplied for the treefile
option.
The parse_command_line
function call takes the supplied command line arguments (argc
holds the number of these, while argv
is an array of strings that holds what the user typed), along with the desc
object, and parses the options, storing the results in the vm
object.
The try
/catch
block encloses a call to the parse_config_file
function. This function attempts to read a file named strom.conf containing command line options in the form key = value, one per line. If the strom.conf file is not found, the catch
block is entered and a note is issued to the user letting them know that the strom.conf file was not found. This is not an error, since the user may enter all options on the command line, but when the number of options grows large, being able to store them all in a config file is very convenient for the user, avoiding really long, complicated command line invocations.
You will recognize the first two arguments to the parse_config_file
function: it is obvious that this function needs the name of the config file to search for (“strom.conf”) and it needs the desc
object so that it knows what options to expect in the config file, but what about that third argument: false
? This last argument tells the parse_config_file
function that we do not want to allow any options in the configuration file that have not been defined in the desc
object. If the program discovers sample_every = 100
in the configuration file but sample_every
has not been specified in the desc
object, then strom
will refuse to run. This is good behavior for now because it prevents users from thinking that an option is being used and understood by strom when in fact that line in the config file is being ignored.
The notify
function call takes the parsed command line, now stored in the map variable vm
, and sets the appropriate Strom
object data members. For example, _data_file_name
will be set to the value stored in vm["datafile"]
and _tree_file_name
will be set to the value stored in vm["treefile"]
. You could do this yourself (e.g. _data_file_name = vm["datafile"].as<std::string>();
), but the notify function makes it easy.
After calling parse_command_line
and notify
, we first check to see if the user asked for help. If so, we ignore any other command line options that may have been supplied by the user and show the usage string generated when a desc
object is output.
Assuming that the user did not type -h
or --help
, we next check to see whether -v
or --version
was specified. If so, we again ignore any other command line options and output the program version number and then exit.
If the user did not ask for either help or version, we must assume they wish to run the program. In order for the program to run, we need to have a tree file name. If this was not specified, the program exits with a suitable error message.
The Strom::run
function can now take over all the duties that the main function performed before. Because _data_file_name
and _tree_file_name
are data members, and we’ve guaranteed that the user has supplied a value for the tree file name on the command line, we are free to use _tree_file_name
in place of the hard coded literal string we used before.
inline void Strom::run() {
std::cout << "Starting..." << std::endl;
try {
// Create a new TreeSummary object and let _tree_summary point to it
_tree_summary = TreeSummary::SharedPtr(new TreeSummary());
// Read the tree file specified by the user
_tree_summary->readTreefile(_tree_file_name, 0);
Tree::SharedPtr tree = _tree_summary->getTree(0);
// Summarize the trees read
_tree_summary->showSummary();
}
catch (XStrom & x) {
std::cerr << "Strom encountered a problem:\n " << x.what() << std::endl;
}
std::cout << "\nFinished!" << std::endl;
}