Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support repeating multi-argument options #128

Closed
seanfisk opened this issue May 19, 2018 · 12 comments
Closed

Support repeating multi-argument options #128

seanfisk opened this issue May 19, 2018 · 12 comments

Comments

@seanfisk
Copy link
Contributor

First of all, thank you for creating CLI11! It's much better than anything else out there. I've used Boost.Program_Options, which was ok, and QCommandLineParser, which was awful. CLI11 is a breath of fresh air and has saved me a lot of time, so, thank you!

I am looking to implement a command-line interface that allows a repeating option that takes 2 arguments. Here is an example Python program using argparse that implements it:

#!/usr/bin/env python3.6
# repeating_multi_arg_option.py

import argparse

parser = argparse.ArgumentParser(
    description='Repeating options that take multiple arguments')
parser.add_argument('--entry', nargs=2, metavar=('KEY', 'VALUE'),
                    action='append', help='key & value')
print(parser.parse_args())

Example runs:

$ ./repeating_multi_arg_option.py --entry key1 val1 --entry key2 val2
Namespace(entry=[['key1', 'val1'], ['key2', 'val2']])

$ ./repeating_multi_arg_option.py --entry
usage: repeating_multi_arg_option.py [-h] [--entry KEY VALUE]
repeating_multi_arg_option.py: error: argument --entry: expected 2 arguments

$ ./repeating_multi_arg_option.py --entry key1 --entry key2 val2
usage: repeating_multi_arg_option.py [-h] [--entry KEY VALUE]
repeating_multi_arg_option.py: error: argument --entry: expected 2 arguments

$ ./repeating_multi_arg_option.py --help
usage: repeating_multi_arg_option.py [-h] [--entry KEY VALUE]

Repeating options that take multiple arguments

optional arguments:
  -h, --help         show this help message and exit
  --entry KEY VALUE  key & value

It's almost possible with a combination of …->expected(2)->join(), but what I'm really looking for is a result that shows accurate error messages and preserves the nested structure (a list of lists).

If there is a path forward, I may also be able to help implement. Thank you!

@henryiii
Copy link
Collaborator

henryiii commented May 19, 2018

This was added in 1.5. See #92. You can now do:

...->set_custom_option("KEY VALUE", -2);

If you really want to go all out and capture the structure too, something like this should work:

#include<map>

std::map<std::string, std::string> options;
app.add_option("--entry",
               [&options](CLI::results_t vals){ // results_t is just a vector of strings
    for(size_t i=0; i<vals.size()/2; i++) // will always be a multiple of 2
        options[vals.at(i*2)] = vals.at(i*2+1);
    return true;
    },
               "key & value")
->set_custom_option("KEY VALUE", -2);

Or you could make a vector of pairs or something else.

DONE: make sure set_custom_option is mentioned in the README, as it's now a user-facing function (before 1.5 it wasn't).

@seanfisk
Copy link
Contributor Author

Thanks for the quick response! Sorry that I missed set_custom_option — thanks for adding it to the README.

I tried the example you provided, but I'm having a couple issues. First, set_custom_option doesn't return App*, so it can't be chained as in the example — no big deal; worked around that. Here is my current code:

#include <iostream>
#include <map>

#include <CLI/CLI.hpp>

int main(int argc, char *argv[]) {
	CLI::App app{"Repeating options that take multiple arguments"};

	std::map<std::string, std::string> entry_map;
	auto entry_option{
	    app.add_option("--entry",
	                   [&entry_map](CLI::results_t vals) { // results_t is just a vector of strings
		                   for (size_t i{0}; i < vals.size() / 2; ++i) // will always be a multiple of 2
			                   entry_map[vals.at(i * 2)] = vals.at(i * 2 + 1);
		                   return true;
	                   },
	                   "key & value")};
	entry_option->set_custom_option("KEY VALUE", 2);
	entry_option->expected(-1);

	CLI11_PARSE(app, argc, argv);

	for (const auto &entry : entry_map)
		std::cout << "Key: " << entry.first << "; Value: " << entry.second << std::endl;

	return 0;
}

