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

Config short #443

Merged
merged 9 commits into from
Mar 22, 2020
Merged
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,7 @@ sub.subcommand = true
Spaces before and after the name and argument are ignored. Multiple arguments are separated by spaces. One set of quotes will be removed, preserving spaces (the same way the command line works). Boolean options can be `true`, `on`, `1`, `yes`, `enable`; or `false`, `off`, `0`, `no`, `disable` (case insensitive). Sections (and `.` separated names) are treated as subcommands (note: this does not necessarily mean that subcommand was passed, it just sets the "defaults"). You cannot set positional-only arguments. 🆕 Subcommands can be triggered from configuration files if the `configurable` flag was set on the subcommand. Then the use of `[subcommand]` notation will trigger a subcommand and cause it to act as if it were on the command line.

To print a configuration file from the passed
arguments, use `.config_to_str(default_also=false, prefix="", write_description=false)`, where `default_also` will also show any defaulted arguments, `prefix` will add a prefix, and `write_description` will include option descriptions. See [Config files](https://cliutils.github.io/CLI11/book/chapters/config.html) for some additional details.
arguments, use `.config_to_str(default_also=false, write_description=false)`, where `default_also` will also show any defaulted arguments, and `write_description` will include the app and option descriptions. See [Config files](https://cliutils.github.io/CLI11/book/chapters/config.html) for some additional details.

### Inheriting defaults

Expand Down
36 changes: 30 additions & 6 deletions book/chapters/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,27 @@ The main differences are in vector notation and comment character. Note: CLI11

## Writing out a configure file

To print a configuration file from the passed arguments, use `.config_to_str(default_also=false, prefix="", write_description=false)`, where `default_also` will also show any defaulted arguments, `prefix` will add a prefix, and `write_description` will include option descriptions.
To print a configuration file from the passed arguments, use `.config_to_str(default_also=false, write_description=false)`, where `default_also` will also show any defaulted arguments, and `write_description` will include option descriptions and the App description

```cpp

CLI::App app;
app.add_option(...);
// several other options
CLI11_PARSE(app, argc, argv);
//the config printout should be after the parse to capture the given arguments
std::cout<<app.config_to_str(true,true);
```

if a prefix is needed to print before the options, for example to print a config for just a subcommand, the config formatter can be obtained directly.

```cpp

auto fmtr=app.get_config_formatter();
//std::string to_config(const App *app, bool default_also, bool write_description, std::string prefix)
fmtr->to_config(&app,true,true,"sub.");
//prefix can be used to set a prefix before each argument, like "sub."
```

### Customization of configure file output
The default config parser/generator has some customization points that allow variations on the TOML format. The default formatter has a base configuration that matches the TOML format. It defines 5 characters that define how different aspects of the configuration are handled
Expand Down Expand Up @@ -115,14 +135,10 @@ The default configuration file will read INI files, but will write out files in
```cpp
app.config_formatter(std::make_shared<CLI::ConfigINI>());
```
which makes use of a predefined modification of the ConfigBase class which TOML also uses.
which makes use of a predefined modification of the ConfigBase class which TOML also uses. If a custom formatter is used that is not inheriting from the from ConfigBase class `get_config_formatter_base() will return a nullptr, so some care must be exercised in its us with custom configurations.

## Custom formats

{% hint style='info' %}
New in CLI11 1.6
{% endhint %}

You can invent a custom format and set that instead of the default INI formatter. You need to inherit from `CLI::Config` and implement the following two functions:

```cpp
Expand All @@ -145,3 +161,11 @@ See [`examples/json.cpp`](https://github.com/CLIUtils/CLI11/blob/master/examples
Configuration files can be used to trigger subcommands if a subcommand is set to configure. By default configuration file just set the default values of a subcommand. But if the `configure()` option is set on a subcommand then the if the subcommand is utilized via a `[subname]` block in the configuration file it will act as if it were called from the command line. Subsubcommands can be triggered via [subname.subsubname]. Using the `[[subname]]` will be as if the subcommand were triggered multiple times from the command line. This functionality can allow the configuration file to act as a scripting file.

For custom configuration files this behavior can be triggered by specifying the parent subcommands in the structure and `++` as the name to open a new subcommand scope and `--` to close it. These names trigger the different callbacks of configurable subcommands.

## Implementation Notes
The config file input works with any form of the option given: Long, short, positional, or the environment variable name. When generating a config file it will create a name in following priority.

1. First long name
1. Positional name
1. First short name
1. Environment name
10 changes: 9 additions & 1 deletion include/CLI/App.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1491,7 +1491,7 @@ class App {
return this;
}
/// Produce a string that could be read in as a config of the current values of the App. Set default_also to
/// include default arguments. Prefix will add a string to the beginning of each option.
/// include default arguments. write_descriptions will print a description for the App and for each option.
std::string config_to_str(bool default_also = false, bool write_description = false) const {
return config_formatter_->to_config(this, default_also, write_description, "");
}
Expand Down Expand Up @@ -2335,6 +2335,14 @@ class App {
return true;
}
Option *op = get_option_no_throw("--" + item.name);
if(op == nullptr) {
if(item.name.size() == 1) {
op = get_option_no_throw("-" + item.name);
}
}
if(op == nullptr) {
op = get_option_no_throw(item.name);
}
if(op == nullptr) {
// If the option was not present
if(get_allow_config_extras() == config_extras_mode::capture)
Expand Down
6 changes: 3 additions & 3 deletions include/CLI/Config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -282,14 +282,14 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description,
}
for(const Option *opt : app->get_options({})) {

// Only process option with a long-name and configurable
if(!opt->get_lnames().empty() && opt->get_configurable()) {
// Only process options that are configurable
if(opt->get_configurable()) {
if(opt->get_group() != group) {
if(!(group == "Options" && opt->get_group().empty())) {
continue;
}
}
std::string name = prefix + opt->get_lnames()[0];
std::string name = prefix + opt->get_single_name();
std::string value = detail::ini_join(opt->reduced_results(), arraySeparator, arrayStart, arrayEnd);

if(value.empty() && default_also) {
Expand Down
46 changes: 34 additions & 12 deletions include/CLI/Option.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ class Option : public OptionBase<Option> {
/// @name Other
///@{

/// Remember the parent app
/// link back up to the parent App for fallthrough
App *parent_{nullptr};

/// Options store a callback to do all the work
Expand Down Expand Up @@ -681,7 +681,19 @@ class Option : public OptionBase<Option> {

/// Get the flag names with specified default values
const std::vector<std::string> &get_fnames() const { return fnames_; }

/// Get a single name for the option, first of lname, pname, sname, envname
const std::string &get_single_name() const {
if(!lnames_.empty()) {
return lnames_[0];
}
if(!pname_.empty()) {
return pname_;
}
if(!snames_.empty()) {
return snames_[0];
}
return envname_;
}
/// The number of times the option expects to be included
int get_expected() const { return expected_min_; }

Expand Down Expand Up @@ -836,23 +848,33 @@ class Option : public OptionBase<Option> {
bool operator==(const Option &other) const { return !matching_name(other).empty(); }

/// Check a name. Requires "-" or "--" for short / long, supports positional name
bool check_name(std::string name) const {
bool check_name(const std::string &name) const {

if(name.length() > 2 && name[0] == '-' && name[1] == '-')
return check_lname(name.substr(2));
if(name.length() > 1 && name.front() == '-')
return check_sname(name.substr(1));

std::string local_pname = pname_;
if(ignore_underscore_) {
local_pname = detail::remove_underscore(local_pname);
name = detail::remove_underscore(name);
if(!pname_.empty()) {
std::string local_pname = pname_;
std::string local_name = name;
if(ignore_underscore_) {
local_pname = detail::remove_underscore(local_pname);
local_name = detail::remove_underscore(local_name);
}
if(ignore_case_) {
local_pname = detail::to_lower(local_pname);
local_name = detail::to_lower(local_name);
}
if(local_name == local_pname) {
return true;
}
}
if(ignore_case_) {
local_pname = detail::to_lower(local_pname);
name = detail::to_lower(name);

if(!envname_.empty()) {
// this needs to be the original since envname_ shouldn't match on case insensitivity
return (name == envname_);
}
return name == local_pname;
return false;
}

/// Requires "-" to be removed from string
Expand Down
20 changes: 20 additions & 0 deletions tests/AppTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2131,6 +2131,26 @@ TEST_F(TApp, Env) {
EXPECT_THROW(run(), CLI::RequiredError);
}

// curiously check if an environmental only option works
TEST_F(TApp, EnvOnly) {

put_env("CLI11_TEST_ENV_TMP", "2");

int val{1};
CLI::Option *vopt = app.add_option("", val)->envname("CLI11_TEST_ENV_TMP");

run();

EXPECT_EQ(2, val);
EXPECT_EQ(1u, vopt->count());

vopt->required();
run();

unset_env("CLI11_TEST_ENV_TMP");
EXPECT_THROW(run(), CLI::RequiredError);
}

TEST_F(TApp, RangeInt) {
int x{0};
app.add_option("--one", x)->check(CLI::Range(3, 6));
Expand Down
94 changes: 94 additions & 0 deletions tests/ConfigFileTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,57 @@ TEST_F(TApp, IniFlagDual) {
EXPECT_THROW(run(), CLI::ConversionError);
}

TEST_F(TApp, IniShort) {

TempFile tmpini{"TestIniTmp.ini"};

int key{0};
app.add_option("--flag,-f", key);
app.set_config("--config", tmpini);

{
std::ofstream out{tmpini};
out << "f=3" << std::endl;
}

ASSERT_NO_THROW(run());
EXPECT_EQ(key, 3);
}

TEST_F(TApp, IniPositional) {

TempFile tmpini{"TestIniTmp.ini"};

int key{0};
app.add_option("key", key);
app.set_config("--config", tmpini);

{
std::ofstream out{tmpini};
out << "key=3" << std::endl;
}

ASSERT_NO_THROW(run());
EXPECT_EQ(key, 3);
}

TEST_F(TApp, IniEnvironmental) {

TempFile tmpini{"TestIniTmp.ini"};

int key{0};
app.add_option("key", key)->envname("CLI11_TEST_ENV_KEY_TMP");
app.set_config("--config", tmpini);

{
std::ofstream out{tmpini};
out << "CLI11_TEST_ENV_KEY_TMP=3" << std::endl;
}

ASSERT_NO_THROW(run());
EXPECT_EQ(key, 3);
}

TEST_F(TApp, IniFlagText) {

TempFile tmpini{"TestIniTmp.ini"};
Expand Down Expand Up @@ -1376,6 +1427,49 @@ TEST_F(TApp, TomlOutputSimple) {
EXPECT_EQ("simple=3\n", str);
}

TEST_F(TApp, TomlOutputShort) {

int v{0};
app.add_option("-s", v);

args = {"-s3"};

run();

std::string str = app.config_to_str();
EXPECT_EQ("s=3\n", str);
}

TEST_F(TApp, TomlOutputPositional) {

int v{0};
app.add_option("pos", v);

args = {"3"};

run();

std::string str = app.config_to_str();
EXPECT_EQ("pos=3\n", str);
}

// try the output with environmental only arguments
TEST_F(TApp, TomlOutputEnvironmental) {

put_env("CLI11_TEST_ENV_TMP", "2");

int val{1};
app.add_option(std::string{}, val)->envname("CLI11_TEST_ENV_TMP");

run();

EXPECT_EQ(2, val);
std::string str = app.config_to_str();
EXPECT_EQ("CLI11_TEST_ENV_TMP=2\n", str);

unset_env("CLI11_TEST_ENV_TMP");
}

TEST_F(TApp, TomlOutputNoConfigurable) {

int v1{0}, v2{0};
Expand Down