CLI11 icon indicating copy to clipboard operation
CLI11 copied to clipboard

Config subcommand validators are enforced even when the subcommand is not requested

Open eddyashton opened this issue 5 years ago • 3 comments

If I create a config for an app which contains subcommands, with default_also = true, the created config contains the default values of all options for all subcommands. When parsing this config, the Validators for this option are run, even if the subcommand is not triggered. This means the config may not be useable, as the Validators fail for options we weren't planning to use.

When writing the config, is it possible to only include the options on the active subcommand(s)? Or else to avoid parsing/processing options from config on inactive subcommand(s)?

Here's a small repro illustrating the problem, and why we have options with conflicting/unsatisfiable Validators - one subcommand requires that a file is not present, while another requires that it is present.

#include <CLI11/CLI11.hpp>
#include <fstream>
#include <iostream>

int main(int argc, char** argv)
{
  CLI::App app{"repro"};
  app.set_config("--config", "", "Read config", false);
  app.allow_config_extras(false);

  auto create = app.add_subcommand("create");
  create->configurable();
  std::string create_path = "./my_file.txt";
  create->add_option("--create-path", create_path, "A file which must not exist")
    ->capture_default_str()
    ->check(CLI::NonexistentPath);

  auto load = app.add_subcommand("load");
  load->configurable();
  std::string load_path = "./my_file.txt";
  load->add_option("--load-path", load_path, "A file which must exist")
    ->capture_default_str()
    ->check(CLI::ExistingPath);

  CLI11_PARSE(app, argc, argv);

  std::cout << "Reached end of main!" << std::endl;
  const auto config_file = "./config.ini";
  std::cout << "Writing config to " << config_file << std::endl;
  std::fstream fs;
  fs.open(config_file, std::fstream::out | std::fstream::trunc);
  fs << app.config_to_str(true, false);
  fs.close();

  return 0;
}
$ ./a.out 
 Reached end of main!
 Writing config to ./config.ini
$ cat ./config.ini 
 create.create-path="./my_file.txt"
 load.load-path="./my_file.txt"
$ ./a.out --config ./config.ini 
 --load-path: Path does not exist: ./my_file.txt
 Run with --help for more information.
$ touch ./my_file.txt
$ ./a.out --config ./config.ini 
 --create-path: Path already exists: ./my_file.txt
 Run with --help for more information.

eddyashton avatar Nov 25 '20 14:11 eddyashton

Let me look into this a little more. I thought there was a flag that could control this but I might have to dig a little as I don't remember it right now.

By default having a subcommand section in the config file causes it to trigger which is likely what is happening

phlptp avatar Nov 25 '20 14:11 phlptp

Can you try it with removing the lines

create->configurable();

and

load->configurable();

I believe those lines are instructing the config to consider those subcommands as configurable which means they will be executed if any section in the config file. with them out they update values but won't execute if not called from the command line

phlptp avatar Nov 25 '20 14:11 phlptp

Thanks for the rapid response.

Removing the configurable lines didn't help, the config.ini produced is identical and both validators are still run:

$ cat main.cpp 
...
  auto create = app.add_subcommand("create");
  //create->configurable();
  std::string create_path = "./my_file.txt";
  create->add_option("--create-path", create_path, "A file which must not exist")
    ->capture_default_str()
    ->check(CLI::NonexistentPath);

  auto load = app.add_subcommand("load");
  //load->configurable();
  std::string load_path = "./my_file.txt";
...

$ ./a.out 
 Reached end of main!
 Writing config to ./config.ini
$ cat ./config.ini 
 create.create-path="./my_file.txt"
 load.load-path="./my_file.txt"
$ ./a.out --config ./config.ini 
 --load-path: Path does not exist: ./my_file.txt
 Run with --help for more information.

The only difference matches what the docs describe - if I do specify a subcommand and it is configurable(), then it is added to the config. This functionality seems useful (re-triggering subcommands from config), though it can also trigger another parsing error as it puts the unrelated subcommand option under the subcommand's group (this is with configurable() enabled for both again):

$ ./a.out create
 Reached end of main!
 Writing config to ./config.ini
$ cat ./config.ini 
 [create]
 create-path="./my_file.txt"
 load.load-path="./my_file.txt"
$ ./a.out --config ./config.ini create
 INI was not able to parse create.load.load-path
 Run with --help for more information.

eddyashton avatar Nov 25 '20 14:11 eddyashton