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:
parent
d5cd986046
commit
c67ab9dd43
28
README.md
28
README.md
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
|
@ -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 §ion, 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 ¤tSection) {
|
||||
|
||||
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();
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -23,7 +23,7 @@ include(AddGoogletest)
|
||||
|
||||
set(CLI11_TESTS
|
||||
HelpersTest
|
||||
IniTest
|
||||
ConfigFileTest
|
||||
SimpleTest
|
||||
AppTest
|
||||
SetTest
|
||||
|
1757
tests/ConfigFileTest.cpp
Normal file
1757
tests/ConfigFileTest.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
|
@ -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);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user