1
0
mirror of https://github.com/CLIUtils/CLI11.git synced 2025-04-29 12:13:52 +00:00

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
This commit is contained in:
Philip Top 2019-12-31 08:28:25 -08:00 committed by Henry Schreiner
parent d5cd986046
commit c67ab9dd43
13 changed files with 2401 additions and 1120 deletions

View File

@ -83,7 +83,7 @@ An acceptable CLI parser library should be all of the following:
- Easy to execute, with help, parse errors, etc. providing correct exit and details.
- Easy to extend as part of a framework that provides "applications" to users.
- Usable subcommand syntax, with support for multiple subcommands, nested subcommands, option groups, and optional fallthrough (explained later).
- Ability to add a configuration file (`ini` format), and produce it as well.
- Ability to add a configuration file (`ini` or `TOML`🚧 format), and produce it as well.
- Produce real values that can be used directly in code, not something you have pay compute time to look up, for HPC applications.
- Work with standard types, simple custom types, and extensible to exotic types.
- Permissively licensed.
@ -411,7 +411,7 @@ will produce a check for a number less than or equal to 0.
##### Transforming Validators
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.
- 🆕 `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`
- 🆕 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.
- 🆕 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.
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.
Here are some examples
of `IsMember`:
@ -532,6 +532,7 @@ There are several options that are supported on the main app and subcommands and
- `.ignore_underscore()`: Ignore any underscores in the subcommand name. Inherited by added subcommands, so is usually used on the main `App`.
- `.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`.
- `.fallthrough()`: Allow extra unmatched options and positionals to "fall through" and be matched on a parent command. Subcommands always are allowed to fall through.
- `.configurable()`: 🚧 Allow the subcommand to be triggered from a configuration file.
- `.disable()`: 🆕 Specify that the subcommand is disabled, if given with a bool value it will enable or disable the subcommand or option group.
- `.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.
- `.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="",
required=false)
```
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:
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:
```ini
; Comments are supported, using a ;
@ -705,11 +706,26 @@ str_vector = "one" "two" "and three"
in_subcommand = Wow
sub.subcommand = true
```
or equivalently in TOML 🚧
```toml
# Comments are supported, using a #
# The default section is [default], case insensitive
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.
value = 1
str = "A string"
vector = [1,2,3]
str_vector = ["one","two","and three"]
# Sections map to subcommands
[subcommand]
in_subcommand = Wow
sub.subcommand = true
```
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.
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.
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.
### Inheriting defaults
@ -917,7 +933,7 @@ CLI11 was developed at the [University of Cincinnati][] to support of the [GooFi
[doi-badge]: https://zenodo.org/badge/80064252.svg
[doi-link]: https://zenodo.org/badge/latestdoi/80064252
[azure-badge]: https://dev.azure.com/CLIUtils/CLI11/_apis/build/status/CLIUtils.CLI11?branchName=master
[azure]: https://dev.azure.com/CLIUtils/CLI11/_build/|latest?definitionId=1&branchName=master
[azure]: https://dev.azure.com/CLIUtils/CLI11/_build/latest?definitionId=1&branchName=master
[travis-badge]: https://img.shields.io/travis/CLIUtils/CLI11/master.svg?label=Linux/macOS
[travis]: https://travis-ci.org/CLIUtils/CLI11
[appveyor-badge]: https://img.shields.io/appveyor/ci/HenrySchreiner/cli11/master.svg?label=Windows

View File

@ -2,15 +2,49 @@
## Reading a configure file
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.
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.
### Extra fields
Sometimes configuration files are used for multiple purposes so CLI11 allows options on how to deal with extra fields
```cpp
app.allow_config_extras(true);
```
will allow capture the extras in the extras field of the app. (NOTE: This also sets the `allow_extras` in the app to true)
```cpp
app.allow_config_extras(false);
```
will generate an error if there are any extra fields
for slightly finer control there is a scoped enumeration of the modes
or
```cpp
app.allow_config_extras(CLI::config_extras_mode::ignore);
```
will completely ignore extra parameters in the config file. This mode is the default.
```cpp
app.allow_config_extras(CLI::config_extras_mode::capture);
```
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.
```cpp
app.allow_config_extras(CLI::config_extras_mode::error);
```
is equivalent to `app.allow_config_extras(false);`
### Getting the used configuration file name
If it is needed to get the configuration file name used this can be obtained via
`app.get_config_ptr()->as<std::string>()` or
`app["--config"]->as<std::string>()` assuming `--config` was the configuration option name.
## Configure file format
Here is an example configuration file, in INI format:
```ini
; Commments are supported, using a ;
; Comments are supported, using a ;
; The default section is [default], case insensitive
value = 1
@ -23,12 +57,66 @@ in_subcommand = Wow
sub.subcommand = true
```
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".
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.)
CLI11 also supports configuration file in [TOML](https://github.com/toml-lang/toml) format.
```toml
# Comments are supported, using a #
# The default section is [default], case insensitive
value = 1
str = "A string"
vector = [1,2,3]
# Section map to subcommands
[subcommand]
in_subcommand = Wow
[subcommand.sub]
subcommand = true # could also be give as sub.subcommand=true
```
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.
## Writing out a configure file
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.
### Customization of configure file output
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
```cpp
/// the character used for comments
char commentChar = ';';
/// the character used to start an array '\0' is a default to not use
char arrayStart = '\0';
/// the character used to end an array '\0' is a default to not use
char arrayEnd = '\0';
/// the character used to separate elements in an array
char arraySeparator = ' ';
/// the character used separate the name from the value
char valueDelimiter = '=';
```
These can be modified via setter functions
- ` ConfigBase *comment(char cchar)` Specify the character to start a comment block
- `ConfigBase *arrayBounds(char aStart, char aEnd)` Specify the start and end characters for an array
- `ConfigBase *arrayDelimiter(char aSep)` Specify the delimiter character for an array
- `ConfigBase *valueSeparator(char vSep)` Specify the delimiter between a name and value
For example to specify reading a configure file that used `:` to separate name and values
```cpp
auto config_base=app.get_config_formatter_base();
config_base->valueSeparator(':');
```
The default configuration file will read TOML files, but will write out files in the INI format. To specify outputting TOML formatted files use
```cpp
app.config_formatter(std::make_shared<CLI::ConfigTOML>());
```
which makes use of a predefined modification of the ConfigBase class which INI also uses.
## Custom formats
{% hint style='info' %}
@ -51,3 +139,9 @@ app.config_formatter(std::make_shared<NewConfig>());
```
See [`examples/json.cpp`](https://github.com/CLIUtils/CLI11/blob/master/examples/json.cpp) for a complete JSON config example.
## Triggering Subcommands
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.
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.

View File

@ -126,7 +126,7 @@ app.add_flag("--CaSeLeSs");
app.get_group() // is "Required"
```
Groups are mostly for visual organisation, but an empty string for a group name will hide the option.
Groups are mostly for visual organization, but an empty string for a group name will hide the option.
## Listing of specialty options:

View File

@ -45,6 +45,10 @@ std::string simple(const App *app, const Error &e);
std::string help(const App *app, const Error &e);
} // namespace FailureMessage
/// enumeration of modes of how to deal with extras in config files
enum class config_extras_mode : char { error = 0, ignore, capture };
class App;
using App_p = std::shared_ptr<App>;
@ -73,8 +77,9 @@ class App {
/// If true, allow extra arguments (ie, don't throw an error). INHERITABLE
bool allow_extras_{false};
/// If true, allow extra arguments in the ini file (ie, don't throw an error). INHERITABLE
bool allow_config_extras_{false};
/// If ignore, allow extra arguments in the ini file (ie, don't throw an error). INHERITABLE
/// if error error on an extra argument, and if capture feed it to the app
config_extras_mode allow_config_extras_{config_extras_mode::ignore};
/// If true, return immediately on an unrecognized option (implies allow_extras) INHERITABLE
bool prefix_command_{false};
@ -194,12 +199,17 @@ class App {
/// specify that positional arguments come at the end of the argument sequence not inheritable
bool positionals_at_end_{false};
/// If set to true the subcommand will start each parse disabled
bool disabled_by_default_{false};
/// If set to true the subcommand will be reenabled at the start of each parse
bool enabled_by_default_{false};
enum class startup_mode : char { stable, enabled, disabled };
/// specify the startup mode for the app
/// stable=no change, enabled= startup enabled, disabled=startup disabled
startup_mode default_startup{startup_mode::stable};
/// if set to true the subcommand can be triggered via configuration files INHERITABLE
bool configurable_{false};
/// If set to true positional options are validated before assigning INHERITABLE
bool validate_positionals_{false};
/// A pointer to the parent if this is a subcommand
App *parent_{nullptr};
@ -228,12 +238,6 @@ class App {
/// @name Config
///@{
/// The name of the connected config file
std::string config_name_{};
/// True if ini is required (throws if not present), if false simply keep going.
bool config_required_{false};
/// Pointer to the config option
Option *config_ptr_{nullptr};
@ -266,6 +270,7 @@ class App {
ignore_underscore_ = parent_->ignore_underscore_;
fallthrough_ = parent_->fallthrough_;
validate_positionals_ = parent_->validate_positionals_;
configurable_ = parent_->configurable_;
allow_windows_style_options_ = parent_->allow_windows_style_options_;
group_ = parent_->group_;
footer_ = parent_->footer_;
@ -385,14 +390,23 @@ class App {
/// Set the subcommand to be disabled by default, so on clear(), at the start of each parse it is disabled
App *disabled_by_default(bool disable = true) {
disabled_by_default_ = disable;
if(disable) {
default_startup = startup_mode::disabled;
} else {
default_startup = (default_startup == startup_mode::enabled) ? startup_mode::enabled : startup_mode::stable;
}
return this;
}
/// Set the subcommand to be enabled by default, so on clear(), at the start of each parse it is enabled (not
/// disabled)
App *enabled_by_default(bool enable = true) {
enabled_by_default_ = enable;
if(enable) {
default_startup = startup_mode::enabled;
} else {
default_startup =
(default_startup == startup_mode::disabled) ? startup_mode::disabled : startup_mode::stable;
}
return this;
}
@ -415,11 +429,20 @@ class App {
return this;
}
/// Remove the error when extras are left over on the command line.
/// Will also call App::allow_extras().
/// ignore extras in config files
App *allow_config_extras(bool allow = true) {
allow_extras(allow);
allow_config_extras_ = allow;
if(allow) {
allow_config_extras_ = config_extras_mode::capture;
allow_extras_ = true;
} else {
allow_config_extras_ = config_extras_mode::error;
}
return this;
}
/// ignore extras in config files
App *allow_config_extras(config_extras_mode mode) {
allow_config_extras_ = mode;
return this;
}
@ -457,6 +480,12 @@ class App {
return this;
}
/// Specify that the subcommand can be triggered by a config file
App *configurable(bool value = true) {
configurable_ = value;
return this;
}
/// Ignore underscore. Subcommands inherit value.
App *ignore_underscore(bool value = true) {
if(value && !ignore_underscore_) {
@ -889,22 +918,24 @@ class App {
/// Set a configuration ini file option, or clear it if no name passed
Option *set_config(std::string option_name = "",
std::string default_filename = "",
std::string help_message = "Read an ini file",
const std::string &help_message = "Read an ini file",
bool config_required = false) {
// Remove existing config if present
if(config_ptr_ != nullptr) {
remove_option(config_ptr_);
config_name_ = "";
config_required_ = false; // Not really needed, but complete
config_ptr_ = nullptr; // need to remove the config_ptr completely
}
// Only add config if option passed
if(!option_name.empty()) {
config_name_ = default_filename;
config_required_ = config_required;
config_ptr_ = add_option(option_name, config_name_, help_message, !default_filename.empty());
config_ptr_ = add_option(option_name, help_message);
if(config_required) {
config_ptr_->required();
}
if(!default_filename.empty()) {
config_ptr_->default_str(std::move(default_filename));
}
config_ptr_->configurable(false);
}
@ -989,12 +1020,12 @@ class App {
}
/// Check to see if a subcommand is part of this command (doesn't have to be in command line)
/// returns the first subcommand if passed a nullptr
App *get_subcommand(App *subcom) const {
App *get_subcommand(const App *subcom) const {
if(subcom == nullptr)
throw OptionNotFound("nullptr passed");
for(const App_p &subcomptr : subcommands_)
if(subcomptr.get() == subcom)
return subcom;
return subcomptr.get();
throw OptionNotFound(subcom->get_name());
}
@ -1342,7 +1373,7 @@ class App {
}
/// Check to see if given subcommand was selected
bool got_subcommand(App *subcom) const {
bool got_subcommand(const App *subcom) const {
// get subcom needed to verify that this was a real subcommand
return get_subcommand(subcom)->parsed_ > 0;
}
@ -1482,6 +1513,11 @@ class App {
/// Access the config formatter
std::shared_ptr<Config> get_config_formatter() const { return config_formatter_; }
/// Access the config formatter as a configBase pointer
std::shared_ptr<ConfigBase> get_config_formatter_base() const {
return std::dynamic_pointer_cast<ConfigBase>(config_formatter_);
}
/// Get the app or subcommand description
std::string get_description() const { return description_; }
@ -1601,6 +1637,9 @@ class App {
/// Check the status of the allow windows style options
bool get_positionals_at_end() const { return positionals_at_end_; }
/// Check the status of the allow windows style options
bool get_configurable() const { return configurable_; }
/// Get the group of this subcommand
const std::string &get_group() const { return group_; }
@ -1635,15 +1674,15 @@ class App {
bool get_immediate_callback() const { return immediate_callback_; }
/// Get the status of disabled by default
bool get_disabled_by_default() const { return disabled_by_default_; }
bool get_disabled_by_default() const { return (default_startup == startup_mode::disabled); }
/// Get the status of disabled by default
bool get_enabled_by_default() const { return enabled_by_default_; }
bool get_enabled_by_default() const { return (default_startup == startup_mode::enabled); }
/// Get the status of validating positionals
bool get_validate_positionals() const { return validate_positionals_; }
/// Get the status of allow extras
bool get_allow_config_extras() const { return allow_config_extras_; }
config_extras_mode get_allow_config_extras() const { return allow_config_extras_; }
/// Get a pointer to the help flag.
Option *get_help_ptr() { return help_ptr_; }
@ -1823,11 +1862,10 @@ class App {
/// set the correct fallthrough and prefix for nameless subcommands and manage the automatic enable or disable
/// makes sure parent is set correctly
void _configure() {
if(disabled_by_default_) {
disabled_ = true;
}
if(enabled_by_default_) {
if(default_startup == startup_mode::enabled) {
disabled_ = false;
} else if(default_startup == startup_mode::disabled) {
disabled_ = true;
}
for(const App_p &app : subcommands_) {
if(app->has_automatic_name_) {
@ -1909,27 +1947,33 @@ class App {
// The parse function is now broken into several parts, and part of process
/// Read and process an ini file (main app only)
void _process_ini() {
// Process an INI file
/// Read and process a configuration file (main app only)
void _process_config_file() {
if(config_ptr_ != nullptr) {
if(*config_ptr_) {
config_ptr_->run_callback();
config_required_ = true;
bool config_required = config_ptr_->get_required();
bool file_given = config_ptr_->count() > 0;
auto config_file = config_ptr_->as<std::string>();
if(config_file.empty()) {
if(config_required) {
throw FileError::Missing("no specified config file");
}
if(!config_name_.empty()) {
try {
auto path_result = detail::check_path(config_name_.c_str());
return;
}
auto path_result = detail::check_path(config_file.c_str());
if(path_result == detail::path_type::file) {
std::vector<ConfigItem> values = config_formatter_->from_file(config_name_);
try {
std::vector<ConfigItem> values = config_formatter_->from_file(config_file);
_parse_config(values);
} else if(config_required_) {
throw FileError::Missing(config_name_);
if(!file_given) {
config_ptr_->add_result(config_file);
}
} catch(const FileError &) {
if(config_required_)
if(config_required || file_given)
throw;
}
} else if(config_required || file_given) {
throw FileError::Missing(config_file);
}
}
}
@ -2145,7 +2189,7 @@ class App {
/// Process callbacks and such.
void _process() {
_process_ini();
_process_config_file();
_process_env();
_process_callbacks();
_process_help_flags();
@ -2244,7 +2288,7 @@ class App {
/// Returns true if it managed to find the option, if false you'll need to remove the arg manually.
void _parse_config(std::vector<ConfigItem> &args) {
for(ConfigItem item : args) {
if(!_parse_single_config(item) && !allow_config_extras_)
if(!_parse_single_config(item) && allow_config_extras_ == config_extras_mode::error)
throw ConfigError::Extras(item.fullname());
}
}
@ -2254,16 +2298,37 @@ class App {
if(level < item.parents.size()) {
try {
auto subcom = get_subcommand(item.parents.at(level));
return subcom->_parse_single_config(item, level + 1);
auto result = subcom->_parse_single_config(item, level + 1);
return result;
} catch(const OptionNotFound &) {
return false;
}
}
// check for section open
if(item.name == "++") {
if(configurable_) {
increment_parsed();
_trigger_pre_parse(2);
if(parent_ != nullptr) {
parent_->parsed_subcommands_.push_back(this);
}
}
return true;
}
// check for section close
if(item.name == "--") {
if(configurable_) {
_process_callbacks();
_process_requirements();
run_callback();
}
return true;
}
Option *op = get_option_no_throw("--" + item.name);
if(op == nullptr) {
// If the option was not present
if(get_allow_config_extras())
if(get_allow_config_extras() == config_extras_mode::capture)
// Should we worry about classifying the extras properly?
missing_.emplace_back(detail::Classifier::NONE, item.fullname());
return false;

View File

@ -14,58 +14,325 @@
namespace CLI {
namespace detail {
inline std::string convert_arg_for_ini(const std::string &arg) {
if(arg.empty()) {
return std::string(2, '"');
}
// some specifically supported strings
if(arg == "true" || arg == "false" || arg == "nan" || arg == "inf") {
return arg;
}
// floating point conversion can convert some hex codes, but don't try that here
if(arg.compare(0, 2, "0x") != 0 && arg.compare(0, 2, "0X") != 0) {
double val;
if(detail::lexical_cast(arg, val)) {
return arg;
}
}
// just quote a single non numeric character
if(arg.size() == 1) {
return std::string("'") + arg + '\'';
}
// handle hex, binary or octal arguments
if(arg.front() == '0') {
if(arg[1] == 'x') {
if(std::all_of(arg.begin() + 2, arg.end(), [](char x) {
return (x >= '0' && x <= '9') || (x >= 'A' && x <= 'F') || (x >= 'a' && x <= 'f');
})) {
return arg;
}
} else if(arg[1] == 'o') {
if(std::all_of(arg.begin() + 2, arg.end(), [](char x) { return (x >= '0' && x <= '7'); })) {
return arg;
}
} else if(arg[1] == 'b') {
if(std::all_of(arg.begin() + 2, arg.end(), [](char x) { return (x == '0' || x == '1'); })) {
return arg;
}
}
}
if(arg.find_first_of('"') == std::string::npos) {
return std::string("\"") + arg + '"';
} else {
return std::string("'") + arg + '\'';
}
}
/// Comma separated join, adds quotes if needed
inline std::string
ConfigINI::to_config(const App *app, bool default_also, bool write_description, std::string prefix) const {
ini_join(const std::vector<std::string> &args, char sepChar = ',', char arrayStart = '[', char arrayEnd = ']') {
std::string joined;
if(args.size() > 1 && arrayStart != '\0') {
joined.push_back(arrayStart);
}
std::size_t start = 0;
for(const auto &arg : args) {
if(start++ > 0) {
joined.push_back(sepChar);
if(isspace(sepChar) == 0) {
joined.push_back(' ');
}
}
joined.append(convert_arg_for_ini(arg));
}
if(args.size() > 1 && arrayEnd != '\0') {
joined.push_back(arrayEnd);
}
return joined;
}
inline std::vector<std::string> generate_parents(const std::string &section, std::string &name) {
std::vector<std::string> parents;
if(detail::to_lower(section) != "default") {
if(section.find('.') != std::string::npos) {
parents = detail::split(section, '.');
} else {
parents = {section};
}
}
if(name.find('.') != std::string::npos) {
std::vector<std::string> plist = detail::split(name, '.');
name = plist.back();
detail::remove_quotes(name);
plist.pop_back();
parents.insert(parents.end(), plist.begin(), plist.end());
}
// clean up quotes on the parents
for(auto &parent : parents) {
detail::remove_quotes(parent);
}
return parents;
}
/// assuming non default segments do a check on the close and open of the segments in a configItem structure
inline void checkParentSegments(std::vector<ConfigItem> &output, const std::string &currentSection) {
std::string estring;
auto parents = detail::generate_parents(currentSection, estring);
if(output.size() > 0 && output.back().name == "--") {
std::size_t msize = (parents.size() > 1U) ? parents.size() : 2;
while(output.back().parents.size() >= msize) {
output.push_back(output.back());
output.back().parents.pop_back();
}
if(parents.size() > 1) {
std::size_t common = 0;
std::size_t mpair = (std::min)(output.back().parents.size(), parents.size() - 1);
for(std::size_t ii = 0; ii < mpair; ++ii) {
if(output.back().parents[ii] != parents[ii]) {
break;
}
++common;
}
if(common == mpair) {
output.pop_back();
} else {
while(output.back().parents.size() > common + 1) {
output.push_back(output.back());
output.back().parents.pop_back();
}
}
for(std::size_t ii = common; ii < parents.size() - 1; ++ii) {
output.emplace_back();
output.back().parents.assign(parents.begin(), parents.begin() + static_cast<std::ptrdiff_t>(ii) + 1);
output.back().name = "++";
}
}
} else if(parents.size() > 1) {
for(std::size_t ii = 0; ii < parents.size() - 1; ++ii) {
output.emplace_back();
output.back().parents.assign(parents.begin(), parents.begin() + static_cast<std::ptrdiff_t>(ii) + 1);
output.back().name = "++";
}
}
// insert a section end which is just an empty items_buffer
output.emplace_back();
output.back().parents = std::move(parents);
output.back().name = "++";
}
} // namespace detail
inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) const {
std::string line;
std::string section = "default";
std::vector<ConfigItem> output;
bool defaultArray = (arrayStart == '\0' || arrayStart == ' ') && arrayStart == arrayEnd;
char aStart = (defaultArray) ? '[' : arrayStart;
char aEnd = (defaultArray) ? ']' : arrayEnd;
char aSep = (defaultArray && arraySeparator == ' ') ? ',' : arraySeparator;
while(getline(input, line)) {
std::vector<std::string> items_buffer;
std::string name;
detail::trim(line);
std::size_t len = line.length();
if(len > 1 && line.front() == '[' && line.back() == ']') {
if(section != "default") {
// insert a section end which is just an empty items_buffer
output.emplace_back();
output.back().parents = detail::generate_parents(section, name);
output.back().name = "--";
}
section = line.substr(1, len - 2);
// deal with double brackets for TOML
if(section.size() > 1 && section.front() == '[' && section.back() == ']') {
section = section.substr(1, section.size() - 2);
}
if(detail::to_lower(section) == "default") {
section = "default";
} else {
detail::checkParentSegments(output, section);
}
continue;
}
if(len == 0) {
continue;
}
// comment lines
if(line.front() == ';' || line.front() == '#' || line.front() == commentChar) {
continue;
}
// Find = in string, split and recombine
auto pos = line.find(valueDelimiter);
if(pos != std::string::npos) {
name = detail::trim_copy(line.substr(0, pos));
std::string item = detail::trim_copy(line.substr(pos + 1));
if(item.size() > 1 && item.front() == aStart && item.back() == aEnd) {
items_buffer = detail::split_up(item.substr(1, item.length() - 2), aSep);
} else if(defaultArray && item.find_first_of(aSep) != std::string::npos) {
items_buffer = detail::split_up(item, aSep);
} else if(defaultArray && item.find_first_of(' ') != std::string::npos) {
items_buffer = detail::split_up(item);
} else {
items_buffer = {item};
}
} else {
name = detail::trim_copy(line);
items_buffer = {"true"};
}
if(name.find('.') == std::string::npos) {
detail::remove_quotes(name);
}
// clean up quotes on the items
for(auto &it : items_buffer) {
detail::remove_quotes(it);
}
std::vector<std::string> parents = detail::generate_parents(section, name);
if(!output.empty() && name == output.back().name && parents == output.back().parents) {
output.back().inputs.insert(output.back().inputs.end(), items_buffer.begin(), items_buffer.end());
} else {
output.emplace_back();
output.back().parents = std::move(parents);
output.back().name = std::move(name);
output.back().inputs = std::move(items_buffer);
}
}
if(section != "default") {
// insert a section end which is just an empty items_buffer
std::string ename;
output.emplace_back();
output.back().parents = detail::generate_parents(section, ename);
output.back().name = "--";
while(output.back().parents.size() > 1) {
output.push_back(output.back());
output.back().parents.pop_back();
}
}
return output;
}
inline std::string
ConfigBase::to_config(const App *app, bool default_also, bool write_description, std::string prefix) const {
std::stringstream out;
std::string commentLead;
commentLead.push_back(commentChar);
commentLead.push_back(' ');
std::vector<std::string> groups = app->get_groups();
bool defaultUsed = false;
groups.insert(groups.begin(), std::string("Options"));
if(write_description) {
out << commentLead << app->get_description() << '\n';
}
for(auto &group : groups) {
if(group == "Options" || group.empty()) {
if(defaultUsed) {
continue;
}
defaultUsed = true;
}
if(write_description && group != "Options" && !group.empty()) {
out << '\n' << commentLead << group << " Options\n";
}
for(const Option *opt : app->get_options({})) {
// Only process option with a long-name and configurable
if(!opt->get_lnames().empty() && opt->get_configurable()) {
if(opt->get_group() != group) {
if(!(group == "Options" && opt->get_group() == "")) {
continue;
}
}
std::string name = prefix + opt->get_lnames()[0];
std::string value;
std::string value = detail::ini_join(opt->reduced_results(), arraySeparator, arrayStart, arrayEnd);
// Non-flags
if(opt->get_expected_min() != 0) {
// If the option was found on command line
if(opt->count() > 0)
value = detail::ini_join(opt->results());
// If the option has a default and is requested by optional argument
else if(default_also && !opt->get_default_str().empty())
value = opt->get_default_str();
// Flag, one passed
} else if(opt->count() == 1) {
value = "true";
// Flag, multiple passed
} else if(opt->count() > 1) {
value = std::to_string(opt->count());
// Flag, not present
} else if(opt->count() == 0 && default_also) {
if(value.empty() && default_also) {
if(!opt->get_default_str().empty()) {
value = detail::convert_arg_for_ini(opt->get_default_str());
} else if(opt->get_expected_min() == 0) {
value = "false";
}
}
if(!value.empty()) {
if(write_description && opt->has_description()) {
if(static_cast<int>(out.tellp()) != 0) {
out << std::endl;
out << '\n';
out << commentLead << detail::fix_newlines(commentLead, opt->get_description()) << '\n';
}
out << "; " << detail::fix_newlines("; ", opt->get_description()) << std::endl;
out << name << valueDelimiter << value << '\n';
}
// Don't try to quote anything that is not size 1
if(opt->get_items_expected_max() != 1)
out << name << "=" << value << std::endl;
else
out << name << "=" << detail::add_quotes_if_needed(value) << std::endl;
}
}
}
auto subcommands = app->get_subcommands({});
for(const App *subcom : subcommands) {
if(subcom->get_name().empty()) {
if(write_description && !subcom->get_group().empty()) {
out << '\n' << commentLead << subcom->get_group() << " Options\n";
}
out << to_config(subcom, default_also, write_description, prefix);
}
}
for(const App *subcom : app->get_subcommands({}))
for(const App *subcom : subcommands)
if(!subcom->get_name().empty()) {
if(subcom->get_configurable() && app->got_subcommand(subcom)) {
if(!prefix.empty() || app->get_parent() == nullptr) {
out << '[' << prefix << subcom->get_name() << "]\n";
} else {
std::string subname = app->get_name() + "." + subcom->get_name();
auto p = app->get_parent();
while(p->get_parent() != nullptr) {
subname = p->get_name() + "." + subname;
p = p->get_parent();
}
out << '[' << subname << "]\n";
}
out << to_config(subcom, default_also, write_description, "");
} else {
out << to_config(subcom, default_also, write_description, prefix + subcom->get_name() + ".");
}
}
return out.str();
}

View File

@ -15,30 +15,6 @@ namespace CLI {
class App;
namespace detail {
/// Comma separated join, adds quotes if needed
inline std::string ini_join(std::vector<std::string> args) {
std::ostringstream s;
std::size_t start = 0;
for(const auto &arg : args) {
if(start++ > 0)
s << " ";
auto it = std::find_if(arg.begin(), arg.end(), [](char ch) { return std::isspace<char>(ch, std::locale()); });
if(it == arg.end())
s << arg;
else if(arg.find_first_of('\"') == std::string::npos)
s << '\"' << arg << '\"';
else
s << '\'' << arg << '\'';
}
return s.str();
}
} // namespace detail
/// Holds values to load into Options
struct ConfigItem {
/// This is the list of parents
@ -91,56 +67,61 @@ class Config {
virtual ~Config() = default;
};
/// This converter works with INI files
class ConfigINI : public Config {
/// This converter works with INI/TOML files; to write proper TOML files use ConfigTOML
class ConfigBase : public Config {
protected:
/// the character used for comments
char commentChar = ';';
/// the character used to start an array '\0' is a default to not use
char arrayStart = '\0';
/// the character used to end an array '\0' is a default to not use
char arrayEnd = '\0';
/// the character used to separate elements in an array
char arraySeparator = ' ';
/// the character used separate the name from the value
char valueDelimiter = '=';
public:
std::string
to_config(const App * /*app*/, bool default_also, bool write_description, std::string prefix) const override;
std::vector<ConfigItem> from_config(std::istream &input) const override {
std::string line;
std::string section = "default";
std::vector<ConfigItem> output;
while(getline(input, line)) {
std::vector<std::string> items_buffer;
detail::trim(line);
std::size_t len = line.length();
if(len > 1 && line[0] == '[' && line[len - 1] == ']') {
section = line.substr(1, len - 2);
} else if(len > 0 && line[0] != ';') {
output.emplace_back();
ConfigItem &out = output.back();
// Find = in string, split and recombine
auto pos = line.find('=');
if(pos != std::string::npos) {
out.name = detail::trim_copy(line.substr(0, pos));
std::string item = detail::trim_copy(line.substr(pos + 1));
items_buffer = detail::split_up(item);
} else {
out.name = detail::trim_copy(line);
items_buffer = {"ON"};
std::vector<ConfigItem> from_config(std::istream &input) const override;
/// Specify the configuration for comment characters
ConfigBase *comment(char cchar) {
commentChar = cchar;
return this;
}
if(detail::to_lower(section) != "default") {
out.parents = {section};
/// Specify the start and end characters for an array
ConfigBase *arrayBounds(char aStart, char aEnd) {
arrayStart = aStart;
arrayEnd = aEnd;
return this;
}
if(out.name.find('.') != std::string::npos) {
std::vector<std::string> plist = detail::split(out.name, '.');
out.name = plist.back();
plist.pop_back();
out.parents.insert(out.parents.end(), plist.begin(), plist.end());
/// Specify the delimiter character for an array
ConfigBase *arrayDelimiter(char aSep) {
arraySeparator = aSep;
return this;
}
out.inputs.insert(std::end(out.inputs), std::begin(items_buffer), std::end(items_buffer));
}
}
return output;
/// Specify the delimiter between a name and value
ConfigBase *valueSeparator(char vSep) {
valueDelimiter = vSep;
return this;
}
};
/// the default Config is the INI file format
using ConfigINI = ConfigBase;
/// ConfigTOML generates a TOML compliant output
class ConfigTOML : public ConfigINI {
public:
ConfigTOML() {
commentChar = '#';
arrayStart = '[';
arrayEnd = ']';
arraySeparator = ',';
valueDelimiter = '=';
}
};
} // namespace CLI

View File

@ -938,12 +938,14 @@ class Option : public OptionBase<Option> {
res = results_;
_validate_results(res);
}
if(!res.empty()) {
results_t extra;
_reduce_results(extra, res);
if(!extra.empty()) {
res = std::move(extra);
}
}
}
return res;
}

View File

@ -135,6 +135,17 @@ inline std::string trim_copy(const std::string &str) {
return trim(s);
}
/// remove quotes at the front and back of a string either '"' or '\''
inline std::string &remove_quotes(std::string &str) {
if(str.length() > 1 && (str.front() == '"' || str.front() == '\'')) {
if(str.front() == str.back()) {
str.pop_back();
str.erase(str.begin(), str.begin() + 1);
}
}
return str;
}
/// Make a copy of the string and then trim it, any filter string can be used (any char in string is filtered)
inline std::string trim_copy(const std::string &str, const std::string &filter) {
std::string s = str;
@ -268,10 +279,12 @@ template <typename Callable> inline std::string find_and_modify(std::string str,
/// Split a string '"one two" "three"' into 'one two', 'three'
/// Quote characters can be ` ' or "
inline std::vector<std::string> split_up(std::string str) {
inline std::vector<std::string> split_up(std::string str, char delimiter = '\0') {
const std::string delims("\'\"`");
auto find_ws = [](char ch) { return std::isspace<char>(ch, std::locale()); };
auto find_ws = [delimiter](char ch) {
return (delimiter == '\0') ? (std::isspace<char>(ch, std::locale()) != 0) : (ch == delimiter);
};
trim(str);
std::vector<std::string> output;
@ -297,7 +310,7 @@ inline std::vector<std::string> split_up(std::string str) {
if(it != std::end(str)) {
std::string value = std::string(str.begin(), it);
output.push_back(value);
str = std::string(it, str.end());
str = std::string(it + 1, str.end());
} else {
output.push_back(str);
str = "";
@ -317,7 +330,7 @@ inline std::vector<std::string> split_up(std::string str) {
/// at the start of the first line). `"; "` would be for ini files
///
/// Can't use Regex, or this would be a subs.
inline std::string fix_newlines(std::string leader, std::string input) {
inline std::string fix_newlines(const std::string &leader, std::string input) {
std::string::size_type n = 0;
while(n != std::string::npos && n < input.size()) {
n = input.find('\n', n);

View File

@ -271,9 +271,12 @@ enum class path_type { nonexistant, file, directory };
#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0
/// get the type of the path from a file name
inline path_type check_path(const char *file) {
try {
auto stat = std::filesystem::status(file);
inline path_type check_path(const char *file) noexcept {
std::error_code ec;
auto stat = std::filesystem::status(file, ec);
if(ec) {
return path_type::nonexistant;
}
switch(stat.type()) {
case std::filesystem::file_type::none:
case std::filesystem::file_type::not_found:
@ -283,13 +286,10 @@ inline path_type check_path(const char *file) {
default:
return path_type::file;
}
} catch(const std::filesystem::filesystem_error &) {
return path_type::nonexistant;
}
}
#else
/// get the type of the path from a file name
inline path_type check_path(const char *file) {
inline path_type check_path(const char *file) noexcept {
#if defined(_MSC_VER)
struct __stat64 buffer;
if(_stat64(file, &buffer) == 0) {

View File

@ -23,7 +23,7 @@ include(AddGoogletest)
set(CLI11_TESTS
HelpersTest
IniTest
ConfigFileTest
SimpleTest
AppTest
SetTest

1757
tests/ConfigFileTest.cpp Normal file

File diff suppressed because it is too large Load Diff

View File

@ -443,6 +443,7 @@ TEST_F(TApp, SubcommandDefaults) {
EXPECT_FALSE(app.get_allow_windows_style_options());
#endif
EXPECT_FALSE(app.get_fallthrough());
EXPECT_FALSE(app.get_configurable());
EXPECT_FALSE(app.get_validate_positionals());
EXPECT_EQ(app.get_footer(), "");
@ -455,6 +456,7 @@ TEST_F(TApp, SubcommandDefaults) {
app.immediate_callback();
app.ignore_case();
app.ignore_underscore();
app.configurable();
#ifdef _WIN32
app.allow_windows_style_options(false);
#else
@ -482,6 +484,7 @@ TEST_F(TApp, SubcommandDefaults) {
#endif
EXPECT_TRUE(app2->get_fallthrough());
EXPECT_TRUE(app2->get_validate_positionals());
EXPECT_TRUE(app2->get_configurable());
EXPECT_EQ(app2->get_footer(), "footy");
EXPECT_EQ(app2->get_group(), "Stuff");
EXPECT_EQ(app2->get_require_subcommand_min(), 0u);

View File

@ -1,917 +0,0 @@
#include "app_helper.hpp"
#include "gmock/gmock.h"
#include <cstdio>
#include <sstream>
using ::testing::HasSubstr;
using ::testing::Not;
TEST(StringBased, IniJoin) {
std::vector<std::string> items = {"one", "two", "three four"};
std::string result = "one two \"three four\"";
EXPECT_EQ(CLI::detail::ini_join(items), result);
}
TEST(StringBased, First) {
std::stringstream ofile;
ofile << "one=three" << std::endl;
ofile << "two=four" << std::endl;
ofile.seekg(0, std::ios::beg);
std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
EXPECT_EQ(2u, output.size());
EXPECT_EQ("one", output.at(0).name);
EXPECT_EQ(1u, output.at(0).inputs.size());
EXPECT_EQ("three", output.at(0).inputs.at(0));
EXPECT_EQ("two", output.at(1).name);
EXPECT_EQ(1u, output.at(1).inputs.size());
EXPECT_EQ("four", output.at(1).inputs.at(0));
}
TEST(StringBased, FirstWithComments) {
std::stringstream ofile;
ofile << ";this is a comment" << std::endl;
ofile << "one=three" << std::endl;
ofile << "two=four" << std::endl;
ofile << "; and another one" << std::endl;
ofile.seekg(0, std::ios::beg);
std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
EXPECT_EQ(2u, output.size());
EXPECT_EQ("one", output.at(0).name);
EXPECT_EQ(1u, output.at(0).inputs.size());
EXPECT_EQ("three", output.at(0).inputs.at(0));
EXPECT_EQ("two", output.at(1).name);
EXPECT_EQ(1u, output.at(1).inputs.size());
EXPECT_EQ("four", output.at(1).inputs.at(0));
}
TEST(StringBased, Quotes) {
std::stringstream ofile;
ofile << R"(one = "three")" << std::endl;
ofile << R"(two = 'four')" << std::endl;
ofile << R"(five = "six and seven")" << std::endl;
ofile.seekg(0, std::ios::beg);
std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
EXPECT_EQ(3u, output.size());
EXPECT_EQ("one", output.at(0).name);
EXPECT_EQ(1u, output.at(0).inputs.size());
EXPECT_EQ("three", output.at(0).inputs.at(0));
EXPECT_EQ("two", output.at(1).name);
EXPECT_EQ(1u, output.at(1).inputs.size());
EXPECT_EQ("four", output.at(1).inputs.at(0));
EXPECT_EQ("five", output.at(2).name);
EXPECT_EQ(1u, output.at(2).inputs.size());
EXPECT_EQ("six and seven", output.at(2).inputs.at(0));
}
TEST(StringBased, Vector) {
std::stringstream ofile;
ofile << "one = three" << std::endl;
ofile << "two = four" << std::endl;
ofile << "five = six and seven" << std::endl;
ofile.seekg(0, std::ios::beg);
std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
EXPECT_EQ(3u, output.size());
EXPECT_EQ("one", output.at(0).name);
EXPECT_EQ(1u, output.at(0).inputs.size());
EXPECT_EQ("three", output.at(0).inputs.at(0));
EXPECT_EQ("two", output.at(1).name);
EXPECT_EQ(1u, output.at(1).inputs.size());
EXPECT_EQ("four", output.at(1).inputs.at(0));
EXPECT_EQ("five", output.at(2).name);
EXPECT_EQ(3u, output.at(2).inputs.size());
EXPECT_EQ("six", output.at(2).inputs.at(0));
EXPECT_EQ("and", output.at(2).inputs.at(1));
EXPECT_EQ("seven", output.at(2).inputs.at(2));
}
TEST(StringBased, Spaces) {
std::stringstream ofile;
ofile << "one = three" << std::endl;
ofile << "two = four" << std::endl;
ofile.seekg(0, std::ios::beg);
std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
EXPECT_EQ(2u, output.size());
EXPECT_EQ("one", output.at(0).name);
EXPECT_EQ(1u, output.at(0).inputs.size());
EXPECT_EQ("three", output.at(0).inputs.at(0));
EXPECT_EQ("two", output.at(1).name);
EXPECT_EQ(1u, output.at(1).inputs.size());
EXPECT_EQ("four", output.at(1).inputs.at(0));
}
TEST(StringBased, Sections) {
std::stringstream ofile;
ofile << "one=three" << std::endl;
ofile << "[second]" << std::endl;
ofile << " two=four" << std::endl;
ofile.seekg(0, std::ios::beg);
std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
EXPECT_EQ(2u, output.size());
EXPECT_EQ("one", output.at(0).name);
EXPECT_EQ(1u, output.at(0).inputs.size());
EXPECT_EQ("three", output.at(0).inputs.at(0));
EXPECT_EQ("two", output.at(1).name);
EXPECT_EQ("second", output.at(1).parents.at(0));
EXPECT_EQ(1u, output.at(1).inputs.size());
EXPECT_EQ("four", output.at(1).inputs.at(0));
EXPECT_EQ("second.two", output.at(1).fullname());
}
TEST(StringBased, SpacesSections) {
std::stringstream ofile;
ofile << "one=three" << std::endl;
ofile << std::endl;
ofile << "[second]" << std::endl;
ofile << " " << std::endl;
ofile << " two=four" << std::endl;
ofile.seekg(0, std::ios::beg);
std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile);
EXPECT_EQ(2u, output.size());
EXPECT_EQ("one", output.at(0).name);
EXPECT_EQ(1u, output.at(0).inputs.size());
EXPECT_EQ("three", output.at(0).inputs.at(0));
EXPECT_EQ("two", output.at(1).name);
EXPECT_EQ(1u, output.at(1).parents.size());
EXPECT_EQ("second", output.at(1).parents.at(0));
EXPECT_EQ(1u, output.at(1).inputs.size());
EXPECT_EQ("four", output.at(1).inputs.at(0));
}
TEST(StringBased, file_error) {
EXPECT_THROW(std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_file("nonexist_file"), CLI::FileError);
}
TEST_F(TApp, IniNotRequired) {
TempFile tmpini{"TestIniTmp.ini"};
app.set_config("--config", tmpini);
{
std::ofstream out{tmpini};
out << "[default]" << std::endl;
out << "two=99" << std::endl;
out << "three=3" << std::endl;
}
int one = 0, two = 0, three = 0;
app.add_option("--one", one);
app.add_option("--two", two);
app.add_option("--three", three);
args = {"--one=1"};
run();
EXPECT_EQ(1, one);
EXPECT_EQ(99, two);
EXPECT_EQ(3, three);
one = two = three = 0;
args = {"--one=1", "--two=2"};
run();
EXPECT_EQ(1, one);
EXPECT_EQ(2, two);
EXPECT_EQ(3, three);
}
TEST_F(TApp, IniSuccessOnUnknownOption) {
TempFile tmpini{"TestIniTmp.ini"};
app.set_config("--config", tmpini);
app.allow_config_extras(true);
{
std::ofstream out{tmpini};
out << "three=3" << std::endl;
out << "two=99" << std::endl;
}
int two = 0;
app.add_option("--two", two);
run();
EXPECT_EQ(99, two);
}
TEST_F(TApp, IniGetRemainingOption) {
TempFile tmpini{"TestIniTmp.ini"};
app.set_config("--config", tmpini);
app.allow_config_extras(true);
std::string ExtraOption = "three";
std::string ExtraOptionValue = "3";
{
std::ofstream out{tmpini};
out << ExtraOption << "=" << ExtraOptionValue << std::endl;
out << "two=99" << std::endl;
}
int two = 0;
app.add_option("--two", two);
ASSERT_NO_THROW(run());
std::vector<std::string> ExpectedRemaining = {ExtraOption};
EXPECT_EQ(app.remaining(), ExpectedRemaining);
}
TEST_F(TApp, IniGetNoRemaining) {
TempFile tmpini{"TestIniTmp.ini"};
app.set_config("--config", tmpini);
app.allow_config_extras(true);
{
std::ofstream out{tmpini};
out << "two=99" << std::endl;
}
int two = 0;
app.add_option("--two", two);
ASSERT_NO_THROW(run());
EXPECT_EQ(app.remaining().size(), 0u);
}
TEST_F(TApp, IniNotRequiredNotDefault) {
TempFile tmpini{"TestIniTmp.ini"};
TempFile tmpini2{"TestIniTmp2.ini"};
app.set_config("--config", tmpini);
{
std::ofstream out{tmpini};
out << "[default]" << std::endl;
out << "two=99" << std::endl;
out << "three=3" << std::endl;
}
{
std::ofstream out{tmpini2};
out << "[default]" << std::endl;
out << "two=98" << std::endl;
out << "three=4" << std::endl;
}
int one = 0, two = 0, three = 0;
app.add_option("--one", one);
app.add_option("--two", two);
app.add_option("--three", three);
run();
EXPECT_EQ(99, two);
EXPECT_EQ(3, three);
args = {"--config", tmpini2};
run();
EXPECT_EQ(98, two);
EXPECT_EQ(4, three);
}
TEST_F(TApp, IniRequiredNotFound) {
std::string noini = "TestIniNotExist.ini";
app.set_config("--config", noini, "", true);
EXPECT_THROW(run(), CLI::FileError);
}
TEST_F(TApp, IniNotRequiredPassedNotFound) {
std::string noini = "TestIniNotExist.ini";
app.set_config("--config", "", "", false);
args = {"--config", noini};
EXPECT_THROW(run(), CLI::FileError);
}
TEST_F(TApp, IniOverwrite) {
TempFile tmpini{"TestIniTmp.ini"};
{
std::ofstream out{tmpini};
out << "[default]" << std::endl;
out << "two=99" << std::endl;
}
std::string orig = "filename_not_exist.ini";
std::string next = "TestIniTmp.ini";
app.set_config("--config", orig);
// Make sure this can be overwritten
app.set_config("--conf", next);
int two = 7;
app.add_option("--two", two);
run();
EXPECT_EQ(99, two);
}
TEST_F(TApp, IniRequired) {
TempFile tmpini{"TestIniTmp.ini"};
app.set_config("--config", tmpini, "", true);
{
std::ofstream out{tmpini};
out << "[default]" << std::endl;
out << "two=99" << std::endl;
out << "three=3" << std::endl;
}
int one = 0, two = 0, three = 0;
app.add_option("--one", one)->required();
app.add_option("--two", two)->required();
app.add_option("--three", three)->required();
args = {"--one=1"};
run();
one = two = three = 0;
args = {"--one=1", "--two=2"};
run();
args = {};
EXPECT_THROW(run(), CLI::RequiredError);
args = {"--two=2"};
EXPECT_THROW(run(), CLI::RequiredError);
}
TEST_F(TApp, IniVector) {
TempFile tmpini{"TestIniTmp.ini"};
app.set_config("--config", tmpini);
{
std::ofstream out{tmpini};
out << "[default]" << std::endl;
out << "two=2 3" << std::endl;
out << "three=1 2 3" << std::endl;
}
std::vector<int> two, three;
app.add_option("--two", two)->expected(2)->required();
app.add_option("--three", three)->required();
run();
EXPECT_EQ(std::vector<int>({2, 3}), two);
EXPECT_EQ(std::vector<int>({1, 2, 3}), three);
}
TEST_F(TApp, IniLayered) {
TempFile tmpini{"TestIniTmp.ini"};
app.set_config("--config", tmpini);
{
std::ofstream out{tmpini};
out << "[default]" << std::endl;
out << "val=1" << std::endl;
out << "[subcom]" << std::endl;
out << "val=2" << std::endl;
out << "subsubcom.val=3" << std::endl;
}
int one = 0, two = 0, three = 0;
app.add_option("--val", one);
auto subcom = app.add_subcommand("subcom");
subcom->add_option("--val", two);
auto subsubcom = subcom->add_subcommand("subsubcom");
subsubcom->add_option("--val", three);
run();
EXPECT_EQ(1, one);
EXPECT_EQ(2, two);
EXPECT_EQ(3, three);
}
TEST_F(TApp, IniFailure) {
TempFile tmpini{"TestIniTmp.ini"};
app.set_config("--config", tmpini);
{
std::ofstream out{tmpini};
out << "[default]" << std::endl;
out << "val=1" << std::endl;
}
EXPECT_THROW(run(), CLI::ConfigError);
}
TEST_F(TApp, IniConfigurable) {
TempFile tmpini{"TestIniTmp.ini"};
app.set_config("--config", tmpini);
bool value;
app.add_flag("--val", value)->configurable(true);
{
std::ofstream out{tmpini};
out << "[default]" << std::endl;
out << "val=1" << std::endl;
}
ASSERT_NO_THROW(run());
EXPECT_TRUE(value);
}
TEST_F(TApp, IniNotConfigurable) {
TempFile tmpini{"TestIniTmp.ini"};
app.set_config("--config", tmpini);
bool value;
app.add_flag("--val", value)->configurable(false);
{
std::ofstream out{tmpini};
out << "[default]" << std::endl;
out << "val=1" << std::endl;
}
EXPECT_THROW(run(), CLI::ConfigError);
}
TEST_F(TApp, IniSubFailure) {
TempFile tmpini{"TestIniTmp.ini"};
app.add_subcommand("other");
app.set_config("--config", tmpini);
{
std::ofstream out{tmpini};
out << "[other]" << std::endl;
out << "val=1" << std::endl;
}
EXPECT_THROW(run(), CLI::ConfigError);
}
TEST_F(TApp, IniNoSubFailure) {
TempFile tmpini{"TestIniTmp.ini"};
app.set_config("--config", tmpini);
{
std::ofstream out{tmpini};
out << "[other]" << std::endl;
out << "val=1" << std::endl;
}
EXPECT_THROW(run(), CLI::ConfigError);
}
TEST_F(TApp, IniFlagConvertFailure) {
TempFile tmpini{"TestIniTmp.ini"};
app.add_flag("--flag");
app.set_config("--config", tmpini);
{
std::ofstream out{tmpini};
out << "flag=moobook" << std::endl;
}
run();
bool result;
auto *opt = app.get_option("--flag");
EXPECT_THROW(opt->results(result), CLI::ConversionError);
std::string res;
opt->results(res);
EXPECT_EQ(res, "moobook");
}
TEST_F(TApp, IniFlagNumbers) {
TempFile tmpini{"TestIniTmp.ini"};
bool boo;
app.add_flag("--flag", boo);
app.set_config("--config", tmpini);
{
std::ofstream out{tmpini};
out << "flag=3" << std::endl;
}
ASSERT_NO_THROW(run());
EXPECT_TRUE(boo);
}
TEST_F(TApp, IniFlagDual) {
TempFile tmpini{"TestIniTmp.ini"};
bool boo;
app.add_flag("--flag", boo);
app.set_config("--config", tmpini);
{
std::ofstream out{tmpini};
out << "flag=1 1" << std::endl;
}
EXPECT_THROW(run(), CLI::ConversionError);
}
TEST_F(TApp, IniFlagText) {
TempFile tmpini{"TestIniTmp.ini"};
bool flag1, flag2, flag3, flag4;
app.add_flag("--flag1", flag1);
app.add_flag("--flag2", flag2);
app.add_flag("--flag3", flag3);
app.add_flag("--flag4", flag4);
app.set_config("--config", tmpini);
{
std::ofstream out{tmpini};
out << "flag1=true" << std::endl;
out << "flag2=on" << std::endl;
out << "flag3=off" << std::endl;
out << "flag4=1" << std::endl;
}
run();
EXPECT_TRUE(flag1);
EXPECT_TRUE(flag2);
EXPECT_FALSE(flag3);
EXPECT_TRUE(flag4);
}
TEST_F(TApp, IniFlags) {
TempFile tmpini{"TestIniTmp.ini"};
app.set_config("--config", tmpini);
{
std::ofstream out{tmpini};
out << "[default]" << std::endl;
out << "two=2" << std::endl;
out << "three=true" << std::endl;
out << "four=on" << std::endl;
out << "five" << std::endl;
}
int two;
bool three, four, five;
app.add_flag("--two", two);
app.add_flag("--three", three);
app.add_flag("--four", four);
app.add_flag("--five", five);
run();
EXPECT_EQ(2, two);
EXPECT_TRUE(three);
EXPECT_TRUE(four);
EXPECT_TRUE(five);
}
TEST_F(TApp, IniFalseFlags) {
TempFile tmpini{"TestIniTmp.ini"};
app.set_config("--config", tmpini);
{
std::ofstream out{tmpini};
out << "[default]" << std::endl;
out << "two=-2" << std::endl;
out << "three=false" << std::endl;
out << "four=1" << std::endl;
out << "five" << std::endl;
}
int two;
bool three, four, five;
app.add_flag("--two", two);
app.add_flag("--three", three);
app.add_flag("--four", four);
app.add_flag("--five", five);
run();
EXPECT_EQ(-2, two);
EXPECT_FALSE(three);
EXPECT_TRUE(four);
EXPECT_TRUE(five);
}
TEST_F(TApp, IniFalseFlagsDef) {
TempFile tmpini{"TestIniTmp.ini"};
app.set_config("--config", tmpini);
{
std::ofstream out{tmpini};
out << "[default]" << std::endl;
out << "two=2" << std::endl;
out << "three=true" << std::endl;
out << "four=on" << std::endl;
out << "five" << std::endl;
}
int two;
bool three, four, five;
app.add_flag("--two{false}", two);
app.add_flag("--three", three);
app.add_flag("!--four", four);
app.add_flag("--five", five);
run();
EXPECT_EQ(-2, two);
EXPECT_TRUE(three);
EXPECT_FALSE(four);
EXPECT_TRUE(five);
}
TEST_F(TApp, IniFalseFlagsDefDisableOverrideError) {
TempFile tmpini{"TestIniTmp.ini"};
app.set_config("--config", tmpini);
{
std::ofstream out{tmpini};
out << "[default]" << std::endl;
out << "two=2" << std::endl;
out << "four=on" << std::endl;
out << "five" << std::endl;
}
int two;
bool four, five;
app.add_flag("--two{false}", two)->disable_flag_override();
app.add_flag("!--four", four);
app.add_flag("--five", five);
EXPECT_THROW(run(), CLI::ArgumentMismatch);
}
TEST_F(TApp, IniFalseFlagsDefDisableOverrideSuccess) {
TempFile tmpini{"TestIniTmp.ini"};
app.set_config("--config", tmpini);
{
std::ofstream out{tmpini};
out << "[default]" << std::endl;
out << "two=2" << std::endl;
out << "four={}" << std::endl;
out << "val=15" << std::endl;
}
int two, four, val;
app.add_flag("--two{2}", two)->disable_flag_override();
app.add_flag("--four{4}", four)->disable_flag_override();
app.add_flag("--val", val);
run();
EXPECT_EQ(2, two);
EXPECT_EQ(4, four);
EXPECT_EQ(15, val);
}
TEST_F(TApp, IniOutputSimple) {
int v;
app.add_option("--simple", v);
args = {"--simple=3"};
run();
std::string str = app.config_to_str();
EXPECT_EQ("simple=3\n", str);
}
TEST_F(TApp, IniOutputNoConfigurable) {
int v1, v2;
app.add_option("--simple", v1);
app.add_option("--noconf", v2)->configurable(false);
args = {"--simple=3", "--noconf=2"};
run();
std::string str = app.config_to_str();
EXPECT_EQ("simple=3\n", str);
}
TEST_F(TApp, IniOutputShortSingleDescription) {
std::string flag = "some_flag";
const std::string description = "Some short description.";
app.add_flag("--" + flag, description);
run();
std::string str = app.config_to_str(true, true);
EXPECT_THAT(str, HasSubstr("; " + description + "\n" + flag + "=false\n"));
}
TEST_F(TApp, IniOutputShortDoubleDescription) {
std::string flag1 = "flagnr1";
std::string flag2 = "flagnr2";
const std::string description1 = "First description.";
const std::string description2 = "Second description.";
app.add_flag("--" + flag1, description1);
app.add_flag("--" + flag2, description2);
run();
std::string str = app.config_to_str(true, true);
EXPECT_EQ(str, "; " + description1 + "\n" + flag1 + "=false\n\n; " + description2 + "\n" + flag2 + "=false\n");
}
TEST_F(TApp, IniOutputMultiLineDescription) {
std::string flag = "some_flag";
const std::string description = "Some short description.\nThat has lines.";
app.add_flag("--" + flag, description);
run();
std::string str = app.config_to_str(true, true);
EXPECT_THAT(str, HasSubstr("; Some short description.\n"));
EXPECT_THAT(str, HasSubstr("; That has lines.\n"));
EXPECT_THAT(str, HasSubstr(flag + "=false\n"));
}
TEST_F(TApp, IniOutputVector) {
std::vector<int> v;
app.add_option("--vector", v);
args = {"--vector", "1", "2", "3"};
run();
std::string str = app.config_to_str();
EXPECT_EQ("vector=1 2 3\n", str);
}
TEST_F(TApp, IniOutputFlag) {
int v, q;
app.add_option("--simple", v);
app.add_flag("--nothing");
app.add_flag("--onething");
app.add_flag("--something", q);
args = {"--simple=3", "--onething", "--something", "--something"};
run();
std::string str = app.config_to_str();
EXPECT_THAT(str, HasSubstr("simple=3"));
EXPECT_THAT(str, Not(HasSubstr("nothing")));
EXPECT_THAT(str, HasSubstr("onething=true"));
EXPECT_THAT(str, HasSubstr("something=2"));
str = app.config_to_str(true);
EXPECT_THAT(str, HasSubstr("nothing"));
}
TEST_F(TApp, IniOutputSet) {
int v;
app.add_option("--simple", v)->check(CLI::IsMember({1, 2, 3}));
args = {"--simple=2"};
run();
std::string str = app.config_to_str();
EXPECT_THAT(str, HasSubstr("simple=2"));
}
TEST_F(TApp, IniOutputDefault) {
int v = 7;
app.add_option("--simple", v, "", true);
run();
std::string str = app.config_to_str();
EXPECT_THAT(str, Not(HasSubstr("simple=7")));
str = app.config_to_str(true);
EXPECT_THAT(str, HasSubstr("simple=7"));
}
TEST_F(TApp, IniOutputSubcom) {
app.add_flag("--simple");
auto subcom = app.add_subcommand("other");
subcom->add_flag("--newer");
args = {"--simple", "other", "--newer"};
run();
std::string str = app.config_to_str();
EXPECT_THAT(str, HasSubstr("simple=true"));
EXPECT_THAT(str, HasSubstr("other.newer=true"));
}
TEST_F(TApp, IniQuotedOutput) {
std::string val1;
app.add_option("--val1", val1);
std::string val2;
app.add_option("--val2", val2);
args = {"--val1", "I am a string", "--val2", R"(I am a "confusing" string)"};
run();
EXPECT_EQ("I am a string", val1);
EXPECT_EQ("I am a \"confusing\" string", val2);
std::string str = app.config_to_str();
EXPECT_THAT(str, HasSubstr("val1=\"I am a string\""));
EXPECT_THAT(str, HasSubstr("val2='I am a \"confusing\" string'"));
}
TEST_F(TApp, DefaultsIniQuotedOutput) {
std::string val1{"I am a string"};
app.add_option("--val1", val1, "", true);
std::string val2{R"(I am a "confusing" string)"};
app.add_option("--val2", val2, "", true);
run();
std::string str = app.config_to_str(true);
EXPECT_THAT(str, HasSubstr("val1=\"I am a string\""));
EXPECT_THAT(str, HasSubstr("val2='I am a \"confusing\" string'"));
}
// #298
TEST_F(TApp, StopReadingConfigOnClear) {
TempFile tmpini{"TestIniTmp.ini"};
app.set_config("--config", tmpini);
auto ptr = app.set_config(); // Should *not* read config file
EXPECT_EQ(ptr, nullptr);
{
std::ofstream out{tmpini};
out << "volume=1" << std::endl;
}
int volume = 0;
app.add_option("--volume", volume, "volume1");
run();
EXPECT_EQ(volume, 0);
}