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:
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.
|
||||
|
||||
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
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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}));
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user