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,7 +263,7 @@ App* subcom = app.add_subcommand(name, description);
263 Option_group *app.add_option_group(name,description); 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 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. 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,7 +557,7 @@ even exit the program through the callback.
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. 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 Subcommands may also have an empty name either by calling `add_subcommand` with an empty string for the name or with no arguments. 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 #### Subcommand options 562 #### Subcommand options
563 563
@@ -668,7 +668,7 @@ The subcommand method @@ -668,7 +668,7 @@ The subcommand method
668 .add_option_group(name,description) 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 ```cpp 673 ```cpp
674 ogroup->add_option(option_pointer); 674 ogroup->add_option(option_pointer);
book/chapters/flags.md
@@ -11,7 +11,7 @@ bool my_flag{false}; @@ -11,7 +11,7 @@ bool my_flag{false};
11 app.add_flag("-f", my_flag, "Optional description"); 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 ## Integer flags 16 ## Integer flags
17 17
@@ -120,4 +120,4 @@ Flag int: 3 @@ -120,4 +120,4 @@ Flag int: 3
120 Flag plain: 1 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,12 +26,12 @@ You can use any C++ int-like type, not just `int`. CLI11 understands the followi
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" | 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 | enumeration | any enum or enum class type is supported through conversion from the underlying type(typically int, though it can be specified otherwise) | 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 | container-like | a container(like vector) of any available types including other containers | 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 | 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 | 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 | 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. (`{}`) | 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 | streamable | any other type with a `<<` operator will also work | 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 ## Positional options and aliases 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,4 +282,4 @@ There are some additional options that can be specified to modify an option for
282 282
283 ## Unusual circumstances 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,23 +368,9 @@ class App {
368 368
369 /// Set an alias for the app 369 /// Set an alias for the app
370 App *alias(std::string app_name) { 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 if(parent_ != nullptr) { 374 if(parent_ != nullptr) {
389 aliases_.push_back(app_name); 375 aliases_.push_back(app_name);
390 auto &res = _compare_subcommand_names(*this, *_get_fallthrough_parent()); 376 auto &res = _compare_subcommand_names(*this, *_get_fallthrough_parent());
@@ -961,6 +947,9 @@ class App { @@ -961,6 +947,9 @@ class App {
961 /// creates an option group as part of the given app 947 /// creates an option group as part of the given app
962 template <typename T = Option_group> 948 template <typename T = Option_group>
963 T *add_option_group(std::string group_name, std::string group_description = "") { 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 auto option_group = std::make_shared<T>(std::move(group_description), group_name, this); 953 auto option_group = std::make_shared<T>(std::move(group_description), group_name, this);
965 auto ptr = option_group.get(); 954 auto ptr = option_group.get();
966 // move to App_p for overload resolution on older gcc versions 955 // move to App_p for overload resolution on older gcc versions
@@ -978,13 +967,13 @@ class App { @@ -978,13 +967,13 @@ class App {
978 if(!subcommand_name.empty() && !detail::valid_name_string(subcommand_name)) { 967 if(!subcommand_name.empty() && !detail::valid_name_string(subcommand_name)) {
979 if(!detail::valid_first_char(subcommand_name[0])) { 968 if(!detail::valid_first_char(subcommand_name[0])) {
980 throw IncorrectConstruction( 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 for(auto c : subcommand_name) { 972 for(auto c : subcommand_name) {
984 if(!detail::valid_later_char(c)) { 973 if(!detail::valid_later_char(c)) {
985 throw IncorrectConstruction(std::string("Subcommand name contains invalid character ('") + c + 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,6 +94,9 @@ template &lt;typename CRTP&gt; class OptionBase {
94 94
95 /// Changes the group membership 95 /// Changes the group membership
96 CRTP *group(const std::string &name) { 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 group_ = name; 100 group_ = name;
98 return static_cast<CRTP *>(this); 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,6 +157,22 @@ inline std::string &amp;remove_quotes(std::string &amp;str) {
157 return str; 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 /// Make a copy of the string and then trim it, any filter string can be used (any char in string is filtered) 176 /// Make a copy of the string and then trim it, any filter string can be used (any char in string is filtered)
161 inline std::string trim_copy(const std::string &str, const std::string &filter) { 177 inline std::string trim_copy(const std::string &str, const std::string &filter) {
162 std::string s = str; 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,7 +207,7 @@ inline std::ostream &amp;format_aliases(std::ostream &amp;out, const std::vector&lt;std::st
191 } else { 207 } else {
192 front = false; 208 front = false;
193 } 209 }
194 - out << alias; 210 + out << detail::fix_newlines(" ", alias);
195 } 211 }
196 out << "\n"; 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,23 +215,35 @@ inline std::ostream &amp;format_aliases(std::ostream &amp;out, const std::vector&lt;std::st
199 } 215 }
200 216
201 /// Verify the first character of an option 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 /// Verify following characters of an option 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 inline bool valid_name_string(const std::string &str) { 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 return false; 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 return false; 237 return false;
216 return true; 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 /// check if a string is a container segment separator (empty or "%%") 247 /// check if a string is a container segment separator (empty or "%%")
220 inline bool is_separator(const std::string &str) { 248 inline bool is_separator(const std::string &str) {
221 static const std::string sep("%%"); 249 static const std::string sep("%%");
@@ -260,7 +288,7 @@ inline bool has_default_flag_values(const std::string &amp;flags) { @@ -260,7 +288,7 @@ inline bool has_default_flag_values(const std::string &amp;flags) {
260 } 288 }
261 289
262 inline void remove_default_flag_values(std::string &flags) { 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 while(loc != std::string::npos) { 292 while(loc != std::string::npos) {
265 auto finish = flags.find_first_of("},", loc + 1); 293 auto finish = flags.find_first_of("},", loc + 1);
266 if((finish != std::string::npos) && (flags[finish] == '}')) { 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,22 +395,6 @@ inline std::vector&lt;std::string&gt; split_up(std::string str, char delimiter = &#39;\0&#39;)
367 return output; 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 /// This function detects an equal or colon followed by an escaped quote after an argument 398 /// This function detects an equal or colon followed by an escaped quote after an argument
387 /// then modifies the string to replace the equality with a space. This is needed 399 /// then modifies the string to replace the equality with a space. This is needed
388 /// to allow the split up function to work properly and is intended to be used with the find_and_modify function 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,6 +127,17 @@ TEST_CASE_METHOD(TApp, &quot;DashedOptionsSingleString&quot;, &quot;[app]&quot;) {
127 CHECK(app.count("--that") == 2u); 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 TEST_CASE_METHOD(TApp, "RequireOptionsError", "[app]") { 141 TEST_CASE_METHOD(TApp, "RequireOptionsError", "[app]") {
131 using Catch::Matchers::Contains; 142 using Catch::Matchers::Contains;
132 143
@@ -582,6 +593,20 @@ TEST_CASE_METHOD(TApp, &quot;SingleArgVector&quot;, &quot;[app]&quot;) { @@ -582,6 +593,20 @@ TEST_CASE_METHOD(TApp, &quot;SingleArgVector&quot;, &quot;[app]&quot;) {
582 CHECK("happy" == path); 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 TEST_CASE_METHOD(TApp, "FlagLikeOption", "[app]") { 610 TEST_CASE_METHOD(TApp, "FlagLikeOption", "[app]") {
586 bool val{false}; 611 bool val{false};
587 auto opt = app.add_option("--flag", val)->type_size(0)->default_str("true"); 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,13 +155,14 @@ TEST_CASE(&quot;String: InvalidName&quot;, &quot;[helpers]&quot;) {
155 CHECK(CLI::detail::valid_name_string("valid")); 155 CHECK(CLI::detail::valid_name_string("valid"));
156 CHECK_FALSE(CLI::detail::valid_name_string("-invalid")); 156 CHECK_FALSE(CLI::detail::valid_name_string("-invalid"));
157 CHECK(CLI::detail::valid_name_string("va-li-d")); 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 CHECK(CLI::detail::valid_name_string("_valid")); 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 CHECK(CLI::detail::valid_name_string("vali?d")); 161 CHECK(CLI::detail::valid_name_string("vali?d"));
162 CHECK(CLI::detail::valid_name_string("@@@@")); 162 CHECK(CLI::detail::valid_name_string("@@@@"));
163 CHECK(CLI::detail::valid_name_string("b@d2?")); 163 CHECK(CLI::detail::valid_name_string("b@d2?"));
164 CHECK(CLI::detail::valid_name_string("2vali?d")); 164 CHECK(CLI::detail::valid_name_string("2vali?d"));
  165 + CHECK_FALSE(CLI::detail::valid_name_string("!valid"));
165 } 166 }
166 167
167 TEST_CASE("StringTools: Modify", "[helpers]") { 168 TEST_CASE("StringTools: Modify", "[helpers]") {
tests/OptionGroupTest.cpp
@@ -23,6 +23,16 @@ TEST_CASE_METHOD(TApp, &quot;BasicOptionGroup&quot;, &quot;[optiongroup]&quot;) { @@ -23,6 +23,16 @@ TEST_CASE_METHOD(TApp, &quot;BasicOptionGroup&quot;, &quot;[optiongroup]&quot;) {
23 CHECK(1u == app.count_all()); 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 TEST_CASE_METHOD(TApp, "BasicOptionGroupExact", "[optiongroup]") { 36 TEST_CASE_METHOD(TApp, "BasicOptionGroupExact", "[optiongroup]") {
27 auto ogroup = app.add_option_group("clusters"); 37 auto ogroup = app.add_option_group("clusters");
28 int res{0}; 38 int res{0};
tests/OptionTypeTest.cpp
@@ -269,6 +269,17 @@ TEST_CASE_METHOD(TApp, &quot;vectorDefaults&quot;, &quot;[optiontype]&quot;) { @@ -269,6 +269,17 @@ TEST_CASE_METHOD(TApp, &quot;vectorDefaults&quot;, &quot;[optiontype]&quot;) {
269 CHECK(std::vector<int>({5}) == res); 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 TEST_CASE_METHOD(TApp, "CallbackBoolFlags", "[optiontype]") { 283 TEST_CASE_METHOD(TApp, "CallbackBoolFlags", "[optiontype]") {
273 284
274 bool value{false}; 285 bool value{false};
tests/SubcommandTest.cpp
@@ -815,10 +815,10 @@ TEST_CASE_METHOD(TApp, &quot;invalidSubcommandName&quot;, &quot;[subcom]&quot;) { @@ -815,10 +815,10 @@ TEST_CASE_METHOD(TApp, &quot;invalidSubcommandName&quot;, &quot;[subcom]&quot;) {
815 815
816 bool gotError{false}; 816 bool gotError{false};
817 try { 817 try {
818 - app.add_subcommand("foo/foo", "Foo a bar"); 818 + app.add_subcommand("!foo/foo", "Foo a bar");
819 } catch(const CLI::IncorrectConstruction &e) { 819 } catch(const CLI::IncorrectConstruction &e) {
820 gotError = true; 820 gotError = true;
821 - CHECK_THAT(e.what(), Contains("/")); 821 + CHECK_THAT(e.what(), Contains("!"));
822 } 822 }
823 CHECK(gotError); 823 CHECK(gotError);
824 } 824 }
@@ -1645,6 +1645,28 @@ TEST_CASE_METHOD(TApp, &quot;OptionGroupAlias&quot;, &quot;[subcom]&quot;) { @@ -1645,6 +1645,28 @@ TEST_CASE_METHOD(TApp, &quot;OptionGroupAlias&quot;, &quot;[subcom]&quot;) {
1645 CHECK(-3 == val); 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 TEST_CASE_METHOD(TApp, "subcommand_help", "[subcom]") { 1670 TEST_CASE_METHOD(TApp, "subcommand_help", "[subcom]") {
1649 auto sub1 = app.add_subcommand("help")->silent(); 1671 auto sub1 = app.add_subcommand("help")->silent();
1650 bool flag{false}; 1672 bool flag{false};
@@ -1666,9 +1688,8 @@ TEST_CASE_METHOD(TApp, &quot;AliasErrors&quot;, &quot;[subcom]&quot;) { @@ -1666,9 +1688,8 @@ TEST_CASE_METHOD(TApp, &quot;AliasErrors&quot;, &quot;[subcom]&quot;) {
1666 auto sub1 = app.add_subcommand("sub1"); 1688 auto sub1 = app.add_subcommand("sub1");
1667 auto sub2 = app.add_subcommand("sub2"); 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 CHECK_THROWS_AS(app.add_subcommand("--bad_subcommand_name", "documenting the bad subcommand"), 1694 CHECK_THROWS_AS(app.add_subcommand("--bad_subcommand_name", "documenting the bad subcommand"),
1674 CLI::IncorrectConstruction); 1695 CLI::IncorrectConstruction);