Commit c67ab9dd435ad2b9db96889e74db04016c207cf0

Authored by Philip Top
Committed by Henry Schreiner
1 parent d5cd9860

Config file handling refactor. (#362)

Refactor some of the configuration file handling code.  Make it easier to get the actual file that was processed, and allow extras in the config file to be ignored (default now), captured or errored.

fix std::error reference and formatting

add test for required but no default and fix a shadow warning on 'required' from gcc 4.8

Test correctness of config write-read loop

fix config generation for flag definitions

make the config output conform with toml

continue work on the config file interpretation and construction

get all the ini tests working again with the cleaned up features.

update formatting

rename IniTest to ConfigFileTest to better reflect actual tests and add a few more test of the configTOML
disambiguate enable/disable by default to an enumeration, and to make room for a configurable option to allow subcommands to be triggered by a config file.
add a ConfigBase class to generally reflect a broader class of configuration files formats of similar nature to INI files

add configurable to app and allow it to trigger subcommands

add test of ini formatting

add section support to the config files so sections can be opened and closed and the callbacks triggered as appropriate.

add handling of option groups to the config file output

add subcommand and option group configuration to config file output

subsubcom test on config files

fix a few sign comparison warnings and formatting

start working on the book edits for configuration and a few more tests

more test to check for subcommand close in config files

more tests for coverage

generalize section opening and closing

add more tests and some fixes for different configurations

yet more tests of different situations related to configuration files

test more paths for configuration file sections

remove some unused code and fix some codacy warnings

update readme with updates from configuration files

more book edits and README formatting

remove extra space

Apply suggestions from code review

Co-Authored-By: Henry Schreiner <HenrySchreinerIII@gmail.com>

fix some comments and documentation

fix spacing

Rename size_t -> std::size_t

Fix compiler warnings with -Wsign-conversion

Fix new warnings with -Wsign-conversion in PR
README.md
@@ -83,7 +83,7 @@ An acceptable CLI parser library should be all of the following: @@ -83,7 +83,7 @@ An acceptable CLI parser library should be all of the following:
83 - Easy to execute, with help, parse errors, etc. providing correct exit and details. 83 - Easy to execute, with help, parse errors, etc. providing correct exit and details.
84 - Easy to extend as part of a framework that provides "applications" to users. 84 - Easy to extend as part of a framework that provides "applications" to users.
85 - Usable subcommand syntax, with support for multiple subcommands, nested subcommands, option groups, and optional fallthrough (explained later). 85 - Usable subcommand syntax, with support for multiple subcommands, nested subcommands, option groups, and optional fallthrough (explained later).
86 -- Ability to add a configuration file (`ini` format), and produce it as well. 86 +- Ability to add a configuration file (`ini` or `TOML`๐Ÿšง format), and produce it as well.
87 - Produce real values that can be used directly in code, not something you have pay compute time to look up, for HPC applications. 87 - Produce real values that can be used directly in code, not something you have pay compute time to look up, for HPC applications.
88 - Work with standard types, simple custom types, and extensible to exotic types. 88 - Work with standard types, simple custom types, and extensible to exotic types.
89 - Permissively licensed. 89 - Permissively licensed.
@@ -411,7 +411,7 @@ will produce a check for a number less than or equal to 0. @@ -411,7 +411,7 @@ will produce a check for a number less than or equal to 0.
411 ##### Transforming Validators 411 ##### Transforming Validators
412 There are a few built in Validators that let you transform values if used with the `transform` function. If they also do some checks then they can be used `check` but some may do nothing in that case. 412 There are a few built in Validators that let you transform values if used with the `transform` function. If they also do some checks then they can be used `check` but some may do nothing in that case.
413 - ๐Ÿ†• `CLI::Bounded(min,max)` will bound values between min and max and values outside of that range are limited to min or max, it will fail if the value cannot be converted and produce a `ValidationError` 413 - ๐Ÿ†• `CLI::Bounded(min,max)` will bound values between min and max and values outside of that range are limited to min or max, it will fail if the value cannot be converted and produce a `ValidationError`
414 -- ๐Ÿ†• The `IsMember` Validator lets you specify a set of predefined options. You can pass any container or copyable pointer (including `std::shared_ptr`) to a container to this validator; the container just needs to be iterable and have a `::value_type`. The key type should be convertible from a string, You can use an initializer list directly if you like. If you need to modify the set later, the pointer form lets you do that; the type message and check will correctly refer to the current version of the set. The container passed in can be a set, vector, or a map like structure. If used in the `transform` method the output value will be the matching key as it could be modified by filters. 414 +- ๐Ÿ†• The `IsMember` Validator lets you specify a set of predefined options. You can pass any container or copyable pointer (including `std::shared_ptr`) to a container to this Validator; the container just needs to be iterable and have a `::value_type`. The key type should be convertible from a string, You can use an initializer list directly if you like. If you need to modify the set later, the pointer form lets you do that; the type message and check will correctly refer to the current version of the set. The container passed in can be a set, vector, or a map like structure. If used in the `transform` method the output value will be the matching key as it could be modified by filters.
415 After specifying a set of options, you can also specify "filter" functions of the form `T(T)`, where `T` is the type of the values. The most common choices probably will be `CLI::ignore_case` an `CLI::ignore_underscore`, and `CLI::ignore_space`. These all work on strings but it is possible to define functions that work on other types. 415 After specifying a set of options, you can also specify "filter" functions of the form `T(T)`, where `T` is the type of the values. The most common choices probably will be `CLI::ignore_case` an `CLI::ignore_underscore`, and `CLI::ignore_space`. These all work on strings but it is possible to define functions that work on other types.
416 Here are some examples 416 Here are some examples
417 of `IsMember`: 417 of `IsMember`:
@@ -532,6 +532,7 @@ There are several options that are supported on the main app and subcommands and @@ -532,6 +532,7 @@ There are several options that are supported on the main app and subcommands and
532 - `.ignore_underscore()`: Ignore any underscores in the subcommand name. Inherited by added subcommands, so is usually used on the main `App`. 532 - `.ignore_underscore()`: Ignore any underscores in the subcommand name. Inherited by added subcommands, so is usually used on the main `App`.
533 - `.allow_windows_style_options()`: Allow command line options to be parsed in the form of `/s /long /file:file_name.ext` This option does not change how options are specified in the `add_option` calls or the ability to process options in the form of `-s --long --file=file_name.ext`. 533 - `.allow_windows_style_options()`: Allow command line options to be parsed in the form of `/s /long /file:file_name.ext` This option does not change how options are specified in the `add_option` calls or the ability to process options in the form of `-s --long --file=file_name.ext`.
534 - `.fallthrough()`: Allow extra unmatched options and positionals to "fall through" and be matched on a parent command. Subcommands always are allowed to fall through. 534 - `.fallthrough()`: Allow extra unmatched options and positionals to "fall through" and be matched on a parent command. Subcommands always are allowed to fall through.
  535 +- `.configurable()`: ๐Ÿšง Allow the subcommand to be triggered from a configuration file.
535 - `.disable()`: ๐Ÿ†• Specify that the subcommand is disabled, if given with a bool value it will enable or disable the subcommand or option group. 536 - `.disable()`: ๐Ÿ†• Specify that the subcommand is disabled, if given with a bool value it will enable or disable the subcommand or option group.
536 - `.disabled_by_default()`: ๐Ÿ†• Specify that at the start of parsing the subcommand/option_group should be disabled. This is useful for allowing some Subcommands to trigger others. 537 - `.disabled_by_default()`: ๐Ÿ†• Specify that at the start of parsing the subcommand/option_group should be disabled. This is useful for allowing some Subcommands to trigger others.
537 - `.enabled_by_default()`: ๐Ÿ†• Specify that at the start of each parse the subcommand/option_group should be enabled. This is useful for allowing some Subcommands to disable others. 538 - `.enabled_by_default()`: ๐Ÿ†• Specify that at the start of each parse the subcommand/option_group should be enabled. This is useful for allowing some Subcommands to disable others.
@@ -689,7 +690,7 @@ app.set_config(option_name=&quot;&quot;, @@ -689,7 +690,7 @@ app.set_config(option_name=&quot;&quot;,
689 required=false) 690 required=false)
690 ``` 691 ```
691 692
692 -If this is called with no arguments, it will remove the configuration file option (like `set_help_flag`). Setting a configuration option is special. If it is present, it will be read along with the normal command line arguments. The file will be read if it exists, and does not throw an error unless `required` is `true`. Configuration files are in `ini` format by default (other formats can be added by an adept user). An example of a file: 693 +If this is called with no arguments, it will remove the configuration file option (like `set_help_flag`). Setting a configuration option is special. If it is present, it will be read along with the normal command line arguments. The file will be read if it exists, and does not throw an error unless `required` is `true`. Configuration files are in `ini` format by default, The reader can also accept many files in [TOML] format ๐Ÿšง. (other formats can be added by an adept user, some variations are available through customization points in the default formatter). An example of a file:
693 694
694 ```ini 695 ```ini
695 ; Comments are supported, using a ; 696 ; Comments are supported, using a ;
@@ -705,11 +706,26 @@ str_vector = &quot;one&quot; &quot;two&quot; &quot;and three&quot; @@ -705,11 +706,26 @@ str_vector = &quot;one&quot; &quot;two&quot; &quot;and three&quot;
705 in_subcommand = Wow 706 in_subcommand = Wow
706 sub.subcommand = true 707 sub.subcommand = true
707 ``` 708 ```
  709 + or equivalently in TOML ๐Ÿšง
  710 +```toml
  711 +# Comments are supported, using a #
  712 +# The default section is [default], case insensitive
708 713
709 -Spaces before and after the name and argument are ignored. Multiple arguments are separated by spaces. One set of quotes will be removed, preserving spaces (the same way the command line works). Boolean options can be `true`, `on`, `1`, `yes`, ๐Ÿ†• `enable`; or `false`, `off`, `0`, `no`, ๐Ÿ†• `disable` (case insensitive). Sections (and `.` separated names) are treated as subcommands (note: this does not mean that subcommand was passed, it just sets the "defaults". You cannot set positional-only arguments or force subcommands to be present in the command line. 714 +value = 1
  715 +str = "A string"
  716 +vector = [1,2,3]
  717 +str_vector = ["one","two","and three"]
  718 +
  719 +# Sections map to subcommands
  720 +[subcommand]
  721 +in_subcommand = Wow
  722 +sub.subcommand = true
  723 +```
  724 +
  725 +Spaces before and after the name and argument are ignored. Multiple arguments are separated by spaces. One set of quotes will be removed, preserving spaces (the same way the command line works). Boolean options can be `true`, `on`, `1`, `yes`, ๐Ÿ†• `enable`; or `false`, `off`, `0`, `no`, ๐Ÿ†• `disable` (case insensitive). Sections (and `.` separated names) are treated as subcommands (note: this does not necessarily mean that subcommand was passed, it just sets the "defaults"). You cannot set positional-only arguments. ๐Ÿšง Subcommands can be triggered from config files if the `configurable` flag was set on the subcommand. Then use `[subcommand]` notation will trigger a subcommand and cause it to act as if it were on the command line.
710 726
711 To print a configuration file from the passed 727 To print a configuration file from the passed
712 -arguments, use `.config_to_str(default_also=false, prefix="", write_description=false)`, where `default_also` will also show any defaulted arguments, `prefix` will add a prefix, and `write_description` will include option descriptions. 728 +arguments, use `.config_to_str(default_also=false, prefix="", write_description=false)`, where `default_also` will also show any defaulted arguments, `prefix` will add a prefix, and `write_description` will include option descriptions. See [Config files](https://cliutils.github.io/CLI11/book/chapters/config.html) for some additional details.
713 729
714 ### Inheriting defaults 730 ### Inheriting defaults
715 731
@@ -917,7 +933,7 @@ CLI11 was developed at the [University of Cincinnati][] to support of the [GooFi @@ -917,7 +933,7 @@ CLI11 was developed at the [University of Cincinnati][] to support of the [GooFi
917 [doi-badge]: https://zenodo.org/badge/80064252.svg 933 [doi-badge]: https://zenodo.org/badge/80064252.svg
918 [doi-link]: https://zenodo.org/badge/latestdoi/80064252 934 [doi-link]: https://zenodo.org/badge/latestdoi/80064252
919 [azure-badge]: https://dev.azure.com/CLIUtils/CLI11/_apis/build/status/CLIUtils.CLI11?branchName=master 935 [azure-badge]: https://dev.azure.com/CLIUtils/CLI11/_apis/build/status/CLIUtils.CLI11?branchName=master
920 -[azure]: https://dev.azure.com/CLIUtils/CLI11/_build/|latest?definitionId=1&branchName=master 936 +[azure]: https://dev.azure.com/CLIUtils/CLI11/_build/latest?definitionId=1&branchName=master
921 [travis-badge]: https://img.shields.io/travis/CLIUtils/CLI11/master.svg?label=Linux/macOS 937 [travis-badge]: https://img.shields.io/travis/CLIUtils/CLI11/master.svg?label=Linux/macOS
922 [travis]: https://travis-ci.org/CLIUtils/CLI11 938 [travis]: https://travis-ci.org/CLIUtils/CLI11
923 [appveyor-badge]: https://img.shields.io/appveyor/ci/HenrySchreiner/cli11/master.svg?label=Windows 939 [appveyor-badge]: https://img.shields.io/appveyor/ci/HenrySchreiner/cli11/master.svg?label=Windows
book/chapters/config.md
@@ -2,15 +2,49 @@ @@ -2,15 +2,49 @@
2 2
3 ## Reading a configure file 3 ## Reading a configure file
4 4
5 -You can tell your app to allow configure files with `set_config("--config")`. There are arguments: the first is the option name. If empty, it will clear the config flag. The second item is the default file name. If that is specified, the config will try to read that file. The third item is the help string, with a reasonable default, and the final argument is a boolean (default: false) that indicates that the configuration file is required and an error will be thrown if the file  
6 -is not found and this is set to true. 5 +You can tell your app to allow configure files with `set_config("--config")`. There are arguments: the first is the option name. If empty, it will clear the config flag. The second item is the default file name. If that is specified, the config will try to read that file. The third item is the help string, with a reasonable default, and the final argument is a boolean (default: false) that indicates that the configuration file is required and an error will be thrown if the file is not found and this is set to true.
  6 +
  7 +### Extra fields
  8 +Sometimes configuration files are used for multiple purposes so CLI11 allows options on how to deal with extra fields
  9 +
  10 +```cpp
  11 +app.allow_config_extras(true);
  12 +```
  13 +will allow capture the extras in the extras field of the app. (NOTE: This also sets the `allow_extras` in the app to true)
  14 +
  15 +```cpp
  16 +app.allow_config_extras(false);
  17 +```
  18 +will generate an error if there are any extra fields
  19 +
  20 +for slightly finer control there is a scoped enumeration of the modes
  21 +or
  22 +```cpp
  23 +app.allow_config_extras(CLI::config_extras_mode::ignore);
  24 +```
  25 +will completely ignore extra parameters in the config file. This mode is the default.
  26 +
  27 +```cpp
  28 +app.allow_config_extras(CLI::config_extras_mode::capture);
  29 +```
  30 +will store the unrecognized options in the app extras fields. This option is the closest equivalent to `app.allow_config_extras(true);` with the exception that it does not also set the `allow_extras` flag so using this option without also setting `allow_extras(true)` will generate an error which may or may not be the desired behavior.
  31 +
  32 +```cpp
  33 +app.allow_config_extras(CLI::config_extras_mode::error);
  34 +```
  35 +is equivalent to `app.allow_config_extras(false);`
  36 +
  37 +### Getting the used configuration file name
  38 +If it is needed to get the configuration file name used this can be obtained via
  39 +`app.get_config_ptr()->as<std::string>()` or
  40 +`app["--config"]->as<std::string>()` assuming `--config` was the configuration option name.
7 41
8 ## Configure file format 42 ## Configure file format
9 43
10 Here is an example configuration file, in INI format: 44 Here is an example configuration file, in INI format:
11 45
12 ```ini 46 ```ini
13 -; Commments are supported, using a ; 47 +; Comments are supported, using a ;
14 ; The default section is [default], case insensitive 48 ; The default section is [default], case insensitive
15 49
16 value = 1 50 value = 1
@@ -23,12 +57,66 @@ in_subcommand = Wow @@ -23,12 +57,66 @@ in_subcommand = Wow
23 sub.subcommand = true 57 sub.subcommand = true
24 ``` 58 ```
25 59
26 -Spaces before and after the name and argument are ignored. Multiple arguments are separated by spaces. One set of quotes will be removed, preserving spaces (the same way the command line works). Boolean options can be `true`, `on`, `1`, `yes`; or `false`, `off`, `0`, `no` (case insensitive). Sections (and `.` separated names) are treated as subcommands (note: this does not mean that subcommand was passed, it just sets the "defaults". 60 +Spaces before and after the name and argument are ignored. Multiple arguments are separated by spaces. One set of quotes will be removed, preserving spaces (the same way the command line works). Boolean options can be `true`, `on`, `1`, `y`, `t`, `+`, `yes`, `enable`; or `false`, `off`, `0`, `no`, `n`, `f`, `-`, `disable`, (case insensitive). Sections (and `.` separated names) are treated as subcommands (note: this does not necessarily mean that subcommand was passed, it just sets the "defaults". If a subcommand is set to `configurable` then passing the subcommand using `[sub]` in a configuration file will trigger the subcommand.)
  61 +
  62 +CLI11 also supports configuration file in [TOML](https://github.com/toml-lang/toml) format.
  63 +
  64 +```toml
  65 +# Comments are supported, using a #
  66 +# The default section is [default], case insensitive
  67 +
  68 +value = 1
  69 +str = "A string"
  70 +vector = [1,2,3]
  71 +
  72 +# Section map to subcommands
  73 +[subcommand]
  74 +in_subcommand = Wow
  75 +[subcommand.sub]
  76 +subcommand = true # could also be give as sub.subcommand=true
  77 +```
  78 +
  79 +The main differences are in vector notation and comment character. Note: CLI11 is not a full TOML parser as it just reads values as strings. It is possible (but not recommended) to mix notation.
27 80
28 ## Writing out a configure file 81 ## Writing out a configure file
29 82
30 To print a configuration file from the passed arguments, use `.config_to_str(default_also=false, prefix="", write_description=false)`, where `default_also` will also show any defaulted arguments, `prefix` will add a prefix, and `write_description` will include option descriptions. 83 To print a configuration file from the passed arguments, use `.config_to_str(default_also=false, prefix="", write_description=false)`, where `default_also` will also show any defaulted arguments, `prefix` will add a prefix, and `write_description` will include option descriptions.
31 84
  85 +### Customization of configure file output
  86 +The default config parser/generator has some customization points that allow variations on the INI format. The default formatter has a base configuration that matches the INI format. It defines 5 characters that define how different aspects of the configuration are handled
  87 +```cpp
  88 +/// the character used for comments
  89 +char commentChar = ';';
  90 +/// the character used to start an array '\0' is a default to not use
  91 +char arrayStart = '\0';
  92 +/// the character used to end an array '\0' is a default to not use
  93 +char arrayEnd = '\0';
  94 +/// the character used to separate elements in an array
  95 +char arraySeparator = ' ';
  96 +/// the character used separate the name from the value
  97 +char valueDelimiter = '=';
  98 +```
  99 +
  100 +These can be modified via setter functions
  101 +
  102 +- ` ConfigBase *comment(char cchar)` Specify the character to start a comment block
  103 +- `ConfigBase *arrayBounds(char aStart, char aEnd)` Specify the start and end characters for an array
  104 +- `ConfigBase *arrayDelimiter(char aSep)` Specify the delimiter character for an array
  105 +- `ConfigBase *valueSeparator(char vSep)` Specify the delimiter between a name and value
  106 +
  107 +For example to specify reading a configure file that used `:` to separate name and values
  108 +
  109 +```cpp
  110 +auto config_base=app.get_config_formatter_base();
  111 +config_base->valueSeparator(':');
  112 +```
  113 +
  114 +The default configuration file will read TOML files, but will write out files in the INI format. To specify outputting TOML formatted files use
  115 +```cpp
  116 +app.config_formatter(std::make_shared<CLI::ConfigTOML>());
  117 +```
  118 +which makes use of a predefined modification of the ConfigBase class which INI also uses.
  119 +
32 ## Custom formats 120 ## Custom formats
33 121
34 {% hint style='info' %} 122 {% hint style='info' %}
@@ -51,3 +139,9 @@ app.config_formatter(std::make_shared&lt;NewConfig&gt;()); @@ -51,3 +139,9 @@ app.config_formatter(std::make_shared&lt;NewConfig&gt;());
51 ``` 139 ```
52 140
53 See [`examples/json.cpp`](https://github.com/CLIUtils/CLI11/blob/master/examples/json.cpp) for a complete JSON config example. 141 See [`examples/json.cpp`](https://github.com/CLIUtils/CLI11/blob/master/examples/json.cpp) for a complete JSON config example.
  142 +
  143 +
  144 +## Triggering Subcommands
  145 +Configuration files can be used to trigger subcommands if a subcommand is set to configure. By default configuration file just set the default values of a subcommand. But if the `configure()` option is set on a subcommand then the if the subcommand is utilized via a `[subname]` block in the configuration file it will act as if it were called from the command line. Subsubcommands can be triggered via [subname.subsubname]. Using the `[[subname]]` will be as if the subcommand were triggered multiple times from the command line. This functionality can allow the configuration file to act as a scripting file.
  146 +
  147 +For custom configuration files this behavior can be triggered by specifying the parent subcommands in the structure and `++` as the name to open a new subcommand scope and `--` to close it. These names trigger the different callbacks of configurable subcommands.
book/chapters/options.md
@@ -126,7 +126,7 @@ app.add_flag(&quot;--CaSeLeSs&quot;); @@ -126,7 +126,7 @@ app.add_flag(&quot;--CaSeLeSs&quot;);
126 app.get_group() // is "Required" 126 app.get_group() // is "Required"
127 ``` 127 ```
128 128
129 -Groups are mostly for visual organisation, but an empty string for a group name will hide the option. 129 +Groups are mostly for visual organization, but an empty string for a group name will hide the option.
130 130
131 131
132 ## Listing of specialty options: 132 ## Listing of specialty options:
include/CLI/App.hpp
@@ -45,6 +45,10 @@ std::string simple(const App *app, const Error &amp;e); @@ -45,6 +45,10 @@ std::string simple(const App *app, const Error &amp;e);
45 std::string help(const App *app, const Error &e); 45 std::string help(const App *app, const Error &e);
46 } // namespace FailureMessage 46 } // namespace FailureMessage
47 47
  48 +/// enumeration of modes of how to deal with extras in config files
  49 +
  50 +enum class config_extras_mode : char { error = 0, ignore, capture };
  51 +
48 class App; 52 class App;
49 53
50 using App_p = std::shared_ptr<App>; 54 using App_p = std::shared_ptr<App>;
@@ -73,8 +77,9 @@ class App { @@ -73,8 +77,9 @@ class App {
73 /// If true, allow extra arguments (ie, don't throw an error). INHERITABLE 77 /// If true, allow extra arguments (ie, don't throw an error). INHERITABLE
74 bool allow_extras_{false}; 78 bool allow_extras_{false};
75 79
76 - /// If true, allow extra arguments in the ini file (ie, don't throw an error). INHERITABLE  
77 - bool allow_config_extras_{false}; 80 + /// If ignore, allow extra arguments in the ini file (ie, don't throw an error). INHERITABLE
  81 + /// if error error on an extra argument, and if capture feed it to the app
  82 + config_extras_mode allow_config_extras_{config_extras_mode::ignore};
78 83
79 /// If true, return immediately on an unrecognized option (implies allow_extras) INHERITABLE 84 /// If true, return immediately on an unrecognized option (implies allow_extras) INHERITABLE
80 bool prefix_command_{false}; 85 bool prefix_command_{false};
@@ -194,12 +199,17 @@ class App { @@ -194,12 +199,17 @@ class App {
194 /// specify that positional arguments come at the end of the argument sequence not inheritable 199 /// specify that positional arguments come at the end of the argument sequence not inheritable
195 bool positionals_at_end_{false}; 200 bool positionals_at_end_{false};
196 201
197 - /// If set to true the subcommand will start each parse disabled  
198 - bool disabled_by_default_{false};  
199 - /// If set to true the subcommand will be reenabled at the start of each parse  
200 - bool enabled_by_default_{false}; 202 + enum class startup_mode : char { stable, enabled, disabled };
  203 + /// specify the startup mode for the app
  204 + /// stable=no change, enabled= startup enabled, disabled=startup disabled
  205 + startup_mode default_startup{startup_mode::stable};
  206 +
  207 + /// if set to true the subcommand can be triggered via configuration files INHERITABLE
  208 + bool configurable_{false};
  209 +
201 /// If set to true positional options are validated before assigning INHERITABLE 210 /// If set to true positional options are validated before assigning INHERITABLE
202 bool validate_positionals_{false}; 211 bool validate_positionals_{false};
  212 +
203 /// A pointer to the parent if this is a subcommand 213 /// A pointer to the parent if this is a subcommand
204 App *parent_{nullptr}; 214 App *parent_{nullptr};
205 215
@@ -228,12 +238,6 @@ class App { @@ -228,12 +238,6 @@ class App {
228 /// @name Config 238 /// @name Config
229 ///@{ 239 ///@{
230 240
231 - /// The name of the connected config file  
232 - std::string config_name_{};  
233 -  
234 - /// True if ini is required (throws if not present), if false simply keep going.  
235 - bool config_required_{false};  
236 -  
237 /// Pointer to the config option 241 /// Pointer to the config option
238 Option *config_ptr_{nullptr}; 242 Option *config_ptr_{nullptr};
239 243
@@ -266,6 +270,7 @@ class App { @@ -266,6 +270,7 @@ class App {
266 ignore_underscore_ = parent_->ignore_underscore_; 270 ignore_underscore_ = parent_->ignore_underscore_;
267 fallthrough_ = parent_->fallthrough_; 271 fallthrough_ = parent_->fallthrough_;
268 validate_positionals_ = parent_->validate_positionals_; 272 validate_positionals_ = parent_->validate_positionals_;
  273 + configurable_ = parent_->configurable_;
269 allow_windows_style_options_ = parent_->allow_windows_style_options_; 274 allow_windows_style_options_ = parent_->allow_windows_style_options_;
270 group_ = parent_->group_; 275 group_ = parent_->group_;
271 footer_ = parent_->footer_; 276 footer_ = parent_->footer_;
@@ -385,14 +390,23 @@ class App { @@ -385,14 +390,23 @@ class App {
385 390
386 /// Set the subcommand to be disabled by default, so on clear(), at the start of each parse it is disabled 391 /// Set the subcommand to be disabled by default, so on clear(), at the start of each parse it is disabled
387 App *disabled_by_default(bool disable = true) { 392 App *disabled_by_default(bool disable = true) {
388 - disabled_by_default_ = disable; 393 + if(disable) {
  394 + default_startup = startup_mode::disabled;
  395 + } else {
  396 + default_startup = (default_startup == startup_mode::enabled) ? startup_mode::enabled : startup_mode::stable;
  397 + }
389 return this; 398 return this;
390 } 399 }
391 400
392 /// Set the subcommand to be enabled by default, so on clear(), at the start of each parse it is enabled (not 401 /// Set the subcommand to be enabled by default, so on clear(), at the start of each parse it is enabled (not
393 /// disabled) 402 /// disabled)
394 App *enabled_by_default(bool enable = true) { 403 App *enabled_by_default(bool enable = true) {
395 - enabled_by_default_ = enable; 404 + if(enable) {
  405 + default_startup = startup_mode::enabled;
  406 + } else {
  407 + default_startup =
  408 + (default_startup == startup_mode::disabled) ? startup_mode::disabled : startup_mode::stable;
  409 + }
396 return this; 410 return this;
397 } 411 }
398 412
@@ -415,11 +429,20 @@ class App { @@ -415,11 +429,20 @@ class App {
415 return this; 429 return this;
416 } 430 }
417 431
418 - /// Remove the error when extras are left over on the command line.  
419 - /// Will also call App::allow_extras(). 432 + /// ignore extras in config files
420 App *allow_config_extras(bool allow = true) { 433 App *allow_config_extras(bool allow = true) {
421 - allow_extras(allow);  
422 - allow_config_extras_ = allow; 434 + if(allow) {
  435 + allow_config_extras_ = config_extras_mode::capture;
  436 + allow_extras_ = true;
  437 + } else {
  438 + allow_config_extras_ = config_extras_mode::error;
  439 + }
  440 + return this;
  441 + }
  442 +
  443 + /// ignore extras in config files
  444 + App *allow_config_extras(config_extras_mode mode) {
  445 + allow_config_extras_ = mode;
423 return this; 446 return this;
424 } 447 }
425 448
@@ -457,6 +480,12 @@ class App { @@ -457,6 +480,12 @@ class App {
457 return this; 480 return this;
458 } 481 }
459 482
  483 + /// Specify that the subcommand can be triggered by a config file
  484 + App *configurable(bool value = true) {
  485 + configurable_ = value;
  486 + return this;
  487 + }
  488 +
460 /// Ignore underscore. Subcommands inherit value. 489 /// Ignore underscore. Subcommands inherit value.
461 App *ignore_underscore(bool value = true) { 490 App *ignore_underscore(bool value = true) {
462 if(value && !ignore_underscore_) { 491 if(value && !ignore_underscore_) {
@@ -889,22 +918,24 @@ class App { @@ -889,22 +918,24 @@ class App {
889 /// Set a configuration ini file option, or clear it if no name passed 918 /// Set a configuration ini file option, or clear it if no name passed
890 Option *set_config(std::string option_name = "", 919 Option *set_config(std::string option_name = "",
891 std::string default_filename = "", 920 std::string default_filename = "",
892 - std::string help_message = "Read an ini file", 921 + const std::string &help_message = "Read an ini file",
893 bool config_required = false) { 922 bool config_required = false) {
894 923
895 // Remove existing config if present 924 // Remove existing config if present
896 if(config_ptr_ != nullptr) { 925 if(config_ptr_ != nullptr) {
897 remove_option(config_ptr_); 926 remove_option(config_ptr_);
898 - config_name_ = "";  
899 - config_required_ = false; // Not really needed, but complete  
900 - config_ptr_ = nullptr; // need to remove the config_ptr completely 927 + config_ptr_ = nullptr; // need to remove the config_ptr completely
901 } 928 }
902 929
903 // Only add config if option passed 930 // Only add config if option passed
904 if(!option_name.empty()) { 931 if(!option_name.empty()) {
905 - config_name_ = default_filename;  
906 - config_required_ = config_required;  
907 - config_ptr_ = add_option(option_name, config_name_, help_message, !default_filename.empty()); 932 + config_ptr_ = add_option(option_name, help_message);
  933 + if(config_required) {
  934 + config_ptr_->required();
  935 + }
  936 + if(!default_filename.empty()) {
  937 + config_ptr_->default_str(std::move(default_filename));
  938 + }
908 config_ptr_->configurable(false); 939 config_ptr_->configurable(false);
909 } 940 }
910 941
@@ -989,12 +1020,12 @@ class App { @@ -989,12 +1020,12 @@ class App {
989 } 1020 }
990 /// Check to see if a subcommand is part of this command (doesn't have to be in command line) 1021 /// Check to see if a subcommand is part of this command (doesn't have to be in command line)
991 /// returns the first subcommand if passed a nullptr 1022 /// returns the first subcommand if passed a nullptr
992 - App *get_subcommand(App *subcom) const { 1023 + App *get_subcommand(const App *subcom) const {
993 if(subcom == nullptr) 1024 if(subcom == nullptr)
994 throw OptionNotFound("nullptr passed"); 1025 throw OptionNotFound("nullptr passed");
995 for(const App_p &subcomptr : subcommands_) 1026 for(const App_p &subcomptr : subcommands_)
996 if(subcomptr.get() == subcom) 1027 if(subcomptr.get() == subcom)
997 - return subcom; 1028 + return subcomptr.get();
998 throw OptionNotFound(subcom->get_name()); 1029 throw OptionNotFound(subcom->get_name());
999 } 1030 }
1000 1031
@@ -1342,7 +1373,7 @@ class App { @@ -1342,7 +1373,7 @@ class App {
1342 } 1373 }
1343 1374
1344 /// Check to see if given subcommand was selected 1375 /// Check to see if given subcommand was selected
1345 - bool got_subcommand(App *subcom) const { 1376 + bool got_subcommand(const App *subcom) const {
1346 // get subcom needed to verify that this was a real subcommand 1377 // get subcom needed to verify that this was a real subcommand
1347 return get_subcommand(subcom)->parsed_ > 0; 1378 return get_subcommand(subcom)->parsed_ > 0;
1348 } 1379 }
@@ -1482,6 +1513,11 @@ class App { @@ -1482,6 +1513,11 @@ class App {
1482 /// Access the config formatter 1513 /// Access the config formatter
1483 std::shared_ptr<Config> get_config_formatter() const { return config_formatter_; } 1514 std::shared_ptr<Config> get_config_formatter() const { return config_formatter_; }
1484 1515
  1516 + /// Access the config formatter as a configBase pointer
  1517 + std::shared_ptr<ConfigBase> get_config_formatter_base() const {
  1518 + return std::dynamic_pointer_cast<ConfigBase>(config_formatter_);
  1519 + }
  1520 +
1485 /// Get the app or subcommand description 1521 /// Get the app or subcommand description
1486 std::string get_description() const { return description_; } 1522 std::string get_description() const { return description_; }
1487 1523
@@ -1601,6 +1637,9 @@ class App { @@ -1601,6 +1637,9 @@ class App {
1601 /// Check the status of the allow windows style options 1637 /// Check the status of the allow windows style options
1602 bool get_positionals_at_end() const { return positionals_at_end_; } 1638 bool get_positionals_at_end() const { return positionals_at_end_; }
1603 1639
  1640 + /// Check the status of the allow windows style options
  1641 + bool get_configurable() const { return configurable_; }
  1642 +
1604 /// Get the group of this subcommand 1643 /// Get the group of this subcommand
1605 const std::string &get_group() const { return group_; } 1644 const std::string &get_group() const { return group_; }
1606 1645
@@ -1635,15 +1674,15 @@ class App { @@ -1635,15 +1674,15 @@ class App {
1635 bool get_immediate_callback() const { return immediate_callback_; } 1674 bool get_immediate_callback() const { return immediate_callback_; }
1636 1675
1637 /// Get the status of disabled by default 1676 /// Get the status of disabled by default
1638 - bool get_disabled_by_default() const { return disabled_by_default_; } 1677 + bool get_disabled_by_default() const { return (default_startup == startup_mode::disabled); }
1639 1678
1640 /// Get the status of disabled by default 1679 /// Get the status of disabled by default
1641 - bool get_enabled_by_default() const { return enabled_by_default_; } 1680 + bool get_enabled_by_default() const { return (default_startup == startup_mode::enabled); }
1642 /// Get the status of validating positionals 1681 /// Get the status of validating positionals
1643 bool get_validate_positionals() const { return validate_positionals_; } 1682 bool get_validate_positionals() const { return validate_positionals_; }
1644 1683
1645 /// Get the status of allow extras 1684 /// Get the status of allow extras
1646 - bool get_allow_config_extras() const { return allow_config_extras_; } 1685 + config_extras_mode get_allow_config_extras() const { return allow_config_extras_; }
1647 1686
1648 /// Get a pointer to the help flag. 1687 /// Get a pointer to the help flag.
1649 Option *get_help_ptr() { return help_ptr_; } 1688 Option *get_help_ptr() { return help_ptr_; }
@@ -1823,11 +1862,10 @@ class App { @@ -1823,11 +1862,10 @@ class App {
1823 /// set the correct fallthrough and prefix for nameless subcommands and manage the automatic enable or disable 1862 /// set the correct fallthrough and prefix for nameless subcommands and manage the automatic enable or disable
1824 /// makes sure parent is set correctly 1863 /// makes sure parent is set correctly
1825 void _configure() { 1864 void _configure() {
1826 - if(disabled_by_default_) {  
1827 - disabled_ = true;  
1828 - }  
1829 - if(enabled_by_default_) { 1865 + if(default_startup == startup_mode::enabled) {
1830 disabled_ = false; 1866 disabled_ = false;
  1867 + } else if(default_startup == startup_mode::disabled) {
  1868 + disabled_ = true;
1831 } 1869 }
1832 for(const App_p &app : subcommands_) { 1870 for(const App_p &app : subcommands_) {
1833 if(app->has_automatic_name_) { 1871 if(app->has_automatic_name_) {
@@ -1909,27 +1947,33 @@ class App { @@ -1909,27 +1947,33 @@ class App {
1909 1947
1910 // The parse function is now broken into several parts, and part of process 1948 // The parse function is now broken into several parts, and part of process
1911 1949
1912 - /// Read and process an ini file (main app only)  
1913 - void _process_ini() {  
1914 - // Process an INI file 1950 + /// Read and process a configuration file (main app only)
  1951 + void _process_config_file() {
1915 if(config_ptr_ != nullptr) { 1952 if(config_ptr_ != nullptr) {
1916 - if(*config_ptr_) {  
1917 - config_ptr_->run_callback();  
1918 - config_required_ = true; 1953 + bool config_required = config_ptr_->get_required();
  1954 + bool file_given = config_ptr_->count() > 0;
  1955 + auto config_file = config_ptr_->as<std::string>();
  1956 + if(config_file.empty()) {
  1957 + if(config_required) {
  1958 + throw FileError::Missing("no specified config file");
  1959 + }
  1960 + return;
1919 } 1961 }
1920 - if(!config_name_.empty()) { 1962 +
  1963 + auto path_result = detail::check_path(config_file.c_str());
  1964 + if(path_result == detail::path_type::file) {
1921 try { 1965 try {
1922 - auto path_result = detail::check_path(config_name_.c_str());  
1923 - if(path_result == detail::path_type::file) {  
1924 - std::vector<ConfigItem> values = config_formatter_->from_file(config_name_);  
1925 - _parse_config(values);  
1926 - } else if(config_required_) {  
1927 - throw FileError::Missing(config_name_); 1966 + std::vector<ConfigItem> values = config_formatter_->from_file(config_file);
  1967 + _parse_config(values);
  1968 + if(!file_given) {
  1969 + config_ptr_->add_result(config_file);
1928 } 1970 }
1929 } catch(const FileError &) { 1971 } catch(const FileError &) {
1930 - if(config_required_) 1972 + if(config_required || file_given)
1931 throw; 1973 throw;
1932 } 1974 }
  1975 + } else if(config_required || file_given) {
  1976 + throw FileError::Missing(config_file);
1933 } 1977 }
1934 } 1978 }
1935 } 1979 }
@@ -2145,7 +2189,7 @@ class App { @@ -2145,7 +2189,7 @@ class App {
2145 2189
2146 /// Process callbacks and such. 2190 /// Process callbacks and such.
2147 void _process() { 2191 void _process() {
2148 - _process_ini(); 2192 + _process_config_file();
2149 _process_env(); 2193 _process_env();
2150 _process_callbacks(); 2194 _process_callbacks();
2151 _process_help_flags(); 2195 _process_help_flags();
@@ -2244,7 +2288,7 @@ class App { @@ -2244,7 +2288,7 @@ class App {
2244 /// Returns true if it managed to find the option, if false you'll need to remove the arg manually. 2288 /// Returns true if it managed to find the option, if false you'll need to remove the arg manually.
2245 void _parse_config(std::vector<ConfigItem> &args) { 2289 void _parse_config(std::vector<ConfigItem> &args) {
2246 for(ConfigItem item : args) { 2290 for(ConfigItem item : args) {
2247 - if(!_parse_single_config(item) && !allow_config_extras_) 2291 + if(!_parse_single_config(item) && allow_config_extras_ == config_extras_mode::error)
2248 throw ConfigError::Extras(item.fullname()); 2292 throw ConfigError::Extras(item.fullname());
2249 } 2293 }
2250 } 2294 }
@@ -2254,16 +2298,37 @@ class App { @@ -2254,16 +2298,37 @@ class App {
2254 if(level < item.parents.size()) { 2298 if(level < item.parents.size()) {
2255 try { 2299 try {
2256 auto subcom = get_subcommand(item.parents.at(level)); 2300 auto subcom = get_subcommand(item.parents.at(level));
2257 - return subcom->_parse_single_config(item, level + 1); 2301 + auto result = subcom->_parse_single_config(item, level + 1);
  2302 +
  2303 + return result;
2258 } catch(const OptionNotFound &) { 2304 } catch(const OptionNotFound &) {
2259 return false; 2305 return false;
2260 } 2306 }
2261 } 2307 }
2262 - 2308 + // check for section open
  2309 + if(item.name == "++") {
  2310 + if(configurable_) {
  2311 + increment_parsed();
  2312 + _trigger_pre_parse(2);
  2313 + if(parent_ != nullptr) {
  2314 + parent_->parsed_subcommands_.push_back(this);
  2315 + }
  2316 + }
  2317 + return true;
  2318 + }
  2319 + // check for section close
  2320 + if(item.name == "--") {
  2321 + if(configurable_) {
  2322 + _process_callbacks();
  2323 + _process_requirements();
  2324 + run_callback();
  2325 + }
  2326 + return true;
  2327 + }
2263 Option *op = get_option_no_throw("--" + item.name); 2328 Option *op = get_option_no_throw("--" + item.name);
2264 if(op == nullptr) { 2329 if(op == nullptr) {
2265 // If the option was not present 2330 // If the option was not present
2266 - if(get_allow_config_extras()) 2331 + if(get_allow_config_extras() == config_extras_mode::capture)
2267 // Should we worry about classifying the extras properly? 2332 // Should we worry about classifying the extras properly?
2268 missing_.emplace_back(detail::Classifier::NONE, item.fullname()); 2333 missing_.emplace_back(detail::Classifier::NONE, item.fullname());
2269 return false; 2334 return false;
include/CLI/Config.hpp
@@ -14,58 +14,325 @@ @@ -14,58 +14,325 @@
14 14
15 namespace CLI { 15 namespace CLI {
16 16
  17 +namespace detail {
  18 +
  19 +inline std::string convert_arg_for_ini(const std::string &arg) {
  20 + if(arg.empty()) {
  21 + return std::string(2, '"');
  22 + }
  23 + // some specifically supported strings
  24 + if(arg == "true" || arg == "false" || arg == "nan" || arg == "inf") {
  25 + return arg;
  26 + }
  27 + // floating point conversion can convert some hex codes, but don't try that here
  28 + if(arg.compare(0, 2, "0x") != 0 && arg.compare(0, 2, "0X") != 0) {
  29 + double val;
  30 + if(detail::lexical_cast(arg, val)) {
  31 + return arg;
  32 + }
  33 + }
  34 + // just quote a single non numeric character
  35 + if(arg.size() == 1) {
  36 + return std::string("'") + arg + '\'';
  37 + }
  38 + // handle hex, binary or octal arguments
  39 + if(arg.front() == '0') {
  40 + if(arg[1] == 'x') {
  41 + if(std::all_of(arg.begin() + 2, arg.end(), [](char x) {
  42 + return (x >= '0' && x <= '9') || (x >= 'A' && x <= 'F') || (x >= 'a' && x <= 'f');
  43 + })) {
  44 + return arg;
  45 + }
  46 + } else if(arg[1] == 'o') {
  47 + if(std::all_of(arg.begin() + 2, arg.end(), [](char x) { return (x >= '0' && x <= '7'); })) {
  48 + return arg;
  49 + }
  50 + } else if(arg[1] == 'b') {
  51 + if(std::all_of(arg.begin() + 2, arg.end(), [](char x) { return (x == '0' || x == '1'); })) {
  52 + return arg;
  53 + }
  54 + }
  55 + }
  56 + if(arg.find_first_of('"') == std::string::npos) {
  57 + return std::string("\"") + arg + '"';
  58 + } else {
  59 + return std::string("'") + arg + '\'';
  60 + }
  61 +}
  62 +
  63 +/// Comma separated join, adds quotes if needed
17 inline std::string 64 inline std::string
18 -ConfigINI::to_config(const App *app, bool default_also, bool write_description, std::string prefix) const { 65 +ini_join(const std::vector<std::string> &args, char sepChar = ',', char arrayStart = '[', char arrayEnd = ']') {
  66 + std::string joined;
  67 + if(args.size() > 1 && arrayStart != '\0') {
  68 + joined.push_back(arrayStart);
  69 + }
  70 + std::size_t start = 0;
  71 + for(const auto &arg : args) {
  72 + if(start++ > 0) {
  73 + joined.push_back(sepChar);
  74 + if(isspace(sepChar) == 0) {
  75 + joined.push_back(' ');
  76 + }
  77 + }
  78 + joined.append(convert_arg_for_ini(arg));
  79 + }
  80 + if(args.size() > 1 && arrayEnd != '\0') {
  81 + joined.push_back(arrayEnd);
  82 + }
  83 + return joined;
  84 +}
  85 +
  86 +inline std::vector<std::string> generate_parents(const std::string &section, std::string &name) {
  87 + std::vector<std::string> parents;
  88 + if(detail::to_lower(section) != "default") {
  89 + if(section.find('.') != std::string::npos) {
  90 + parents = detail::split(section, '.');
  91 + } else {
  92 + parents = {section};
  93 + }
  94 + }
  95 + if(name.find('.') != std::string::npos) {
  96 + std::vector<std::string> plist = detail::split(name, '.');
  97 + name = plist.back();
  98 + detail::remove_quotes(name);
  99 + plist.pop_back();
  100 + parents.insert(parents.end(), plist.begin(), plist.end());
  101 + }
  102 +
  103 + // clean up quotes on the parents
  104 + for(auto &parent : parents) {
  105 + detail::remove_quotes(parent);
  106 + }
  107 + return parents;
  108 +}
  109 +
  110 +/// assuming non default segments do a check on the close and open of the segments in a configItem structure
  111 +inline void checkParentSegments(std::vector<ConfigItem> &output, const std::string &currentSection) {
  112 +
  113 + std::string estring;
  114 + auto parents = detail::generate_parents(currentSection, estring);
  115 + if(output.size() > 0 && output.back().name == "--") {
  116 + std::size_t msize = (parents.size() > 1U) ? parents.size() : 2;
  117 + while(output.back().parents.size() >= msize) {
  118 + output.push_back(output.back());
  119 + output.back().parents.pop_back();
  120 + }
  121 +
  122 + if(parents.size() > 1) {
  123 + std::size_t common = 0;
  124 + std::size_t mpair = (std::min)(output.back().parents.size(), parents.size() - 1);
  125 + for(std::size_t ii = 0; ii < mpair; ++ii) {
  126 + if(output.back().parents[ii] != parents[ii]) {
  127 + break;
  128 + }
  129 + ++common;
  130 + }
  131 + if(common == mpair) {
  132 + output.pop_back();
  133 + } else {
  134 + while(output.back().parents.size() > common + 1) {
  135 + output.push_back(output.back());
  136 + output.back().parents.pop_back();
  137 + }
  138 + }
  139 + for(std::size_t ii = common; ii < parents.size() - 1; ++ii) {
  140 + output.emplace_back();
  141 + output.back().parents.assign(parents.begin(), parents.begin() + static_cast<std::ptrdiff_t>(ii) + 1);
  142 + output.back().name = "++";
  143 + }
  144 + }
  145 + } else if(parents.size() > 1) {
  146 + for(std::size_t ii = 0; ii < parents.size() - 1; ++ii) {
  147 + output.emplace_back();
  148 + output.back().parents.assign(parents.begin(), parents.begin() + static_cast<std::ptrdiff_t>(ii) + 1);
  149 + output.back().name = "++";
  150 + }
  151 + }
  152 +
  153 + // insert a section end which is just an empty items_buffer
  154 + output.emplace_back();
  155 + output.back().parents = std::move(parents);
  156 + output.back().name = "++";
  157 +}
  158 +} // namespace detail
  159 +
  160 +inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) const {
  161 + std::string line;
  162 + std::string section = "default";
  163 +
  164 + std::vector<ConfigItem> output;
  165 + bool defaultArray = (arrayStart == '\0' || arrayStart == ' ') && arrayStart == arrayEnd;
  166 + char aStart = (defaultArray) ? '[' : arrayStart;
  167 + char aEnd = (defaultArray) ? ']' : arrayEnd;
  168 + char aSep = (defaultArray && arraySeparator == ' ') ? ',' : arraySeparator;
  169 +
  170 + while(getline(input, line)) {
  171 + std::vector<std::string> items_buffer;
  172 + std::string name;
  173 +
  174 + detail::trim(line);
  175 + std::size_t len = line.length();
  176 + if(len > 1 && line.front() == '[' && line.back() == ']') {
  177 + if(section != "default") {
  178 + // insert a section end which is just an empty items_buffer
  179 + output.emplace_back();
  180 + output.back().parents = detail::generate_parents(section, name);
  181 + output.back().name = "--";
  182 + }
  183 + section = line.substr(1, len - 2);
  184 + // deal with double brackets for TOML
  185 + if(section.size() > 1 && section.front() == '[' && section.back() == ']') {
  186 + section = section.substr(1, section.size() - 2);
  187 + }
  188 + if(detail::to_lower(section) == "default") {
  189 + section = "default";
  190 + } else {
  191 + detail::checkParentSegments(output, section);
  192 + }
  193 + continue;
  194 + }
  195 + if(len == 0) {
  196 + continue;
  197 + }
  198 + // comment lines
  199 + if(line.front() == ';' || line.front() == '#' || line.front() == commentChar) {
  200 + continue;
  201 + }
  202 +
  203 + // Find = in string, split and recombine
  204 + auto pos = line.find(valueDelimiter);
  205 + if(pos != std::string::npos) {
  206 + name = detail::trim_copy(line.substr(0, pos));
  207 + std::string item = detail::trim_copy(line.substr(pos + 1));
  208 + if(item.size() > 1 && item.front() == aStart && item.back() == aEnd) {
  209 + items_buffer = detail::split_up(item.substr(1, item.length() - 2), aSep);
  210 + } else if(defaultArray && item.find_first_of(aSep) != std::string::npos) {
  211 + items_buffer = detail::split_up(item, aSep);
  212 + } else if(defaultArray && item.find_first_of(' ') != std::string::npos) {
  213 + items_buffer = detail::split_up(item);
  214 + } else {
  215 + items_buffer = {item};
  216 + }
  217 + } else {
  218 + name = detail::trim_copy(line);
  219 + items_buffer = {"true"};
  220 + }
  221 + if(name.find('.') == std::string::npos) {
  222 + detail::remove_quotes(name);
  223 + }
  224 + // clean up quotes on the items
  225 + for(auto &it : items_buffer) {
  226 + detail::remove_quotes(it);
  227 + }
  228 +
  229 + std::vector<std::string> parents = detail::generate_parents(section, name);
  230 +
  231 + if(!output.empty() && name == output.back().name && parents == output.back().parents) {
  232 + output.back().inputs.insert(output.back().inputs.end(), items_buffer.begin(), items_buffer.end());
  233 + } else {
  234 + output.emplace_back();
  235 + output.back().parents = std::move(parents);
  236 + output.back().name = std::move(name);
  237 + output.back().inputs = std::move(items_buffer);
  238 + }
  239 + }
  240 + if(section != "default") {
  241 + // insert a section end which is just an empty items_buffer
  242 + std::string ename;
  243 + output.emplace_back();
  244 + output.back().parents = detail::generate_parents(section, ename);
  245 + output.back().name = "--";
  246 + while(output.back().parents.size() > 1) {
  247 + output.push_back(output.back());
  248 + output.back().parents.pop_back();
  249 + }
  250 + }
  251 + return output;
  252 +}
  253 +
  254 +inline std::string
  255 +ConfigBase::to_config(const App *app, bool default_also, bool write_description, std::string prefix) const {
19 std::stringstream out; 256 std::stringstream out;
20 - for(const Option *opt : app->get_options({})) {  
21 -  
22 - // Only process option with a long-name and configurable  
23 - if(!opt->get_lnames().empty() && opt->get_configurable()) {  
24 - std::string name = prefix + opt->get_lnames()[0];  
25 - std::string value;  
26 -  
27 - // Non-flags  
28 - if(opt->get_expected_min() != 0) {  
29 -  
30 - // If the option was found on command line  
31 - if(opt->count() > 0)  
32 - value = detail::ini_join(opt->results());  
33 -  
34 - // If the option has a default and is requested by optional argument  
35 - else if(default_also && !opt->get_default_str().empty())  
36 - value = opt->get_default_str();  
37 - // Flag, one passed  
38 - } else if(opt->count() == 1) {  
39 - value = "true";  
40 -  
41 - // Flag, multiple passed  
42 - } else if(opt->count() > 1) {  
43 - value = std::to_string(opt->count());  
44 -  
45 - // Flag, not present  
46 - } else if(opt->count() == 0 && default_also) {  
47 - value = "false"; 257 + std::string commentLead;
  258 + commentLead.push_back(commentChar);
  259 + commentLead.push_back(' ');
  260 +
  261 + std::vector<std::string> groups = app->get_groups();
  262 + bool defaultUsed = false;
  263 + groups.insert(groups.begin(), std::string("Options"));
  264 + if(write_description) {
  265 + out << commentLead << app->get_description() << '\n';
  266 + }
  267 + for(auto &group : groups) {
  268 + if(group == "Options" || group.empty()) {
  269 + if(defaultUsed) {
  270 + continue;
48 } 271 }
  272 + defaultUsed = true;
  273 + }
  274 + if(write_description && group != "Options" && !group.empty()) {
  275 + out << '\n' << commentLead << group << " Options\n";
  276 + }
  277 + for(const Option *opt : app->get_options({})) {
  278 +
  279 + // Only process option with a long-name and configurable
  280 + if(!opt->get_lnames().empty() && opt->get_configurable()) {
  281 + if(opt->get_group() != group) {
  282 + if(!(group == "Options" && opt->get_group() == "")) {
  283 + continue;
  284 + }
  285 + }
  286 + std::string name = prefix + opt->get_lnames()[0];
  287 + std::string value = detail::ini_join(opt->reduced_results(), arraySeparator, arrayStart, arrayEnd);
49 288
50 - if(!value.empty()) {  
51 - if(write_description && opt->has_description()) {  
52 - if(static_cast<int>(out.tellp()) != 0) {  
53 - out << std::endl; 289 + if(value.empty() && default_also) {
  290 + if(!opt->get_default_str().empty()) {
  291 + value = detail::convert_arg_for_ini(opt->get_default_str());
  292 + } else if(opt->get_expected_min() == 0) {
  293 + value = "false";
54 } 294 }
55 - out << "; " << detail::fix_newlines("; ", opt->get_description()) << std::endl;  
56 } 295 }
57 296
58 - // Don't try to quote anything that is not size 1  
59 - if(opt->get_items_expected_max() != 1)  
60 - out << name << "=" << value << std::endl;  
61 - else  
62 - out << name << "=" << detail::add_quotes_if_needed(value) << std::endl; 297 + if(!value.empty()) {
  298 + if(write_description && opt->has_description()) {
  299 + out << '\n';
  300 + out << commentLead << detail::fix_newlines(commentLead, opt->get_description()) << '\n';
  301 + }
  302 + out << name << valueDelimiter << value << '\n';
  303 + }
  304 + }
  305 + }
  306 + }
  307 + auto subcommands = app->get_subcommands({});
  308 + for(const App *subcom : subcommands) {
  309 + if(subcom->get_name().empty()) {
  310 + if(write_description && !subcom->get_group().empty()) {
  311 + out << '\n' << commentLead << subcom->get_group() << " Options\n";
63 } 312 }
  313 + out << to_config(subcom, default_also, write_description, prefix);
64 } 314 }
65 } 315 }
66 316
67 - for(const App *subcom : app->get_subcommands({}))  
68 - out << to_config(subcom, default_also, write_description, prefix + subcom->get_name() + "."); 317 + for(const App *subcom : subcommands)
  318 + if(!subcom->get_name().empty()) {
  319 + if(subcom->get_configurable() && app->got_subcommand(subcom)) {
  320 + if(!prefix.empty() || app->get_parent() == nullptr) {
  321 + out << '[' << prefix << subcom->get_name() << "]\n";
  322 + } else {
  323 + std::string subname = app->get_name() + "." + subcom->get_name();
  324 + auto p = app->get_parent();
  325 + while(p->get_parent() != nullptr) {
  326 + subname = p->get_name() + "." + subname;
  327 + p = p->get_parent();
  328 + }
  329 + out << '[' << subname << "]\n";
  330 + }
  331 + out << to_config(subcom, default_also, write_description, "");
  332 + } else {
  333 + out << to_config(subcom, default_also, write_description, prefix + subcom->get_name() + ".");
  334 + }
  335 + }
69 336
70 return out.str(); 337 return out.str();
71 } 338 }
include/CLI/ConfigFwd.hpp
@@ -15,30 +15,6 @@ namespace CLI { @@ -15,30 +15,6 @@ namespace CLI {
15 15
16 class App; 16 class App;
17 17
18 -namespace detail {  
19 -  
20 -/// Comma separated join, adds quotes if needed  
21 -inline std::string ini_join(std::vector<std::string> args) {  
22 - std::ostringstream s;  
23 - std::size_t start = 0;  
24 - for(const auto &arg : args) {  
25 - if(start++ > 0)  
26 - s << " ";  
27 -  
28 - auto it = std::find_if(arg.begin(), arg.end(), [](char ch) { return std::isspace<char>(ch, std::locale()); });  
29 - if(it == arg.end())  
30 - s << arg;  
31 - else if(arg.find_first_of('\"') == std::string::npos)  
32 - s << '\"' << arg << '\"';  
33 - else  
34 - s << '\'' << arg << '\'';  
35 - }  
36 -  
37 - return s.str();  
38 -}  
39 -  
40 -} // namespace detail  
41 -  
42 /// Holds values to load into Options 18 /// Holds values to load into Options
43 struct ConfigItem { 19 struct ConfigItem {
44 /// This is the list of parents 20 /// This is the list of parents
@@ -91,56 +67,61 @@ class Config { @@ -91,56 +67,61 @@ class Config {
91 virtual ~Config() = default; 67 virtual ~Config() = default;
92 }; 68 };
93 69
94 -/// This converter works with INI files  
95 -class ConfigINI : public Config { 70 +/// This converter works with INI/TOML files; to write proper TOML files use ConfigTOML
  71 +class ConfigBase : public Config {
  72 + protected:
  73 + /// the character used for comments
  74 + char commentChar = ';';
  75 + /// the character used to start an array '\0' is a default to not use
  76 + char arrayStart = '\0';
  77 + /// the character used to end an array '\0' is a default to not use
  78 + char arrayEnd = '\0';
  79 + /// the character used to separate elements in an array
  80 + char arraySeparator = ' ';
  81 + /// the character used separate the name from the value
  82 + char valueDelimiter = '=';
  83 +
96 public: 84 public:
97 std::string 85 std::string
98 to_config(const App * /*app*/, bool default_also, bool write_description, std::string prefix) const override; 86 to_config(const App * /*app*/, bool default_also, bool write_description, std::string prefix) const override;
99 87
100 - std::vector<ConfigItem> from_config(std::istream &input) const override {  
101 - std::string line;  
102 - std::string section = "default";  
103 -  
104 - std::vector<ConfigItem> output;  
105 -  
106 - while(getline(input, line)) {  
107 - std::vector<std::string> items_buffer;  
108 -  
109 - detail::trim(line);  
110 - std::size_t len = line.length();  
111 - if(len > 1 && line[0] == '[' && line[len - 1] == ']') {  
112 - section = line.substr(1, len - 2);  
113 - } else if(len > 0 && line[0] != ';') {  
114 - output.emplace_back();  
115 - ConfigItem &out = output.back();  
116 -  
117 - // Find = in string, split and recombine  
118 - auto pos = line.find('=');  
119 - if(pos != std::string::npos) {  
120 - out.name = detail::trim_copy(line.substr(0, pos));  
121 - std::string item = detail::trim_copy(line.substr(pos + 1));  
122 - items_buffer = detail::split_up(item);  
123 - } else {  
124 - out.name = detail::trim_copy(line);  
125 - items_buffer = {"ON"};  
126 - }  
127 -  
128 - if(detail::to_lower(section) != "default") {  
129 - out.parents = {section};  
130 - }  
131 -  
132 - if(out.name.find('.') != std::string::npos) {  
133 - std::vector<std::string> plist = detail::split(out.name, '.');  
134 - out.name = plist.back();  
135 - plist.pop_back();  
136 - out.parents.insert(out.parents.end(), plist.begin(), plist.end());  
137 - }  
138 -  
139 - out.inputs.insert(std::end(out.inputs), std::begin(items_buffer), std::end(items_buffer));  
140 - }  
141 - }  
142 - return output; 88 + std::vector<ConfigItem> from_config(std::istream &input) const override;
  89 + /// Specify the configuration for comment characters
  90 + ConfigBase *comment(char cchar) {
  91 + commentChar = cchar;
  92 + return this;
  93 + }
  94 + /// Specify the start and end characters for an array
  95 + ConfigBase *arrayBounds(char aStart, char aEnd) {
  96 + arrayStart = aStart;
  97 + arrayEnd = aEnd;
  98 + return this;
  99 + }
  100 + /// Specify the delimiter character for an array
  101 + ConfigBase *arrayDelimiter(char aSep) {
  102 + arraySeparator = aSep;
  103 + return this;
  104 + }
  105 + /// Specify the delimiter between a name and value
  106 + ConfigBase *valueSeparator(char vSep) {
  107 + valueDelimiter = vSep;
  108 + return this;
143 } 109 }
144 }; 110 };
145 111
  112 +/// the default Config is the INI file format
  113 +using ConfigINI = ConfigBase;
  114 +
  115 +/// ConfigTOML generates a TOML compliant output
  116 +class ConfigTOML : public ConfigINI {
  117 +
  118 + public:
  119 + ConfigTOML() {
  120 + commentChar = '#';
  121 + arrayStart = '[';
  122 + arrayEnd = ']';
  123 + arraySeparator = ',';
  124 + valueDelimiter = '=';
  125 + }
  126 +};
146 } // namespace CLI 127 } // namespace CLI
include/CLI/Option.hpp
@@ -938,10 +938,12 @@ class Option : public OptionBase&lt;Option&gt; { @@ -938,10 +938,12 @@ class Option : public OptionBase&lt;Option&gt; {
938 res = results_; 938 res = results_;
939 _validate_results(res); 939 _validate_results(res);
940 } 940 }
941 - results_t extra;  
942 - _reduce_results(extra, res);  
943 - if(!extra.empty()) {  
944 - res = std::move(extra); 941 + if(!res.empty()) {
  942 + results_t extra;
  943 + _reduce_results(extra, res);
  944 + if(!extra.empty()) {
  945 + res = std::move(extra);
  946 + }
945 } 947 }
946 } 948 }
947 return res; 949 return res;
include/CLI/StringTools.hpp
@@ -135,6 +135,17 @@ inline std::string trim_copy(const std::string &amp;str) { @@ -135,6 +135,17 @@ inline std::string trim_copy(const std::string &amp;str) {
135 return trim(s); 135 return trim(s);
136 } 136 }
137 137
  138 +/// remove quotes at the front and back of a string either '"' or '\''
  139 +inline std::string &remove_quotes(std::string &str) {
  140 + if(str.length() > 1 && (str.front() == '"' || str.front() == '\'')) {
  141 + if(str.front() == str.back()) {
  142 + str.pop_back();
  143 + str.erase(str.begin(), str.begin() + 1);
  144 + }
  145 + }
  146 + return str;
  147 +}
  148 +
138 /// Make a copy of the string and then trim it, any filter string can be used (any char in string is filtered) 149 /// Make a copy of the string and then trim it, any filter string can be used (any char in string is filtered)
139 inline std::string trim_copy(const std::string &str, const std::string &filter) { 150 inline std::string trim_copy(const std::string &str, const std::string &filter) {
140 std::string s = str; 151 std::string s = str;
@@ -268,10 +279,12 @@ template &lt;typename Callable&gt; inline std::string find_and_modify(std::string str, @@ -268,10 +279,12 @@ template &lt;typename Callable&gt; inline std::string find_and_modify(std::string str,
268 279
269 /// Split a string '"one two" "three"' into 'one two', 'three' 280 /// Split a string '"one two" "three"' into 'one two', 'three'
270 /// Quote characters can be ` ' or " 281 /// Quote characters can be ` ' or "
271 -inline std::vector<std::string> split_up(std::string str) { 282 +inline std::vector<std::string> split_up(std::string str, char delimiter = '\0') {
272 283
273 const std::string delims("\'\"`"); 284 const std::string delims("\'\"`");
274 - auto find_ws = [](char ch) { return std::isspace<char>(ch, std::locale()); }; 285 + auto find_ws = [delimiter](char ch) {
  286 + return (delimiter == '\0') ? (std::isspace<char>(ch, std::locale()) != 0) : (ch == delimiter);
  287 + };
275 trim(str); 288 trim(str);
276 289
277 std::vector<std::string> output; 290 std::vector<std::string> output;
@@ -297,7 +310,7 @@ inline std::vector&lt;std::string&gt; split_up(std::string str) { @@ -297,7 +310,7 @@ inline std::vector&lt;std::string&gt; split_up(std::string str) {
297 if(it != std::end(str)) { 310 if(it != std::end(str)) {
298 std::string value = std::string(str.begin(), it); 311 std::string value = std::string(str.begin(), it);
299 output.push_back(value); 312 output.push_back(value);
300 - str = std::string(it, str.end()); 313 + str = std::string(it + 1, str.end());
301 } else { 314 } else {
302 output.push_back(str); 315 output.push_back(str);
303 str = ""; 316 str = "";
@@ -317,7 +330,7 @@ inline std::vector&lt;std::string&gt; split_up(std::string str) { @@ -317,7 +330,7 @@ inline std::vector&lt;std::string&gt; split_up(std::string str) {
317 /// at the start of the first line). `"; "` would be for ini files 330 /// at the start of the first line). `"; "` would be for ini files
318 /// 331 ///
319 /// Can't use Regex, or this would be a subs. 332 /// Can't use Regex, or this would be a subs.
320 -inline std::string fix_newlines(std::string leader, std::string input) { 333 +inline std::string fix_newlines(const std::string &leader, std::string input) {
321 std::string::size_type n = 0; 334 std::string::size_type n = 0;
322 while(n != std::string::npos && n < input.size()) { 335 while(n != std::string::npos && n < input.size()) {
323 n = input.find('\n', n); 336 n = input.find('\n', n);
include/CLI/Validators.hpp
@@ -271,25 +271,25 @@ enum class path_type { nonexistant, file, directory }; @@ -271,25 +271,25 @@ enum class path_type { nonexistant, file, directory };
271 271
272 #if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 272 #if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0
273 /// get the type of the path from a file name 273 /// get the type of the path from a file name
274 -inline path_type check_path(const char *file) {  
275 - try {  
276 - auto stat = std::filesystem::status(file);  
277 - switch(stat.type()) {  
278 - case std::filesystem::file_type::none:  
279 - case std::filesystem::file_type::not_found:  
280 - return path_type::nonexistant;  
281 - case std::filesystem::file_type::directory:  
282 - return path_type::directory;  
283 - default:  
284 - return path_type::file;  
285 - }  
286 - } catch(const std::filesystem::filesystem_error &) { 274 +inline path_type check_path(const char *file) noexcept {
  275 + std::error_code ec;
  276 + auto stat = std::filesystem::status(file, ec);
  277 + if(ec) {
  278 + return path_type::nonexistant;
  279 + }
  280 + switch(stat.type()) {
  281 + case std::filesystem::file_type::none:
  282 + case std::filesystem::file_type::not_found:
287 return path_type::nonexistant; 283 return path_type::nonexistant;
  284 + case std::filesystem::file_type::directory:
  285 + return path_type::directory;
  286 + default:
  287 + return path_type::file;
288 } 288 }
289 } 289 }
290 #else 290 #else
291 /// get the type of the path from a file name 291 /// get the type of the path from a file name
292 -inline path_type check_path(const char *file) { 292 +inline path_type check_path(const char *file) noexcept {
293 #if defined(_MSC_VER) 293 #if defined(_MSC_VER)
294 struct __stat64 buffer; 294 struct __stat64 buffer;
295 if(_stat64(file, &buffer) == 0) { 295 if(_stat64(file, &buffer) == 0) {
tests/CMakeLists.txt
@@ -23,7 +23,7 @@ include(AddGoogletest) @@ -23,7 +23,7 @@ include(AddGoogletest)
23 23
24 set(CLI11_TESTS 24 set(CLI11_TESTS
25 HelpersTest 25 HelpersTest
26 - IniTest 26 + ConfigFileTest
27 SimpleTest 27 SimpleTest
28 AppTest 28 AppTest
29 SetTest 29 SetTest
tests/IniTest.cpp renamed to tests/ConfigFileTest.cpp
@@ -7,18 +7,60 @@ @@ -7,18 +7,60 @@
7 using ::testing::HasSubstr; 7 using ::testing::HasSubstr;
8 using ::testing::Not; 8 using ::testing::Not;
9 9
  10 +TEST(StringBased, convert_arg_for_ini) {
  11 +
  12 + EXPECT_EQ(CLI::detail::convert_arg_for_ini(std::string{}), "\"\"");
  13 +
  14 + EXPECT_EQ(CLI::detail::convert_arg_for_ini("true"), "true");
  15 +
  16 + EXPECT_EQ(CLI::detail::convert_arg_for_ini("nan"), "nan");
  17 +
  18 + EXPECT_EQ(CLI::detail::convert_arg_for_ini("happy hippo"), "\"happy hippo\"");
  19 +
  20 + EXPECT_EQ(CLI::detail::convert_arg_for_ini("47"), "47");
  21 +
  22 + EXPECT_EQ(CLI::detail::convert_arg_for_ini("47.365225"), "47.365225");
  23 +
  24 + EXPECT_EQ(CLI::detail::convert_arg_for_ini("+3.28e-25"), "+3.28e-25");
  25 + EXPECT_EQ(CLI::detail::convert_arg_for_ini("-22E14"), "-22E14");
  26 +
  27 + EXPECT_EQ(CLI::detail::convert_arg_for_ini("a"), "'a'");
  28 + // hex
  29 + EXPECT_EQ(CLI::detail::convert_arg_for_ini("0x5461FAED"), "0x5461FAED");
  30 + // hex fail
  31 + EXPECT_EQ(CLI::detail::convert_arg_for_ini("0x5461FAEG"), "\"0x5461FAEG\"");
  32 +
  33 + // octal
  34 + EXPECT_EQ(CLI::detail::convert_arg_for_ini("0o546123567"), "0o546123567");
  35 + // octal fail
  36 + EXPECT_EQ(CLI::detail::convert_arg_for_ini("0o546123587"), "\"0o546123587\"");
  37 +
  38 + // binary
  39 + EXPECT_EQ(CLI::detail::convert_arg_for_ini("0b01101110010"), "0b01101110010");
  40 + // binary fail
  41 + EXPECT_EQ(CLI::detail::convert_arg_for_ini("0b01102110010"), "\"0b01102110010\"");
  42 +}
  43 +
10 TEST(StringBased, IniJoin) { 44 TEST(StringBased, IniJoin) {
11 std::vector<std::string> items = {"one", "two", "three four"}; 45 std::vector<std::string> items = {"one", "two", "three four"};
12 - std::string result = "one two \"three four\""; 46 + std::string result = "\"one\" \"two\" \"three four\"";
  47 +
  48 + EXPECT_EQ(CLI::detail::ini_join(items, ' ', '\0', '\0'), result);
  49 +
  50 + result = "[\"one\", \"two\", \"three four\"]";
13 51
14 EXPECT_EQ(CLI::detail::ini_join(items), result); 52 EXPECT_EQ(CLI::detail::ini_join(items), result);
  53 +
  54 + result = "{\"one\"; \"two\"; \"three four\"}";
  55 +
  56 + EXPECT_EQ(CLI::detail::ini_join(items, ';', '{', '}'), result);
15 } 57 }
16 58
17 TEST(StringBased, First) { 59 TEST(StringBased, First) {
18 std::stringstream ofile; 60 std::stringstream ofile;
19 61
20 - ofile << "one=three" << std::endl;  
21 - ofile << "two=four" << std::endl; 62 + ofile << "one=three\n";
  63 + ofile << "two=four\n";
22 64
23 ofile.seekg(0, std::ios::beg); 65 ofile.seekg(0, std::ios::beg);
24 66
@@ -36,10 +78,10 @@ TEST(StringBased, First) { @@ -36,10 +78,10 @@ TEST(StringBased, First) {
36 TEST(StringBased, FirstWithComments) { 78 TEST(StringBased, FirstWithComments) {
37 std::stringstream ofile; 79 std::stringstream ofile;
38 80
39 - ofile << ";this is a comment" << std::endl;  
40 - ofile << "one=three" << std::endl;  
41 - ofile << "two=four" << std::endl;  
42 - ofile << "; and another one" << std::endl; 81 + ofile << ";this is a comment\n";
  82 + ofile << "one=three\n";
  83 + ofile << "two=four\n";
  84 + ofile << "; and another one\n";
43 85
44 ofile.seekg(0, std::ios::beg); 86 ofile.seekg(0, std::ios::beg);
45 87
@@ -57,9 +99,9 @@ TEST(StringBased, FirstWithComments) { @@ -57,9 +99,9 @@ TEST(StringBased, FirstWithComments) {
57 TEST(StringBased, Quotes) { 99 TEST(StringBased, Quotes) {
58 std::stringstream ofile; 100 std::stringstream ofile;
59 101
60 - ofile << R"(one = "three")" << std::endl;  
61 - ofile << R"(two = 'four')" << std::endl;  
62 - ofile << R"(five = "six and seven")" << std::endl; 102 + ofile << R"(one = "three")" << '\n';
  103 + ofile << R"(two = 'four')" << '\n';
  104 + ofile << R"(five = "six and seven")" << '\n';
63 105
64 ofile.seekg(0, std::ios::beg); 106 ofile.seekg(0, std::ios::beg);
65 107
@@ -80,9 +122,9 @@ TEST(StringBased, Quotes) { @@ -80,9 +122,9 @@ TEST(StringBased, Quotes) {
80 TEST(StringBased, Vector) { 122 TEST(StringBased, Vector) {
81 std::stringstream ofile; 123 std::stringstream ofile;
82 124
83 - ofile << "one = three" << std::endl;  
84 - ofile << "two = four" << std::endl;  
85 - ofile << "five = six and seven" << std::endl; 125 + ofile << "one = three\n";
  126 + ofile << "two = four\n";
  127 + ofile << "five = six and seven\n";
86 128
87 ofile.seekg(0, std::ios::beg); 129 ofile.seekg(0, std::ios::beg);
88 130
@@ -105,8 +147,8 @@ TEST(StringBased, Vector) { @@ -105,8 +147,8 @@ TEST(StringBased, Vector) {
105 TEST(StringBased, Spaces) { 147 TEST(StringBased, Spaces) {
106 std::stringstream ofile; 148 std::stringstream ofile;
107 149
108 - ofile << "one = three" << std::endl;  
109 - ofile << "two = four" << std::endl; 150 + ofile << "one = three\n";
  151 + ofile << "two = four";
110 152
111 ofile.seekg(0, std::ios::beg); 153 ofile.seekg(0, std::ios::beg);
112 154
@@ -124,47 +166,224 @@ TEST(StringBased, Spaces) { @@ -124,47 +166,224 @@ TEST(StringBased, Spaces) {
124 TEST(StringBased, Sections) { 166 TEST(StringBased, Sections) {
125 std::stringstream ofile; 167 std::stringstream ofile;
126 168
127 - ofile << "one=three" << std::endl;  
128 - ofile << "[second]" << std::endl;  
129 - ofile << " two=four" << std::endl; 169 + ofile << "one=three\n";
  170 + ofile << "[second]\n";
  171 + ofile << " two=four\n";
130 172
131 ofile.seekg(0, std::ios::beg); 173 ofile.seekg(0, std::ios::beg);
132 174
133 std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile); 175 std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
134 176
135 - EXPECT_EQ(2u, output.size()); 177 + EXPECT_EQ(4u, output.size());
136 EXPECT_EQ("one", output.at(0).name); 178 EXPECT_EQ("one", output.at(0).name);
137 EXPECT_EQ(1u, output.at(0).inputs.size()); 179 EXPECT_EQ(1u, output.at(0).inputs.size());
138 EXPECT_EQ("three", output.at(0).inputs.at(0)); 180 EXPECT_EQ("three", output.at(0).inputs.at(0));
139 - EXPECT_EQ("two", output.at(1).name);  
140 - EXPECT_EQ("second", output.at(1).parents.at(0));  
141 - EXPECT_EQ(1u, output.at(1).inputs.size());  
142 - EXPECT_EQ("four", output.at(1).inputs.at(0));  
143 - EXPECT_EQ("second.two", output.at(1).fullname()); 181 + EXPECT_EQ("two", output.at(2).name);
  182 + EXPECT_EQ("second", output.at(2).parents.at(0));
  183 + EXPECT_EQ(1u, output.at(2).inputs.size());
  184 + EXPECT_EQ("four", output.at(2).inputs.at(0));
  185 + EXPECT_EQ("second.two", output.at(2).fullname());
144 } 186 }
145 187
146 TEST(StringBased, SpacesSections) { 188 TEST(StringBased, SpacesSections) {
147 std::stringstream ofile; 189 std::stringstream ofile;
148 190
149 - ofile << "one=three" << std::endl;  
150 - ofile << std::endl;  
151 - ofile << "[second]" << std::endl;  
152 - ofile << " " << std::endl;  
153 - ofile << " two=four" << std::endl; 191 + ofile << "one=three\n\n";
  192 + ofile << "[second] \n";
  193 + ofile << " \n";
  194 + ofile << " two=four\n";
154 195
155 ofile.seekg(0, std::ios::beg); 196 ofile.seekg(0, std::ios::beg);
156 197
157 std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile); 198 std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
158 199
159 - EXPECT_EQ(2u, output.size()); 200 + EXPECT_EQ(4u, output.size());
160 EXPECT_EQ("one", output.at(0).name); 201 EXPECT_EQ("one", output.at(0).name);
161 EXPECT_EQ(1u, output.at(0).inputs.size()); 202 EXPECT_EQ(1u, output.at(0).inputs.size());
162 EXPECT_EQ("three", output.at(0).inputs.at(0)); 203 EXPECT_EQ("three", output.at(0).inputs.at(0));
163 - EXPECT_EQ("two", output.at(1).name);  
164 - EXPECT_EQ(1u, output.at(1).parents.size());  
165 EXPECT_EQ("second", output.at(1).parents.at(0)); 204 EXPECT_EQ("second", output.at(1).parents.at(0));
166 - EXPECT_EQ(1u, output.at(1).inputs.size());  
167 - EXPECT_EQ("four", output.at(1).inputs.at(0)); 205 + EXPECT_EQ("++", output.at(1).name);
  206 + EXPECT_EQ("two", output.at(2).name);
  207 + EXPECT_EQ(1u, output.at(2).parents.size());
  208 + EXPECT_EQ("second", output.at(2).parents.at(0));
  209 + EXPECT_EQ(1u, output.at(2).inputs.size());
  210 + EXPECT_EQ("four", output.at(2).inputs.at(0));
  211 + EXPECT_EQ("second", output.at(3).parents.at(0));
  212 + EXPECT_EQ("--", output.at(3).name);
  213 +}
  214 +
  215 +// check function to make sure that open sections match close sections
  216 +bool checkSections(const std::vector<CLI::ConfigItem> &output) {
  217 + std::set<std::string> open;
  218 + for(auto &ci : output) {
  219 + if(ci.name == "++") {
  220 + auto nm = ci.fullname();
  221 + nm.pop_back();
  222 + nm.pop_back();
  223 + auto rv = open.insert(nm);
  224 + if(!rv.second) {
  225 + return false;
  226 + }
  227 + }
  228 + if(ci.name == "--") {
  229 + auto nm = ci.fullname();
  230 + nm.pop_back();
  231 + nm.pop_back();
  232 + auto rv = open.erase(nm);
  233 + if(rv != 1U) {
  234 + return false;
  235 + }
  236 + }
  237 + }
  238 + return open.empty();
  239 +}
  240 +TEST(StringBased, Layers) {
  241 + std::stringstream ofile;
  242 +
  243 + ofile << "simple = true\n\n";
  244 + ofile << "[other]\n";
  245 + ofile << "[other.sub2]\n";
  246 + ofile << "[other.sub2.sub-level2]\n";
  247 + ofile << "[other.sub2.sub-level2.sub-level3]\n";
  248 + ofile << "absolute_newest = true\n";
  249 + ofile.seekg(0, std::ios::beg);
  250 +
  251 + std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
  252 +
  253 + // 2 flags and 4 openings and 4 closings
  254 + EXPECT_EQ(10u, output.size());
  255 + EXPECT_TRUE(checkSections(output));
  256 +}
  257 +
  258 +TEST(StringBased, LayersSkip) {
  259 + std::stringstream ofile;
  260 +
  261 + ofile << "simple = true\n\n";
  262 + ofile << "[other.sub2]\n";
  263 + ofile << "[other.sub2.sub-level2.sub-level3]\n";
  264 + ofile << "absolute_newest = true\n";
  265 + ofile.seekg(0, std::ios::beg);
  266 +
  267 + std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
  268 +
  269 + // 2 flags and 4 openings and 4 closings
  270 + EXPECT_EQ(10u, output.size());
  271 + EXPECT_TRUE(checkSections(output));
  272 +}
  273 +
  274 +TEST(StringBased, LayersSkipOrdered) {
  275 + std::stringstream ofile;
  276 +
  277 + ofile << "simple = true\n\n";
  278 + ofile << "[other.sub2.sub-level2.sub-level3]\n";
  279 + ofile << "[other.sub2]\n";
  280 + ofile << "absolute_newest = true\n";
  281 + ofile.seekg(0, std::ios::beg);
  282 +
  283 + std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
  284 +
  285 + // 2 flags and 4 openings and 4 closings
  286 + EXPECT_EQ(12u, output.size());
  287 + EXPECT_TRUE(checkSections(output));
  288 +}
  289 +
  290 +TEST(StringBased, LayersChange) {
  291 + std::stringstream ofile;
  292 +
  293 + ofile << "simple = true\n\n";
  294 + ofile << "[other.sub2]\n";
  295 + ofile << "[other.sub3]\n";
  296 + ofile << "absolute_newest = true\n";
  297 + ofile.seekg(0, std::ios::beg);
  298 +
  299 + std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
  300 +
  301 + // 2 flags and 3 openings and 3 closings
  302 + EXPECT_EQ(8u, output.size());
  303 + EXPECT_TRUE(checkSections(output));
  304 +}
  305 +
  306 +TEST(StringBased, Layers2LevelChange) {
  307 + std::stringstream ofile;
  308 +
  309 + ofile << "simple = true\n\n";
  310 + ofile << "[other.sub2.cmd]\n";
  311 + ofile << "[other.sub3.cmd]\n";
  312 + ofile << "absolute_newest = true\n";
  313 + ofile.seekg(0, std::ios::beg);
  314 +
  315 + std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
  316 +
  317 + // 2 flags and 5 openings and 5 closings
  318 + EXPECT_EQ(12u, output.size());
  319 + EXPECT_TRUE(checkSections(output));
  320 +}
  321 +
  322 +TEST(StringBased, Layers3LevelChange) {
  323 + std::stringstream ofile;
  324 +
  325 + ofile << "[other.sub2.subsub.cmd]\n";
  326 + ofile << "[other.sub3.subsub.cmd]\n";
  327 + ofile << "absolute_newest = true\n";
  328 + ofile.seekg(0, std::ios::beg);
  329 +
  330 + std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
  331 +
  332 + // 1 flags and 7 openings and 7 closings
  333 + EXPECT_EQ(15u, output.size());
  334 + EXPECT_TRUE(checkSections(output));
  335 +}
  336 +
  337 +TEST(StringBased, newSegment) {
  338 + std::stringstream ofile;
  339 +
  340 + ofile << "[other.sub2.subsub.cmd]\n";
  341 + ofile << "flag = true\n";
  342 + ofile << "[another]\n";
  343 + ofile << "absolute_newest = true\n";
  344 + ofile.seekg(0, std::ios::beg);
  345 +
  346 + std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
  347 +
  348 + // 2 flags and 5 openings and 5 closings
  349 + EXPECT_EQ(12u, output.size());
  350 + EXPECT_TRUE(checkSections(output));
  351 +}
  352 +
  353 +TEST(StringBased, LayersDirect) {
  354 + std::stringstream ofile;
  355 +
  356 + ofile << "simple = true\n\n";
  357 + ofile << "[other.sub2.sub-level2.sub-level3]\n";
  358 + ofile << "absolute_newest = true\n";
  359 +
  360 + ofile.seekg(0, std::ios::beg);
  361 +
  362 + std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
  363 +
  364 + // 2 flags and 4 openings and 4 closings
  365 + EXPECT_EQ(10u, output.size());
  366 + EXPECT_TRUE(checkSections(output));
  367 +}
  368 +
  369 +TEST(StringBased, LayersComplex) {
  370 + std::stringstream ofile;
  371 +
  372 + ofile << "simple = true\n\n";
  373 + ofile << "[other.sub2.sub-level2.sub-level3]\n";
  374 + ofile << "absolute_newest = true\n";
  375 + ofile << "[other.sub2.sub-level2]\n";
  376 + ofile << "still_newer = true\n";
  377 + ofile << "[other.sub2]\n";
  378 + ofile << "newest = true\n";
  379 +
  380 + ofile.seekg(0, std::ios::beg);
  381 +
  382 + std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
  383 +
  384 + // 4 flags and 6 openings and 6 closings
  385 + EXPECT_EQ(16u, output.size());
  386 + EXPECT_TRUE(checkSections(output));
168 } 387 }
169 388
170 TEST(StringBased, file_error) { 389 TEST(StringBased, file_error) {
@@ -205,6 +424,7 @@ TEST_F(TApp, IniNotRequired) { @@ -205,6 +424,7 @@ TEST_F(TApp, IniNotRequired) {
205 EXPECT_EQ(1, one); 424 EXPECT_EQ(1, one);
206 EXPECT_EQ(2, two); 425 EXPECT_EQ(2, two);
207 EXPECT_EQ(3, three); 426 EXPECT_EQ(3, three);
  427 + EXPECT_EQ(app["--config"]->as<std::string>(), "TestIniTmp.ini");
208 } 428 }
209 429
210 TEST_F(TApp, IniSuccessOnUnknownOption) { 430 TEST_F(TApp, IniSuccessOnUnknownOption) {
@@ -263,120 +483,479 @@ TEST_F(TApp, IniGetNoRemaining) { @@ -263,120 +483,479 @@ TEST_F(TApp, IniGetNoRemaining) {
263 EXPECT_EQ(app.remaining().size(), 0u); 483 EXPECT_EQ(app.remaining().size(), 0u);
264 } 484 }
265 485
266 -TEST_F(TApp, IniNotRequiredNotDefault) { 486 +TEST_F(TApp, IniRequiredNoDefault) {
  487 +
  488 + app.set_config("--config")->required();
  489 +
  490 + int two = 0;
  491 + app.add_option("--two", two);
  492 + ASSERT_THROW(run(), CLI::FileError);
  493 +}
  494 +
  495 +TEST_F(TApp, IniNotRequiredNoDefault) {
  496 +
  497 + app.set_config("--config");
  498 +
  499 + int two = 0;
  500 + app.add_option("--two", two);
  501 + ASSERT_NO_THROW(run());
  502 +}
  503 +
  504 +/// Define a class for testing purposes that does bad things
  505 +class EvilConfig : public CLI::Config {
  506 + public:
  507 + EvilConfig() = default;
  508 + virtual std::string to_config(const CLI::App *, bool, bool, std::string) const { throw CLI::FileError("evil"); }
  509 +
  510 + virtual std::vector<CLI::ConfigItem> from_config(std::istream &) const { throw CLI::FileError("evil"); }
  511 +};
  512 +
  513 +TEST_F(TApp, IniRequiredbadConfigurator) {
  514 +
  515 + TempFile tmpini{"TestIniTmp.ini"};
  516 +
  517 + {
  518 + std::ofstream out{tmpini};
  519 + out << "[default]" << std::endl;
  520 + out << "two=99" << std::endl;
  521 + out << "three=3" << std::endl;
  522 + }
  523 +
  524 + app.set_config("--config", tmpini)->required();
  525 + app.config_formatter(std::make_shared<EvilConfig>());
  526 + int two = 0;
  527 + app.add_option("--two", two);
  528 + ASSERT_THROW(run(), CLI::FileError);
  529 +}
  530 +
  531 +TEST_F(TApp, IniNotRequiredbadConfigurator) {
  532 +
  533 + TempFile tmpini{"TestIniTmp.ini"};
  534 +
  535 + {
  536 + std::ofstream out{tmpini};
  537 + out << "[default]" << std::endl;
  538 + out << "two=99" << std::endl;
  539 + out << "three=3" << std::endl;
  540 + }
  541 +
  542 + app.set_config("--config", tmpini);
  543 + app.config_formatter(std::make_shared<EvilConfig>());
  544 + int two = 0;
  545 + app.add_option("--two", two);
  546 + ASSERT_NO_THROW(run());
  547 +}
  548 +
  549 +TEST_F(TApp, IniNotRequiredNotDefault) {
  550 +
  551 + TempFile tmpini{"TestIniTmp.ini"};
  552 + TempFile tmpini2{"TestIniTmp2.ini"};
  553 +
  554 + app.set_config("--config", tmpini);
  555 +
  556 + {
  557 + std::ofstream out{tmpini};
  558 + out << "[default]" << std::endl;
  559 + out << "two=99" << std::endl;
  560 + out << "three=3" << std::endl;
  561 + }
  562 +
  563 + {
  564 + std::ofstream out{tmpini2};
  565 + out << "[default]" << std::endl;
  566 + out << "two=98" << std::endl;
  567 + out << "three=4" << std::endl;
  568 + }
  569 +
  570 + int one = 0, two = 0, three = 0;
  571 + app.add_option("--one", one);
  572 + app.add_option("--two", two);
  573 + app.add_option("--three", three);
  574 +
  575 + run();
  576 + EXPECT_EQ(app["--config"]->as<std::string>(), tmpini.c_str());
  577 + EXPECT_EQ(99, two);
  578 + EXPECT_EQ(3, three);
  579 +
  580 + args = {"--config", tmpini2};
  581 + run();
  582 +
  583 + EXPECT_EQ(98, two);
  584 + EXPECT_EQ(4, three);
  585 + EXPECT_EQ(app.get_config_ptr()->as<std::string>(), tmpini2.c_str());
  586 +}
  587 +
  588 +TEST_F(TApp, IniRequiredNotFound) {
  589 +
  590 + std::string noini = "TestIniNotExist.ini";
  591 + app.set_config("--config", noini, "", true);
  592 +
  593 + EXPECT_THROW(run(), CLI::FileError);
  594 +}
  595 +
  596 +TEST_F(TApp, IniNotRequiredPassedNotFound) {
  597 +
  598 + std::string noini = "TestIniNotExist.ini";
  599 + app.set_config("--config", "", "", false);
  600 +
  601 + args = {"--config", noini};
  602 + EXPECT_THROW(run(), CLI::FileError);
  603 +}
  604 +
  605 +TEST_F(TApp, IniOverwrite) {
  606 +
  607 + TempFile tmpini{"TestIniTmp.ini"};
  608 + {
  609 + std::ofstream out{tmpini};
  610 + out << "[default]" << std::endl;
  611 + out << "two=99" << std::endl;
  612 + }
  613 +
  614 + std::string orig = "filename_not_exist.ini";
  615 + std::string next = "TestIniTmp.ini";
  616 + app.set_config("--config", orig);
  617 + // Make sure this can be overwritten
  618 + app.set_config("--conf", next);
  619 + int two = 7;
  620 + app.add_option("--two", two);
  621 +
  622 + run();
  623 +
  624 + EXPECT_EQ(99, two);
  625 +}
  626 +
  627 +TEST_F(TApp, IniRequired) {
  628 +
  629 + TempFile tmpini{"TestIniTmp.ini"};
  630 +
  631 + app.set_config("--config", tmpini, "", true);
  632 +
  633 + {
  634 + std::ofstream out{tmpini};
  635 + out << "[default]" << std::endl;
  636 + out << "two=99" << std::endl;
  637 + out << "three=3" << std::endl;
  638 + }
  639 +
  640 + int one = 0, two = 0, three = 0;
  641 + app.add_option("--one", one)->required();
  642 + app.add_option("--two", two)->required();
  643 + app.add_option("--three", three)->required();
  644 +
  645 + args = {"--one=1"};
  646 +
  647 + run();
  648 + EXPECT_EQ(one, 1);
  649 + EXPECT_EQ(two, 99);
  650 + EXPECT_EQ(three, 3);
  651 +
  652 + one = two = three = 0;
  653 + args = {"--one=1", "--two=2"};
  654 +
  655 + EXPECT_NO_THROW(run());
  656 + EXPECT_EQ(one, 1);
  657 + EXPECT_EQ(two, 2);
  658 + EXPECT_EQ(three, 3);
  659 +
  660 + args = {};
  661 +
  662 + EXPECT_THROW(run(), CLI::RequiredError);
  663 +
  664 + args = {"--two=2"};
  665 +
  666 + EXPECT_THROW(run(), CLI::RequiredError);
  667 +}
  668 +
  669 +TEST_F(TApp, IniVector) {
  670 +
  671 + TempFile tmpini{"TestIniTmp.ini"};
  672 +
  673 + app.set_config("--config", tmpini);
  674 +
  675 + {
  676 + std::ofstream out{tmpini};
  677 + out << "[default]" << std::endl;
  678 + out << "two=2 3" << std::endl;
  679 + out << "three=1 2 3" << std::endl;
  680 + }
  681 +
  682 + std::vector<int> two, three;
  683 + app.add_option("--two", two)->expected(2)->required();
  684 + app.add_option("--three", three)->required();
  685 +
  686 + run();
  687 +
  688 + EXPECT_EQ(std::vector<int>({2, 3}), two);
  689 + EXPECT_EQ(std::vector<int>({1, 2, 3}), three);
  690 +}
  691 +TEST_F(TApp, TOMLVector) {
  692 +
  693 + TempFile tmpini{"TestIniTmp.ini"};
  694 +
  695 + app.set_config("--config", tmpini);
  696 +
  697 + {
  698 + std::ofstream out{tmpini};
  699 + out << "#this is a comment line\n";
  700 + out << "[default]\n";
  701 + out << "two=[2,3]\n";
  702 + out << "three=[1,2,3]\n";
  703 + }
  704 +
  705 + std::vector<int> two, three;
  706 + app.add_option("--two", two)->expected(2)->required();
  707 + app.add_option("--three", three)->required();
  708 +
  709 + run();
  710 +
  711 + EXPECT_EQ(std::vector<int>({2, 3}), two);
  712 + EXPECT_EQ(std::vector<int>({1, 2, 3}), three);
  713 +}
  714 +
  715 +TEST_F(TApp, ColonValueSep) {
  716 +
  717 + TempFile tmpini{"TestIniTmp.ini"};
  718 +
  719 + app.set_config("--config", tmpini);
  720 +
  721 + {
  722 + std::ofstream out{tmpini};
  723 + out << "#this is a comment line\n";
  724 + out << "[default]\n";
  725 + out << "two:2\n";
  726 + out << "three:3\n";
  727 + }
  728 +
  729 + int two, three;
  730 + app.add_option("--two", two);
  731 + app.add_option("--three", three);
  732 +
  733 + app.get_config_formatter_base()->valueSeparator(':');
  734 +
  735 + run();
  736 +
  737 + EXPECT_EQ(2, two);
  738 + EXPECT_EQ(3, three);
  739 +}
  740 +
  741 +TEST_F(TApp, TOMLVectordirect) {
  742 +
  743 + TempFile tmpini{"TestIniTmp.ini"};
  744 +
  745 + app.set_config("--config", tmpini);
  746 +
  747 + app.config_formatter(std::make_shared<CLI::ConfigTOML>());
  748 +
  749 + {
  750 + std::ofstream out{tmpini};
  751 + out << "#this is a comment line\n";
  752 + out << "[default]\n";
  753 + out << "two=[2,3]\n";
  754 + out << "three=[1,2,3]\n";
  755 + }
  756 +
  757 + std::vector<int> two, three;
  758 + app.add_option("--two", two)->expected(2)->required();
  759 + app.add_option("--three", three)->required();
  760 +
  761 + run();
  762 +
  763 + EXPECT_EQ(std::vector<int>({2, 3}), two);
  764 + EXPECT_EQ(std::vector<int>({1, 2, 3}), three);
  765 +}
  766 +
  767 +TEST_F(TApp, IniVectorCsep) {
  768 +
  769 + TempFile tmpini{"TestIniTmp.ini"};
  770 +
  771 + app.set_config("--config", tmpini);
  772 +
  773 + {
  774 + std::ofstream out{tmpini};
  775 + out << "#this is a comment line\n";
  776 + out << "[default]\n";
  777 + out << "two=[2,3]\n";
  778 + out << "three=1,2,3\n";
  779 + }
  780 +
  781 + std::vector<int> two, three;
  782 + app.add_option("--two", two)->expected(2)->required();
  783 + app.add_option("--three", three)->required();
  784 +
  785 + run();
  786 +
  787 + EXPECT_EQ(std::vector<int>({2, 3}), two);
  788 + EXPECT_EQ(std::vector<int>({1, 2, 3}), three);
  789 +}
  790 +
  791 +TEST_F(TApp, IniVectorMultiple) {
  792 +
  793 + TempFile tmpini{"TestIniTmp.ini"};
  794 +
  795 + app.set_config("--config", tmpini);
  796 +
  797 + {
  798 + std::ofstream out{tmpini};
  799 + out << "#this is a comment line\n";
  800 + out << "[default]\n";
  801 + out << "two=2\n";
  802 + out << "two=3\n";
  803 + out << "three=1\n";
  804 + out << "three=2\n";
  805 + out << "three=3\n";
  806 + }
  807 +
  808 + std::vector<int> two, three;
  809 + app.add_option("--two", two)->expected(2)->required();
  810 + app.add_option("--three", three)->required();
  811 +
  812 + run();
  813 +
  814 + EXPECT_EQ(std::vector<int>({2, 3}), two);
  815 + EXPECT_EQ(std::vector<int>({1, 2, 3}), three);
  816 +}
  817 +
  818 +TEST_F(TApp, IniLayered) {
  819 +
  820 + TempFile tmpini{"TestIniTmp.ini"};
  821 +
  822 + app.set_config("--config", tmpini);
  823 +
  824 + {
  825 + std::ofstream out{tmpini};
  826 + out << "[default]" << std::endl;
  827 + out << "val=1" << std::endl;
  828 + out << "[subcom]" << std::endl;
  829 + out << "val=2" << std::endl;
  830 + out << "subsubcom.val=3" << std::endl;
  831 + }
  832 +
  833 + int one = 0, two = 0, three = 0;
  834 + app.add_option("--val", one);
  835 + auto subcom = app.add_subcommand("subcom");
  836 + subcom->add_option("--val", two);
  837 + auto subsubcom = subcom->add_subcommand("subsubcom");
  838 + subsubcom->add_option("--val", three);
  839 +
  840 + run();
  841 +
  842 + EXPECT_EQ(1, one);
  843 + EXPECT_EQ(2, two);
  844 + EXPECT_EQ(3, three);
  845 +
  846 + EXPECT_EQ(subcom->count(), 0U);
  847 + EXPECT_FALSE(*subcom);
  848 +}
  849 +
  850 +TEST_F(TApp, IniLayeredDotSection) {
267 851
268 TempFile tmpini{"TestIniTmp.ini"}; 852 TempFile tmpini{"TestIniTmp.ini"};
269 - TempFile tmpini2{"TestIniTmp2.ini"};  
270 853
271 app.set_config("--config", tmpini); 854 app.set_config("--config", tmpini);
272 855
273 { 856 {
274 std::ofstream out{tmpini}; 857 std::ofstream out{tmpini};
275 out << "[default]" << std::endl; 858 out << "[default]" << std::endl;
276 - out << "two=99" << std::endl;  
277 - out << "three=3" << std::endl;  
278 - }  
279 -  
280 - {  
281 - std::ofstream out{tmpini2};  
282 - out << "[default]" << std::endl;  
283 - out << "two=98" << std::endl;  
284 - out << "three=4" << std::endl; 859 + out << "val=1" << std::endl;
  860 + out << "[subcom]" << std::endl;
  861 + out << "val=2" << std::endl;
  862 + out << "[subcom.subsubcom]" << std::endl;
  863 + out << "val=3" << std::endl;
285 } 864 }
286 865
287 int one = 0, two = 0, three = 0; 866 int one = 0, two = 0, three = 0;
288 - app.add_option("--one", one);  
289 - app.add_option("--two", two);  
290 - app.add_option("--three", three); 867 + app.add_option("--val", one);
  868 + auto subcom = app.add_subcommand("subcom");
  869 + subcom->add_option("--val", two);
  870 + auto subsubcom = subcom->add_subcommand("subsubcom");
  871 + subsubcom->add_option("--val", three);
291 872
292 run(); 873 run();
293 874
294 - EXPECT_EQ(99, two); 875 + EXPECT_EQ(1, one);
  876 + EXPECT_EQ(2, two);
295 EXPECT_EQ(3, three); 877 EXPECT_EQ(3, three);
296 878
297 - args = {"--config", tmpini2};  
298 - run();  
299 -  
300 - EXPECT_EQ(98, two);  
301 - EXPECT_EQ(4, three);  
302 -}  
303 -  
304 -TEST_F(TApp, IniRequiredNotFound) {  
305 -  
306 - std::string noini = "TestIniNotExist.ini";  
307 - app.set_config("--config", noini, "", true);  
308 -  
309 - EXPECT_THROW(run(), CLI::FileError); 879 + EXPECT_EQ(subcom->count(), 0U);
  880 + EXPECT_FALSE(*subcom);
310 } 881 }
311 882
312 -TEST_F(TApp, IniNotRequiredPassedNotFound) {  
313 -  
314 - std::string noini = "TestIniNotExist.ini";  
315 - app.set_config("--config", "", "", false); 883 +TEST_F(TApp, IniSubcommandConfigurable) {
316 884
317 - args = {"--config", noini};  
318 - EXPECT_THROW(run(), CLI::FileError);  
319 -} 885 + TempFile tmpini{"TestIniTmp.ini"};
320 886
321 -TEST_F(TApp, IniOverwrite) { 887 + app.set_config("--config", tmpini);
322 888
323 - TempFile tmpini{"TestIniTmp.ini"};  
324 { 889 {
325 std::ofstream out{tmpini}; 890 std::ofstream out{tmpini};
326 out << "[default]" << std::endl; 891 out << "[default]" << std::endl;
327 - out << "two=99" << std::endl; 892 + out << "val=1" << std::endl;
  893 + out << "[subcom]" << std::endl;
  894 + out << "val=2" << std::endl;
  895 + out << "subsubcom.val=3" << std::endl;
328 } 896 }
329 897
330 - std::string orig = "filename_not_exist.ini";  
331 - std::string next = "TestIniTmp.ini";  
332 - app.set_config("--config", orig);  
333 - // Make sure this can be overwritten  
334 - app.set_config("--conf", next);  
335 - int two = 7;  
336 - app.add_option("--two", two); 898 + int one = 0, two = 0, three = 0;
  899 + app.add_option("--val", one);
  900 + auto subcom = app.add_subcommand("subcom");
  901 + subcom->configurable();
  902 + subcom->add_option("--val", two);
  903 + auto subsubcom = subcom->add_subcommand("subsubcom");
  904 + subsubcom->add_option("--val", three);
337 905
338 run(); 906 run();
339 907
340 - EXPECT_EQ(99, two); 908 + EXPECT_EQ(1, one);
  909 + EXPECT_EQ(2, two);
  910 + EXPECT_EQ(3, three);
  911 +
  912 + EXPECT_EQ(subcom->count(), 1U);
  913 + EXPECT_TRUE(*subcom);
  914 + EXPECT_TRUE(app.got_subcommand(subcom));
341 } 915 }
342 916
343 -TEST_F(TApp, IniRequired) { 917 +TEST_F(TApp, IniSubcommandConfigurablePreParse) {
344 918
345 TempFile tmpini{"TestIniTmp.ini"}; 919 TempFile tmpini{"TestIniTmp.ini"};
346 920
347 - app.set_config("--config", tmpini, "", true); 921 + app.set_config("--config", tmpini);
348 922
349 { 923 {
350 std::ofstream out{tmpini}; 924 std::ofstream out{tmpini};
351 out << "[default]" << std::endl; 925 out << "[default]" << std::endl;
352 - out << "two=99" << std::endl;  
353 - out << "three=3" << std::endl; 926 + out << "val=1" << std::endl;
  927 + out << "[subcom]" << std::endl;
  928 + out << "val=2" << std::endl;
  929 + out << "subsubcom.val=3" << std::endl;
354 } 930 }
355 931
356 - int one = 0, two = 0, three = 0;  
357 - app.add_option("--one", one)->required();  
358 - app.add_option("--two", two)->required();  
359 - app.add_option("--three", three)->required();  
360 -  
361 - args = {"--one=1"};  
362 -  
363 - run();  
364 -  
365 - one = two = three = 0;  
366 - args = {"--one=1", "--two=2"}; 932 + int one = 0, two = 0, three = 0, four = 0;
  933 + app.add_option("--val", one);
  934 + auto subcom = app.add_subcommand("subcom");
  935 + auto subcom2 = app.add_subcommand("subcom2");
  936 + subcom->configurable();
  937 + std::vector<std::size_t> parse_c;
  938 + subcom->preparse_callback([&parse_c](std::size_t cnt) { parse_c.push_back(cnt); });
  939 + subcom->add_option("--val", two);
  940 + subcom2->add_option("--val", four);
  941 + subcom2->preparse_callback([&parse_c](std::size_t cnt) { parse_c.push_back(cnt + 2623); });
  942 + auto subsubcom = subcom->add_subcommand("subsubcom");
  943 + subsubcom->add_option("--val", three);
367 944
368 run(); 945 run();
369 946
370 - args = {};  
371 -  
372 - EXPECT_THROW(run(), CLI::RequiredError); 947 + EXPECT_EQ(1, one);
  948 + EXPECT_EQ(2, two);
  949 + EXPECT_EQ(3, three);
  950 + EXPECT_EQ(0, four);
373 951
374 - args = {"--two=2"}; 952 + EXPECT_EQ(parse_c.size(), 1U);
  953 + EXPECT_EQ(parse_c[0], 2U);
375 954
376 - EXPECT_THROW(run(), CLI::RequiredError); 955 + EXPECT_EQ(subcom2->count(), 0U);
377 } 956 }
378 957
379 -TEST_F(TApp, IniVector) { 958 +TEST_F(TApp, IniSubcommandConfigurableParseComplete) {
380 959
381 TempFile tmpini{"TestIniTmp.ini"}; 960 TempFile tmpini{"TestIniTmp.ini"};
382 961
@@ -385,21 +964,43 @@ TEST_F(TApp, IniVector) { @@ -385,21 +964,43 @@ TEST_F(TApp, IniVector) {
385 { 964 {
386 std::ofstream out{tmpini}; 965 std::ofstream out{tmpini};
387 out << "[default]" << std::endl; 966 out << "[default]" << std::endl;
388 - out << "two=2 3" << std::endl;  
389 - out << "three=1 2 3" << std::endl; 967 + out << "val=1" << std::endl;
  968 + out << "[subcom]" << std::endl;
  969 + out << "val=2" << std::endl;
  970 + out << "[subcom.subsubcom]" << std::endl;
  971 + out << "val=3" << std::endl;
390 } 972 }
391 973
392 - std::vector<int> two, three;  
393 - app.add_option("--two", two)->expected(2)->required();  
394 - app.add_option("--three", three)->required(); 974 + int one = 0, two = 0, three = 0, four = 0;
  975 + app.add_option("--val", one);
  976 + auto subcom = app.add_subcommand("subcom");
  977 + auto subcom2 = app.add_subcommand("subcom2");
  978 + subcom->configurable();
  979 + std::vector<std::size_t> parse_c;
  980 + subcom->parse_complete_callback([&parse_c]() { parse_c.push_back(58); });
  981 + subcom->add_option("--val", two);
  982 + subcom2->add_option("--val", four);
  983 + subcom2->parse_complete_callback([&parse_c]() { parse_c.push_back(2623); });
  984 + auto subsubcom = subcom->add_subcommand("subsubcom");
  985 + // configurable should be inherited
  986 + subsubcom->parse_complete_callback([&parse_c]() { parse_c.push_back(68); });
  987 + subsubcom->add_option("--val", three);
395 988
396 run(); 989 run();
397 990
398 - EXPECT_EQ(std::vector<int>({2, 3}), two);  
399 - EXPECT_EQ(std::vector<int>({1, 2, 3}), three); 991 + EXPECT_EQ(1, one);
  992 + EXPECT_EQ(2, two);
  993 + EXPECT_EQ(3, three);
  994 + EXPECT_EQ(0, four);
  995 +
  996 + ASSERT_EQ(parse_c.size(), 2u);
  997 + EXPECT_EQ(parse_c[0], 68U);
  998 + EXPECT_EQ(parse_c[1], 58U);
  999 + EXPECT_EQ(subsubcom->count(), 1u);
  1000 + EXPECT_EQ(subcom2->count(), 0u);
400 } 1001 }
401 1002
402 -TEST_F(TApp, IniLayered) { 1003 +TEST_F(TApp, IniSubcommandMultipleSections) {
403 1004
404 TempFile tmpini{"TestIniTmp.ini"}; 1005 TempFile tmpini{"TestIniTmp.ini"};
405 1006
@@ -411,14 +1012,26 @@ TEST_F(TApp, IniLayered) { @@ -411,14 +1012,26 @@ TEST_F(TApp, IniLayered) {
411 out << "val=1" << std::endl; 1012 out << "val=1" << std::endl;
412 out << "[subcom]" << std::endl; 1013 out << "[subcom]" << std::endl;
413 out << "val=2" << std::endl; 1014 out << "val=2" << std::endl;
414 - out << "subsubcom.val=3" << std::endl; 1015 + out << "[subcom.subsubcom]" << std::endl;
  1016 + out << "val=3" << std::endl;
  1017 + out << "[subcom2]" << std::endl;
  1018 + out << "val=4" << std::endl;
415 } 1019 }
416 1020
417 - int one = 0, two = 0, three = 0; 1021 + int one = 0, two = 0, three = 0, four = 0;
418 app.add_option("--val", one); 1022 app.add_option("--val", one);
419 auto subcom = app.add_subcommand("subcom"); 1023 auto subcom = app.add_subcommand("subcom");
  1024 + auto subcom2 = app.add_subcommand("subcom2");
  1025 + subcom->configurable();
  1026 + std::vector<std::size_t> parse_c;
  1027 + subcom->parse_complete_callback([&parse_c]() { parse_c.push_back(58); });
420 subcom->add_option("--val", two); 1028 subcom->add_option("--val", two);
  1029 + subcom2->add_option("--val", four);
  1030 + subcom2->parse_complete_callback([&parse_c]() { parse_c.push_back(2623); });
  1031 + subcom2->configurable(false);
421 auto subsubcom = subcom->add_subcommand("subsubcom"); 1032 auto subsubcom = subcom->add_subcommand("subsubcom");
  1033 + // configurable should be inherited
  1034 + subsubcom->parse_complete_callback([&parse_c]() { parse_c.push_back(68); });
422 subsubcom->add_option("--val", three); 1035 subsubcom->add_option("--val", three);
423 1036
424 run(); 1037 run();
@@ -426,9 +1039,16 @@ TEST_F(TApp, IniLayered) { @@ -426,9 +1039,16 @@ TEST_F(TApp, IniLayered) {
426 EXPECT_EQ(1, one); 1039 EXPECT_EQ(1, one);
427 EXPECT_EQ(2, two); 1040 EXPECT_EQ(2, two);
428 EXPECT_EQ(3, three); 1041 EXPECT_EQ(3, three);
  1042 + EXPECT_EQ(4, four);
  1043 +
  1044 + ASSERT_EQ(parse_c.size(), 2u);
  1045 + EXPECT_EQ(parse_c[0], 68U);
  1046 + EXPECT_EQ(parse_c[1], 58U);
  1047 + EXPECT_EQ(subsubcom->count(), 1u);
  1048 + EXPECT_EQ(subcom2->count(), 0u); // not configurable but value is updated
429 } 1049 }
430 1050
431 -TEST_F(TApp, IniFailure) { 1051 +TEST_F(TApp, DuplicateSubcommandCallbacks) {
432 1052
433 TempFile tmpini{"TestIniTmp.ini"}; 1053 TempFile tmpini{"TestIniTmp.ini"};
434 1054
@@ -436,6 +1056,30 @@ TEST_F(TApp, IniFailure) { @@ -436,6 +1056,30 @@ TEST_F(TApp, IniFailure) {
436 1056
437 { 1057 {
438 std::ofstream out{tmpini}; 1058 std::ofstream out{tmpini};
  1059 + out << "[[foo]]" << std::endl;
  1060 + out << "[[foo]]" << std::endl;
  1061 + out << "[[foo]]" << std::endl;
  1062 + }
  1063 +
  1064 + auto foo = app.add_subcommand("foo");
  1065 + int count = 0;
  1066 + foo->callback([&count]() { ++count; });
  1067 + foo->immediate_callback();
  1068 + EXPECT_TRUE(foo->get_immediate_callback());
  1069 + foo->configurable();
  1070 +
  1071 + run();
  1072 + EXPECT_EQ(count, 3);
  1073 +}
  1074 +
  1075 +TEST_F(TApp, IniFailure) {
  1076 +
  1077 + TempFile tmpini{"TestIniTmp.ini"};
  1078 +
  1079 + app.set_config("--config", tmpini);
  1080 + app.allow_config_extras(false);
  1081 + {
  1082 + std::ofstream out{tmpini};
439 out << "[default]" << std::endl; 1083 out << "[default]" << std::endl;
440 out << "val=1" << std::endl; 1084 out << "val=1" << std::endl;
441 } 1085 }
@@ -484,7 +1128,7 @@ TEST_F(TApp, IniSubFailure) { @@ -484,7 +1128,7 @@ TEST_F(TApp, IniSubFailure) {
484 1128
485 app.add_subcommand("other"); 1129 app.add_subcommand("other");
486 app.set_config("--config", tmpini); 1130 app.set_config("--config", tmpini);
487 - 1131 + app.allow_config_extras(false);
488 { 1132 {
489 std::ofstream out{tmpini}; 1133 std::ofstream out{tmpini};
490 out << "[other]" << std::endl; 1134 out << "[other]" << std::endl;
@@ -499,7 +1143,7 @@ TEST_F(TApp, IniNoSubFailure) { @@ -499,7 +1143,7 @@ TEST_F(TApp, IniNoSubFailure) {
499 TempFile tmpini{"TestIniTmp.ini"}; 1143 TempFile tmpini{"TestIniTmp.ini"};
500 1144
501 app.set_config("--config", tmpini); 1145 app.set_config("--config", tmpini);
502 - 1146 + app.allow_config_extras(CLI::config_extras_mode::error);
503 { 1147 {
504 std::ofstream out{tmpini}; 1148 std::ofstream out{tmpini};
505 out << "[other]" << std::endl; 1149 out << "[other]" << std::endl;
@@ -767,7 +1411,47 @@ TEST_F(TApp, IniOutputShortDoubleDescription) { @@ -767,7 +1411,47 @@ TEST_F(TApp, IniOutputShortDoubleDescription) {
767 run(); 1411 run();
768 1412
769 std::string str = app.config_to_str(true, true); 1413 std::string str = app.config_to_str(true, true);
770 - EXPECT_EQ(str, "; " + description1 + "\n" + flag1 + "=false\n\n; " + description2 + "\n" + flag2 + "=false\n"); 1414 + EXPECT_THAT(
  1415 + str, HasSubstr("; " + description1 + "\n" + flag1 + "=false\n\n; " + description2 + "\n" + flag2 + "=false\n"));
  1416 +}
  1417 +
  1418 +TEST_F(TApp, IniOutputGroups) {
  1419 + std::string flag1 = "flagnr1";
  1420 + std::string flag2 = "flagnr2";
  1421 + const std::string description1 = "First description.";
  1422 + const std::string description2 = "Second description.";
  1423 + app.add_flag("--" + flag1, description1)->group("group1");
  1424 + app.add_flag("--" + flag2, description2)->group("group2");
  1425 +
  1426 + run();
  1427 +
  1428 + std::string str = app.config_to_str(true, true);
  1429 + EXPECT_THAT(str, HasSubstr("group1"));
  1430 + EXPECT_THAT(str, HasSubstr("group2"));
  1431 +}
  1432 +
  1433 +TEST_F(TApp, IniOutputHiddenOptions) {
  1434 + std::string flag1 = "flagnr1";
  1435 + std::string flag2 = "flagnr2";
  1436 + double val = 12.7;
  1437 + const std::string description1 = "First description.";
  1438 + const std::string description2 = "Second description.";
  1439 + app.add_flag("--" + flag1, description1)->group("group1");
  1440 + app.add_flag("--" + flag2, description2)->group("group2");
  1441 + app.add_option("--dval", val, "", true)->group("");
  1442 +
  1443 + run();
  1444 +
  1445 + std::string str = app.config_to_str(true, true);
  1446 + EXPECT_THAT(str, HasSubstr("group1"));
  1447 + EXPECT_THAT(str, HasSubstr("group2"));
  1448 + EXPECT_THAT(str, HasSubstr("dval=12.7"));
  1449 + auto loc = str.find("dval=12.7");
  1450 + auto locg1 = str.find("group1");
  1451 + EXPECT_GT(locg1, loc);
  1452 + // make sure it doesn't come twice
  1453 + loc = str.find("dval=12.7", loc + 4);
  1454 + EXPECT_EQ(loc, std::string::npos);
771 } 1455 }
772 1456
773 TEST_F(TApp, IniOutputMultiLineDescription) { 1457 TEST_F(TApp, IniOutputMultiLineDescription) {
@@ -783,6 +1467,35 @@ TEST_F(TApp, IniOutputMultiLineDescription) { @@ -783,6 +1467,35 @@ TEST_F(TApp, IniOutputMultiLineDescription) {
783 EXPECT_THAT(str, HasSubstr(flag + "=false\n")); 1467 EXPECT_THAT(str, HasSubstr(flag + "=false\n"));
784 } 1468 }
785 1469
  1470 +TEST_F(TApp, IniOutputOptionGroup) {
  1471 + std::string flag1 = "flagnr1";
  1472 + std::string flag2 = "flagnr2";
  1473 + double val = 12.7;
  1474 + const std::string description1 = "First description.";
  1475 + const std::string description2 = "Second description.";
  1476 + app.add_flag("--" + flag1, description1)->group("group1");
  1477 + app.add_flag("--" + flag2, description2)->group("group2");
  1478 + auto og = app.add_option_group("group3", "g3 desc");
  1479 + og->add_option("--dval", val, "", true)->group("");
  1480 +
  1481 + run();
  1482 +
  1483 + std::string str = app.config_to_str(true, true);
  1484 + EXPECT_THAT(str, HasSubstr("group1"));
  1485 + EXPECT_THAT(str, HasSubstr("group2"));
  1486 + EXPECT_THAT(str, HasSubstr("dval=12.7"));
  1487 + EXPECT_THAT(str, HasSubstr("group3"));
  1488 + EXPECT_THAT(str, HasSubstr("g3 desc"));
  1489 + auto loc = str.find("dval=12.7");
  1490 + auto locg1 = str.find("group1");
  1491 + auto locg3 = str.find("group3");
  1492 + EXPECT_LT(locg1, loc);
  1493 + // make sure it doesn't come twice
  1494 + loc = str.find("dval=12.7", loc + 4);
  1495 + EXPECT_EQ(loc, std::string::npos);
  1496 + EXPECT_GT(locg3, locg1);
  1497 +}
  1498 +
786 TEST_F(TApp, IniOutputVector) { 1499 TEST_F(TApp, IniOutputVector) {
787 1500
788 std::vector<int> v; 1501 std::vector<int> v;
@@ -796,6 +1509,34 @@ TEST_F(TApp, IniOutputVector) { @@ -796,6 +1509,34 @@ TEST_F(TApp, IniOutputVector) {
796 EXPECT_EQ("vector=1 2 3\n", str); 1509 EXPECT_EQ("vector=1 2 3\n", str);
797 } 1510 }
798 1511
  1512 +TEST_F(TApp, IniOutputVectorTOML) {
  1513 +
  1514 + std::vector<int> v;
  1515 + app.add_option("--vector", v);
  1516 + app.config_formatter(std::make_shared<CLI::ConfigTOML>());
  1517 + args = {"--vector", "1", "2", "3"};
  1518 +
  1519 + run();
  1520 +
  1521 + std::string str = app.config_to_str();
  1522 + EXPECT_EQ("vector=[1, 2, 3]\n", str);
  1523 +}
  1524 +
  1525 +TEST_F(TApp, IniOutputVectorCustom) {
  1526 +
  1527 + std::vector<int> v;
  1528 + app.add_option("--vector", v);
  1529 + auto V = std::make_shared<CLI::ConfigBase>();
  1530 + V->arrayBounds('{', '}')->arrayDelimiter(';')->valueSeparator(':');
  1531 + app.config_formatter(V);
  1532 + args = {"--vector", "1", "2", "3"};
  1533 +
  1534 + run();
  1535 +
  1536 + std::string str = app.config_to_str();
  1537 + EXPECT_EQ("vector:{1; 2; 3}\n", str);
  1538 +}
  1539 +
799 TEST_F(TApp, IniOutputFlag) { 1540 TEST_F(TApp, IniOutputFlag) {
800 1541
801 int v, q; 1542 int v, q;
@@ -812,7 +1553,7 @@ TEST_F(TApp, IniOutputFlag) { @@ -812,7 +1553,7 @@ TEST_F(TApp, IniOutputFlag) {
812 EXPECT_THAT(str, HasSubstr("simple=3")); 1553 EXPECT_THAT(str, HasSubstr("simple=3"));
813 EXPECT_THAT(str, Not(HasSubstr("nothing"))); 1554 EXPECT_THAT(str, Not(HasSubstr("nothing")));
814 EXPECT_THAT(str, HasSubstr("onething=true")); 1555 EXPECT_THAT(str, HasSubstr("onething=true"));
815 - EXPECT_THAT(str, HasSubstr("something=2")); 1556 + EXPECT_THAT(str, HasSubstr("something=true true"));
816 1557
817 str = app.config_to_str(true); 1558 str = app.config_to_str(true);
818 EXPECT_THAT(str, HasSubstr("nothing")); 1559 EXPECT_THAT(str, HasSubstr("nothing"));
@@ -859,6 +1600,83 @@ TEST_F(TApp, IniOutputSubcom) { @@ -859,6 +1600,83 @@ TEST_F(TApp, IniOutputSubcom) {
859 EXPECT_THAT(str, HasSubstr("other.newer=true")); 1600 EXPECT_THAT(str, HasSubstr("other.newer=true"));
860 } 1601 }
861 1602
  1603 +TEST_F(TApp, IniOutputSubcomConfigurable) {
  1604 +
  1605 + app.add_flag("--simple");
  1606 + auto subcom = app.add_subcommand("other")->configurable();
  1607 + subcom->add_flag("--newer");
  1608 +
  1609 + args = {"--simple", "other", "--newer"};
  1610 + run();
  1611 +
  1612 + std::string str = app.config_to_str();
  1613 + EXPECT_THAT(str, HasSubstr("simple=true"));
  1614 + EXPECT_THAT(str, HasSubstr("[other]"));
  1615 + EXPECT_THAT(str, HasSubstr("newer=true"));
  1616 + EXPECT_EQ(str.find("other.newer=true"), std::string::npos);
  1617 +}
  1618 +
  1619 +TEST_F(TApp, IniOutputSubsubcom) {
  1620 +
  1621 + app.add_flag("--simple");
  1622 + auto subcom = app.add_subcommand("other");
  1623 + subcom->add_flag("--newer");
  1624 + auto subsubcom = subcom->add_subcommand("sub2");
  1625 + subsubcom->add_flag("--newest");
  1626 +
  1627 + args = {"--simple", "other", "--newer", "sub2", "--newest"};
  1628 + run();
  1629 +
  1630 + std::string str = app.config_to_str();
  1631 + EXPECT_THAT(str, HasSubstr("simple=true"));
  1632 + EXPECT_THAT(str, HasSubstr("other.newer=true"));
  1633 + EXPECT_THAT(str, HasSubstr("other.sub2.newest=true"));
  1634 +}
  1635 +
  1636 +TEST_F(TApp, IniOutputSubsubcomConfigurable) {
  1637 +
  1638 + app.add_flag("--simple");
  1639 + auto subcom = app.add_subcommand("other")->configurable();
  1640 + subcom->add_flag("--newer");
  1641 +
  1642 + auto subsubcom = subcom->add_subcommand("sub2");
  1643 + subsubcom->add_flag("--newest");
  1644 +
  1645 + args = {"--simple", "other", "--newer", "sub2", "--newest"};
  1646 + run();
  1647 +
  1648 + std::string str = app.config_to_str();
  1649 + EXPECT_THAT(str, HasSubstr("simple=true"));
  1650 + EXPECT_THAT(str, HasSubstr("[other]"));
  1651 + EXPECT_THAT(str, HasSubstr("newer=true"));
  1652 + EXPECT_THAT(str, HasSubstr("[other.sub2]"));
  1653 + EXPECT_THAT(str, HasSubstr("newest=true"));
  1654 + EXPECT_EQ(str.find("sub2.newest=true"), std::string::npos);
  1655 +}
  1656 +
  1657 +TEST_F(TApp, IniOutputSubsubcomConfigurableDeep) {
  1658 +
  1659 + app.add_flag("--simple");
  1660 + auto subcom = app.add_subcommand("other")->configurable();
  1661 + subcom->add_flag("--newer");
  1662 +
  1663 + auto subsubcom = subcom->add_subcommand("sub2");
  1664 + subsubcom->add_flag("--newest");
  1665 + auto sssscom = subsubcom->add_subcommand("sub-level2");
  1666 + subsubcom->add_flag("--still_newer");
  1667 + auto s5com = sssscom->add_subcommand("sub-level3");
  1668 + s5com->add_flag("--absolute_newest");
  1669 +
  1670 + args = {"--simple", "other", "sub2", "sub-level2", "sub-level3", "--absolute_newest"};
  1671 + run();
  1672 +
  1673 + std::string str = app.config_to_str();
  1674 + EXPECT_THAT(str, HasSubstr("simple=true"));
  1675 + EXPECT_THAT(str, HasSubstr("[other.sub2.sub-level2.sub-level3]"));
  1676 + EXPECT_THAT(str, HasSubstr("absolute_newest=true"));
  1677 + EXPECT_EQ(str.find(".absolute_newest=true"), std::string::npos);
  1678 +}
  1679 +
862 TEST_F(TApp, IniQuotedOutput) { 1680 TEST_F(TApp, IniQuotedOutput) {
863 1681
864 std::string val1; 1682 std::string val1;
@@ -915,3 +1733,25 @@ TEST_F(TApp, StopReadingConfigOnClear) { @@ -915,3 +1733,25 @@ TEST_F(TApp, StopReadingConfigOnClear) {
915 1733
916 EXPECT_EQ(volume, 0); 1734 EXPECT_EQ(volume, 0);
917 } 1735 }
  1736 +
  1737 +TEST_F(TApp, ConfigWriteReadWrite) {
  1738 +
  1739 + TempFile tmpini{"TestIniTmp.ini"};
  1740 +
  1741 + app.add_flag("--flag");
  1742 + run();
  1743 +
  1744 + // Save config, with default values too
  1745 + std::string config1 = app.config_to_str(true, true);
  1746 + {
  1747 + std::ofstream out{tmpini};
  1748 + out << config1 << std::endl;
  1749 + }
  1750 +
  1751 + app.set_config("--config", tmpini, "Read an ini file", true);
  1752 + run();
  1753 +
  1754 + std::string config2 = app.config_to_str(true, true);
  1755 +
  1756 + EXPECT_EQ(config1, config2);
  1757 +}
tests/CreationTest.cpp
@@ -443,6 +443,7 @@ TEST_F(TApp, SubcommandDefaults) { @@ -443,6 +443,7 @@ TEST_F(TApp, SubcommandDefaults) {
443 EXPECT_FALSE(app.get_allow_windows_style_options()); 443 EXPECT_FALSE(app.get_allow_windows_style_options());
444 #endif 444 #endif
445 EXPECT_FALSE(app.get_fallthrough()); 445 EXPECT_FALSE(app.get_fallthrough());
  446 + EXPECT_FALSE(app.get_configurable());
446 EXPECT_FALSE(app.get_validate_positionals()); 447 EXPECT_FALSE(app.get_validate_positionals());
447 448
448 EXPECT_EQ(app.get_footer(), ""); 449 EXPECT_EQ(app.get_footer(), "");
@@ -455,6 +456,7 @@ TEST_F(TApp, SubcommandDefaults) { @@ -455,6 +456,7 @@ TEST_F(TApp, SubcommandDefaults) {
455 app.immediate_callback(); 456 app.immediate_callback();
456 app.ignore_case(); 457 app.ignore_case();
457 app.ignore_underscore(); 458 app.ignore_underscore();
  459 + app.configurable();
458 #ifdef _WIN32 460 #ifdef _WIN32
459 app.allow_windows_style_options(false); 461 app.allow_windows_style_options(false);
460 #else 462 #else
@@ -482,6 +484,7 @@ TEST_F(TApp, SubcommandDefaults) { @@ -482,6 +484,7 @@ TEST_F(TApp, SubcommandDefaults) {
482 #endif 484 #endif
483 EXPECT_TRUE(app2->get_fallthrough()); 485 EXPECT_TRUE(app2->get_fallthrough());
484 EXPECT_TRUE(app2->get_validate_positionals()); 486 EXPECT_TRUE(app2->get_validate_positionals());
  487 + EXPECT_TRUE(app2->get_configurable());
485 EXPECT_EQ(app2->get_footer(), "footy"); 488 EXPECT_EQ(app2->get_footer(), "footy");
486 EXPECT_EQ(app2->get_group(), "Stuff"); 489 EXPECT_EQ(app2->get_group(), "Stuff");
487 EXPECT_EQ(app2->get_require_subcommand_min(), 0u); 490 EXPECT_EQ(app2->get_require_subcommand_min(), 0u);