Search code examples
c++boost-program-options

boost::program_options validation per argument instead of per argument type?


boost::program_options appears to support some level of custom validation but it seems odd to me that the validation is written in terms of types, and not per-argument, and I'm wondering if I'm missing something here.

For example, say you have a "file editing" program that takes input and output filenames from the command line. Next assume that you want to store these into variables of type boost::filesystem::path. Now, let's say we have a requirement that the input file must exist, but the output file doesn't (i.e. if the output file doesn't exist, we'll create it.) Ideally, we'd have a way to test that the input argument exists, and separately that the output file either exists and is writable or is in a directory that we can write to. (The specific differences here aren't actually relevant. This applies to any situation where you're using the same type in multiple places where you'd like to have different validation rules depending on the use.)

Because we configure validators by creating overrides of validate (which are presumably found at compile time by their type signatures, it appears we can only have one validator for all instances of boost::filesystem::path. I've seen the notify hook, but the signature of those callbacks has a const qualifier, so it doesn't seem like you could modify the value, and it's not clear in the documentation how throwing in a notify callback would get worked in to the validation system.

This seems like a pretty fundamental limitation to the point where I think I'm probably missing something. Am I? Thoughts?


Solution

  • It never hurts to have one type per concept in a program. If the two paths represent different concepts, give them their own types:

    #include <iostream>
    #include <cstdlib>
    #include <boost/program_options.hpp>
    #include <boost/filesystem.hpp>
    
    namespace po = boost::program_options;
    namespace fs = boost::filesystem;
    
    template<class Type, class Tag>
    struct tagged
    {
        Type const& get() const { return value_; }
        Type& get() { return value_; }
        operator Type& () { get(); }
    
        friend decltype(auto) operator>>(std::istream& is, tagged& t) {
            return is >> t.get();
        }
    
        friend decltype(auto) operator<<(std::ostream& os, tagged const& t) {
            return os << t.get();
        }
    
        // and so on...
    
        Type value_;
    };
    
    using bar_path = tagged<fs::path, class bar_path_tag>;
    using foo_path = tagged<fs::path, class foo_path_tag>;
    
    int main()
    {
        bar_path bar;
        foo_path foo;
    
        po::options_description desc("prog");
        desc.add_options()
        ("foo", po::value(&foo), "a foo path")
        ("bar", po::value(&bar), "a bar path")
        ;
    }