1
0
mirror of https://github.com/CLIUtils/CLI11.git synced 2025-05-02 13:43:52 +00:00

feat: support empty vector in TOML (#660)

* add tests which suppose to pass

* Update ConfigFileTest.cpp

* Update ConfigFileTest.cpp

* style: pre-commit.ci fixes

* add the possibility for an empty vector result if allowed.

* style: pre-commit.ci fixes

* add empty vector command line tests

* update book and readme

* add no default test

Co-authored-by: puchneiner <90352207+puchneiner@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Philip Top 2022-02-09 10:12:55 -08:00 committed by GitHub
parent e8265f9102
commit e29cb3f1b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 120 additions and 5 deletions

View File

@ -798,6 +798,8 @@ app.set_config("--config")->expected(1, X);
Where X is some positive number and will allow up to `X` configuration files to be specified by separate `--config` arguments. Value strings with quote characters in it will be printed with a single quote. All other arguments will use double quote. Empty strings will use a double quoted argument. Numerical or boolean values are not quoted.
For options or flags which allow 0 arguments to be passed using an empty string in the config file, `{}`, or `[]` will convert the result to the default value specified via `default_str` or `default_val` on the option 🚧. If no user specified default is given the result is an empty string or the converted value of an empty string.
NOTE: Transforms and checks can be used with the option pointer returned from set_config like any other option to validate the input if needed. It can also be used with the built in transform `CLI::FileOnDefaultPath` to look in a default path as well as the current one. For example
```cpp

View File

@ -72,6 +72,38 @@ Vectors will be replaced by the parsed content if the option is given on the com
A definition of a container for purposes of CLI11 is a type with a `end()`, `insert(...)`, `clear()` and `value_type` definitions. This includes `vector`, `set`, `deque`, `list`, `forward_iist`, `map`, `unordered_map` and a few others from the standard library, and many other containers from the boost library.
### Empty containers
By default a container will never return an empty container. If it is desired to allow an empty container to be returned, then the option must be modified with a 0 as the minimum expected value
```cpp
std::vector<int> int_vec;
app.add_option("--vec", int_vec, "Empty vector allowed")->expected(0,-1);
```
An empty vector can than be specified on the command line as `--vec {}`
To allow an empty vector from config file, the default must be set in addition to the above modification.
```cpp
std::vector<int> int_vec;
app.add_option("--vec", int_vec, "Empty vector allowed")->expected(0,-1)->default_str("{}");
```
Then in the file
```toml
vec={}
```
or
```toml
vec=[]
```
will generate an empty vector in `int_vec`.
### Containers of containers
Containers of containers are also supported.

View File

@ -2451,8 +2451,9 @@ class App {
}
if(op->empty()) {
// Flag parsing
if(op->get_expected_min() == 0) {
// Flag parsing
auto res = config_formatter_->to_flag(item);
res = op->get_flag_value(item.name, res);

View File

@ -58,6 +58,9 @@ class Config {
if(item.inputs.size() == 1) {
return item.inputs.at(0);
}
if(item.inputs.empty()) {
return "{}";
}
throw ConversionError::TooManyInputsFlag(item.fullname());
}

View File

@ -1162,7 +1162,7 @@ class Option : public OptionBase<Option> {
add_result(val_str);
// if trigger_on_result_ is set the callback already ran
if(run_callback_for_default_ && !trigger_on_result_) {
run_callback(); // run callback sets the state we need to reset it again
run_callback(); // run callback sets the state, we need to reset it again
current_option_state_ = option_state::parsing;
} else {
_validate_results(results_);
@ -1285,6 +1285,16 @@ class Option : public OptionBase<Option> {
break;
}
}
// this check is to allow an empty vector in certain circumstances but not if expected is not zero.
// {} is the indicator for a an empty container
if(res.empty()) {
if(original.size() == 1 && original[0] == "{}" && get_items_expected_min() > 0) {
res.push_back("{}");
res.push_back("%%");
}
} else if(res.size() == 1 && res[0] == "{}" && get_items_expected_min() > 0) {
res.push_back("%%");
}
}
// Run a result through the Validators

View File

@ -304,9 +304,12 @@ template <typename T,
is_readable_container<T>::value,
detail::enabler> = detail::dummy>
std::string to_string(T &&variable) {
std::vector<std::string> defaults;
auto cval = variable.begin();
auto end = variable.end();
if(cval == end) {
return std::string("{}");
}
std::vector<std::string> defaults;
while(cval != end) {
defaults.emplace_back(CLI::detail::to_string(*cval));
++cval;
@ -1208,6 +1211,13 @@ template <class AssignTo,
detail::enabler> = detail::dummy>
bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
output.erase(output.begin(), output.end());
if(strings.size() == 1 && strings[0] == "{}") {
return true;
}
bool skip_remaining = false;
if(strings.size() == 2 && strings[0] == "{}" && is_separator(strings[1])) {
skip_remaining = true;
}
for(const auto &elem : strings) {
typename AssignTo::value_type out;
bool retval = lexical_assign<typename AssignTo::value_type, typename ConvertTo::value_type>(elem, out);
@ -1215,6 +1225,9 @@ bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &outp
return false;
}
output.insert(output.end(), std::move(out));
if(skip_remaining) {
break;
}
}
return (!output.empty());
}

View File

@ -939,6 +939,35 @@ TEST_CASE_METHOD(TApp, "RequiredOptsDouble", "[app]") {
CHECK(std::vector<std::string>({"one", "two"}) == strs);
}
TEST_CASE_METHOD(TApp, "emptyVectorReturn", "[app]") {
std::vector<std::string> strs;
std::vector<std::string> strs2;
auto opt1 = app.add_option("--str", strs)->required()->expected(0, 2);
app.add_option("--str2", strs2);
args = {"--str"};
CHECK_NOTHROW(run());
CHECK(std::vector<std::string>({""}) == strs);
args = {"--str", "one", "two"};
run();
CHECK(std::vector<std::string>({"one", "two"}) == strs);
args = {"--str", "{}", "--str2", "{}"};
run();
CHECK(std::vector<std::string>{} == strs);
CHECK(std::vector<std::string>{"{}"} == strs2);
opt1->default_str("{}");
args = {"--str"};
CHECK_NOTHROW(run());
CHECK(std::vector<std::string>{} == strs);
}
TEST_CASE_METHOD(TApp, "RequiredOptsDoubleShort", "[app]") {
std::vector<std::string> strs;

View File

@ -1014,16 +1014,32 @@ TEST_CASE_METHOD(TApp, "TOMLStringVector", "[config]") {
std::ofstream out{tmptoml};
out << "#this is a comment line\n";
out << "[default]\n";
out << "zero1=[]\n";
out << "zero2={}\n";
out << "zero3={}\n";
out << "nzero={}\n";
out << "one=[\"1\"]\n";
out << "two=[\"2\",\"3\"]\n";
out << "three=[\"1\",\"2\",\"3\"]\n";
}
std::vector<std::string> two, three;
std::vector<std::string> nzero, zero1, zero2, zero3, one, two, three;
app.add_option("--zero1", zero1)->required()->expected(0, 99)->default_str("{}");
app.add_option("--zero2", zero2)->required()->expected(0, 99)->default_val(std::vector<std::string>{});
// if no default is specified the argument results in an empty string
app.add_option("--zero3", zero3)->required()->expected(0, 99);
app.add_option("--nzero", nzero)->required();
app.add_option("--one", one)->required();
app.add_option("--two", two)->required();
app.add_option("--three", three)->required();
run();
CHECK(zero1 == std::vector<std::string>({}));
CHECK(zero2 == std::vector<std::string>({}));
CHECK(zero3 == std::vector<std::string>({""}));
CHECK(nzero == std::vector<std::string>({"{}"}));
CHECK(one == std::vector<std::string>({"1"}));
CHECK(two == std::vector<std::string>({"2", "3"}));
CHECK(three == std::vector<std::string>({"1", "2", "3"}));
}
@ -1038,16 +1054,25 @@ TEST_CASE_METHOD(TApp, "IniVectorCsep", "[config]") {
std::ofstream out{tmpini};
out << "#this is a comment line\n";
out << "[default]\n";
out << "zero1=[]\n";
out << "zero2=[]\n";
out << "one=[1]\n";
out << "two=[2,3]\n";
out << "three=1,2,3\n";
}
std::vector<int> two, three;
std::vector<int> zero1, zero2, one, two, three;
app.add_option("--zero1", zero1)->required()->expected(0, 99)->default_str("{}");
app.add_option("--zero2", zero2)->required()->expected(0, 99)->default_val(std::vector<int>{});
app.add_option("--one", one)->required();
app.add_option("--two", two)->expected(2)->required();
app.add_option("--three", three)->required();
run();
CHECK(zero1 == std::vector<int>({}));
CHECK(zero2 == std::vector<int>({}));
CHECK(one == std::vector<int>({1}));
CHECK(two == std::vector<int>({2, 3}));
CHECK(three == std::vector<int>({1, 2, 3}));
}