CLI11 icon indicating copy to clipboard operation
CLI11 copied to clipboard

Requiring one Option or another, not both

Open gbitzes opened this issue 7 years ago • 22 comments
trafficstars

Hi, is there some way to require that one of { "--option1", "--option2" } is present, but not necessarily both? I looked at the documentation and examples, and couldn't figure out if this is possible. Do I need a custom callback?

By the way, excellent work on this library! Something like this was sorely needed in C++.

gbitzes avatar Mar 19 '18 13:03 gbitzes

Not really, the current way to do that would be to use a set, but that's a different syntax. What would a good api for that be? Maybe passing other options to the ->required() method?

henryiii avatar Mar 19 '18 17:03 henryiii

I was thinking of either having a variadic require_one method in App:

auto opt1 = app->add_option( ... );
auto opt2 = app->add_option( ... );

app->require_one(opt1, opt2, opt3);
// could also be app->require_at_least(2, opt1, opt2, opt3, opt4);

Or the ability to make a Group required, such as app->require_group("Some group");

The first seems more flexible (allows more complicated networks of requirements, and overlapping sets), but the second easily allows expressing this in the auto-generated help string for groups.

But then again, we could auto-detect if a group has an "at least N" requirement.. I'm leaning towards the first approach.

I'm not really sure.. not a big fan of a variadic ->required() though, the intent doesn't seem clear when reading the code, IMHO.

Python's argparse takes the second approach.

gbitzes avatar Mar 19 '18 18:03 gbitzes

Hi, I am not completely sure this is the same issue but quite an identical need so I comment this flow.

Here is the simple example:

#include <CLI/CLI.hpp>

int main(int argc, char **argv) {

	CLI::App app{"App to demonstrate a paradox."};

	std::vector<int> range;
	auto range_option = app.add_option("--range,-R", range, "A range")->required();

	int min, max, step = 1;
	app.add_option("--min,-m", min, "The minimum")->required()->excludes(range_option);
	app.add_option("--max,-M", max, "The maximum")->required()->excludes(range_option);
	app.add_option("--step,-s", step,  "The step", true)->excludes(range_option);

	CLI11_PARSE(app, argc, argv);
}

With "--range 1 2 3" argument, I get: "--min is required". With "--min 2 --max 3 --step 1" arguments, I get: "--range: At least 1 required" With "--min 2 --max 3 --step 1 --range 1 2 3" arguments, I get: "--range excludes --min"

A solution would be to remove the "required" flag but then the user may give nonsense. Is there a solution ?

Thank you.

OlivierHartmann avatar Oct 09 '18 12:10 OlivierHartmann

@henryiii, you marked this issue as closed and resolved in 1.5, but it looks like only the "not both" part is actually resolved. At least I was unable to find a way to "Require one option or another". I mean, as @OlivierHartmann shows above, you can require one AND another option to be required, but that doesn't work if they are mutually exclusive. How do you specify OR ? An example would come in handy. There is currently no example for excludes().

AlexanderAmelkin avatar Feb 14 '19 15:02 AlexanderAmelkin

This issue is still open, only part of it was resolved in 1.5, which is why it was mentioned in 1.5 but only the 1.5 release issue was closed.

I think the proper way to do this might be by expanding groups to be a full object, then allowing a group of options to just require 1 (or x) to pass. It's not in the immediate plans but a PR would be welcome, at least if well done. require_at_least is possible instead, but I'm not sure how that would internally be represented, groups might be easier/better/more transparent in the help.

henryiii avatar Feb 14 '19 15:02 henryiii

One possibility would be adding require_option functions just like require_subcommand functions to App class. Then if you wanted only one of a subset of options you could define those options in a "nameless" subcommand and specify the require_option as desired. I think the nameless subcommands could function as a group object like you mentioned already.

The app would count the number of options that had results after parsing just like it does for subcommands.

phlptp avatar Feb 14 '19 15:02 phlptp

That might work, actually. So the idea is just to require N options for an app (say, 1), and then nameless subcommands allow you to build groups. Not sure how it would look in the help, but it would be a simple addition.

henryiii avatar Feb 14 '19 15:02 henryiii

You would probably want a min and max just like for require_subcommand but it should be reasonably straightforward to implement.

phlptp avatar Feb 14 '19 15:02 phlptp