Compiling and running this yields the following:

$ clang++ -o repeating_multi_arg_option -std=c++14 repeating_multi_arg_option.cpp && ./repeating_multi_arg_option --entry key val
libc++abi.dylib: terminating with uncaught exception of type CLI::IncorrectConstruction: --entry: You can only change the expected arguments for vectors

So it seems like expected() is only supported for vectors, not functions. Let me know if I've got something wrong; I'll keep working on it. Thanks!

@henryiii
Copy link
Collaborator

A) Can you make a PR? It should return this like all the other methods.
B) Sorry, my mistake, -2 means variable by twos. 2 means exactly two. N<-1 may not be as well tested, so feel free to make a test case PR for your use case. :)

@henryiii
Copy link
Collaborator

How about renaming? The set_ names are inconsistent, and set_typename is almost the same function. Both would become ->type_info(name, optional size).

@seanfisk
Copy link
Contributor Author

Sounds good to me 👍 I'm not sure how much time I'll have to work on it this week, but I'll start by setting up a development environment.

Regarding N<-1, the README currently states:

->expected(N): Take N values instead of as many as possible, only for vector args. If negative, require at least -N; end with -- or another recognized option.

I interpreted this as "the option takes a minimum of -N arguments." So for ->expected(-2), --entry e1 e2 -- would be valid as well as --entry e1 e2 e3 --.

Is that an incorrect interpretation, or are you suggesting changing that behavior to require multiples of -N? I'm ok either way; just trying to understand the desired implementation.

Thank you for your input on this!

@henryiii
Copy link
Collaborator

henryiii commented May 21, 2018

That is the correct interpretation. The type size is separate from the expected value. So, for example:

...->set_custom_option("A B", /* type size*/ -2)->expected(-2)

Would mean you have to enter A B twice or more.

A B # Invalid
A B A B # Valid
A B A B A # Invalid
A B A B A B #Valid 

This is already how it works (except for the broken this return), I'm just thinking that renaming this to type_info would be helpful. We currently have a set_typename that is almost (and should be exactly) the same as not including the size argument here.

By the way, setting a negative type size automatically sets expected to -1.

@henryiii
Copy link
Collaborator

henryiii commented May 21, 2018

This is what we have now:

->set_custom_option("KEY VALUE", -2); // Using -> broken

These are options:

->type_info("KEY VALUE", -2);

Or

->type_name("KEY VALUE")->type_size(-2);

Since you probably always want to set the type name if you set the type size, that's why I was thinking of using one method instead of two. Maybe the other way is clearer, though.

@seanfisk
Copy link
Contributor Author

Ok, I think I understand how it is supposed to work now. Thank you for the explanation!

I think the single method type_info is a good choice here — I can work on implementing that. Should the previous methods be retained for backward compatibility?

@henryiii
Copy link
Collaborator

henryiii commented May 21, 2018

Yes, for a release, at least for set_type_name. set_custom_option probably can just be removed, since it wasn't really documented until a few days ago. We can use CLI11_DEPRECATED, like this:

/// Sets required options \deprecated
CLI11_DEPRECATED("Use needs instead of requires (eventual keyword clash)")
Option *requires(Option *opt) { return needs(opt); }

@henryiii
Copy link
Collaborator

What's the ETA on this? Looking at a 1.6 release soon.

@henryiii henryiii mentioned this issue Jun 16, 2018
@henryiii
Copy link
Collaborator

That ought to fix it, let me know if it doesn't work!

@seanfisk
Copy link
Contributor Author

@henryiii Thank you! I'm sorry for not responding earlier —we have had some changes at work and I haven't had as much time to work on other projects (such as this). I will let you know how it goes when I try it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants