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 83 - Easy to execute, with help, parse errors, etc. providing correct exit and details.
84 84 - Easy to extend as part of a framework that provides "applications" to users.
85 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 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 88 - Work with standard types, simple custom types, and extensible to exotic types.
89 89 - Permissively licensed.
... ... @@ -411,7 +411,7 @@ will produce a check for a number less than or equal to 0.
411 411 ##### Transforming Validators
412 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 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 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 416 Here are some examples
417 417 of `IsMember`:
... ... @@ -532,6 +532,7 @@ There are several options that are supported on the main app and subcommands and
532 532 - `.ignore_underscore()`: Ignore any underscores in the subcommand name. Inherited by added subcommands, so is usually used on the main `App`.
533 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 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 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 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 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 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 695 ```ini
695 696 ; Comments are supported, using a ;
... ... @@ -705,11 +706,26 @@ str_vector = &quot;one&quot; &quot;two&quot; &quot;and three&quot;
705 706 in_subcommand = Wow
706 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 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 730 ### Inheriting defaults
715 731  
... ... @@ -917,7 +933,7 @@ CLI11 was developed at the [University of Cincinnati][] to support of the [GooFi
917 933 [doi-badge]: https://zenodo.org/badge/80064252.svg
918 934 [doi-link]: https://zenodo.org/badge/latestdoi/80064252
919 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 937 [travis-badge]: https://img.shields.io/travis/CLIUtils/CLI11/master.svg?label=Linux/macOS
922 938 [travis]: https://travis-ci.org/CLIUtils/CLI11
923 939 [appveyor-badge]: https://img.shields.io/appveyor/ci/HenrySchreiner/cli11/master.svg?label=Windows
... ...
book/chapters/config.md
... ... @@ -2,15 +2,49 @@
2 2  
3 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 42 ## Configure file format
9 43  
10 44 Here is an example configuration file, in INI format:
11 45  
12 46 ```ini
13   -; Commments are supported, using a ;
  47 +; Comments are supported, using a ;