This "nameless subcommand" approach sounds to me like an ugly kludge. I would prefer the existing groups to become full-scale objects with properties. However, there definitely are cases where mutually exclusive option groups can exist within a logical options group. Then binding mutual-exclusiveness to a group wouldn't work unless groups would be able to have child groups.

AlexanderAmelkin avatar Feb 15 '19 15:02 AlexanderAmelkin

I think this issue can be closed. As best as I can tell this capability is in place now with the 1.8 release.

phlptp avatar Jun 11 '19 22:06 phlptp

I think this issue can be closed. As best as I can tell this capability is in place now with the 1.8 release.

so what do you have to use to require only one option to be provided? I can't find such info

sergio-eld avatar Feb 08 '21 16:02 sergio-eld

@ElDesalmado , same here, I am looking for this, and can't any information on how to implement this. Could anyone please point us to some example or explanation of this? Thanks!

lczech avatar Apr 04 '21 06:04 lczech

@ElDesalmado and @lczech you can have a look here as for one way of doing this here is an example :

    auto cpuOpt = app.add_flag("--cpu", isOnCpu, "Run the computation on the CPU")->ignore_case();
    app.add_flag("--gpu", isOnGpu, "Run the computation on the GPU")->ignore_case()->excludes(cpuOpt);

Now either --cpu can be used or `--gpu`` but not both at the same time!

Coderx7 avatar May 05 '21 05:05 Coderx7

@Coderx7 but there is no required option. Will it work when both are with required?

sergio-eld avatar May 05 '21 07:05 sergio-eld

Thanks for the suggestion, @Coderx7, but this is not what @ElDesalmado and I are looking for. Excluding other options has been part of CLI11 since its early days. What we are rather interested in is a way to say "exactly one of these options HAS to be provided by the user".

To make that an actual part of the CLI11 API might be tricky. I have not yet figured out what I would like the help message for that to even look like, for example...

The way I am currently solving this is by having a simple condition in the execution of the program that checks that exactly one of the options is set, and if not, throws an exception. Works well, but compared to build-in ways it does not show up in the help as "one of them has to be set" or the like. Additionally, this way, my program already has started (printing its header etc), instead of failing early on (as is the case with other types of user input errors that CLI11 takes care of).

lczech avatar May 05 '21 07:05 lczech

Just in case this has not been answered yet the way to do this is that the options are placed in an option group.

group=app.add_option_group("subgroup");
group->add_flag("--cpu", isOnCpu, "Run the computation on the CPU")->ignore_case();
    group->add_flag("--gpu", isOnGpu, "Run the computation on the GPU")->ignore_case();

group->require_option(1); 

The require_option(1) will state that 1 and only 1 option from the group is required. Any more or less is an error.

phlptp avatar Nov 25 '21 01:11 phlptp

Reopening this issue to be closed when additional documentation on this is added to the book

phlptp avatar Nov 25 '21 01:11 phlptp

@phlptp looks like a workaround, rather than an intended way. Though thank you

sergio-eld avatar Nov 25 '21 08:11 sergio-eld

Option groups are a way to cluster different flags and options together for logical or control purposes. It may seem like a workaround for this particular case, but if you wanted 1 of 5 options, or 2 of 5 options, or 1 option from a group of 3 and 1 option from another group of 5, all are similar with option groups, whereas trying to do it through another mechanism starts to look ugly fast.

phlptp avatar Nov 25 '21 14:11 phlptp

Just in case this has not been answered yet the way to do this is that the options are placed in an option group.

group=app.add_option_group("subgroup");
group->add_flag("--cpu", isOnCpu, "Run the computation on the CPU")->ignore_case();
    group->add_flag("--gpu", isOnGpu, "Run the computation on the GPU")->ignore_case();

group->require_option(1); 

The require_option(1) will state that 1 and only 1 option from the group is required. Any more or less is an error.

Hi @phlptp,

This solution creates a new group in the help message with new a new subtitle. Is it possible to maintain the original help format? (i.e., the one that I have without calling add_option_group)

ddiazdom avatar Mar 21 '22 13:03 ddiazdom

if you wanted 1 of 5 options, or 2 of 5 options, or 1 option from a group of 3 and 1 option from another group of 5

@phlptp what do you recommend for choosing between group A or group B? A and B are exclusive. If any option from group B is specified, then having an option from A is a mistake. Like std::variant<A, B>.

Either that, or option A vs group B (my group A has only 1 option).

battlmonstr avatar Apr 04 '23 13:04 battlmonstr