Showing posts with label Boost. Show all posts
Showing posts with label Boost. Show all posts

Tuesday, 10 June 2014

Processing command line options in Ruby

The story so far

I've been setting up a scripting environment at work, as I've mentioned previously. After considering Perl, Python, and Ruby, we chose the latter.
 
Ruby itself was installed from RPMs by the sysadmins. Then, I've installed Ruby Gems locally, and all the gems are also locally installed. This minimizes our dependency on the sysadmins, i.e., we don't have to open a request every time we need a new gem installed. The server has no internet connection, so we download the gems and run a local install, which means sorting out dependencies manually. So far, it has been going smoothly, and I'm quite impressed by the way gem management is handled.
 
One of our top goals is working with Informix DBs, and I'm happy to report that the Ruby Informix gem installed without a single hiccup (did I already mention I'm impressed?), and worked right out of the box (because I'd like to leave that quite clear - I'm impressed).
 
This move to Ruby was caused, in part, by a server migration, where we decided to take the opportunity to simplify our batch environment. We currently have a mix of Java and shell scripts, with a liberal sprinkling of awk and some Perl. We'll keep the Java processes as-is, but we're planning on moving the scripts to Ruby.
 
The first goal is migrating everything to the new servers, making only the necessary changes in order to keep things working with a new set of system requirements (e.g., replacing FTP with SFTP). With time, we'll be converting all the scripting logic to Ruby.
 
So, I've been diving into Ruby, in a mix of planning and prototyping, trying to get a grasp of the language's basics, and creating some building blocks for future scripts.
 

Ruby & Command line options - GetoptLong

I'm currently working on a component to process command line arguments, similar (in goal, if not in design) to what I've done here.
 
As I looked at how this is done in Ruby, the first option I found was GetoptLong. I set up the options and began writing the code to do something with it. And I immediately thought "I need something else". Why?
 
Well, when you have a class that lacks a method to get an option by name, forcing you to loop through all the options with a switch/case that says "If the current option is this, then do that", I have to wonder - does anyone actually think this is elegant?
 
Yes, I know. I could build such a method myself. After all, Ruby allows me to add methods to existing classes, so I could add a getter that took a name as argument, and returned the value of the option with that name or nil/exception if it didn't exist. But I couldn't believe there was no alternative. I couldn't be the only one thinking "This is sub-par design", there had to be a better way.
 

Ruby & Command line options - OptionParser

There is. It's called OptionParser. It's definitely better, and it seems quite powerful and complete. However, I've yet to find an example/tutorial that shows me how to do a couple of things that are dead simple in, e.g., Boost.Program_options:
 
The first is setting up a required option, making the option processing throw an exception if it's not present. Notice how easy it is with Boost:
 
("fich,f", po::value<string>(&fileName)->required(), "Ficheiro a processar")
 
OptionParser has a concept called "mandatory", but it means "If you include this option in the command line, you must specify its value". However, if you don't include it, no exception is thrown.
 
The second is setting up a default value directly in the options definition, instead of having to do it in some other code, even if it's local to the options definition. Once again, courtesy of Boost:
 
("validnc,c", po::bool_switch(&validateFieldNr)->default_value(false),
    "Valida nr. de campos por linha e termina")

And I already have a pet peeve with OptionParser. Suppose you have this:

options[:delimiter] = ';'
opts.on('-d', '--delimiter', 'Delimitador utilizado no ficheiro de dados') do |delim|
  options[:delimiter] = delim
end

Looking at this, you'd think "I have a default delimiter, ';', and I have an option to specify a different delimiter on the command line". Seems logical, right? Well, not quite. If you specify this option on the command line, options[:delimiter] will be set to true, and your delimiter will be left in ARGV, i.e., it won't be consumed by OptionParser.
 
Here's what you need, instead:
 
options[:delimiter] = ';'
opts.on('-d', '--delimiter delim', 'Delimitador utilizado no ficheiro de dados') do |delim|
  options[:delimiter] = delim
end
 
