`force_callback` prevents subcommands with excludes from running
While adding a new subcommand to our app and using excludes to setup mutual exclusivity with the other subcommands, we found the subcommands refuse to run, even when only one was specified at command line.
This seems to happen when force_callback is used, which sets a variable on the subcommand before the exclusion tests are performed.
Here is a minimal app to reproduce the issue. subcommand_1 has a forced callback, and prevents subcommand_2 to run, despite no parameters being passed for subcommand_1.
#include <iostream>
#include "CLI11.hpp" // v2.4.0
int main(int argc, char** argv)
{
CLI::App app{"CLI11-excludes-bug"};
auto subcommand_1 = app.add_subcommand("subcommand_1", "subcommand 1");
subcommand_1
->add_flag_function(
"-f",
[](bool f) {
std::cout << "subcommand_1 callback " << f << std::endl;
},
"subcommand_1 flag")
->force_callback();
auto subcommand_2 = app.add_subcommand("subcommand_2", "subcommand 2");
subcommand_1->excludes(subcommand_2);
CLI11_PARSE(app, argc, argv);
return 0;
}
Expected output:
>CLI11-excludes-bug.exe subcommand_2
subcomand_1 callback 0
[Exit Code 0]
Actual output:
>CLI11-excludes-bug.exe subcommand_2
subcommand_1 callback 0
subcommand_2 excludes subcommand_1
[Exit Code 108]
Basically, CLI11 is processing the forced callback for subcommand_1 first, which is setting the flag. Then, the requirements for subcommand_2 are processed; when it reaches the exclusion for subcommand_1, it sees a variable is set, and believes it is a conflict.
https://github.com/CLIUtils/CLI11/blob/fd483ea006946245fb1b0618cf271ca5893be11b/include/CLI/impl/App_inl.hpp#L1188-L1193
I don't know the best way of fixing this in the library, other than perhaps marking the variable as being set by a forced callback (and not explicitly set by the user/program otherwise), and ignore those when counting the variables only for the exclusion check?
Our workaround has involved removing the flag lambda entirely, and putting the logic into app.parse_complete_callback() instead. This works, but decouples the flag logic a bit.
I will be looking at this in the near future
So apparently this was resolved by #1060, and didn't know it. Was adding a test and couldn't get it to fail. So dug in a bit more and some of the changes for handling round trips on config files, found by the fuzzer, resolved this issue as well.
I added this test
TEST_CASE_METHOD(TApp, "ForcedSubcommandExclude", "[subcom]") {
auto *subcommand_1 = app.add_subcommand("sub_1");
std::string forced;
subcommand_1
->add_flag_function(
"-f",
[&forced](bool f) {
forced=f?"got true":"got false";
})
->force_callback();
auto subcommand_2 = app.add_subcommand("sub2");
subcommand_1->excludes(subcommand_2);
args={"sub2"};
CHECK_NOTHROW(run());
CHECK(forced=="got false");
}