Thursday 5 December 2013

Boost program options

I've been using boost program_options, and I've grown to like it. However, after setting up the options for a few programs, I felt the need to minimize the code repetition involved.

My first idea was to put the options in a configuration file and read it at start-up. But I found the add_options() syntax ill-suited for this, and it would require a great deal of work to get all the possible combinations working (required options, default values, etc), even assuming I'd limit all options to value<string>; and that's without going into multivalue options (e.g., value<vector<string>>). So, I've decided to settle for good enough - I've divided the work required to configure the options in two classes.

The first class is a class template, AppOptions, and it takes care of the generic stuff, i.e., that which remains unchanged in every app.

template <typename SpecificOptions>
class AppOptions
{
public:
    AppOptions(int argc, char* const argv[], char const* opTitle);

 
    void PrintHelp() const { std::cout << desc << std::endl; }
    const SpecificOptions& GetOptions() const { return so; }
    bool HasShownHelp() const { return shownHelp; }
private:
    template <typename SpecOpt, typename CharT>
        friend std::basic_ostream<CharT>&
        operator<<(std::basic_ostream<CharT>& os, 
        AppOptions<SpecOpt> const& obj);

 
    bool GotRequestForHelp(po::variables_map const& vm, 
        SpecificOptions const& so) const
        { return vm.count(so.GetHelpOption()); }

 
    bool shownHelp;
    po::options_description desc;
    po::variables_map vm;
    SpecificOptions so;
};


The SpecificOptions template parameter will contain all the different options/logic for each application. We expose it through GetOptions(), because the app will need it to access the actual option values.

opTitle is the caption shown on the help message's first line. shownHelp is set to true when the user requests the "help message", i.e., a list of the app's options.

template <typename SpecOpt, typename CharT>
friend std::basic_ostream<CharT>&
operator<<(std::basic_ostream<CharT>& os, AppOptions<SpecOpt> const& obj)
{
    os << obj.so;
    return os;
}


operator<<() is rather basic, not much to see here, except to note that this requires that SpecificOptions also overloads operator<<().

template <typename SpecificOptions>
AppOptions<SpecificOptions>::AppOptions(
    int argc, char* const argv[], char const* optTitle) :
    shownHelp{false}, desc{optTitle}
{
    so.DefineOptions(desc);
    po::store(po::parse_command_line(argc, argv, desc), vm);

 
    if (GotRequestForHelp(vm, so))
    {
        shownHelp = true;
        PrintHelp();
    }
    else
    {
        try
        {
            po::notify(vm);
            so.Validate();
        }
        catch (po::required_option& ro)
        {
            BOOST_THROW_EXCEPTION(ConfigRequiredOptionMissing() <<
                config_error_string("Required option missing: " +
                ro.get_option_name()));

        }
    }
}


The ctor is where everything is set up. After calling SpecificOptions::DefineOptions() we get the atual command line arguments passed to our app. I borrowed an idea from a Stack Overflow article, to check for the help option before calling program_options::notify(); this call performs validations (e.g., required options), and it makes no sense to perform those validations if the user just wants to see our help message and option list (there's no reason why I couldn't make GotRequestForHelp() a free function, but I'll leave it as a member, for now).

Also, if that's all the user wants to see, it makes no sense to continue processing and we have to let the app know that. At first, I thought about throwing an exception from the ctor. In the end, I settled for setting a flag and allowing the app to check it.

You'll note the so.Validate() call. I first considered custom validators, thinking it could make for a neater design. However, the docs mention these as a way of customizing parsing/conversion, not actually performing validation, and I didn't like the design I found in the examples that used them (with regards to validation requirements). Also, I figured such a design would make it more difficult to validate sets of related options. And then I noticed the example that performs validations of related options ($BOOST_DIR/libs/program_options/example/real.cpp) also uses auxiliary functions to perform such validation, so that settled it.

The exception thrown is defined in its own header, which contains other exceptions, and it's a simple boost exception.

So much for the generic part. What about the specific?

Let's use an example from one of my apps (renamed, to protect critical trade secrets):

class ExampleOptions
{
public:
    void DefineOptions(po::options_description& desc);
    void Validate();
    std::string GetHelpOption() const { return "help"; }
    std::string GetOperation() const { return operation; }
private:
    friend std::ostream& operator<<(
        std::ostream& os, ExampleOptions const& obj);
 

    std::string operation;
};

 
ostream& operator<<(ostream& os, ExampleOptions const& obj)
{
    os << "Operação: " << obj.operation;

    return os;
}

 
void ExampleOptions::DefineOptions(options_description& desc)
{
    desc.add_options()
        ("help,h", "Mensagem de ajuda")
        ("oper,o", value<string>(&operation)->required(),
            "Operação a efectuar. C - Operação de crédito;\n"
            "P - Operação de parametrização")
    ;
}

 
void ExampleOptions::Validate()
{
    if (operation == "P" || operation == "p" ||
        operation == "C" || operation == "c")
    {
        return;
    }

 
    BOOST_THROW_EXCEPTION(ConfigInvalidOption() << 
        config_error_string("Operação desconhecida: " + operation));
}

Nothing tricky here, just a couple of notes.
  1. We only defined operator<<() for ostream, not for wostream, even though the operator<<() we defined for AppOptions is prepared for both.
  2. We're outputting plenty of non-ANSI characters with little concern for locale issues. Obviously, this causes problems, particularly on a Windows console. On my next post I'll show the solution I came up with to deal with this.
And how do we use it?

int main(int argc, char *argv[])
{
    AppOptions<ExampleOptions> appOpt{argc, argv, "Opções de Exemplo"};
  
    if (appOpt.HasShownHelp())
    {
        return 0;
    }
  
    cout << appOpt << endl;

 
    string const op = appOpt.GetOptions().GetOperation();
    if ((op == "P") || (op == "p"))
    {
    ...
}


Pretty simple, and all we have to repeat for each app is the code that is actually different. I still itch to tackle that "options in a config file" design, but I'll leave that to another day.