Commit a83109002c00ef51f0cc99aefbf1d9ef935a92a3

Authored by Philip Top
Committed by Henry Schreiner
1 parent c3d8d4a2

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
... ... @@ -162,7 +162,7 @@ try {
162 162 }
163 163 ```
164 164  
165   -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.
  165 +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.
166 166  
167 167 </p></details>
168 168 </br>
... ... @@ -197,6 +197,10 @@ app.add_set(option_name,
197 197  
198 198 app.add_set_ignore_case(... // String only
199 199  
  200 +app.add_set_ignore_underscore(... // String only
  201 +
  202 +app.add_set_ignore_case_underscore(... // String only
  203 +
200 204 App* subcom = app.add_subcommand(name, description);
201 205 ```
202 206  
... ... @@ -212,7 +216,7 @@ On a compiler that supports C++17&#39;s `__has_include`, you can also use `std::opti
212 216 - `"this"` Can only be passed positionally
213 217 - `"-a,-b,-c"` No limit to the number of non-positional option names
214 218  
215   -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.
  219 +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.
216 220  
217 221 #### Option options
218 222  
... ... @@ -227,6 +231,7 @@ Before parsing, you can set the following options:
227 231 - `->envname(name)`: Gets the value from the environment if present and not passed on the command line.
228 232 - `->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).
229 233 - `->ignore_case()`: Ignore the case on the command line (also works on subcommands, does not affect arguments).
  234 +- `->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
230 235 - `->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).
231 236 - `->check(CLI::ExistingFile)`: Requires that the file exists if given.
232 237 - `->check(CLI::ExistingDirectory)`: Requires that the directory exists.
... ... @@ -279,6 +284,8 @@ Multiple subcommands are allowed, to allow [`Click`][click] like series of comma
279 284 There are several options that are supported on the main app and subcommands. These are:
280 285  
281 286 - `.ignore_case()`: Ignore the case of this subcommand. Inherited by added subcommands, so is usually used on the main `App`.
  287 +- `.ignore_underscore()`: Ignore any underscores in the subcommand name. Inherited by added subcommands, so is usually used on the main `App`.
  288 +
282 289 - `.fallthrough()`: Allow extra unmatched options and positionals to "fall through" and be matched on a parent command. Subcommands always are allowed to fall through.
283 290 - `.require_subcommand()`: Require 1 or more subcommands.
284 291 - `.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=&quot;&quot;, write_description=
340 347  
341 348 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.
342 349  
343   -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:
  350 +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:
