Commit 19047d8d68d49aded57137529879d46ecc65a5cc

Authored by Philip Top
Committed by GitHub
1 parent d17016e7

feat: relaxed option naming (#627)

* add a test for std::map

* add some test of the relaxed naming and other checks

* add validator for aliases, group names and option groups

* add extra tests and update readme

* style: pre-commit.ci fixes

* update the book chapters

* fix codacy issue

* Apply suggestions from code review

Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>
README.md
... ... @@ -263,7 +263,7 @@ App* subcom = app.add_subcommand(name, description);
263 263 Option_group *app.add_option_group(name,description);
264 264 ```
265 265  
266   -An option name must start with a alphabetic character, underscore, a number, '?', or '@'. For long options, after the first character '.', and '-' are also valid characters. For the `add_flag*` functions '{' has special meaning. Names are given as a comma separated string, with the dash or dashes. An option or flag can have as many names as you want, and afterward, using `count`, you can use any of the names, with dashes as needed, to count the options. One of the names is allowed to be given without proceeding dash(es); if present the option is a positional option, and that name will be used on the help line for its positional form.
  266 +An option name may start with any character except ('-', ' ', '\n', and '!') ๐Ÿšง. For long options, after the first character all characters are allowed except ('=',':','{',' ', '\n')๐Ÿšง. For the `add_flag*` functions '{' and '!' have special meaning which is why they are not allowed. Names are given as a comma separated string, with the dash or dashes. An option or flag can have as many names as you want, and afterward, using `count`, you can use any of the names, with dashes as needed, to count the options. One of the names is allowed to be given without proceeding dash(es); if present the option is a positional option, and that name will be used on the help line for its positional form.
267 267  
268 268 The `add_option_function<type>(...` function will typically require the template parameter be given unless a `std::function` object with an exact match is passed. The type can be any type supported by the `add_option` function. The function should throw an error (`CLI::ConversionError` or `CLI::ValidationError` possibly) if the value is not valid.
269 269  
... ... @@ -557,7 +557,7 @@ even exit the program through the callback.
557 557 Multiple subcommands are allowed, to allow [`Click`][click] like series of commands (order is preserved). The same subcommand can be triggered multiple times but all positional arguments will take precedence over the second and future calls of the subcommand. `->count()` on the subcommand will return the number of times the subcommand was called. The subcommand callback will only be triggered once unless the `.immediate_callback()` flag is set or the callback is specified through the `parse_complete_callback()` function. The `final_callback()` is triggered only once. In which case the callback executes on completion of the subcommand arguments but after the arguments for that subcommand have been parsed, and can be triggered multiple times.
558 558  
559 559 Subcommands may also have an empty name either by calling `add_subcommand` with an empty string for the name or with no arguments.
560   -Nameless subcommands function a similarly to groups in the main `App`. See [Option groups](#option-groups) to see how this might work. If an option is not defined in the main App, all nameless subcommands are checked as well. This allows for the options to be defined in a composable group. The `add_subcommand` function has an overload for adding a `shared_ptr<App>` so the subcommand(s) could be defined in different components and merged into a main `App`, or possibly multiple `Apps`. Multiple nameless subcommands are allowed. Callbacks for nameless subcommands are only triggered if any options from the subcommand were parsed.
  560 +Nameless subcommands function a similarly to groups in the main `App`. See [Option groups](#option-groups) to see how this might work. If an option is not defined in the main App, all nameless subcommands are checked as well. This allows for the options to be defined in a composable group. The `add_subcommand` function has an overload for adding a `shared_ptr<App>` so the subcommand(s) could be defined in different components and merged into a main `App`, or possibly multiple `Apps`. Multiple nameless subcommands are allowed. Callbacks for nameless subcommands are only triggered if any options from the subcommand were parsed. Subcommand names given through the `add_subcommand` method have the same restrictions as option names.
561 561  
562 562 #### Subcommand options
563 563  
... ... @@ -668,7 +668,7 @@ The subcommand method
668 668 .add_option_group(name,description)
669 669 ```
670 670  
671   -Will create an option group, and return a pointer to it. The argument for `description` is optional and can be omitted. An option group allows creation of a collection of options, similar to the groups function on options, but with additional controls and requirements. They allow specific sets of options to be composed and controlled as a collective. For an example see [range example](https://github.com/CLIUtils/CLI11/blob/master/examples/ranges.cpp). Option groups are a specialization of an App so all [functions](#subcommand-options) that work with an App or subcommand also work on option groups. Options can be created as part of an option group using the add functions just like a subcommand, or previously created options can be added through
  671 +Will create an option group, and return a pointer to it. The argument for `description` is optional and can be omitted. An option group allows creation of a collection of options, similar to the groups function on options, but with additional controls and requirements. They allow specific sets of options to be composed and controlled as a collective. For an example see [range example](https://github.com/CLIUtils/CLI11/blob/master/examples/ranges.cpp). Option groups are a specialization of an App so all [functions](#subcommand-options) that work with an App or subcommand also work on option groups. Options can be created as part of an option group using the add functions just like a subcommand, or previously created options can be added through. The name given in an option group must not contain newlines or null characters.๐Ÿšง
672 672  
673 673 ```cpp
674 674 ogroup->add_option(option_pointer);
... ...
book/chapters/flags.md
... ... @@ -11,7 +11,7 @@ bool my_flag{false};
11 11 app.add_flag("-f", my_flag, "Optional description");
12 12 ```
13 13  
14   -This will bind the flag `-f` to the boolean `my_flag`. After the parsing step, `my_flag` will be `false` if the flag was not found on the command line, or `true` if it was. By default, it will be allowed any number of times, but if you explicitly[^1] request `->take_last(false)`, it will only be allowed once; passing something like `./my_app -f -f` or `./my_app -ff` will throw a `ParseError` with a nice help description.
  14 +This will bind the flag `-f` to the boolean `my_flag`. After the parsing step, `my_flag` will be `false` if the flag was not found on the command line, or `true` if it was. By default, it will be allowed any number of times, but if you explicitly\[^1\] request `->take_last(false)`, it will only be allowed once; passing something like `./my_app -f -f` or `./my_app -ff` will throw a `ParseError` with a nice help description. A flag name may start with any character except ('-', ' ', '\n', and '!'). For long flags, after the first character all characters are allowed except ('=',':','{',' ', '\n'). Names are given as a comma separated string, with the dash or dashes. An flag can have as many names as you want, and afterward, using `count`, you can use any of the names, with dashes as needed.
15 15  
16 16 ## Integer flags
17 17  
... ... @@ -120,4 +120,4 @@ Flag int: 3
120 120 Flag plain: 1
121 121 ```
122 122  
123   -[^1]: It will not inherit this from the parent defaults, since this is often useful even if you don't want all options to allow multiple passed options.
  123 +\[^1\]: It will not inherit this from the parent defaults, since this is often useful even if you don't want all options to allow multiple passed options.
... ...
book/chapters/options.md
... ... @@ -26,12 +26,12 @@ You can use any C++ int-like type, not just `int`. CLI11 understands the followi
26 26 | complex-number | std::complex or any type which has a real(), and imag() operations available, will allow 1 or 2 string definitions like "1+2j" or two arguments "1","2" |
27 27 | enumeration | any enum or enum class type is supported through conversion from the underlying type(typically int, though it can be specified otherwise) |
28 28 | container-like | a container(like vector) of any available types including other containers |
29   -| wrapper | any other object with a `value_type` static definition where the type specified by `value_type` is one of type in this list |
  29 +| wrapper | any other object with a `value_type` static definition where the type specified by `value_type` is one of the type in this list, including `std::atomic<>` |
30 30 | tuple | a tuple, pair, or array, or other type with a tuple size and tuple_type operations defined and the members being a type contained in this list |
31 31 | function | A function that takes an array of strings and returns a string that describes the conversion failure or empty for success. May be the empty function. (`{}`) |
32 32 | streamable | any other type with a `<<` operator will also work |
33 33  
34   -By default, CLI11 will assume that an option is optional, and one value is expected if you do not use a vector. You can change this on a specific option using option modifiers.
  34 +By default, CLI11 will assume that an option is optional, and one value is expected if you do not use a vector. You can change this on a specific option using option modifiers. An option name may start with any character except ('-', ' ', '\n', and '!'). For long options, after the first character all characters are allowed except ('=',':','{',' ', '\n'). Names are given as a comma separated string, with the dash or dashes. An option can have as many names as you want, and afterward, using `count`, you can use any of the names, with dashes as needed, to count the options. One of the names is allowed to be given without proceeding dash(es); if present the option is a positional option, and that name will be used on the help line for its positional form.
35 35  
36 36 ## Positional options and aliases
37 37  
... ... @@ -282,4 +282,4 @@ There are some additional options that can be specified to modify an option for
282 282  
283 283 ## Unusual circumstances
284 284  
285   -There are a few cases where some things break down in the type system managing options and definitions. Using the `add_option` method defines a lambda function to extract a default value if required. In most cases this either straightforward or a failure is detected automatically and handled. But in a few cases a streaming template is available that several layers down may not actually be defined. The conditions in CLI11 cannot detect this circumstance automatically and will result in compile error. One specific known case is `boost::optional` if the boost optional_io header is included. This header defines a template for all boost optional values even if they do no actually have a streaming operator. For example `boost::optional<std::vector>` does not have a streaming operator but one is detected since it is part of a template. For these cases a secondary method `app->add_option_no_stream(...)` is provided that bypasses this operation completely and should compile in these cases.
  285 +There are a few cases where some things break down in the type system managing options and definitions. Using the `add_option` method defines a lambda function to extract a default value if required. In most cases this is either straightforward or a failure is detected automatically and handled. But in a few cases a streaming template is available that several layers down may not actually be defined. This results in CLI11 not being able to detect this circumstance automatically and will result in compile error. One specific known case is `boost::optional` if the boost optional_io header is included. This header defines a template for all boost optional values even if they do not actually have a streaming operator. For example `boost::optional<std::vector>` does not have a streaming operator but one is detected since it is part of a template. For these cases a secondary method `app->add_option_no_stream(...)` is provided that bypasses this operation completely and should compile in these cases.
... ...
include/CLI/App.hpp
... ... @@ -368,23 +368,9 @@ class App {
368 368  
369 369 /// Set an alias for the app
370 370 App *alias(std::string app_name) {
371   - if(!detail::valid_name_string(app_name)) {
372   - if(app_name.empty()) {
373   - throw IncorrectConstruction("Empty aliases are not allowed");
374   - }
375   - if(!detail::valid_first_char(app_name[0])) {
376   - throw IncorrectConstruction(
377   - "Alias starts with invalid character, allowed characters are [a-zA-z0-9]+'_','?','@' ");
378   - }
379   - for(auto c : app_name) {
380   - if(!detail::valid_later_char(c)) {
381   - throw IncorrectConstruction(std::string("Alias contains invalid character ('") + c +
382   - "'), allowed characters are "
383   - "[a-zA-z0-9]+'_','?','@','.','-' ");
384   - }
385   - }
  371 + if(app_name.empty() || !detail::valid_alias_name_string(app_name)) {
  372 + throw IncorrectConstruction("Aliases may not be empty or contain newlines or null characters");
386 373 }
387   -
388 374 if(parent_ != nullptr) {
389 375 aliases_.push_back(app_name);
390 376 auto &res = _compare_subcommand_names(*this, *_get_fallthrough_parent());
... ... @@ -961,6 +947,9 @@ class App {
961 947 /// creates an option group as part of the given app
962 948 template <typename T = Option_group>
963 949 T *add_option_group(std::string group_name, std::string group_description = "") {
  950 + if(!detail::valid_alias_name_string(group_name)) {
  951 + throw IncorrectConstruction("option group names may not contain newlines or null characters");
  952 + }
964 953 auto option_group = std::make_shared<T>(std::move(group_description), group_name, this);
965 954 auto ptr = option_group.get();
966 955 // move to App_p for overload resolution on older gcc versions
... ... @@ -978,13 +967,13 @@ class App {
978 967 if(!subcommand_name.empty() && !detail::valid_name_string(subcommand_name)) {
979 968 if(!detail::valid_first_char(subcommand_name[0])) {
980 969 throw IncorrectConstruction(
981   - "Subcommand name starts with invalid character, allowed characters are [a-zA-z0-9]+'_','?','@' ");
  970 + "Subcommand name starts with invalid character, '!' and '-' are not allowed");
982 971 }
983 972 for(auto c : subcommand_name) {
984 973 if(!detail::valid_later_char(c)) {
985 974 throw IncorrectConstruction(std::string("Subcommand name contains invalid character ('") + c +
986   - "'), allowed characters are "
987   - "[a-zA-z0-9]+'_','?','@','.','-' ");
  975 + "'), all characters are allowed except"
  976 + "'=',':','{','}', and ' '");
988 977 }
989 978 }
990 979 }
... ...
include/CLI/Option.hpp
... ... @@ -94,6 +94,9 @@ template &lt;typename CRTP&gt; class OptionBase {
94 94  
95 95 /// Changes the group membership
96 96 CRTP *group(const std::string &name) {
  97 + if(!detail::valid_alias_name_string(name)) {
  98 + throw IncorrectConstruction("Group names may not contain newlines or null characters");
  99 + }
97 100 group_ = name;
98 101 return static_cast<CRTP *>(this);
99 102 }
... ...
include/CLI/StringTools.hpp
... ... @@ -157,6 +157,22 @@ inline std::string &amp;remove_quotes(std::string &amp;str) {
157 157 return str;
158 158 }
159 159  
  160 +/// Add a leader to the beginning of all new lines (nothing is added
  161 +/// at the start of the first line). `"; "` would be for ini files
  162 +///
  163 +/// Can't use Regex, or this would be a subs.
  164 +inline std::string fix_newlines(const std::string &leader, std::string input) {
  165 + std::string::size_type n = 0;
  166 + while(n != std::string::npos && n < input.size()) {
  167 + n = input.find('\n', n);
  168 + if(n != std::string::npos) {
  169 + input = input.substr(0, n + 1) + leader + input.substr(n + 1);
  170 + n += leader.size();
  171 + }
  172 + }
  173 + return input;
  174 +}
  175 +
160 176 /// Make a copy of the string and then trim it, any filter string can be used (any char in string is filtered)
161 177 inline std::string trim_copy(const std::string &str, const std::string &filter) {
162 178 std::string s = str;
... ... @@ -191,7 +207,7 @@ inline std::ostream &amp;format_aliases(std::ostream &amp;out, const std::vector&lt;std::st
191 207 } else {
192 208 front = false;
193 209 }
194   - out << alias;
  210 + out << detail::fix_newlines(" ", alias);
195 211 }
196 212 out << "\n";
197 213 }
... ... @@ -199,23 +215,35 @@ inline std::ostream &amp;format_aliases(std::ostream &amp;out, const std::vector&lt;std::st
199 215 }
200 216  
201 217 /// Verify the first character of an option
202   -template <typename T> bool valid_first_char(T c) {
203   - return std::isalnum(c, std::locale()) || c == '_' || c == '?' || c == '@';
204   -}
  218 +/// - is a trigger character, ! has special meaning and new lines would just be annoying to deal with
  219 +template <typename T> bool valid_first_char(T c) { return ((c != '-') && (c != '!') && (c != ' ') && c != '\n'); }
205 220  
206 221 /// Verify following characters of an option
207   -template <typename T> bool valid_later_char(T c) { return valid_first_char(c) || c == '.' || c == '-'; }
  222 +template <typename T> bool valid_later_char(T c) {
  223 + // = and : are value separators, { has special meaning for option defaults,
  224 + // and \n would just be annoying to deal with in many places allowing space here has too much potential for
  225 + // inadvertent entry errors and bugs
  226 + return ((c != '=') && (c != ':') && (c != '{') && (c != ' ') && c != '\n');
  227 +}
208 228  
209   -/// Verify an option name
  229 +/// Verify an option/subcommand name
210 230 inline bool valid_name_string(const std::string &str) {
211   - if(str.empty() || !valid_first_char(str[0]))
  231 + if(str.empty() || !valid_first_char(str[0])) {
212 232 return false;
213   - for(auto c : str.substr(1))
214   - if(!valid_later_char(c))
  233 + }
  234 + auto e = str.end();
  235 + for(auto c = str.begin() + 1; c != e; ++c)
  236 + if(!valid_later_char(*c))
215 237 return false;
216 238 return true;
217 239 }
218 240  
  241 +/// Verify an app name
  242 +inline bool valid_alias_name_string(const std::string &str) {
  243 + static const std::string badChars(std::string("\n") + '\0');
  244 + return (str.find_first_of(badChars) == std::string::npos);
  245 +}
  246 +
219 247 /// check if a string is a container segment separator (empty or "%%")
220 248 inline bool is_separator(const std::string &str) {
221 249 static const std::string sep("%%");
... ... @@ -260,7 +288,7 @@ inline bool has_default_flag_values(const std::string &amp;flags) {
260 288 }
261 289  
262 290 inline void remove_default_flag_values(std::string &flags) {
263   - auto loc = flags.find_first_of('{');
  291 + auto loc = flags.find_first_of('{', 2);
264 292 while(loc != std::string::npos) {
265 293 auto finish = flags.find_first_of("},", loc + 1);
266 294 if((finish != std::string::npos) && (flags[finish] == '}')) {
... ... @@ -367,22 +395,6 @@ inline std::vector&lt;std::string&gt; split_up(std::string str, char delimiter = &#39;\0&#39;)
367 395 return output;
368 396 }
369 397  
370   -/// Add a leader to the beginning of all new lines (nothing is added
371   -/// at the start of the first line). `"; "` would be for ini files
372   -///
373   -/// Can't use Regex, or this would be a subs.
374   -inline std::string fix_newlines(const std::string &leader, std::string input) {
375   - std::string::size_type n = 0;
376   - while(n != std::string::npos && n < input.size()) {
377   - n = input.find('\n', n);
378   - if(n != std::string::npos) {
379   - input = input.substr(0, n + 1) + leader + input.substr(n + 1);
380   - n += leader.size();
381   - }
382   - }
383   - return input;
384   -}
385   -
386 398 /// This function detects an equal or colon followed by an escaped quote after an argument
387 399 /// then modifies the string to replace the equality with a space. This is needed
388 400 /// to allow the split up function to work properly and is intended to be used with the find_and_modify function
... ...
tests/AppTest.cpp
... ... @@ -127,6 +127,17 @@ TEST_CASE_METHOD(TApp, &quot;DashedOptionsSingleString&quot;, &quot;[app]&quot;) {
127 127 CHECK(app.count("--that") == 2u);
128 128 }
129 129  
  130 +TEST_CASE_METHOD(TApp, "StrangeFlagNames", "[app]") {
  131 + app.add_flag("-=");
  132 + app.add_flag("--t\tt");
  133 + app.add_flag("-{");
  134 + CHECK_THROWS_AS(app.add_flag("--t t"), CLI::ConstructionError);
  135 + args = {"-=", "--t\tt"};
  136 + run();
  137 + CHECK(app.count("-=") == 1u);
  138 + CHECK(app.count("--t\tt") == 1u);
  139 +}
  140 +
130 141 TEST_CASE_METHOD(TApp, "RequireOptionsError", "[app]") {
131 142 using Catch::Matchers::Contains;
132 143  
... ... @@ -582,6 +593,20 @@ TEST_CASE_METHOD(TApp, &quot;SingleArgVector&quot;, &quot;[app]&quot;) {
582 593 CHECK("happy" == path);
583 594 }
584 595  
  596 +TEST_CASE_METHOD(TApp, "StrangeOptionNames", "[app]") {
  597 + app.add_option("-:");
  598 + app.add_option("--t\tt");
  599 + app.add_option("--{}");
  600 + app.add_option("--:)");
  601 + CHECK_THROWS_AS(app.add_option("--t t"), CLI::ConstructionError);
  602 + args = {"-:)", "--{}", "5"};
  603 + run();
  604 + CHECK(app.count("-:") == 1u);
  605 + CHECK(app.count("--{}") == 1u);
  606 + CHECK(app["-:"]->as<char>() == ')');
  607 + CHECK(app["--{}"]->as<int>() == 5);
  608 +}
  609 +
585 610 TEST_CASE_METHOD(TApp, "FlagLikeOption", "[app]") {
586 611 bool val{false};
587 612 auto opt = app.add_option("--flag", val)->type_size(0)->default_str("true");
... ...
tests/HelpersTest.cpp
... ... @@ -155,13 +155,14 @@ TEST_CASE(&quot;String: InvalidName&quot;, &quot;[helpers]&quot;) {
155 155 CHECK(CLI::detail::valid_name_string("valid"));
156 156 CHECK_FALSE(CLI::detail::valid_name_string("-invalid"));
157 157 CHECK(CLI::detail::valid_name_string("va-li-d"));
158   - CHECK_FALSE(CLI::detail::valid_name_string("vali&d"));
  158 + CHECK_FALSE(CLI::detail::valid_name_string("valid{}"));
159 159 CHECK(CLI::detail::valid_name_string("_valid"));
160   - CHECK_FALSE(CLI::detail::valid_name_string("/valid"));
  160 + CHECK(CLI::detail::valid_name_string("/valid"));
161 161 CHECK(CLI::detail::valid_name_string("vali?d"));
162 162 CHECK(CLI::detail::valid_name_string("@@@@"));
163 163 CHECK(CLI::detail::valid_name_string("b@d2?"));
164 164 CHECK(CLI::detail::valid_name_string("2vali?d"));
  165 + CHECK_FALSE(CLI::detail::valid_name_string("!valid"));
165 166 }
166 167  
167 168 TEST_CASE("StringTools: Modify", "[helpers]") {
... ...
tests/OptionGroupTest.cpp
... ... @@ -23,6 +23,16 @@ TEST_CASE_METHOD(TApp, &quot;BasicOptionGroup&quot;, &quot;[optiongroup]&quot;) {
23 23 CHECK(1u == app.count_all());
24 24 }
25 25  
  26 +TEST_CASE_METHOD(TApp, "OptionGroupInvalidNames", "[optiongroup]") {
  27 + CHECK_THROWS_AS(app.add_option_group("clusters\ncluster2", "description"), CLI::IncorrectConstruction);
  28 +
  29 + std::string groupName("group1");
  30 + groupName += '\0';
  31 + groupName.append("group2");
  32 +
  33 + CHECK_THROWS_AS(app.add_option_group(groupName), CLI::IncorrectConstruction);
  34 +}
  35 +
26 36 TEST_CASE_METHOD(TApp, "BasicOptionGroupExact", "[optiongroup]") {
27 37 auto ogroup = app.add_option_group("clusters");
28 38 int res{0};
... ...
tests/OptionTypeTest.cpp
... ... @@ -269,6 +269,17 @@ TEST_CASE_METHOD(TApp, &quot;vectorDefaults&quot;, &quot;[optiontype]&quot;) {
269 269 CHECK(std::vector<int>({5}) == res);
270 270 }
271 271  
  272 +TEST_CASE_METHOD(TApp, "mapInput", "[optiontype]") {
  273 + std::map<int, std::string> vals{};
  274 + app.add_option("--long", vals);
  275 +
  276 + args = {"--long", "5", "test"};
  277 +
  278 + run();
  279 +
  280 + CHECK(vals.at(5) == "test");
  281 +}
  282 +
272 283 TEST_CASE_METHOD(TApp, "CallbackBoolFlags", "[optiontype]") {
273 284  
274 285 bool value{false};
... ...
tests/SubcommandTest.cpp
... ... @@ -815,10 +815,10 @@ TEST_CASE_METHOD(TApp, &quot;invalidSubcommandName&quot;, &quot;[subcom]&quot;) {
815 815  
816 816 bool gotError{false};
817 817 try {
818   - app.add_subcommand("foo/foo", "Foo a bar");
  818 + app.add_subcommand("!foo/foo", "Foo a bar");
819 819 } catch(const CLI::IncorrectConstruction &e) {
820 820 gotError = true;
821   - CHECK_THAT(e.what(), Contains("/"));
  821 + CHECK_THAT(e.what(), Contains("!"));
822 822 }
823 823 CHECK(gotError);
824 824 }
... ... @@ -1645,6 +1645,28 @@ TEST_CASE_METHOD(TApp, &quot;OptionGroupAlias&quot;, &quot;[subcom]&quot;) {
1645 1645 CHECK(-3 == val);
1646 1646 }
1647 1647  
  1648 +TEST_CASE_METHOD(TApp, "OptionGroupAliasWithSpaces", "[subcom]") {
  1649 + double val{0.0};
  1650 + auto sub = app.add_option_group("sub1");
  1651 + sub->alias("sub2 bb");
  1652 + sub->alias("sub3/b");
  1653 + sub->add_option("-v,--value", val);
  1654 + args = {"sub1", "-v", "-3"};
  1655 + CHECK_THROWS_AS(run(), CLI::ExtrasError);
  1656 +
  1657 + args = {"sub2 bb", "--value", "-5"};
  1658 + run();
  1659 + CHECK(-5.0 == val);
  1660 +
  1661 + args = {"sub3/b", "-v", "7"};
  1662 + run();
  1663 + CHECK(7 == val);
  1664 +
  1665 + args = {"-v", "-3"};
  1666 + run();
  1667 + CHECK(-3 == val);
  1668 +}
  1669 +
1648 1670 TEST_CASE_METHOD(TApp, "subcommand_help", "[subcom]") {
1649 1671 auto sub1 = app.add_subcommand("help")->silent();
1650 1672 bool flag{false};
... ... @@ -1666,9 +1688,8 @@ TEST_CASE_METHOD(TApp, &quot;AliasErrors&quot;, &quot;[subcom]&quot;) {
1666 1688 auto sub1 = app.add_subcommand("sub1");
1667 1689 auto sub2 = app.add_subcommand("sub2");
1668 1690  
1669   - CHECK_THROWS_AS(sub2->alias("this is a not a valid alias"), CLI::IncorrectConstruction);
1670   - CHECK_THROWS_AS(sub2->alias("-alias"), CLI::IncorrectConstruction);
1671   - CHECK_THROWS_AS(sub2->alias("alia$"), CLI::IncorrectConstruction);
  1691 + CHECK_THROWS_AS(sub2->alias("this is a not\n a valid alias"), CLI::IncorrectConstruction);
  1692 + CHECK_NOTHROW(sub2->alias("-alias")); // this is allowed but would be unusable on command line parsers
1672 1693  
1673 1694 CHECK_THROWS_AS(app.add_subcommand("--bad_subcommand_name", "documenting the bad subcommand"),
1674 1695 CLI::IncorrectConstruction);
... ...