mirror of
https://github.com/CLIUtils/CLI11.git
synced 2025-05-03 14:03: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:
parent
e8265f9102
commit
e29cb3f1b4
@ -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.
|
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
|
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
|
```cpp
|
||||||
|
@ -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.
|
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
|
||||||
|
|
||||||
Containers of containers are also supported.
|
Containers of containers are also supported.
|
||||||
|
@ -2451,8 +2451,9 @@ class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(op->empty()) {
|
if(op->empty()) {
|
||||||
// Flag parsing
|
|
||||||
if(op->get_expected_min() == 0) {
|
if(op->get_expected_min() == 0) {
|
||||||
|
// Flag parsing
|
||||||
auto res = config_formatter_->to_flag(item);
|
auto res = config_formatter_->to_flag(item);
|
||||||
res = op->get_flag_value(item.name, res);
|
res = op->get_flag_value(item.name, res);
|
||||||
|
|
||||||
|
@ -58,6 +58,9 @@ class Config {
|
|||||||
if(item.inputs.size() == 1) {
|
if(item.inputs.size() == 1) {
|
||||||
return item.inputs.at(0);
|
return item.inputs.at(0);
|
||||||
}
|
}
|
||||||
|
if(item.inputs.empty()) {
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
throw ConversionError::TooManyInputsFlag(item.fullname());
|
throw ConversionError::TooManyInputsFlag(item.fullname());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1162,7 +1162,7 @@ class Option : public OptionBase<Option> {
|
|||||||
add_result(val_str);
|
add_result(val_str);
|
||||||
// if trigger_on_result_ is set the callback already ran
|
// if trigger_on_result_ is set the callback already ran
|
||||||
if(run_callback_for_default_ && !trigger_on_result_) {
|
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;
|
current_option_state_ = option_state::parsing;
|
||||||
} else {
|
} else {
|
||||||
_validate_results(results_);
|
_validate_results(results_);
|
||||||
@ -1285,6 +1285,16 @@ class Option : public OptionBase<Option> {
|
|||||||
break;
|
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
|
// Run a result through the Validators
|
||||||
|
@ -304,9 +304,12 @@ template <typename T,
|
|||||||
is_readable_container<T>::value,
|
is_readable_container<T>::value,
|
||||||
detail::enabler> = detail::dummy>
|
detail::enabler> = detail::dummy>
|
||||||
std::string to_string(T &&variable) {
|
std::string to_string(T &&variable) {
|
||||||
std::vector<std::string> defaults;
|
|
||||||
auto cval = variable.begin();
|
auto cval = variable.begin();
|
||||||
auto end = variable.end();
|
auto end = variable.end();
|
||||||
|
if(cval == end) {
|
||||||
|
return std::string("{}");
|
||||||
|
}
|
||||||
|
std::vector<std::string> defaults;
|
||||||
while(cval != end) {
|
while(cval != end) {
|
||||||
defaults.emplace_back(CLI::detail::to_string(*cval));
|
defaults.emplace_back(CLI::detail::to_string(*cval));
|
||||||
++cval;
|
++cval;
|
||||||
@ -1208,6 +1211,13 @@ template <class AssignTo,
|
|||||||
detail::enabler> = detail::dummy>
|
detail::enabler> = detail::dummy>
|
||||||
bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
|
bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
|
||||||
output.erase(output.begin(), output.end());
|
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) {
|
for(const auto &elem : strings) {
|
||||||
typename AssignTo::value_type out;
|
typename AssignTo::value_type out;
|
||||||
bool retval = lexical_assign<typename AssignTo::value_type, typename ConvertTo::value_type>(elem, 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;
|
return false;
|
||||||
}
|
}
|
||||||
output.insert(output.end(), std::move(out));
|
output.insert(output.end(), std::move(out));
|
||||||
|
if(skip_remaining) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (!output.empty());
|
return (!output.empty());
|
||||||
}
|
}
|
||||||
|
@ -939,6 +939,35 @@ TEST_CASE_METHOD(TApp, "RequiredOptsDouble", "[app]") {
|
|||||||
CHECK(std::vector<std::string>({"one", "two"}) == strs);
|
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]") {
|
TEST_CASE_METHOD(TApp, "RequiredOptsDoubleShort", "[app]") {
|
||||||
|
|
||||||
std::vector<std::string> strs;
|
std::vector<std::string> strs;
|
||||||
|
@ -1014,16 +1014,32 @@ TEST_CASE_METHOD(TApp, "TOMLStringVector", "[config]") {
|
|||||||
std::ofstream out{tmptoml};
|
std::ofstream out{tmptoml};
|
||||||
out << "#this is a comment line\n";
|
out << "#this is a comment line\n";
|
||||||
out << "[default]\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 << "two=[\"2\",\"3\"]\n";
|
||||||
out << "three=[\"1\",\"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("--two", two)->required();
|
||||||
app.add_option("--three", three)->required();
|
app.add_option("--three", three)->required();
|
||||||
|
|
||||||
run();
|
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(two == std::vector<std::string>({"2", "3"}));
|
||||||
CHECK(three == std::vector<std::string>({"1", "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};
|
std::ofstream out{tmpini};
|
||||||
out << "#this is a comment line\n";
|
out << "#this is a comment line\n";
|
||||||
out << "[default]\n";
|
out << "[default]\n";
|
||||||
|
out << "zero1=[]\n";
|
||||||
|
out << "zero2=[]\n";
|
||||||
|
out << "one=[1]\n";
|
||||||
out << "two=[2,3]\n";
|
out << "two=[2,3]\n";
|
||||||
out << "three=1,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("--two", two)->expected(2)->required();
|
||||||
app.add_option("--three", three)->required();
|
app.add_option("--three", three)->required();
|
||||||
|
|
||||||
run();
|
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(two == std::vector<int>({2, 3}));
|
||||||
CHECK(three == std::vector<int>({1, 2, 3}));
|
CHECK(three == std::vector<int>({1, 2, 3}));
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user