By the way, you don't need to use "delim" in '--delimiter delim', you can use anything, as long as it's there. Fortunately, I wasn't the first one to get bitten by this little detail, and I quickly found someone else wondering why were all his values true or false.
 
While I can't help but wonder at the reasoning that led to "This looks like a good idea", my ignorance on the language/class means I'll just live with it, since I don't feel competent to do better.
 

Pet Peeve - Documentation

Yes, this is my pet peeve. Docs. OptionsParser's minimal example is too minimal to be useful, and the complete example is so crowded, that details like this get lost in the noise; not that there's anything wrong with that, that's what a complete example is for, it's the minimal example that shouldn't be quite so minimal.
 
I believe a rule like "If you want your options to have a value, you have to put something - anything - following it on the long form string" should be made more visible in the documentation. Actually, most of the Ruby documentation I've found so far is quite minimalistic/optimistic. Tutorials like Boost's (yes, I'm using Boost as an example of good documentation; no, I never thought that day would come), or troubleshooting sections are absent from most of the docs I've found.
 
E.g., take a look at make_switch and see what you can make of it. Then, see the  complete example. Finally, look at this tutorial. It's only after I read this tutorial that I understood the semantics of "mandatory" and "optional" in this context, and gained a better understanding of how OptionParser works.
 
And going back to the "optimistic" bit I mentioned above, here's what I mean - sentences like these (which are from the tutorial) should be in the class's docs:
  • Switches that take a parameter only need to state the parameter name in the long form of the switch.
  • While your option string can define the parameter to be called "a,b,c", OptionParser will blindly allow any number of elements in the list. So, if you need a specific number of elements, be sure to check the array length yourself.

Another example - if you go to rubygems.org, you have the installation instructions, which end with this:
For more details and other options, see:  
ruby setup.rb --help
And, after a few hours of dealing with issues on Ruby Gems setup, I had to ask - why doesn't it end with this:
For more details and other options, see <this link for online docs, where we explain said details and options with more depth than we ever will in a help message>

Then, we have the guides. Filled with useful info, no denying that. However, when I needed to learn about things like GEM_HOME and RUBYLIB, I had to go elsewhere. While we could argue the latter is not actually Gem specific, I still believe it should be there, close to the sections regarding Ruby Gems installation and setup. Not all of us can just go gem update or ruby setup.rb with "you may need admin/root". Some of us don't even have an internet connection to begin with.
 

TL;DR

As I dive into Ruby, I like most of what I see, especially after having learnt to appreciate duck typing, available with C++ templates, and the total decoupling it provides.
 
So far, I've met a few design choices that baffle me, and the docs are definitely a weak point. Fortunately, there's this internet thingie where people are more than willing to share their knowledge.
 
As for processing command line arguments, I've settled for this "pattern":
options = {}
 
options[:delimiter] = ';'
opts.on('-d', '--delimiter delim', 
'Delimitador utilizado no ficheiro de dados') do |delim|
  options[:delimiter] = delim
end
 
options[:title] = false
opts.on('-t', '--title', 
'Indica se o ficheiro de dados tem header de título') do
  options[:title] = true
end
 
options[:settings] = nil
opts.on('-s', '--settings sf', 'Ficheiro de configurações') do |sf|
  options[:settings] = sf
end
...
if options[:settings] == nil
  puts('Tem que indicar o ficheiro a processar')
  usage()
  exit
end
 

Thursday, 22 May 2014

Logger Abstraction (PoC)

I've finally uploaded my logging macros to github, along with a sample program (you must have either Boost Log or Poco installed to run it). In this post, I'll detail its design.
 
My original requirements were prompted by a particular "use-case" - adding reusable classes to an application and allowing those classes to use the application's logging implementation, instead of whatever implementation was originally used. So, in this scenario, my requirements translate to: 1) The reusable classes must be used with no changes; and 2) The application developer should only need to create a header file with a list of well-defined macros that will invoke the app's logging implementation, thus causing the reusable classes to invoke that same implementation.
 
I've divided this into several header files, keeping in mind requirement #2. Let's take a look at these header files, then. 
 

Macro Overloading - macro_overload_base.h

This is the foundation of it all. It's a group of macros that allow overloading based on the number of arguments, up to a limit of 16.
 
My first design required the distinction between zero arguments, one argument, and more than one argument. However, the complexity of correctly detecting zero arguments was more than I was willing to accept, so I've worked around that requirement. Detecting invocations with one argument proved to be good enough, and the resulting macros, although still ugly, are a lot simpler.
 
I've been all over the web while searching for this, and I've been a bit beyond my knowledge quite some times (which was one of the reasons why I decided to work around the zero arguments requirement); These were my starting points, in case you're interested.
 
Detecting the number of arguments is up to the PCBASE__MOVERLOAD_SELECT_VALUE macro. In order to make it work, you must call it with the correct list of values. PCBASE__MOVERLOAD_ONE_ARG_OR_MORE calls it like this

#define PCBASE__MOVERLOAD_ONE_ARG_OR_MORE(...) \
    PCBASE__MOVERLOAD_SELECT_VALUE(__VA_ARGS__,\
    2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1)
 
because we just want to know if we have one argument or more than one argument.

OTOH, PCBASE__MOVERLOAD_VA_NUM_ARGS calls it like this
 
#define PCBASE__MOVERLOAD_VA_NUM_ARGS(...) \
    PCBASE__MOVERLOAD_SELECT_VALUE(__VA_ARGS__, \
    16,15,14,13,12,11,9,8,7,6,5,4,3,2,1)

because we need to know the exact number of arguments. PCBASE__MOVERLOAD_FOR_EACH does something similar, but passes as arguments the names of the macros that will allow to apply an action to each argument. E.g.,
   
PCBASE__MOVERLOAD_FOR_EACH(<<, "[", __FILE__, ":", __LINE__, "] ", 
    "Blimey! I didn't expect the Spanish Inquisition!", 
    chiefWeapons, mill.Trouble(), 42);
 
becomes
   
<< "[" << __FILE__ << ":" << __LINE__ << "] " 
    << "Blimey! I didn't expect the Spanish Inquisition!" 
    << chiefWeapons << mill.Trouble() << 42
 
You may have noticed the macro names look quite ugly. Since there's no namespace partitioning with macros, I've decided to make these names as ugly as I possibly can, to minimize name clashing.
 
Now, Let's move up one layer.
 

Logging Interface Type Abstraction - li_concat.h / li_outop.h

Here, we build on macro_overload_base.h to create the macros that will receive the logging arguments and call the user-supplied macros (USMs), which will, in turn, call the logging implementation.
 
There is an abstraction leak at this point. At some point in this "macro chain", we have to invoke the USMs. This means we have a contact point, similar to this: 
 
#define PCBASE__LAMOVERLOAD_LOG_IMPL_1(level, ...) \
    PCBLUESY__##level(__VA_ARGS__)
#define PCBASE__LAMOVERLOAD_LOG_IMPL_2(level, x, ...) \
    PCBLUESY__##level((x) PCBASE__MOVERLOAD_FOR_EACH(+, __VA_ARGS__))
 
On the left-hand side we have the names from the logging abstraction macros, on the right-hand side we have the USMs.
 
I considered three locations for this contact point:
  1. Place it in the logging abstraction headers, i.e., in li_concat.h and li_outop.h. I rejected this idea because it would require the user to edit an extra header file, thus going against requirement #2.
  2. Place it in the user-supplied header.
  3. Create a separate header just for these macros.
While I don't like the fact the we'll have references to "abstraction names" in the user-supplied header, it seemed the best alternative, so I went for alternative #2.
 
li_output.h is quite simple - for each argument, prepend a "<<" to it. li_concat.h is more complex, because we can't add a "+" to a single argument.
 
In fact, li_output.h is so simple it could almost be dispensed with; however I couldn't come up with a design clean enough without it. Besides, keeping it maintains the parallelism between the design for these two interfaces, concatenation and stream.
 
NOTE: Concatenation is working, but it doesn't actually respect requirement #1 (read why here), because it's not automatically converting its arguments to string.
 

Logging Interface Type Selection - log_interface_type.h

This is where we define the interface type, which can be one of:
  • Comma. Function call interface with several arguments, such as we would have in a printf()-like output function. I have not implemented this.
  • Stream output operator (operator<<).
  • Concatenation (using +). Function call interface with only one argument, created from the concatenation of several arguments. Since it's a binary operator, it requires a more complex implementation, because it needs to distinguish between two different cases: one argument, which must not involve any concatenation ("a" + is not a valid expression); and more than one argument, which must be concatenated.
This header file will also #include the correct header file for the interface type chosen. For now, it's a choice of either li_concat.h or li_outop.h. Both are described above.
 
We use this it by adding a #define to the project file/makefile, defining which interface type we want. E.g., on Qt Creator, to use the stream output operator, we'd do something like this on the .pro file:

DEFINES += "PCBLUESY__LOGINTERFACETYPE=3"

The default type (if we haven't #defined PCBLUESY__LOGINTERFACETYPE anywhere) is the stream output operator.

For now, I'll leave this header specific to the application, i.e., I'll have to create a new header for each application. I suspect this is not the best design, but I'll wait until I've used it a few times to see how it turns out.
 

Logging implementation - e.g., poco_log.h or boost_log.h

This is where we define the macros that will directly invoke the required functionality in the logging implementation (e.g., Poco or Boost).

The way I've set it up, we have a macro to get the logger (e.g., for Boost Log):

#define PCBLUESY__GETLOGGER(PCBLUESY_LOG_NAME) \
    src::severity_logger<boost::log::trivial::severity_level> \
    PCBLUESY__BOOSTLOG

Then, we have the top level logging macro, i.e., the macro that will get used in all the logging statements in the code:

#define PCBLUESY__LOG(level, ...) \
    PCBASE__LOG_ABSTRACTION(level, __VA_ARGS__)

These are the only two macros used in the application code. We then have the actual logging macros, i.e., the macros that call the logging implementation. Again, example for Boost Log:

#define PCBLUESY__PCBLUESY__TRACE(...) \
    BOOST_LOG_SEV(PCBLUESY__BOOSTLOG, \
    boost::log::trivial::severity_level::trace) __VA_ARGS__

Why the PCBLUESY__ repetition? This is the way the macro is used in the code:
   
PCBLUESY__LOG(PCBLUESY__ERROR, \
    "Blimey! I didn't expect the Spanish Inquisition ERROR!", \
    chiefWeapons, mill.Trouble(), 42);
 
I'm using PCBLUESY__ERROR as logging level in order to distinguish it from any other *ERROR* defined "out there". Since these names are defined in this header and won't be used anywhere else, I figured a little uglyness would be harmless, and could actually be useful.
 
Finally...
 
How do we use this? I've set up an example on my "SimpleSampleTuts" github. You'll need Boost Log and/or Poco C++ to see it in action. And if you use any other logging lib, provided it has an interface compatible with the ones discussed here, you should be able to create a header like poco_log.h/boost_log.h and start using it.
 
One final detail - initializing the logger. If you use a different implementation, you'll have to add that code, too.

Monday, 12 May 2014

Logging - The argument for the stream interface

Of all the things mentioned in my "resurface" post, I've been dedicating attention to my "logging macro front-end". To recap, the goal is to abstract the user code from the logging implementation used, thus allowing to substitute for another logging implementation with no changes to the code.
 
I want something like this:
 
LOG_FUNCTION_MACRO(ERROR_DEFINE_MACRO, 
    "Blimey! I didn't expect the Spanish Inquisition!", 
    chiefWeapons, mill.Trouble(), 42);

This is then "translated" into whatever interface the logging implementation provides for logging. So, with Boost Log, this could become something like this:
 
BOOST_LOG_SEV(someLog, boost::log::trivial::severity_level::error) 
    << __FILE__ << __LINE__ 
    << "Blimey! I didn't expect the Spanish Inquisition!" 
    << chiefWeapons << mill.Trouble() << 42;
 
Or, using Poco Logger's stream interface:
 
if (someLogRef.rdbuf()->logger().error())
    someLogRef.error() << __FILE__ << __LINE__
        << "Blimey! I didn't expect the Spanish Inquisition!" 
        << chiefWeapons << mill.Trouble() << 42 << endl;
else (void) 0;
 
Since I prefer a stream interface, that's where I began my work. Then, I moved on to what I call the "concatenation interface", where everything is concatenated into a single string. This was my first attempt, a few months ago, when I was using Poco Logger, but wasn't aware of Poco LogStream. The reason I settled on this was because I'm not a fan of the "format string" interface (i.e., printf()-like).
 
So, starting from our logging macro:
 
LOG_FUNCTION_MACRO(ERROR_DEFINE_MACRO, 
    "Blimey! I didn't expect the Spanish Inquisition!", 
    chiefWeapons, mill.Trouble(), 42);

we'd have something similar to this:
 
poco_error(someLogRef, __FILE__ + __LINE__ 
    + "Blimey! I didn't expect the Spanish Inquisition!" 
    + chiefWeapons + mill.Trouble() + 42);

And while I always preferred the stream interface, as I worked more on concatenation, I became aware it was not just a matter of preference; the stream interface is vastly superior. What do I mean by "superior"?
 
Let's look at this:
 
someStream << __FILE__ << __LINE__
    << "Blimey! I didn't expect the Spanish Inquisition!" 
    << chiefWeapons << mill.Trouble() << 42
 
__FILE__ and "Blimey! etc..." are char* (I'll ignore constness here), and work right out of the box. Ditto for __LINE__ and 42. So, our wildcards here are mill.Trouble() and chiefWeapons; for the sake of argument, let's assume mill.Trouble() returns a float and chiefWeapons is a container that defines its own operator<<(). This means close to 84% of our logging line works with no work required on our part. Our only additional work is defining operator<<() for whatever type chiefWeapons happens to be. And we probably would define it anyway, since output to a stream is always a handy feature, IMHO.
 
Now, let's look at this:

__FILE__ + __LINE__
    + "Blimey! I didn't expect the Spanish Inquisition!" 
    + chiefWeapons + mill.Trouble() + 42
  
This is supposed to be concatenation; concatenation assumes some string type. Since none of these arguments is a string type, we'd need to convert them. That's not difficult, but the question is - where would the conversion occur?

We don't want to place it in the original logging line, because we want it to be interface-agnostic. However, that's the only place where we know the type of each argument; we certainly couldn't place it in our macro mechanism, because macro parameters have no type.

We could create a family of template functions for this, and solve our problem through specialization - if we pass a string type, just return the string itself (yes, I'm ignoring the several string types in C++ libs and the necessity to copy those into a single type); if we pass a char*, build a string with it; if we pass an int, call to_string() (or similar); etc, meaning, every type we use would need a way to convert to string. And, while we might not require a template specialization for each type (e.g., we could use to_string() for int, long, or float), we'd be pretty close to that mark.

So, assuming this could be pulled off, with a proper design, it's still more work than taking advantage of a group of core types that have an already-functioning operator<<(), and only adding this to other types that require it.

Then, there is the question of performance - unless we use some mechanism like QstringBuilder, concatenation will be much more expensive than streaming output.
 
Finally, there is one last point that makes me prefer the stream interface, one that I've come upon as my logging usage became more "complex" - no conversions necessary. Conversions are one of the sources of problems in C/C++, and while we can mark them as explicit, I prefer sticking to a simple rule of defining no unnecessary conversions. If I only need a conversion to string when I'm outputting to log, then it is an unnecessary conversion.
 
So, where does this leaves my "logging macro front-end"? I'm going to finish my work and publish it with only the stream interface functional. The concatenation interface will be semi-functional, meaning no provision for conversion to string. I realize this is mostly self-defeating, since this means I probably won't use it. But I have two good options with operator<<() at the moment, so I'll stick to it, for the time being.
 

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.