From a83109002c00ef51f0cc99aefbf1d9ef935a92a3 Mon Sep 17 00:00:00 2001
From: Philip Top
Date: Thu, 3 Jan 2019 00:22:09 -0800
Subject: [PATCH] Add ignore underscore (#185)
* add ignore_underscore test cases and options to app
* add ignore_underscore for add_sets and some more tests for the sets and subcommands
* add some documentation lines and some failing tests
* update readme with ignore_underscore option
* remove failing tests from known issue
* remove empty line for code coverage
---
README.md | 17 ++-
include/CLI/App.hpp | 275 ++++++++++++++++++++++++++++++++++--
include/CLI/Option.hpp | 52 ++++++-
include/CLI/StringTools.hpp | 6 +
tests/AppTest.cpp | 109 ++++++++++++++
tests/CreationTest.cpp | 107 ++++++++++++++
tests/SubcommandTest.cpp | 37 +++++
7 files changed, 586 insertions(+), 17 deletions(-)
diff --git a/README.md b/README.md
index c03b5e90..5c0093dc 100644
--- a/README.md
+++ b/README.md
@@ -162,7 +162,7 @@ try {
}
```
-The try/catch block ensures that `-h,--help` or a parse error will exit with the correct return code (selected from `CLI::ExitCodes`). (The return here should be inside `main`). You should not assume that the option values have been set inside the catch block; for example, help flags intentionally short-circuit all other processing for speed and to ensure required options and the like do not interfere.
+The try/catch block ensures that `-h,--help` or a parse error will exit with the correct return code (selected from `CLI::ExitCodes`). (The return here should be inside `main`). You should not assume that the option values have been set inside the catch block; for example, help flags intentionally short-circuit all other processing for speed and to ensure required options and the like do not interfere.
@@ -197,6 +197,10 @@ app.add_set(option_name,
app.add_set_ignore_case(... // String only
+app.add_set_ignore_underscore(... // String only
+
+app.add_set_ignore_case_underscore(... // String only
+
App* subcom = app.add_subcommand(name, description);
```
@@ -212,7 +216,7 @@ On a compiler that supports C++17's `__has_include`, you can also use `std::opti
- `"this"` Can only be passed positionally
- `"-a,-b,-c"` No limit to the number of non-positional option names
-The add commands return a pointer to an internally stored `Option`. If you set the final argument to true, the default value is captured and printed on the command line with the help flag. This option can be used directly to check for the count (`->count()`) after parsing to avoid a string based lookup.
+The add commands return a pointer to an internally stored `Option`. If you set the final argument to true, the default value is captured and printed on the command line with the help flag. This option can be used directly to check for the count (`->count()`) after parsing to avoid a string based lookup.
#### Option options
@@ -227,6 +231,7 @@ Before parsing, you can set the following options:
- `->envname(name)`: Gets the value from the environment if present and not passed on the command line.
- `->group(name)`: The help group to put the option in. No effect for positional options. Defaults to `"Options"`. `""` will not show up in the help print (hidden).
- `->ignore_case()`: Ignore the case on the command line (also works on subcommands, does not affect arguments).
+- `->ignore_underscore()`: Ignore any underscores in the options names (also works on subcommands, does not affect arguments). For example "option_one" will match with optionone. This does not apply to short form options since they only have one character
- `->multi_option_policy(CLI::MultiOptionPolicy::Throw)`: Set the multi-option policy. Shortcuts available: `->take_last()`, `->take_first()`, and `->join()`. This will only affect options expecting 1 argument or bool flags (which always default to take last).
- `->check(CLI::ExistingFile)`: Requires that the file exists if given.
- `->check(CLI::ExistingDirectory)`: Requires that the directory exists.
@@ -279,6 +284,8 @@ Multiple subcommands are allowed, to allow [`Click`][click] like series of comma
There are several options that are supported on the main app and subcommands. These are:
- `.ignore_case()`: Ignore the case of this subcommand. Inherited by added subcommands, so is usually used on the main `App`.
+- `.ignore_underscore()`: Ignore any underscores in the subcommand name. Inherited by added subcommands, so is usually used on the main `App`.
+
- `.fallthrough()`: Allow extra unmatched options and positionals to "fall through" and be matched on a parent command. Subcommands always are allowed to fall through.
- `.require_subcommand()`: Require 1 or more subcommands.
- `.require_subcommand(N)`: Require `N` subcommands if `N>0`, or up to `N` if `N<0`. `N=0` resets to the default 0 or more.
@@ -340,7 +347,7 @@ arguments, use `.config_to_str(default_also=false, prefix="", write_description=
Many of the defaults for subcommands and even options are inherited from their creators. The inherited default values for subcommands are `allow_extras`, `prefix_command`, `ignore_case`, `fallthrough`, `group`, `footer`, and maximum number of required subcommands. The help flag existence, name, and description are inherited, as well.
-Options have defaults for `group`, `required`, `multi_option_policy`, and `ignore_case`. To set these defaults, you should set the `option_defaults()` object, for example:
+Options have defaults for `group`, `required`, `multi_option_policy`, `ignore_underscore`, and `ignore_case`. To set these defaults, you should set the `option_defaults()` object, for example:
```cpp
app.option_defaults()->required();
@@ -351,7 +358,7 @@ The default settings for options are inherited to subcommands, as well.
### Formatting
-The job of formatting help printouts is delegated to a formatter callable object on Apps and Options. You are free to replace either formatter by calling `formatter(fmt)` on an `App`, where fmt is any copyable callable with the correct signature.
+The job of formatting help printouts is delegated to a formatter callable object on Apps and Options. You are free to replace either formatter by calling `formatter(fmt)` on an `App`, where fmt is any copyable callable with the correct signature.
CLI11 comes with a default App formatter functional, `Formatter`. It is customizable; you can set `label(key, value)` to replace the default labels like `REQUIRED`, and `column_width(n)` to set the width of the columns before you add the functional to the app or option. You can also override almost any stage of the formatting process in a subclass of either formatter. If you want to make a new formatter from scratch, you can do
that too; you just need to implement the correct signature. The first argument is a const pointer to the in question. The formatter will get a `std::string` usage name as the second option, and a `AppFormatMode` mode for the final option. It should return a `std::string`.
@@ -387,7 +394,7 @@ app.add_option("--fancy-count", [](std::vector val){
### Utilities
-There are a few other utilities that are often useful in CLI programming. These are in separate headers, and do not appear in `CLI11.hpp`, but are completely independent and can be used as needed. The `Timer`/`AutoTimer` class allows you to easily time a block of code, with custom print output.
+There are a few other utilities that are often useful in CLI programming. These are in separate headers, and do not appear in `CLI11.hpp`, but are completely independent and can be used as needed. The `Timer`/`AutoTimer` class allows you to easily time a block of code, with custom print output.
```cpp
{
diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp
index d014fab9..42d3bf88 100644
--- a/include/CLI/App.hpp
+++ b/include/CLI/App.hpp
@@ -139,6 +139,9 @@ class App {
/// If true, the program name is not case sensitive INHERITABLE
bool ignore_case_{false};
+ /// If true, the program should ignore underscores INHERITABLE
+ bool ignore_underscore_{false};
+
/// Allow subcommand fallthrough, so that parent commands can collect commands after subcommand. INHERITABLE
bool fallthrough_{false};
@@ -195,6 +198,7 @@ class App {
allow_config_extras_ = parent_->allow_config_extras_;
prefix_command_ = parent_->prefix_command_;
ignore_case_ = parent_->ignore_case_;
+ ignore_underscore_ = parent_->ignore_underscore_;
fallthrough_ = parent_->fallthrough_;
group_ = parent_->group_;
footer_ = parent_->footer_;
@@ -265,6 +269,18 @@ class App {
return this;
}
+ /// Ignore underscore. Subcommand inherit value.
+ App *ignore_underscore(bool value = true) {
+ ignore_underscore_ = value;
+ if(parent_ != nullptr) {
+ for(const auto &subc : parent_->subcommands_) {
+ if(subc.get() != this && (this->check_name(subc->name_) || subc->check_name(this->name_)))
+ throw OptionAlreadyAdded(subc->name_);
+ }
+ }
+ return this;
+ }
+
/// Set the help formatter
App *formatter(std::shared_ptr fmt) {
formatter_ = fmt;
@@ -510,7 +526,7 @@ class App {
}
#endif
- /// Add set of options (No default, temp refernce, such as an inline set)
+ /// Add set of options (No default, temp reference, such as an inline set)
template
Option *add_set(std::string name,
T &member, ///< The selected member of the set
@@ -532,7 +548,7 @@ class App {
return opt;
}
- /// Add set of options (No default, non-temp refernce, such as an existing set)
+ /// Add set of options (No default, non-temp reference, such as an existing set)
template
Option *add_set(std::string name,
T &member, ///< The selected member of the set
@@ -558,7 +574,7 @@ class App {
template
Option *add_set(std::string name,
T &member, ///< The selected member of the set
- const std::set &&options, ///< The set of posibilities
+ const std::set &&options, ///< The set of possibilities
std::string description,
bool defaulted) {
@@ -582,11 +598,11 @@ class App {
return opt;
}
- /// Add set of options (with default, L value refernce, such as an existing set)
+ /// Add set of options (with default, L value reference, such as an existing set)
template
Option *add_set(std::string name,
T &member, ///< The selected member of the set
- const std::set &options, ///< The set of posibilities
+ const std::set &options, ///< The set of possibilities
std::string description,
bool defaulted) {
@@ -668,7 +684,7 @@ class App {
/// Add set of options, string only, ignore case (default, R value)
Option *add_set_ignore_case(std::string name,
std::string &member, ///< The selected member of the set
- const std::set &&options, ///< The set of posibilities
+ const std::set &&options, ///< The set of possibilities
std::string description,
bool defaulted) {
@@ -699,7 +715,7 @@ class App {
/// Add set of options, string only, ignore case (default, L value)
Option *add_set_ignore_case(std::string name,
std::string &member, ///< The selected member of the set
- const std::set &options, ///< The set of posibilities
+ const std::set &options, ///< The set of possibilities
std::string description,
bool defaulted) {
@@ -727,6 +743,242 @@ class App {
return opt;
}
+ /// Add set of options, string only, ignore underscore (no default, R value)
+ Option *add_set_ignore_underscore(std::string name,
+ std::string &member, ///< The selected member of the set
+ const std::set &&options, ///< The set of possibilities
+ std::string description = "") {
+
+ std::string simple_name = CLI::detail::split(name, ',').at(0);
+ CLI::callback_t fun = [&member, options, simple_name](CLI::results_t res) {
+ member = detail::remove_underscore(res[0]);
+ auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) {
+ return detail::remove_underscore(val) == member;
+ });
+ if(iter == std::end(options))
+ throw ConversionError(member, simple_name);
+ else {
+ member = *iter;
+ return true;
+ }
+ };
+
+ Option *opt = add_option(name, fun, description, false);
+ std::string typeval = detail::type_name();
+ typeval += " in {" + detail::join(options) + "}";
+ opt->type_name(typeval);
+
+ return opt;
+ }
+
+ /// Add set of options, string only, ignore underscore (no default, L value)
+ Option *add_set_ignore_underscore(std::string name,
+ std::string &member, ///< The selected member of the set
+ const std::set &options, ///< The set of possibilities
+ std::string description = "") {
+
+ std::string simple_name = CLI::detail::split(name, ',').at(0);
+ CLI::callback_t fun = [&member, &options, simple_name](CLI::results_t res) {
+ member = detail::remove_underscore(res[0]);
+ auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) {
+ return detail::remove_underscore(val) == member;
+ });
+ if(iter == std::end(options))
+ throw ConversionError(member, simple_name);
+ else {
+ member = *iter;
+ return true;
+ }
+ };
+
+ Option *opt = add_option(name, fun, description, false);
+ opt->type_name_fn([&options]() {
+ return std::string(detail::type_name()) + " in {" + detail::join(options) + "}";
+ });
+
+ return opt;
+ }
+
+ /// Add set of options, string only, ignore underscore (default, R value)
+ Option *add_set_ignore_underscore(std::string name,
+ std::string &member, ///< The selected member of the set
+ const std::set &&options, ///< The set of possibilities
+ std::string description,
+ bool defaulted) {
+
+ std::string simple_name = CLI::detail::split(name, ',').at(0);
+ CLI::callback_t fun = [&member, options, simple_name](CLI::results_t res) {
+ member = detail::remove_underscore(res[0]);
+ auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) {
+ return detail::remove_underscore(val) == member;
+ });
+ if(iter == std::end(options))
+ throw ConversionError(member, simple_name);
+ else {
+ member = *iter;
+ return true;
+ }
+ };
+
+ Option *opt = add_option(name, fun, description, defaulted);
+ std::string typeval = detail::type_name();
+ typeval += " in {" + detail::join(options) + "}";
+ opt->type_name(typeval);
+ if(defaulted) {
+ opt->default_str(member);
+ }
+ return opt;
+ }
+
+ /// Add set of options, string only, ignore underscore (default, L value)
+ Option *add_set_ignore_underscore(std::string name,
+ std::string &member, ///< The selected member of the set
+ const std::set &options, ///< The set of possibilities
+ std::string description,
+ bool defaulted) {
+
+ std::string simple_name = CLI::detail::split(name, ',').at(0);
+ CLI::callback_t fun = [&member, &options, simple_name](CLI::results_t res) {
+ member = detail::remove_underscore(res[0]);
+ auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) {
+ return detail::remove_underscore(val) == member;
+ });
+ if(iter == std::end(options))
+ throw ConversionError(member, simple_name);
+ else {
+ member = *iter;
+ return true;
+ }
+ };
+
+ Option *opt = add_option(name, fun, description, defaulted);
+ opt->type_name_fn([&options]() {
+ return std::string(detail::type_name()) + " in {" + detail::join(options) + "}";
+ });
+ if(defaulted) {
+ opt->default_str(member);
+ }
+ return opt;
+ }
+
+ /// Add set of options, string only, ignore underscore and case(no default, R value)
+ Option *add_set_ignore_case_underscore(std::string name,
+ std::string &member, ///< The selected member of the set
+ const std::set &&options, ///< The set of possibilities
+ std::string description = "") {
+
+ std::string simple_name = CLI::detail::split(name, ',').at(0);
+ CLI::callback_t fun = [&member, options, simple_name](CLI::results_t res) {
+ member = detail::to_lower(detail::remove_underscore(res[0]));
+ auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) {
+ return detail::to_lower(detail::remove_underscore(val)) == member;
+ });
+ if(iter == std::end(options))
+ throw ConversionError(member, simple_name);
+ else {
+ member = *iter;
+ return true;
+ }
+ };
+
+ Option *opt = add_option(name, fun, description, false);
+ std::string typeval = detail::type_name();
+ typeval += " in {" + detail::join(options) + "}";
+ opt->type_name(typeval);
+
+ return opt;
+ }
+
+ /// Add set of options, string only, ignore underscore and case(no default, L value)
+ Option *add_set_ignore_case_underscore(std::string name,
+ std::string &member, ///< The selected member of the set
+ const std::set &options, ///< The set of possibilities
+ std::string description = "") {
+
+ std::string simple_name = CLI::detail::split(name, ',').at(0);
+ CLI::callback_t fun = [&member, &options, simple_name](CLI::results_t res) {
+ member = detail::to_lower(detail::remove_underscore(res[0]));
+ auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) {
+ return detail::to_lower(detail::remove_underscore(val)) == member;
+ });
+ if(iter == std::end(options))
+ throw ConversionError(member, simple_name);
+ else {
+ member = *iter;
+ return true;
+ }
+ };
+
+ Option *opt = add_option(name, fun, description, false);
+ opt->type_name_fn([&options]() {
+ return std::string(detail::type_name()) + " in {" + detail::join(options) + "}";
+ });
+
+ return opt;
+ }
+
+ /// Add set of options, string only, ignore underscore and case (default, R value)
+ Option *add_set_ignore_case_underscore(std::string name,
+ std::string &member, ///< The selected member of the set
+ const std::set &&options, ///< The set of possibilities
+ std::string description,
+ bool defaulted) {
+
+ std::string simple_name = CLI::detail::split(name, ',').at(0);
+ CLI::callback_t fun = [&member, options, simple_name](CLI::results_t res) {
+ member = detail::to_lower(detail::remove_underscore(res[0]));
+ auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) {
+ return detail::to_lower(detail::remove_underscore(val)) == member;
+ });
+ if(iter == std::end(options))
+ throw ConversionError(member, simple_name);
+ else {
+ member = *iter;
+ return true;
+ }
+ };
+
+ Option *opt = add_option(name, fun, description, defaulted);
+ std::string typeval = detail::type_name();
+ typeval += " in {" + detail::join(options) + "}";
+ opt->type_name(typeval);
+ if(defaulted) {
+ opt->default_str(member);
+ }
+ return opt;
+ }
+
+ /// Add set of options, string only, ignore underscore and case (default, L value)
+ Option *add_set_ignore_case_underscore(std::string name,
+ std::string &member, ///< The selected member of the set
+ const std::set &options, ///< The set of possibilities
+ std::string description,
+ bool defaulted) {
+
+ std::string simple_name = CLI::detail::split(name, ',').at(0);
+ CLI::callback_t fun = [&member, &options, simple_name](CLI::results_t res) {
+ member = detail::to_lower(detail::remove_underscore(res[0]));
+ auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) {
+ return detail::to_lower(detail::remove_underscore(val)) == member;
+ });
+ if(iter == std::end(options))
+ throw ConversionError(member, simple_name);
+ else {
+ member = *iter;
+ return true;
+ }
+ };
+
+ Option *opt = add_option(name, fun, description, defaulted);
+ opt->type_name_fn([&options]() {
+ return std::string(detail::type_name()) + " in {" + detail::join(options) + "}";
+ });
+ if(defaulted) {
+ opt->default_str(member);
+ }
+ return opt;
+ }
+
/// Add a complex number
template
Option *add_complex(std::string name,
@@ -1122,6 +1374,9 @@ class App {
/// Check the status of ignore_case
bool get_ignore_case() const { return ignore_case_; }
+ /// Check the status of ignore_underscore
+ bool get_ignore_underscore() const { return ignore_underscore_; }
+
/// Check the status of fallthrough
bool get_fallthrough() const { return fallthrough_; }
@@ -1170,9 +1425,13 @@ class App {
/// Get the name of the current app
std::string get_name() const { return name_; }
- /// Check the name, case insensitive if set
+ /// Check the name, case insensitive and underscore insensitive if set
bool check_name(std::string name_to_check) const {
std::string local_name = name_;
+ if(ignore_underscore_) {
+ local_name = detail::remove_underscore(name_);
+ name_to_check = detail::remove_underscore(name_to_check);
+ }
if(ignore_case_) {
local_name = detail::to_lower(name_);
name_to_check = detail::to_lower(name_to_check);
diff --git a/include/CLI/Option.hpp b/include/CLI/Option.hpp
index bc1c980b..f858ed6c 100644
--- a/include/CLI/Option.hpp
+++ b/include/CLI/Option.hpp
@@ -45,6 +45,9 @@ template class OptionBase {
/// Ignore the case when matching (option, not value)
bool ignore_case_{false};
+ /// Ignore underscores when matching (option, not value)
+ bool ignore_underscore_{false};
+
/// Allow this option to be given in a configuration file
bool configurable_{true};
@@ -56,6 +59,7 @@ template class OptionBase {
other->group(group_);
other->required(required_);
other->ignore_case(ignore_case_);
+ other->ignore_underscore(ignore_underscore_);
other->configurable(configurable_);
other->multi_option_policy(multi_option_policy_);
}
@@ -90,6 +94,9 @@ template class OptionBase {
/// The status of ignore case
bool get_ignore_case() const { return ignore_case_; }
+ /// The status of ignore_underscore
+ bool get_ignore_underscore() const { return ignore_underscore_; }
+
/// The status of configurable
bool get_configurable() const { return configurable_; }
@@ -145,6 +152,12 @@ class OptionDefaults : public OptionBase {
ignore_case_ = value;
return this;
}
+
+ /// Ignore underscores in the option name
+ OptionDefaults *ignore_underscore(bool value = true) {
+ ignore_underscore_ = value;
+ return this;
+ }
};
class Option : public OptionBase