adishavit / argh

Argh! A minimalist argument handler.
BSD 3-Clause "New" or "Revised" License
1.32k stars 94 forks source link

Consider adding named accessor methods #11

Open adishavit opened 7 years ago

adishavit commented 7 years ago

There has been a request to add named accessor methods in addition to operator[] and operator(). See Twitter thread by @azadkuh here. Though simple to do, there are several subtle aspects that need to be addressed. I will use this issue to contemplate, document and discuss them.

I have an (as yet non-public) experimental branch with this, but I'm not yet sure about this. (cc: @arnemertz)

azadkuh commented 7 years ago

following our discussion in twitter:

// also like std or Qt containers std::string at(size_t index) const; std::string at(std::string const& name) const; // or template T value(std::string const& name, T&& default_value) const;


- add `is_set()`
to check if a flag is set, this method seems to be a better choice than overloading `operator[]`:
```cpp
bool is_set(std::string const& name) const;
bool is_set(std::initializer_list<char const* const> init_list) const;

Qt QCommandLineParser has also a similar method.

if you don't like to break the API of the argh::parser, add named functions in the first place and update your README and examples, then flag operators as deprecated.

p.s GSL has also some tips for operators.

adishavit commented 7 years ago

I've already fixed const correctness. Need to push it. Re: the rest will reply soon.

adishavit commented 7 years ago

Argh recognizes and demuxes the command line into, 3 different and independent argument types:

  1. Positional,
  2. Flag (boolean),
  3. Parameter (Name + Value)

Each corresponds to a conceptually different container with different requirements:

  1. Positional: a std::vector<std::string>
    • Sequential and accessed by index.
    • Returns input string
    • Provide convertibility to T
  2. Flag: std::set<std::string> or a std::map<std::string,bool>
    • Can only be checked for existence (the string itself is meaningless, the user knows it)
    • (Support alternate names)
  3. Parameter: std::map<std::string,std::string>
    • Checkable for existence
    • Returns value input string (param name is known to user)
    • Provide convertibility to T
    • (Provide optional default)
    • (Support alternate names)

Design Rationale

Argh supports these multiple intersecting requirements with a minimal set of (conceptually) 3 functions (if we ignore alt-names and defaults for a moment):

So:

  1. Access by [int] means get positional string, like argv[int].
  2. Access by [string] means Flag, and thus, bool is the only return value that makes sense. Like std::map<std::string,bool>::operator[] it would return bool.
  3. Access by (string) means Parameter, and thus, a map lookup. However, returning the value as a string, makes both bool and T conversion very verbose on the user side. So, yes, this is a departure from the STL-like access patterns.

Also:

Design Considerations

Argh's design optimizes for:

[These may not be a comprehensive list but will do for now]

Given these considerations, any additional named functions should retain the exiting principles.

So your suggested function at() for checking flags, what is the value you want to return? bool is a valid answer - so you get 2 overloads just like []. If you return a std::string then the user never needs its value and always needs to check .empty() which is unclear and verbose. Additionally, you would need an unambiguous way to check the existence of a named param. if(cmdl.at(name).empty()) is ambiguous (unclear of flag or param), and if(cmdl.to_stream(name)) does not read clearly.

If we need a named method, I am leaning towards has[] and has() for flags and parameters respectively (the name is from OpenCV's cli class). In fact, .has would be just decoration for readability and can be removed without changing the meaning of the code: if (cmdl["v"])... would be equivalent to: if (cmdl.has["v"])... I guess they could also be renamed to has_flag() and has_param().

Regarding to_stream() although that does not change anything, I'm not sure I see how it makes the code significantly more readable. Compare:

if (cmdl.to_stream(7))          // no sense here
   cmdl.to_stream(7) >> fname;  // Maybe, though it feels like the (7) is in the wrong place
cmdl.to_stream("t",128) >> threshold;

to:

if (cmdl(7))
   cmdl(7) >> fname; 
cmdl("t",128) >> threshold;

This is much shorter, and IMO just as clear if nor clearer. But this is a subjective matter though. I guess one needs to get used to the grammar of the lib.

One thing you mentioned is that it isn't possible to initialize a local variable with a non string cli value. You need to declare it first before you can stream into it. This bugged me because it goes against the principle of lowest-possible-impact on user code.

So, I am experimenting with a templated as<>() method (a-la Boost.Program_Options) to do this. The main problem here is that the internal streaming may fail and how to report this error.

I'm still mulling this over.

Finally, I also like the minimalism of: has and as.

azadkuh commented 7 years ago

your design rationale about three categories of flags, positional and parameters looks familiar and comprehensible for every command line user!

you've already classified them as:

// parser:
std::multiset<std::string> const&          flags();
std::map<std::string, std::string> const&  params();
std::vector<std::string> const&            pos_args() const;

So your suggested function at() for checking flags, what is the value you want to return?

well, i didn't. i expect at() / operator[] to behave as std::vector::at(), std::map::at, ... is_set() and has() (as you suggested) are better options to check against flags.

imho:

minimal changes

with minimal changes (by separating flags from operator[]/at()) the current design:

// operator[] overloads for pos_args / flags
auto action = cmdl[2]; // also may return an empty string
if (action.empty()) {}
if (cmdl["amend"]) {}

// operator() overloads for pos_args / params. may return bad_stream
auto stream = cmdl(2); // stream for positional arg
auto stream = cmdl("username"); // stream for parameter

turns into:

// operator[] or at() overloads for pos_args / params
auto action = cmdl[2];
auto user   = cmdl["username"];

if (cmdl.is_set("amend")) {}

there should be minimal surprise for argh users if they know stl or even Qt containers, and i guess it's not that ambiguous :)

hiding (or deprecating) operator() and providing a named function also helps, cmdl[]/cmdl() is the real ambiguity for someone who is not familiar with argh or code reviewers.

has_xxx()

i really like your idea about has_flag() and has_param(), eps the latter one that saves one single check against std::map::find() or ...

return values

the current design may also return an empty string or a bad stream (and i'm not against it). the user is supposed to check the return values:

// current design:

// given 2 positional args:
auto action = cmdl[7]; // action is empty

// given no argument as port:
uint16_t port{0};
cmdl("port") >> port; // a bad_stream, port is not set

exception safety

as<>()

it is short and looks nice, but value<>() is just 3 characters longer and explicitly shows the intent :))