14 48 ; The default section is [default], case insensitive
15 49  
16 50 value = 1
... ... @@ -23,12 +57,66 @@ in_subcommand = Wow
23 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 81 ## Writing out a configure file
29 82  
30 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 120 ## Custom formats
33 121  
34 122 {% hint style='info' %}
... ... @@ -51,3 +139,9 @@ app.config_formatter(std::make_shared&lt;NewConfig&gt;());
51 139 ```
52 140  
53 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 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 132 ## Listing of specialty options:
... ...
include/CLI/App.hpp
... ... @@ -45,6 +45,10 @@ std::string simple(const App *app, const Error &amp;e);
45 45 std::string help(const App *app, const Error &e);
46 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 52 class App;
49 53  
50 54 using App_p = std::shared_ptr<App>;
... ... @@ -73,8 +77,9 @@ class App {
73 77 /// If true, allow extra arguments (ie, don't throw an error). INHERITABLE
74 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 84 /// If true, return immediately on an unrecognized option (implies allow_extras) INHERITABLE
80 85 bool prefix_command_{false};
... ... @@ -194,12 +199,17 @@ class App {
194 199 /// specify that positional arguments come at the end of the argument sequence not inheritable
195 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 210 /// If set to true positional options are validated before assigning INHERITABLE
202 211 bool validate_positionals_{false};
  212 +
203 213 /// A pointer to the parent if this is a subcommand
204 214 App *parent_{nullptr};
205 215  
... ... @@ -228,12 +238,6 @@ class App {
228 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 241 /// Pointer to the config option
238 242 Option *config_ptr_{nullptr};
239 243  
... ... @@ -266,6 +270,7 @@ class App {
266 270 ignore_underscore_ = parent_->ignore_underscore_;
267 271 fallthrough_ = parent_->fallthrough_;
268 272 validate_positionals_ = parent_->validate_positionals_;
  273 + configurable_ = parent_->configurable_;
269 274 allow_windows_style_options_ = parent_->allow_windows_style_options_;
270 275 group_ = parent_->group_;
271 276 footer_ = parent_->footer_;
... ... @@ -385,14 +390,23 @@ class App {
385 390  
386 391 /// Set the subcommand to be disabled by default, so on clear(), at the start of each parse it is disabled
387 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 398 return this;
390 399 }
391 400  
392 401 /// Set the subcommand to be enabled by default, so on clear(), at the start of each parse it is enabled (not
393 402 /// disabled)
394 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 410 return this;
397 411 }
398 412  
... ... @@ -415,11 +429,20 @@ class App {
415 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 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 446 return this;
424 447 }
425 448  
... ... @@ -457,6 +480,12 @@ class App {
457 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 489 /// Ignore underscore. Subcommands inherit value.
461 490 App *ignore_underscore(bool value = true) {
462 491 if(value && !ignore_underscore_) {
... ... @@ -889,22 +918,24 @@ class App {
889 918 /// Set a configuration ini file option, or clear it if no name passed
890 919 Option *set_config(std::string option_name = "",
891 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 922 bool config_required = false) {
894 923  
895 924 // Remove existing config if present
896 925 if(config_ptr_ != nullptr) {
897 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 930 // Only add config if option passed
904 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 939 config_ptr_->configurable(false);
909 940 }
910 941  
... ... @@ -989,12 +1020,12 @@ class App {
989 1020 }
990 1021 /// Check to see if a subcommand is part of this command (doesn't have to be in command line)
991 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 1024 if(subcom == nullptr)
994 1025 throw OptionNotFound("nullptr passed");
995 1026 for(const App_p &subcomptr : subcommands_)
996 1027 if(subcomptr.get() == subcom)
997   - return subcom;
  1028 + return subcomptr.get();
998 1029 throw OptionNotFound(subcom->get_name());
999 1030 }
1000 1031  
... ... @@ -1342,7 +1373,7 @@ class App {
1342 1373 }
1343 1374  
1344 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 1377 // get subcom needed to verify that this was a real subcommand
1347 1378 return get_subcommand(subcom)->parsed_ > 0;
1348 1379 }
... ... @@ -1482,6 +1513,11 @@ class App {
1482 1513 /// Access the config formatter
1483 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 1521 /// Get the app or subcommand description
1486 1522 std::string get_description() const { return description_; }
1487 1523  
... ... @@ -1601,6 +1637,9 @@ class App {
1601 1637 /// Check the status of the allow windows style options
1602 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 1643 /// Get the group of this subcommand
1605 1644 const std::string &get_group() const { return group_; }
1606 1645  
... ... @@ -1635,15 +1674,15 @@ class App {
1635 1674 bool get_immediate_callback() const { return immediate_callback_; }
1636 1675  
1637 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 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 1681 /// Get the status of validating positionals
1643 1682 bool get_validate_positionals() const { return validate_positionals_; }
1644 1683  
1645 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 1687 /// Get a pointer to the help flag.
1649 1688 Option *get_help_ptr() { return help_ptr_; }
... ... @@ -1823,11 +1862,10 @@ class App {
1823 1862 /// set the correct fallthrough and prefix for nameless subcommands and manage the automatic enable or disable
1824 1863 /// makes sure parent is set correctly
1825 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 1866 disabled_ = false;
  1867 + } else if(default_startup == startup_mode::disabled) {
  1868 + disabled_ = true;
1831 1869 }
1832 1870 for(const App_p &app : subcommands_) {
1833 1871 if(app->has_automatic_name_) {
... ... @@ -1909,27 +1947,33 @@ class App {
1909 1947  
1910 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 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 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 1971 } catch(const FileError &) {
1930   - if(config_required_)
  1972 + if(config_required || file_given)
1931 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 2189  
2146 2190 /// Process callbacks and such.
2147 2191 void _process() {
2148   - _process_ini();
  2192 + _process_config_file();
2149 2193 _process_env();
2150 2194 _process_callbacks();
2151 2195 _process_help_flags();
... ... @@ -2244,7 +2288,7 @@ class App {
2244 2288 /// Returns true if it managed to find the option, if false you'll need to remove the arg manually.
2245 2289 void _parse_config(std::vector<ConfigItem> &args) {
2246 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 2292 throw ConfigError::Extras(item.fullname());
2249 2293 }
2250 2294 }
... ... @@ -2254,16 +2298,37 @@ class App {
2254 2298 if(level < item.parents.size()) {
2255 2299 try {
2256 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 2304 } catch(const OptionNotFound &) {
2259 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 2328 Option *op = get_option_no_throw("--" + item.name);
2264 2329 if(op == nullptr) {
2265 2330 // If the option was not present
2266   - if(get_allow_config_extras())
  2331 + if(get_allow_config_extras() == config_extras_mode::capture)
2267 2332 // Should we worry about classifying the extras properly?
2268 2333 missing_.emplace_back(detail::Classifier::NONE, item.fullname());
2269 2334 return false;
... ...
include/CLI/Config.hpp
... ... @@ -14,58 +14,325 @@
14 14  
15 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 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 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 337 return out.str();
71 338 }
... ...
include/CLI/ConfigFwd.hpp
... ... @@ -15,30 +15,6 @@ namespace CLI {
15 15  
16 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 18 /// Holds values to load into Options
43 19 struct ConfigItem {
44 20 /// This is the list of parents
... ... @@ -91,56 +67,61 @@ class Config {
91 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 84 public:
97 85 std::string
98 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 127 } // namespace CLI
... ...
include/CLI/Option.hpp
... ... @@ -938,10 +938,12 @@ class Option : public OptionBase&lt;Option&gt; {
938 938 res = results_;
939 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 949 return res;
... ...
include/CLI/StringTools.hpp
... ... @@ -135,6 +135,17 @@ inline std::string trim_copy(const std::string &amp;str) {
135 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 149 /// Make a copy of the string and then trim it, any filter string can be used (any char in string is filtered)
139 150 inline std::string trim_copy(const std::string &str, const std::string &filter) {
140 151 std::string s = str;
... ... @@ -268,10 +279,12 @@ template &lt;typename Callable&gt; inline std::string find_and_modify(std::string str,
268 279  
269 280 /// Split a string '"one two" "three"' into 'one two', 'three'
270 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 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 288 trim(str);
276 289  
277 290 std::vector<std::string> output;
... ... @@ -297,7 +310,7 @@ inline std::vector&lt;std::string&gt; split_up(std::string str) {
297 310 if(it != std::end(str)) {
298 311 std::string value = std::string(str.begin(), it);
299 312 output.push_back(value);
300   - str = std::string(it, str.end());
  313 + str = std::string(it + 1, str.end());
301 314 } else {
302 315 output.push_back(str);
303 316 str = "";
... ... @@ -317,7 +330,7 @@ inline std::vector&lt;std::string&gt; split_up(std::string str) {
317 330 /// at the start of the first line). `"; "` would be for ini files
318 331 ///
319 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 334 std::string::size_type n = 0;
322 335 while(n != std::string::npos && n < input.size()) {
323 336 n = input.find('\n', n);
... ...
include/CLI/Validators.hpp
... ... @@ -271,25 +271,25 @@ enum class path_type { nonexistant, file, directory };
271 271  
272 272 #if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0
273 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 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 290 #else
291 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 293 #if defined(_MSC_VER)
294 294 struct __stat64 buffer;
295 295 if(_stat64(file, &buffer) == 0) {
... ...
tests/CMakeLists.txt
... ... @@ -23,7 +23,7 @@ include(AddGoogletest)
23 23  
24 24 set(CLI11_TESTS
25 25 HelpersTest
26   - IniTest
  26 + ConfigFileTest
27 27 SimpleTest
28 28 AppTest
29 29 SetTest
... ...
tests/IniTest.cpp renamed to tests/ConfigFileTest.cpp
... ... @@ -7,18 +7,60 @@
7 7 using ::testing::HasSubstr;
8 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 44 TEST(StringBased, IniJoin) {
11 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 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 59 TEST(StringBased, First) {
18 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 65 ofile.seekg(0, std::ios::beg);
24 66  
... ... @@ -36,10 +78,10 @@ TEST(StringBased, First) {
36 78 TEST(StringBased, FirstWithComments) {
37 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 86 ofile.seekg(0, std::ios::beg);
45 87  
... ... @@ -57,9 +99,9 @@ TEST(StringBased, FirstWithComments) {
57 99 TEST(StringBased, Quotes) {
58 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 106 ofile.seekg(0, std::ios::beg);
65 107  
... ... @@ -80,9 +122,9 @@ TEST(StringBased, Quotes) {
80 122 TEST(StringBased, Vector) {
81 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 129 ofile.seekg(0, std::ios::beg);
88 130  
... ... @@ -105,8 +147,8 @@ TEST(StringBased, Vector) {
105 147 TEST(StringBased, Spaces) {
106 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 153 ofile.seekg(0, std::ios::beg);
112 154  
... ... @@ -124,47 +166,224 @@ TEST(StringBased, Spaces) {
124 166 TEST(StringBased, Sections) {
125 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 173 ofile.seekg(0, std::ios::beg);
132 174  
133 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 178 EXPECT_EQ("one", output.at(0).name);
137 179 EXPECT_EQ(1u, output.at(0).inputs.size());
138 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 188 TEST(StringBased, SpacesSections) {
147 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 196 ofile.seekg(0, std::ios::beg);
156 197  
157 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 201 EXPECT_EQ("one", output.at(0).name);
161 202 EXPECT_EQ(1u, output.at(0).inputs.size());
162 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 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 389 TEST(StringBased, file_error) {
... ... @@ -205,6 +424,7 @@ TEST_F(TApp, IniNotRequired) {
205 424 EXPECT_EQ(1, one);
206 425 EXPECT_EQ(2, two);
207 426 EXPECT_EQ(3, three);
  427 + EXPECT_EQ(app["--config"]->as<std::string>(), "TestIniTmp.ini");
208 428 }
209 429  
210 430 TEST_F(TApp, IniSuccessOnUnknownOption) {
... ... @@ -263,120 +483,479 @@ TEST_F(TApp, IniGetNoRemaining) {
263 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 852 TempFile tmpini{"TestIniTmp.ini"};
269   - TempFile tmpini2{"TestIniTmp2.ini"};
270 853  
271 854 app.set_config("--config", tmpini);
272 855  
273 856 {
274 857 std::ofstream out{tmpini};
275 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 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 873 run();
293 874  
294   - EXPECT_EQ(99, two);
  875 + EXPECT_EQ(1, one);
  876 + EXPECT_EQ(2, two);
295 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 890 std::ofstream out{tmpini};
326 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 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 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 924 std::ofstream out{tmpini};
351 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 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 960 TempFile tmpini{"TestIniTmp.ini"};
382 961  
... ... @@ -385,21 +964,43 @@ TEST_F(TApp, IniVector) {
385 964 {
386 965 std::ofstream out{tmpini};
387 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 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 1005 TempFile tmpini{"TestIniTmp.ini"};
405 1006  
... ... @@ -411,14 +1012,26 @@ TEST_F(TApp, IniLayered) {
411 1012 out << "val=1" << std::endl;
412 1013 out << "[subcom]" << std::endl;
413 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 1022 app.add_option("--val", one);
419 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 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 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 1035 subsubcom->add_option("--val", three);
423 1036  
424 1037 run();
... ... @@ -426,9 +1039,16 @@ TEST_F(TApp, IniLayered) {
426 1039 EXPECT_EQ(1, one);
427 1040 EXPECT_EQ(2, two);
428 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 1053 TempFile tmpini{"TestIniTmp.ini"};
434 1054  
... ... @@ -436,6 +1056,30 @@ TEST_F(TApp, IniFailure) {
436 1056  
437 1057 {
438 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 1083 out << "[default]" << std::endl;
440 1084 out << "val=1" << std::endl;
441 1085 }
... ... @@ -484,7 +1128,7 @@ TEST_F(TApp, IniSubFailure) {
484 1128  
485 1129 app.add_subcommand("other");
486 1130 app.set_config("--config", tmpini);
487   -
  1131 + app.allow_config_extras(false);
488 1132 {
489 1133 std::ofstream out{tmpini};
490 1134 out << "[other]" << std::endl;
... ... @@ -499,7 +1143,7 @@ TEST_F(TApp, IniNoSubFailure) {
499 1143 TempFile tmpini{"TestIniTmp.ini"};
500 1144  
501 1145 app.set_config("--config", tmpini);
502   -
  1146 + app.allow_config_extras(CLI::config_extras_mode::error);
503 1147 {
504 1148 std::ofstream out{tmpini};
505 1149 out << "[other]" << std::endl;
... ... @@ -767,7 +1411,47 @@ TEST_F(TApp, IniOutputShortDoubleDescription) {
767 1411 run();
768 1412  
769 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 1457 TEST_F(TApp, IniOutputMultiLineDescription) {
... ... @@ -783,6 +1467,35 @@ TEST_F(TApp, IniOutputMultiLineDescription) {
783 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 1499 TEST_F(TApp, IniOutputVector) {
787 1500  
788 1501 std::vector<int> v;
... ... @@ -796,6 +1509,34 @@ TEST_F(TApp, IniOutputVector) {
796 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 1540 TEST_F(TApp, IniOutputFlag) {
800 1541  
801 1542 int v, q;
... ... @@ -812,7 +1553,7 @@ TEST_F(TApp, IniOutputFlag) {
812 1553 EXPECT_THAT(str, HasSubstr("simple=3"));
813 1554 EXPECT_THAT(str, Not(HasSubstr("nothing")));
814 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 1558 str = app.config_to_str(true);
818 1559 EXPECT_THAT(str, HasSubstr("nothing"));
... ... @@ -859,6 +1600,83 @@ TEST_F(TApp, IniOutputSubcom) {
859 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 1680 TEST_F(TApp, IniQuotedOutput) {
863 1681  
864 1682 std::string val1;
... ... @@ -915,3 +1733,25 @@ TEST_F(TApp, StopReadingConfigOnClear) {
915 1733  
916 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 443 EXPECT_FALSE(app.get_allow_windows_style_options());
444 444 #endif
445 445 EXPECT_FALSE(app.get_fallthrough());
  446 + EXPECT_FALSE(app.get_configurable());
446 447 EXPECT_FALSE(app.get_validate_positionals());
447 448  
448 449 EXPECT_EQ(app.get_footer(), "");
... ... @@ -455,6 +456,7 @@ TEST_F(TApp, SubcommandDefaults) {
455 456 app.immediate_callback();
456 457 app.ignore_case();
457 458 app.ignore_underscore();
  459 + app.configurable();
458 460 #ifdef _WIN32
459 461 app.allow_windows_style_options(false);
460 462 #else
... ... @@ -482,6 +484,7 @@ TEST_F(TApp, SubcommandDefaults) {
482 484 #endif
483 485 EXPECT_TRUE(app2->get_fallthrough());
484 486 EXPECT_TRUE(app2->get_validate_positionals());
  487 + EXPECT_TRUE(app2->get_configurable());
485 488 EXPECT_EQ(app2->get_footer(), "footy");
486 489 EXPECT_EQ(app2->get_group(), "Stuff");
487 490 EXPECT_EQ(app2->get_require_subcommand_min(), 0u);
... ...