Skip to content

Commit 19047d8

Browse files
phlptppre-commit-ci[bot]henryiii
authored
feat: relaxed option naming (CLIUtils#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 <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Henry Schreiner <[email protected]>
1 parent d17016e commit 19047d8

File tree

11 files changed

+132
-60
lines changed

11 files changed

+132
-60
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ App* subcom = app.add_subcommand(name, description);
263263
Option_group *app.add_option_group(name,description);
264264
```
265265

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.
267267

268268
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.
269269

@@ -557,7 +557,7 @@ even exit the program through the callback.
557557
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.
558558

559559
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.
561561

562562
#### Subcommand options
563563

@@ -668,7 +668,7 @@ The subcommand method
668668
.add_option_group(name,description)
669669
```
670670

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.🚧
672672

673673
```cpp
674674
ogroup->add_option(option_pointer);

book/chapters/flags.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ bool my_flag{false};
1111
app.add_flag("-f", my_flag, "Optional description");
1212
```
1313
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.
1515
1616
## Integer flags
1717
@@ -120,4 +120,4 @@ Flag int: 3
120120
Flag plain: 1
121121
```
122122

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ You can use any C++ int-like type, not just `int`. CLI11 understands the followi
2626
| 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" |
2727
| enumeration | any enum or enum class type is supported through conversion from the underlying type(typically int, though it can be specified otherwise) |
2828
| 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<>` |
3030
| 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 |
3131
| 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. (`{}`) |
3232
| streamable | any other type with a `<<` operator will also work |
3333

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.
3535

3636
## Positional options and aliases
3737

@@ -282,4 +282,4 @@ There are some additional options that can be specified to modify an option for
282282
283283
## Unusual circumstances
284284
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

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -368,23 +368,9 @@ class App {
368368

369369
/// Set an alias for the app
370370
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");
386373
}
387-
388374
if(parent_ != nullptr) {
389375
aliases_.push_back(app_name);
390376
auto &res = _compare_subcommand_names(*this, *_get_fallthrough_parent());
@@ -961,6 +947,9 @@ class App {
961947
/// creates an option group as part of the given app
962948
template <typename T = Option_group>
963949
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+
}
964953
auto option_group = std::make_shared<T>(std::move(group_description), group_name, this);
965954
auto ptr = option_group.get();
966955
// move to App_p for overload resolution on older gcc versions
@@ -978,13 +967,13 @@ class App {
978967
if(!subcommand_name.empty() && !detail::valid_name_string(subcommand_name)) {
979968
if(!detail::valid_first_char(subcommand_name[0])) {
980969
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");
982971
}
983972
for(auto c : subcommand_name) {
984973
if(!detail::valid_later_char(c)) {
985974
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 ' '");
988977
}
989978
}
990979
}

include/CLI/Option.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ template <typename CRTP> class OptionBase {
9494

9595
/// Changes the group membership
9696
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+
}
97100
group_ = name;
98101
return static_cast<CRTP *>(this);
99102
}

include/CLI/StringTools.hpp

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,22 @@ inline std::string &remove_quotes(std::string &str) {
157157
return str;
158158
}
159159

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+
160176
/// Make a copy of the string and then trim it, any filter string can be used (any char in string is filtered)
161177
inline std::string trim_copy(const std::string &str, const std::string &filter) {
162178
std::string s = str;
@@ -191,31 +207,43 @@ inline std::ostream &format_aliases(std::ostream &out, const std::vector<std::st
191207
} else {
192208
front = false;
193209
}
194-
out << alias;
210+
out << detail::fix_newlines(" ", alias);
195211
}
196212
out << "\n";
197213
}
198214
return out;
199215
}
200216

201217
/// 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'); }
205220

206221
/// 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+
}
208228

209-
/// Verify an option name
229+
/// Verify an option/subcommand name
210230
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])) {
212232
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))
215237
return false;
216238
return true;
217239
}
218240

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+
219247
/// check if a string is a container segment separator (empty or "%%")
220248
inline bool is_separator(const std::string &str) {
221249
static const std::string sep("%%");
@@ -260,7 +288,7 @@ inline bool has_default_flag_values(const std::string &flags) {
260288
}
261289

262290
inline void remove_default_flag_values(std::string &flags) {
263-
auto loc = flags.find_first_of('{');
291+
auto loc = flags.find_first_of('{', 2);
264292
while(loc != std::string::npos) {
265293
auto finish = flags.find_first_of("},", loc + 1);
266294
if((finish != std::string::npos) && (flags[finish] == '}')) {
@@ -367,22 +395,6 @@ inline std::vector<std::string> split_up(std::string str, char delimiter = '\0')
367395
return output;
368396
}
369397

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-
386398
/// This function detects an equal or colon followed by an escaped quote after an argument
387399
/// then modifies the string to replace the equality with a space. This is needed
388400
/// to allow the split up function to work properly and is intended to be used with the find_and_modify function

0 commit comments

Comments
 (0)