344 351  
345 352 ```cpp
346 353 app.option_defaults()->required();
... ... @@ -351,7 +358,7 @@ The default settings for options are inherited to subcommands, as well.
351 358  
352 359 ### Formatting
353 360  
354   -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.
  361 +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.
355 362 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
356 363 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`.
357 364  
... ... @@ -387,7 +394,7 @@ app.add_option(&quot;--fancy-count&quot;, [](std::vector&lt;std::string&gt; val){
387 394  
388 395 ### Utilities
389 396  
390   -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.
  397 +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.
391 398  
392 399 ```cpp
393 400 {
... ...
include/CLI/App.hpp
... ... @@ -139,6 +139,9 @@ class App {
139 139 /// If true, the program name is not case sensitive INHERITABLE
140 140 bool ignore_case_{false};
141 141  
  142 + /// If true, the program should ignore underscores INHERITABLE
  143 + bool ignore_underscore_{false};
  144 +
142 145 /// Allow subcommand fallthrough, so that parent commands can collect commands after subcommand. INHERITABLE
143 146 bool fallthrough_{false};
144 147  
... ... @@ -195,6 +198,7 @@ class App {
195 198 allow_config_extras_ = parent_->allow_config_extras_;
196 199 prefix_command_ = parent_->prefix_command_;
197 200 ignore_case_ = parent_->ignore_case_;
  201 + ignore_underscore_ = parent_->ignore_underscore_;
198 202 fallthrough_ = parent_->fallthrough_;
199 203 group_ = parent_->group_;
200 204 footer_ = parent_->footer_;
... ... @@ -265,6 +269,18 @@ class App {
265 269 return this;
266 270 }
267 271  
  272 + /// Ignore underscore. Subcommand inherit value.
  273 + App *ignore_underscore(bool value = true) {
  274 + ignore_underscore_ = value;
  275 + if(parent_ != nullptr) {
  276 + for(const auto &subc : parent_->subcommands_) {
  277 + if(subc.get() != this && (this->check_name(subc->name_) || subc->check_name(this->name_)))
  278 + throw OptionAlreadyAdded(subc->name_);
  279 + }
  280 + }
  281 + return this;
  282 + }
  283 +
268 284 /// Set the help formatter
269 285 App *formatter(std::shared_ptr<FormatterBase> fmt) {
270 286 formatter_ = fmt;
... ... @@ -510,7 +526,7 @@ class App {
510 526 }
511 527 #endif
512 528  
513   - /// Add set of options (No default, temp refernce, such as an inline set)
  529 + /// Add set of options (No default, temp reference, such as an inline set)
514 530 template <typename T>
515 531 Option *add_set(std::string name,
516 532 T &member, ///< The selected member of the set
... ... @@ -532,7 +548,7 @@ class App {
532 548 return opt;
533 549 }
534 550  
535   - /// Add set of options (No default, non-temp refernce, such as an existing set)
  551 + /// Add set of options (No default, non-temp reference, such as an existing set)
536 552 template <typename T>
537 553 Option *add_set(std::string name,
538 554 T &member, ///< The selected member of the set
... ... @@ -558,7 +574,7 @@ class App {
558 574 template <typename T>
559 575 Option *add_set(std::string name,
560 576 T &member, ///< The selected member of the set
561   - const std::set<T> &&options, ///< The set of posibilities
  577 + const std::set<T> &&options, ///< The set of possibilities
562 578 std::string description,
563 579 bool defaulted) {
564 580  
... ... @@ -582,11 +598,11 @@ class App {
582 598 return opt;
583 599 }
584 600  
585   - /// Add set of options (with default, L value refernce, such as an existing set)
  601 + /// Add set of options (with default, L value reference, such as an existing set)
586 602 template <typename T>
587 603 Option *add_set(std::string name,
588 604 T &member, ///< The selected member of the set
589   - const std::set<T> &options, ///< The set of posibilities
  605 + const std::set<T> &options, ///< The set of possibilities
590 606 std::string description,
591 607 bool defaulted) {
592 608  
... ... @@ -668,7 +684,7 @@ class App {
668 684 /// Add set of options, string only, ignore case (default, R value)
669 685 Option *add_set_ignore_case(std::string name,
670 686 std::string &member, ///< The selected member of the set
671   - const std::set<std::string> &&options, ///< The set of posibilities
  687 + const std::set<std::string> &&options, ///< The set of possibilities
672 688 std::string description,
673 689 bool defaulted) {
674 690  
... ... @@ -699,7 +715,7 @@ class App {
699 715 /// Add set of options, string only, ignore case (default, L value)
700 716 Option *add_set_ignore_case(std::string name,
701 717 std::string &member, ///< The selected member of the set
702   - const std::set<std::string> &options, ///< The set of posibilities
  718 + const std::set<std::string> &options, ///< The set of possibilities
703 719 std::string description,
704 720 bool defaulted) {
705 721  
... ... @@ -727,6 +743,242 @@ class App {
727 743 return opt;
728 744 }
729 745  
  746 + /// Add set of options, string only, ignore underscore (no default, R value)
  747 + Option *add_set_ignore_underscore(std::string name,
  748 + std::string &member, ///< The selected member of the set
  749 + const std::set<std::string> &&options, ///< The set of possibilities
  750 + std::string description = "") {
  751 +
  752 + std::string simple_name = CLI::detail::split(name, ',').at(0);
  753 + CLI::callback_t fun = [&member, options, simple_name](CLI::results_t res) {
  754 + member = detail::remove_underscore(res[0]);
  755 + auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) {
  756 + return detail::remove_underscore(val) == member;
  757 + });
  758 + if(iter == std::end(options))
  759 + throw ConversionError(member, simple_name);
  760 + else {
  761 + member = *iter;
  762 + return true;
  763 + }
  764 + };
  765 +
  766 + Option *opt = add_option(name, fun, description, false);
  767 + std::string typeval = detail::type_name<std::string>();
  768 + typeval += " in {" + detail::join(options) + "}";
  769 + opt->type_name(typeval);
  770 +
  771 + return opt;
  772 + }
  773 +
  774 + /// Add set of options, string only, ignore underscore (no default, L value)
  775 + Option *add_set_ignore_underscore(std::string name,
  776 + std::string &member, ///< The selected member of the set
  777 + const std::set<std::string> &options, ///< The set of possibilities
  778 + std::string description = "") {
  779 +
  780 + std::string simple_name = CLI::detail::split(name, ',').at(0);
  781 + CLI::callback_t fun = [&member, &options, simple_name](CLI::results_t res) {
  782 + member = detail::remove_underscore(res[0]);
  783 + auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) {
  784 + return detail::remove_underscore(val) == member;
  785 + });
  786 + if(iter == std::end(options))
  787 + throw ConversionError(member, simple_name);
  788 + else {
  789 + member = *iter;
  790 + return true;
  791 + }
  792 + };
  793 +
  794 + Option *opt = add_option(name, fun, description, false);
  795 + opt->type_name_fn([&options]() {
  796 + return std::string(detail::type_name<std::string>()) + " in {" + detail::join(options) + "}";
  797 + });
  798 +
  799 + return opt;
  800 + }
  801 +
  802 + /// Add set of options, string only, ignore underscore (default, R value)
  803 + Option *add_set_ignore_underscore(std::string name,
  804 + std::string &member, ///< The selected member of the set
  805 + const std::set<std::string> &&options, ///< The set of possibilities
  806 + std::string description,
  807 + bool defaulted) {
  808 +
  809 + std::string simple_name = CLI::detail::split(name, ',').at(0);
  810 + CLI::callback_t fun = [&member, options, simple_name](CLI::results_t res) {
  811 + member = detail::remove_underscore(res[0]);
  812 + auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) {
  813 + return detail::remove_underscore(val) == member;
  814 + });
  815 + if(iter == std::end(options))
  816 + throw ConversionError(member, simple_name);
  817 + else {
  818 + member = *iter;
  819 + return true;
  820 + }
  821 + };
  822 +
  823 + Option *opt = add_option(name, fun, description, defaulted);
  824 + std::string typeval = detail::type_name<std::string>();
  825 + typeval += " in {" + detail::join(options) + "}";
  826 + opt->type_name(typeval);
  827 + if(defaulted) {
  828 + opt->default_str(member);
  829 + }
  830 + return opt;
  831 + }
  832 +
  833 + /// Add set of options, string only, ignore underscore (default, L value)
  834 + Option *add_set_ignore_underscore(std::string name,
  835 + std::string &member, ///< The selected member of the set
  836 + const std::set<std::string> &options, ///< The set of possibilities
  837 + std::string description,
  838 + bool defaulted) {
  839 +
  840 + std::string simple_name = CLI::detail::split(name, ',').at(0);
  841 + CLI::callback_t fun = [&member, &options, simple_name](CLI::results_t res) {
  842 + member = detail::remove_underscore(res[0]);
  843 + auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) {
  844 + return detail::remove_underscore(val) == member;
  845 + });
  846 + if(iter == std::end(options))
  847 + throw ConversionError(member, simple_name);
  848 + else {
  849 + member = *iter;
  850 + return true;
  851 + }
  852 + };
  853 +
  854 + Option *opt = add_option(name, fun, description, defaulted);
  855 + opt->type_name_fn([&options]() {
  856 + return std::string(detail::type_name<std::string>()) + " in {" + detail::join(options) + "}";
  857 + });
  858 + if(defaulted) {
  859 + opt->default_str(member);
  860 + }
  861 + return opt;
  862 + }
  863 +
  864 + /// Add set of options, string only, ignore underscore and case(no default, R value)
  865 + Option *add_set_ignore_case_underscore(std::string name,
  866 + std::string &member, ///< The selected member of the set
  867 + const std::set<std::string> &&options, ///< The set of possibilities
  868 + std::string description = "") {
  869 +
  870 + std::string simple_name = CLI::detail::split(name, ',').at(0);
  871 + CLI::callback_t fun = [&member, options, simple_name](CLI::results_t res) {
  872 + member = detail::to_lower(detail::remove_underscore(res[0]));
  873 + auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) {
  874 + return detail::to_lower(detail::remove_underscore(val)) == member;
  875 + });
  876 + if(iter == std::end(options))
  877 + throw ConversionError(member, simple_name);
  878 + else {
  879 + member = *iter;
  880 + return true;
  881 + }
  882 + };
  883 +
  884 + Option *opt = add_option(name, fun, description, false);
  885 + std::string typeval = detail::type_name<std::string>();
  886 + typeval += " in {" + detail::join(options) + "}";
  887 + opt->type_name(typeval);
  888 +
  889 + return opt;
  890 + }
  891 +
  892 + /// Add set of options, string only, ignore underscore and case(no default, L value)
  893 + Option *add_set_ignore_case_underscore(std::string name,
  894 + std::string &member, ///< The selected member of the set
  895 + const std::set<std::string> &options, ///< The set of possibilities
  896 + std::string description = "") {
  897 +
  898 + std::string simple_name = CLI::detail::split(name, ',').at(0);
  899 + CLI::callback_t fun = [&member, &options, simple_name](CLI::results_t res) {
  900 + member = detail::to_lower(detail::remove_underscore(res[0]));
  901 + auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) {
  902 + return detail::to_lower(detail::remove_underscore(val)) == member;
  903 + });
  904 + if(iter == std::end(options))
  905 + throw ConversionError(member, simple_name);
  906 + else {
  907 + member = *iter;
  908 + return true;
  909 + }
  910 + };
  911 +
  912 + Option *opt = add_option(name, fun, description, false);
  913 + opt->type_name_fn([&options]() {
  914 + return std::string(detail::type_name<std::string>()) + " in {" + detail::join(options) + "}";
  915 + });
  916 +
  917 + return opt;
  918 + }
  919 +
  920 + /// Add set of options, string only, ignore underscore and case (default, R value)
  921 + Option *add_set_ignore_case_underscore(std::string name,
  922 + std::string &member, ///< The selected member of the set
  923 + const std::set<std::string> &&options, ///< The set of possibilities
  924 + std::string description,
  925 + bool defaulted) {
  926 +
  927 + std::string simple_name = CLI::detail::split(name, ',').at(0);
  928 + CLI::callback_t fun = [&member, options, simple_name](CLI::results_t res) {
  929 + member = detail::to_lower(detail::remove_underscore(res[0]));
  930 + auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) {
  931 + return detail::to_lower(detail::remove_underscore(val)) == member;
  932 + });
  933 + if(iter == std::end(options))
  934 + throw ConversionError(member, simple_name);
  935 + else {
  936 + member = *iter;
  937 + return true;
  938 + }
  939 + };
  940 +
  941 + Option *opt = add_option(name, fun, description, defaulted);
  942 + std::string typeval = detail::type_name<std::string>();
  943 + typeval += " in {" + detail::join(options) + "}";
  944 + opt->type_name(typeval);
  945 + if(defaulted) {
  946 + opt->default_str(member);
  947 + }
  948 + return opt;
  949 + }
  950 +
  951 + /// Add set of options, string only, ignore underscore and case (default, L value)
  952 + Option *add_set_ignore_case_underscore(std::string name,
  953 + std::string &member, ///< The selected member of the set
  954 + const std::set<std::string> &options, ///< The set of possibilities
  955 + std::string description,
  956 + bool defaulted) {
  957 +
  958 + std::string simple_name = CLI::detail::split(name, ',').at(0);
  959 + CLI::callback_t fun = [&member, &options, simple_name](CLI::results_t res) {
  960 + member = detail::to_lower(detail::remove_underscore(res[0]));
  961 + auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) {
  962 + return detail::to_lower(detail::remove_underscore(val)) == member;
  963 + });
  964 + if(iter == std::end(options))
  965 + throw ConversionError(member, simple_name);
  966 + else {
  967 + member = *iter;
  968 + return true;
  969 + }
  970 + };
  971 +
  972 + Option *opt = add_option(name, fun, description, defaulted);
  973 + opt->type_name_fn([&options]() {
  974 + return std::string(detail::type_name<std::string>()) + " in {" + detail::join(options) + "}";
  975 + });
  976 + if(defaulted) {
  977 + opt->default_str(member);
  978 + }
  979 + return opt;
  980 + }
  981 +
730 982 /// Add a complex number
731 983 template <typename T>
732 984 Option *add_complex(std::string name,
... ... @@ -1122,6 +1374,9 @@ class App {
1122 1374 /// Check the status of ignore_case
1123 1375 bool get_ignore_case() const { return ignore_case_; }
1124 1376  
  1377 + /// Check the status of ignore_underscore
  1378 + bool get_ignore_underscore() const { return ignore_underscore_; }
  1379 +
1125 1380 /// Check the status of fallthrough
1126 1381 bool get_fallthrough() const { return fallthrough_; }
1127 1382  
... ... @@ -1170,9 +1425,13 @@ class App {
1170 1425 /// Get the name of the current app
1171 1426 std::string get_name() const { return name_; }
1172 1427  
1173   - /// Check the name, case insensitive if set
  1428 + /// Check the name, case insensitive and underscore insensitive if set
1174 1429 bool check_name(std::string name_to_check) const {
1175 1430 std::string local_name = name_;
  1431 + if(ignore_underscore_) {
  1432 + local_name = detail::remove_underscore(name_);
  1433 + name_to_check = detail::remove_underscore(name_to_check);
  1434 + }
1176 1435 if(ignore_case_) {
1177 1436 local_name = detail::to_lower(name_);
1178 1437 name_to_check = detail::to_lower(name_to_check);
... ...
include/CLI/Option.hpp
... ... @@ -45,6 +45,9 @@ template &lt;typename CRTP&gt; class OptionBase {
45 45 /// Ignore the case when matching (option, not value)
46 46 bool ignore_case_{false};
47 47  
  48 + /// Ignore underscores when matching (option, not value)
  49 + bool ignore_underscore_{false};
  50 +
48 51 /// Allow this option to be given in a configuration file
49 52 bool configurable_{true};
50 53  
... ... @@ -56,6 +59,7 @@ template &lt;typename CRTP&gt; class OptionBase {
56 59 other->group(group_);
57 60 other->required(required_);
58 61 other->ignore_case(ignore_case_);
  62 + other->ignore_underscore(ignore_underscore_);
59 63 other->configurable(configurable_);
60 64 other->multi_option_policy(multi_option_policy_);
61 65 }
... ... @@ -90,6 +94,9 @@ template &lt;typename CRTP&gt; class OptionBase {
90 94 /// The status of ignore case
91 95 bool get_ignore_case() const { return ignore_case_; }
92 96  
  97 + /// The status of ignore_underscore
  98 + bool get_ignore_underscore() const { return ignore_underscore_; }
  99 +
93 100 /// The status of configurable
94 101 bool get_configurable() const { return configurable_; }
95 102  
... ... @@ -145,6 +152,12 @@ class OptionDefaults : public OptionBase&lt;OptionDefaults&gt; {
145 152 ignore_case_ = value;
146 153 return this;
147 154 }
  155 +
  156 + /// Ignore underscores in the option name
  157 + OptionDefaults *ignore_underscore(bool value = true) {
  158 + ignore_underscore_ = value;
  159 + return this;
  160 + }
148 161 };
149 162  
150 163 class Option : public OptionBase<Option> {
... ... @@ -411,6 +424,20 @@ class Option : public OptionBase&lt;Option&gt; {
411 424 return this;
412 425 }
413 426  
  427 + /// Ignore underscores in the option names
  428 + ///
  429 + /// The template hides the fact that we don't have the definition of App yet.
  430 + /// You are never expected to add an argument to the template here.
  431 + template <typename T = App> Option *ignore_underscore(bool value = true) {
  432 + ignore_underscore_ = value;
  433 + auto *parent = dynamic_cast<T *>(parent_);
  434 + for(const Option_p &opt : parent->options_)
  435 + if(opt.get() != this && *opt == *this)
  436 + throw OptionAlreadyAdded(opt->get_name(true, true));
  437 +
  438 + return this;
  439 + }
  440 +
414 441 /// Take the last argument if given multiple times (or another policy)
415 442 Option *multi_option_policy(MultiOptionPolicy value = MultiOptionPolicy::Throw) {
416 443  
... ... @@ -602,7 +629,7 @@ class Option : public OptionBase&lt;Option&gt; {
602 629 for(const std::string &lname : lnames_)
603 630 if(other.check_lname(lname))
604 631 return true;
605   - // We need to do the inverse, just in case we are ignore_case
  632 + // We need to do the inverse, just in case we are ignore_case or ignore underscore
606 633 for(const std::string &sname : other.snames_)
607 634 if(check_sname(sname))
608 635 return true;
... ... @@ -625,13 +652,17 @@ class Option : public OptionBase&lt;Option&gt; {
625 652 local_pname = detail::to_lower(local_pname);
626 653 name = detail::to_lower(name);
627 654 }
  655 + if(ignore_underscore_) {
  656 + local_pname = detail::remove_underscore(local_pname);
  657 + name = detail::remove_underscore(name);
  658 + }
628 659 return name == local_pname;
629 660 }
630 661 }
631 662  
632 663 /// Requires "-" to be removed from string
633 664 bool check_sname(std::string name) const {
634   - if(ignore_case_) {
  665 + if(ignore_case_) { // there can be no extra underscores in check_sname
635 666 name = detail::to_lower(name);
636 667 return std::find_if(std::begin(snames_), std::end(snames_), [&name](std::string local_sname) {
637 668 return detail::to_lower(local_sname) == name;
... ... @@ -643,9 +674,22 @@ class Option : public OptionBase&lt;Option&gt; {
643 674 /// Requires "--" to be removed from string
644 675 bool check_lname(std::string name) const {
645 676 if(ignore_case_) {
646   - name = detail::to_lower(name);
  677 + if(ignore_underscore_) {
  678 + name = detail::to_lower(detail::remove_underscore(name));
  679 + return std::find_if(std::begin(lnames_), std::end(lnames_), [&name](std::string local_sname) {
  680 + return detail::to_lower(detail::remove_underscore(local_sname)) == name;
  681 + }) != std::end(lnames_);
  682 + } else {
  683 + name = detail::to_lower(name);
  684 + return std::find_if(std::begin(lnames_), std::end(lnames_), [&name](std::string local_sname) {
  685 + return detail::to_lower(local_sname) == name;
  686 + }) != std::end(lnames_);
  687 + }
  688 +
  689 + } else if(ignore_underscore_) {
  690 + name = detail::remove_underscore(name);
647 691 return std::find_if(std::begin(lnames_), std::end(lnames_), [&name](std::string local_sname) {
648   - return detail::to_lower(local_sname) == name;
  692 + return detail::remove_underscore(local_sname) == name;
649 693 }) != std::end(lnames_);
650 694 } else
651 695 return std::find(std::begin(lnames_), std::end(lnames_), name) != std::end(lnames_);
... ...
include/CLI/StringTools.hpp
... ... @@ -142,6 +142,12 @@ inline std::string to_lower(std::string str) {
142 142 return str;
143 143 }
144 144  
  145 +/// remove underscores from a string
  146 +inline std::string remove_underscore(std::string str) {
  147 + str.erase(std::remove(std::begin(str), std::end(str), '_'), std::end(str));
  148 + return str;
  149 +}
  150 +
145 151 /// Split a string '"one two" "three"' into 'one two', 'three'
146 152 inline std::vector<std::string> split_up(std::string str) {
147 153  
... ...
tests/AppTest.cpp
... ... @@ -994,6 +994,115 @@ TEST_F(TApp, InSetIgnoreCase) {
994 994 EXPECT_THROW(run(), CLI::ArgumentMismatch);
995 995 }
996 996  
  997 +/*
  998 +TEST_F(TApp, InSetIgnoreCaseLValue) {
  999 +
  1000 + std::set<std::string> options{"one", "Two", "THREE"};
  1001 + std::string choice;
  1002 + app.add_set_ignore_case("-q,--quick", choice, options);
  1003 +
  1004 + args = {"--quick", "One"};
  1005 + run();
  1006 + EXPECT_EQ("one", choice);
  1007 +
  1008 + args = {"--quick", "two"};
  1009 + run();
  1010 + EXPECT_EQ("Two", choice); // Keeps caps from set
  1011 +
  1012 + args = {"--quick", "ThrEE"};
  1013 + run();
  1014 + EXPECT_EQ("THREE", choice); // Keeps caps from set
  1015 +
  1016 + options.clear();
  1017 + args = {"--quick", "ThrEE"};
  1018 + run();
  1019 + EXPECT_EQ("THREE", choice); // this will now fail since options was cleared
  1020 +
  1021 + args = {"--quick", "four"};
  1022 + EXPECT_THROW(run(), CLI::ConversionError);
  1023 +
  1024 + args = {"--quick=one", "--quick=two"};
  1025 + EXPECT_THROW(run(), CLI::ArgumentMismatch);
  1026 +}
  1027 +
  1028 +TEST_F(TApp, InSetIgnoreCasePointer) {
  1029 +
  1030 + std::set<std::string> *options = new std::set<std::string>{"one", "Two", "THREE"};
  1031 + std::string choice;
  1032 + app.add_set_ignore_case("-q,--quick", choice, *options);
  1033 +
  1034 + args = {"--quick", "One"};
  1035 + run();
  1036 + EXPECT_EQ("one", choice);
  1037 +
  1038 + args = {"--quick", "two"};
  1039 + run();
  1040 + EXPECT_EQ("Two", choice); // Keeps caps from set
  1041 +
  1042 + args = {"--quick", "ThrEE"};
  1043 + run();
  1044 + EXPECT_EQ("THREE", choice); // Keeps caps from set
  1045 +
  1046 + delete options;
  1047 + args = {"--quick", "ThrEE"};
  1048 + run();
  1049 + EXPECT_EQ("THREE", choice); // this could cause a seg fault
  1050 +
  1051 + args = {"--quick", "four"};
  1052 + EXPECT_THROW(run(), CLI::ConversionError);
  1053 +
  1054 + args = {"--quick=one", "--quick=two"};
  1055 + EXPECT_THROW(run(), CLI::ArgumentMismatch);
  1056 +}
  1057 +*/
  1058 +TEST_F(TApp, InSetIgnoreUnderscore) {
  1059 +
  1060 + std::string choice;
  1061 + app.add_set_ignore_underscore("-q,--quick", choice, {"option_one", "option_two", "optionthree"});
  1062 +
  1063 + args = {"--quick", "option_one"};
  1064 + run();
  1065 + EXPECT_EQ("option_one", choice);
  1066 +
  1067 + args = {"--quick", "optiontwo"};
  1068 + run();
  1069 + EXPECT_EQ("option_two", choice); // Keeps underscore from set
  1070 +
  1071 + args = {"--quick", "_option_thr_ee"};
  1072 + run();
  1073 + EXPECT_EQ("optionthree", choice); // no underscore
  1074 +
  1075 + args = {"--quick", "Option4"};
  1076 + EXPECT_THROW(run(), CLI::ConversionError);
  1077 +
  1078 + args = {"--quick=option_one", "--quick=option_two"};
  1079 + EXPECT_THROW(run(), CLI::ArgumentMismatch);
  1080 +}
  1081 +
  1082 +TEST_F(TApp, InSetIgnoreCaseUnderscore) {
  1083 +
  1084 + std::string choice;
  1085 + app.add_set_ignore_case_underscore("-q,--quick", choice, {"Option_One", "option_two", "OptionThree"});
  1086 +
  1087 + args = {"--quick", "option_one"};
  1088 + run();
  1089 + EXPECT_EQ("Option_One", choice);
  1090 +
  1091 + args = {"--quick", "OptionTwo"};
  1092 + run();
  1093 + EXPECT_EQ("option_two", choice); // Keeps underscore and case from set
  1094 +
  1095 + args = {"--quick", "_OPTION_thr_ee"};
  1096 + run();
  1097 + EXPECT_EQ("OptionThree", choice); // no underscore
  1098 +
  1099 + args = {"--quick", "Option4"};
  1100 + EXPECT_THROW(run(), CLI::ConversionError);
  1101 +
  1102 + args = {"--quick=option_one", "--quick=option_two"};
  1103 + EXPECT_THROW(run(), CLI::ArgumentMismatch);
  1104 +}
  1105 +
997 1106 TEST_F(TApp, VectorFixedString) {
998 1107 std::vector<std::string> strvec;
999 1108 std::vector<std::string> answer{"mystring", "mystring2", "mystring3"};
... ...
tests/CreationTest.cpp
... ... @@ -92,6 +92,17 @@ TEST_F(TApp, MultipleSubcomMatchingWithCaseFirst) {
92 92 EXPECT_THROW(app.add_subcommand("fIrst"), CLI::OptionAlreadyAdded);
93 93 }
94 94  
  95 +TEST_F(TApp, MultipleSubcomMatchingWithUnderscore) {
  96 + app.add_subcommand("first_option")->ignore_underscore();
  97 + EXPECT_THROW(app.add_subcommand("firstoption"), CLI::OptionAlreadyAdded);
  98 +}
  99 +
  100 +TEST_F(TApp, MultipleSubcomMatchingWithUnderscoreFirst) {
  101 + app.ignore_underscore();
  102 + app.add_subcommand("first_option");
  103 + EXPECT_THROW(app.add_subcommand("firstoption"), CLI::OptionAlreadyAdded);
  104 +}
  105 +
95 106 TEST_F(TApp, MultipleSubcomMatchingWithCaseInplace) {
96 107 app.add_subcommand("first");
97 108 auto first = app.add_subcommand("fIrst");
... ... @@ -106,6 +117,20 @@ TEST_F(TApp, MultipleSubcomMatchingWithCaseInplace2) {
106 117 EXPECT_THROW(first->ignore_case(), CLI::OptionAlreadyAdded);
107 118 }
108 119  
  120 +TEST_F(TApp, MultipleSubcomMatchingWithUnderscoreInplace) {
  121 + app.add_subcommand("first_option");
  122 + auto first = app.add_subcommand("firstoption");
  123 +
  124 + EXPECT_THROW(first->ignore_underscore(), CLI::OptionAlreadyAdded);
  125 +}
  126 +
  127 +TEST_F(TApp, MultipleSubcomMatchingWithUnderscoreInplace2) {
  128 + auto first = app.add_subcommand("firstoption");
  129 + app.add_subcommand("first_option");
  130 +
  131 + EXPECT_THROW(first->ignore_underscore(), CLI::OptionAlreadyAdded);
  132 +}
  133 +
109 134 TEST_F(TApp, MultipleSubcomNoMatchingInplace2) {
110 135 auto first = app.add_subcommand("first");
111 136 auto second = app.add_subcommand("second");
... ... @@ -114,6 +139,14 @@ TEST_F(TApp, MultipleSubcomNoMatchingInplace2) {
114 139 EXPECT_NO_THROW(second->ignore_case());
115 140 }
116 141  
  142 +TEST_F(TApp, MultipleSubcomNoMatchingInplaceUnderscore2) {
  143 + auto first = app.add_subcommand("first_option");
  144 + auto second = app.add_subcommand("second_option");
  145 +
  146 + EXPECT_NO_THROW(first->ignore_underscore());
  147 + EXPECT_NO_THROW(second->ignore_underscore());
  148 +}
  149 +
117 150 TEST_F(TApp, IncorrectConstructionFlagPositional1) { EXPECT_THROW(app.add_flag("cat"), CLI::IncorrectConstruction); }
118 151  
119 152 TEST_F(TApp, IncorrectConstructionFlagPositional2) {
... ... @@ -262,6 +295,68 @@ TEST_F(TApp, CheckNameNoCase) {
262 295 EXPECT_TRUE(pos2->check_name("pos2"));
263 296 }
264 297  
  298 +TEST_F(TApp, CheckNameNoUnderscore) {
  299 + auto long1 = app.add_flag("--longoption1")->ignore_underscore();
  300 + auto long2 = app.add_flag("--long_option2")->ignore_underscore();
  301 +
  302 + int x, y;
  303 + auto pos1 = app.add_option("pos_option_1", x)->ignore_underscore();
  304 + auto pos2 = app.add_option("posoption2", y)->ignore_underscore();
  305 +
  306 + EXPECT_TRUE(long1->check_name("--long_option1"));
  307 + EXPECT_TRUE(long1->check_name("--longoption_1"));
  308 + EXPECT_TRUE(long1->check_name("--longoption1"));
  309 + EXPECT_TRUE(long1->check_name("--long__opt_ion__1"));
  310 + EXPECT_TRUE(long1->check_name("--__l_o_n_g_o_p_t_i_o_n_1"));
  311 +
  312 + EXPECT_TRUE(long2->check_name("--long_option2"));
  313 + EXPECT_TRUE(long2->check_name("--longoption2"));
  314 + EXPECT_TRUE(long2->check_name("--longoption_2"));
  315 + EXPECT_TRUE(long2->check_name("--long__opt_ion__2"));
  316 + EXPECT_TRUE(long2->check_name("--__l_o_n_go_p_t_i_o_n_2__"));
  317 +
  318 + EXPECT_TRUE(pos1->check_name("pos_option1"));
  319 + EXPECT_TRUE(pos1->check_name("pos_option_1"));
  320 + EXPECT_TRUE(pos1->check_name("pos_o_p_t_i_on_1"));
  321 + EXPECT_TRUE(pos1->check_name("posoption1"));
  322 +
  323 + EXPECT_TRUE(pos2->check_name("pos_option2"));
  324 + EXPECT_TRUE(pos2->check_name("pos_option_2"));
  325 + EXPECT_TRUE(pos2->check_name("pos_o_p_t_i_on_2"));
  326 + EXPECT_TRUE(pos2->check_name("posoption2"));
  327 +}
  328 +
  329 +TEST_F(TApp, CheckNameNoCaseNoUnderscore) {
  330 + auto long1 = app.add_flag("--LongoptioN1")->ignore_underscore()->ignore_case();
  331 + auto long2 = app.add_flag("--long_Option2")->ignore_case()->ignore_underscore();
  332 +
  333 + int x, y;
  334 + auto pos1 = app.add_option("pos_Option_1", x)->ignore_underscore()->ignore_case();
  335 + auto pos2 = app.add_option("posOption2", y)->ignore_case()->ignore_underscore();
  336 +
  337 + EXPECT_TRUE(long1->check_name("--Long_Option1"));
  338 + EXPECT_TRUE(long1->check_name("--lONgoption_1"));
  339 + EXPECT_TRUE(long1->check_name("--LongOption1"));
  340 + EXPECT_TRUE(long1->check_name("--long__Opt_ion__1"));
  341 + EXPECT_TRUE(long1->check_name("--__l_o_N_g_o_P_t_i_O_n_1"));
  342 +
  343 + EXPECT_TRUE(long2->check_name("--long_Option2"));
  344 + EXPECT_TRUE(long2->check_name("--LongOption2"));
  345 + EXPECT_TRUE(long2->check_name("--longOPTION_2"));
  346 + EXPECT_TRUE(long2->check_name("--long__OPT_ion__2"));
  347 + EXPECT_TRUE(long2->check_name("--__l_o_n_GO_p_t_i_o_n_2__"));
  348 +
  349 + EXPECT_TRUE(pos1->check_name("POS_Option1"));
  350 + EXPECT_TRUE(pos1->check_name("pos_option_1"));
  351 + EXPECT_TRUE(pos1->check_name("pos_o_p_t_i_on_1"));
  352 + EXPECT_TRUE(pos1->check_name("posoption1"));
  353 +
  354 + EXPECT_TRUE(pos2->check_name("pos_option2"));
  355 + EXPECT_TRUE(pos2->check_name("pos_OPTION_2"));
  356 + EXPECT_TRUE(pos2->check_name("poS_o_p_T_I_on_2"));
  357 + EXPECT_TRUE(pos2->check_name("PosOption2"));
  358 +}
  359 +
265 360 TEST_F(TApp, PreSpaces) {
266 361 int x;
267 362 auto myapp = app.add_option(" -a, --long, other", x);
... ... @@ -301,6 +396,12 @@ TEST_F(TApp, OptionFromDefaults) {
301 396 auto opt3 = app.add_option("--simple3", x);
302 397 EXPECT_TRUE(opt3->get_required());
303 398 EXPECT_TRUE(opt3->get_ignore_case());
  399 +
  400 + app.option_defaults()->required()->ignore_underscore();
  401 +
  402 + auto opt4 = app.add_option("--simple4", x);
  403 + EXPECT_TRUE(opt4->get_required());
  404 + EXPECT_TRUE(opt4->get_ignore_underscore());
304 405 }
305 406  
306 407 TEST_F(TApp, OptionFromDefaultsSubcommands) {
... ... @@ -308,6 +409,7 @@ TEST_F(TApp, OptionFromDefaultsSubcommands) {
308 409 EXPECT_FALSE(app.option_defaults()->get_required());
309 410 EXPECT_EQ(app.option_defaults()->get_multi_option_policy(), CLI::MultiOptionPolicy::Throw);
310 411 EXPECT_FALSE(app.option_defaults()->get_ignore_case());
  412 + EXPECT_FALSE(app.option_defaults()->get_ignore_underscore());
311 413 EXPECT_TRUE(app.option_defaults()->get_configurable());
312 414 EXPECT_EQ(app.option_defaults()->get_group(), "Options");
313 415  
... ... @@ -315,6 +417,7 @@ TEST_F(TApp, OptionFromDefaultsSubcommands) {
315 417 ->required()
316 418 ->multi_option_policy(CLI::MultiOptionPolicy::TakeLast)
317 419 ->ignore_case()
  420 + ->ignore_underscore()
318 421 ->configurable(false)
319 422 ->group("Something");
320 423  
... ... @@ -323,6 +426,7 @@ TEST_F(TApp, OptionFromDefaultsSubcommands) {
323 426 EXPECT_TRUE(app2->option_defaults()->get_required());
324 427 EXPECT_EQ(app2->option_defaults()->get_multi_option_policy(), CLI::MultiOptionPolicy::TakeLast);
325 428 EXPECT_TRUE(app2->option_defaults()->get_ignore_case());
  429 + EXPECT_TRUE(app2->option_defaults()->get_ignore_underscore());
326 430 EXPECT_FALSE(app2->option_defaults()->get_configurable());
327 431 EXPECT_EQ(app2->option_defaults()->get_group(), "Something");
328 432 }
... ... @@ -352,6 +456,7 @@ TEST_F(TApp, SubcommandDefaults) {
352 456 EXPECT_FALSE(app.get_allow_extras());
353 457 EXPECT_FALSE(app.get_prefix_command());
354 458 EXPECT_FALSE(app.get_ignore_case());
  459 + EXPECT_FALSE(app.get_ignore_underscore());
355 460 EXPECT_FALSE(app.get_fallthrough());
356 461 EXPECT_EQ(app.get_footer(), "");
357 462 EXPECT_EQ(app.get_group(), "Subcommands");
... ... @@ -361,6 +466,7 @@ TEST_F(TApp, SubcommandDefaults) {
361 466 app.allow_extras();
362 467 app.prefix_command();
363 468 app.ignore_case();
  469 + app.ignore_underscore();
364 470 app.fallthrough();
365 471 app.footer("footy");
366 472 app.group("Stuff");
... ... @@ -372,6 +478,7 @@ TEST_F(TApp, SubcommandDefaults) {
372 478 EXPECT_TRUE(app2->get_allow_extras());
373 479 EXPECT_TRUE(app2->get_prefix_command());
374 480 EXPECT_TRUE(app2->get_ignore_case());
  481 + EXPECT_TRUE(app2->get_ignore_underscore());
375 482 EXPECT_TRUE(app2->get_fallthrough());
376 483 EXPECT_EQ(app2->get_footer(), "footy");
377 484 EXPECT_EQ(app2->get_group(), "Stuff");
... ...
tests/SubcommandTest.cpp
... ... @@ -530,6 +530,43 @@ TEST_F(TApp, SubcomInheritCaseCheck) {
530 530 EXPECT_EQ(sub2, app.get_subcommands().at(0));
531 531 }
532 532  
  533 +TEST_F(SubcommandProgram, UnderscoreCheck) {
  534 + args = {"start_"};
  535 + EXPECT_THROW(run(), CLI::ExtrasError);
  536 +
  537 + args = {"start"};
  538 + run();
  539 +
  540 + start->ignore_underscore();
  541 + run();
  542 +
  543 + args = {"_start_"};
  544 + run();
  545 +}
  546 +
  547 +TEST_F(TApp, SubcomInheritUnderscoreCheck) {
  548 + app.ignore_underscore();
  549 + auto sub1 = app.add_subcommand("sub_option1");
  550 + auto sub2 = app.add_subcommand("sub_option2");
  551 +
  552 + run();
  553 + EXPECT_EQ((size_t)0, app.get_subcommands().size());
  554 + EXPECT_EQ((size_t)2, app.get_subcommands({}).size());
  555 + EXPECT_EQ((size_t)1, app.get_subcommands([](const CLI::App *s) { return s->get_name() == "sub_option1"; }).size());
  556 +
  557 + args = {"suboption1"};
  558 + run();
  559 + EXPECT_EQ(sub1, app.get_subcommands().at(0));
  560 + EXPECT_EQ((size_t)1, app.get_subcommands().size());
  561 +
  562 + app.clear();
  563 + EXPECT_EQ((size_t)0, app.get_subcommands().size());
  564 +
  565 + args = {"_suboption2"};
  566 + run();
  567 + EXPECT_EQ(sub2, app.get_subcommands().at(0));
  568 +}
  569 +
533 570 TEST_F(SubcommandProgram, HelpOrder) {
534 571  
535 572 args = {"-h"};
... ...