Commit e29cb3f1b47a973094813ef82c6e2dab84984336

Authored by Philip Top
Committed by GitHub
1 parent e8265f91

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>
README.md
@@ -798,6 +798,8 @@ app.set_config(&quot;--config&quot;)-&gt;expected(1, X); @@ -798,6 +798,8 @@ app.set_config(&quot;--config&quot;)-&gt;expected(1, X);
798 798
799 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. 799 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.
800 800
  801 +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.
  802 +
801 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 803 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
802 804
803 ```cpp 805 ```cpp
book/chapters/options.md
@@ -72,6 +72,38 @@ Vectors will be replaced by the parsed content if the option is given on the com @@ -72,6 +72,38 @@ Vectors will be replaced by the parsed content if the option is given on the com
72 72
73 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. 73 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.
74 74
  75 +### Empty containers
  76 +
  77 +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
  78 +
  79 +```cpp
  80 +std::vector<int> int_vec;
  81 +app.add_option("--vec", int_vec, "Empty vector allowed")->expected(0,-1);
  82 +```
  83 +
  84 +An empty vector can than be specified on the command line as `--vec {}`
  85 +
  86 +To allow an empty vector from config file, the default must be set in addition to the above modification.
  87 +
  88 +```cpp
  89 +std::vector<int> int_vec;
  90 +app.add_option("--vec", int_vec, "Empty vector allowed")->expected(0,-1)->default_str("{}");
  91 +```
  92 +
  93 +Then in the file
  94 +
  95 +```toml
  96 +vec={}
  97 +```
  98 +
  99 +or
  100 +
  101 +```toml
  102 +vec=[]
  103 +```
  104 +
  105 +will generate an empty vector in `int_vec`.
  106 +
75 ### Containers of containers 107 ### Containers of containers
76 108
77 Containers of containers are also supported. 109 Containers of containers are also supported.
include/CLI/App.hpp
@@ -2451,8 +2451,9 @@ class App { @@ -2451,8 +2451,9 @@ class App {
2451 } 2451 }
2452 2452
2453 if(op->empty()) { 2453 if(op->empty()) {
2454 - // Flag parsing 2454 +
2455 if(op->get_expected_min() == 0) { 2455 if(op->get_expected_min() == 0) {
  2456 + // Flag parsing
2456 auto res = config_formatter_->to_flag(item); 2457 auto res = config_formatter_->to_flag(item);
2457 res = op->get_flag_value(item.name, res); 2458 res = op->get_flag_value(item.name, res);
2458 2459
include/CLI/ConfigFwd.hpp
@@ -58,6 +58,9 @@ class Config { @@ -58,6 +58,9 @@ class Config {
58 if(item.inputs.size() == 1) { 58 if(item.inputs.size() == 1) {
59 return item.inputs.at(0); 59 return item.inputs.at(0);
60 } 60 }
  61 + if(item.inputs.empty()) {
  62 + return "{}";
  63 + }
61 throw ConversionError::TooManyInputsFlag(item.fullname()); 64 throw ConversionError::TooManyInputsFlag(item.fullname());
62 } 65 }
63 66
include/CLI/Option.hpp
@@ -1162,7 +1162,7 @@ class Option : public OptionBase&lt;Option&gt; { @@ -1162,7 +1162,7 @@ class Option : public OptionBase&lt;Option&gt; {
1162 add_result(val_str); 1162 add_result(val_str);
1163 // if trigger_on_result_ is set the callback already ran 1163 // if trigger_on_result_ is set the callback already ran
1164 if(run_callback_for_default_ && !trigger_on_result_) { 1164 if(run_callback_for_default_ && !trigger_on_result_) {
1165 - run_callback(); // run callback sets the state we need to reset it again 1165 + run_callback(); // run callback sets the state, we need to reset it again
1166 current_option_state_ = option_state::parsing; 1166 current_option_state_ = option_state::parsing;
1167 } else { 1167 } else {
1168 _validate_results(results_); 1168 _validate_results(results_);
@@ -1285,6 +1285,16 @@ class Option : public OptionBase&lt;Option&gt; { @@ -1285,6 +1285,16 @@ class Option : public OptionBase&lt;Option&gt; {
1285 break; 1285 break;
1286 } 1286 }
1287 } 1287 }
  1288 + // this check is to allow an empty vector in certain circumstances but not if expected is not zero.
  1289 + // {} is the indicator for a an empty container
  1290 + if(res.empty()) {
  1291 + if(original.size() == 1 && original[0] == "{}" && get_items_expected_min() > 0) {
  1292 + res.push_back("{}");
  1293 + res.push_back("%%");
  1294 + }
  1295 + } else if(res.size() == 1 && res[0] == "{}" && get_items_expected_min() > 0) {
  1296 + res.push_back("%%");
  1297 + }
1288 } 1298 }
1289 1299
1290 // Run a result through the Validators 1300 // Run a result through the Validators
include/CLI/TypeTools.hpp
@@ -304,9 +304,12 @@ template &lt;typename T, @@ -304,9 +304,12 @@ template &lt;typename T,
304 is_readable_container<T>::value, 304 is_readable_container<T>::value,
305 detail::enabler> = detail::dummy> 305 detail::enabler> = detail::dummy>
306 std::string to_string(T &&variable) { 306 std::string to_string(T &&variable) {
307 - std::vector<std::string> defaults;  
308 auto cval = variable.begin(); 307 auto cval = variable.begin();
309 auto end = variable.end(); 308 auto end = variable.end();
  309 + if(cval == end) {
  310 + return std::string("{}");
  311 + }
  312 + std::vector<std::string> defaults;
310 while(cval != end) { 313 while(cval != end) {
311 defaults.emplace_back(CLI::detail::to_string(*cval)); 314 defaults.emplace_back(CLI::detail::to_string(*cval));
312 ++cval; 315 ++cval;
@@ -1208,6 +1211,13 @@ template &lt;class AssignTo, @@ -1208,6 +1211,13 @@ template &lt;class AssignTo,
1208 detail::enabler> = detail::dummy> 1211 detail::enabler> = detail::dummy>
1209 bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) { 1212 bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
1210 output.erase(output.begin(), output.end()); 1213 output.erase(output.begin(), output.end());
  1214 + if(strings.size() == 1 && strings[0] == "{}") {
  1215 + return true;
  1216 + }
  1217 + bool skip_remaining = false;
  1218 + if(strings.size() == 2 && strings[0] == "{}" && is_separator(strings[1])) {
  1219 + skip_remaining = true;
  1220 + }
1211 for(const auto &elem : strings) { 1221 for(const auto &elem : strings) {
1212 typename AssignTo::value_type out; 1222 typename AssignTo::value_type out;
1213 bool retval = lexical_assign<typename AssignTo::value_type, typename ConvertTo::value_type>(elem, out); 1223 bool retval = lexical_assign<typename AssignTo::value_type, typename ConvertTo::value_type>(elem, out);
@@ -1215,6 +1225,9 @@ bool lexical_conversion(const std::vector&lt;std ::string&gt; &amp;strings, AssignTo &amp;outp @@ -1215,6 +1225,9 @@ bool lexical_conversion(const std::vector&lt;std ::string&gt; &amp;strings, AssignTo &amp;outp
1215 return false; 1225 return false;
1216 } 1226 }
1217 output.insert(output.end(), std::move(out)); 1227 output.insert(output.end(), std::move(out));
  1228 + if(skip_remaining) {
  1229 + break;
  1230 + }
1218 } 1231 }
1219 return (!output.empty()); 1232 return (!output.empty());
1220 } 1233 }
tests/AppTest.cpp
@@ -939,6 +939,35 @@ TEST_CASE_METHOD(TApp, &quot;RequiredOptsDouble&quot;, &quot;[app]&quot;) { @@ -939,6 +939,35 @@ TEST_CASE_METHOD(TApp, &quot;RequiredOptsDouble&quot;, &quot;[app]&quot;) {
939 CHECK(std::vector<std::string>({"one", "two"}) == strs); 939 CHECK(std::vector<std::string>({"one", "two"}) == strs);
940 } 940 }
941 941
  942 +TEST_CASE_METHOD(TApp, "emptyVectorReturn", "[app]") {
  943 +
  944 + std::vector<std::string> strs;
  945 + std::vector<std::string> strs2;
  946 + auto opt1 = app.add_option("--str", strs)->required()->expected(0, 2);
  947 + app.add_option("--str2", strs2);
  948 + args = {"--str"};
  949 +
  950 + CHECK_NOTHROW(run());
  951 + CHECK(std::vector<std::string>({""}) == strs);
  952 + args = {"--str", "one", "two"};
  953 +
  954 + run();
  955 +
  956 + CHECK(std::vector<std::string>({"one", "two"}) == strs);
  957 +
  958 + args = {"--str", "{}", "--str2", "{}"};
  959 +
  960 + run();
  961 +
  962 + CHECK(std::vector<std::string>{} == strs);
  963 + CHECK(std::vector<std::string>{"{}"} == strs2);
  964 + opt1->default_str("{}");
  965 + args = {"--str"};
  966 +
  967 + CHECK_NOTHROW(run());
  968 + CHECK(std::vector<std::string>{} == strs);
  969 +}
  970 +
942 TEST_CASE_METHOD(TApp, "RequiredOptsDoubleShort", "[app]") { 971 TEST_CASE_METHOD(TApp, "RequiredOptsDoubleShort", "[app]") {
943 972
944 std::vector<std::string> strs; 973 std::vector<std::string> strs;
tests/ConfigFileTest.cpp
@@ -1014,16 +1014,32 @@ TEST_CASE_METHOD(TApp, &quot;TOMLStringVector&quot;, &quot;[config]&quot;) { @@ -1014,16 +1014,32 @@ TEST_CASE_METHOD(TApp, &quot;TOMLStringVector&quot;, &quot;[config]&quot;) {
1014 std::ofstream out{tmptoml}; 1014 std::ofstream out{tmptoml};
1015 out << "#this is a comment line\n"; 1015 out << "#this is a comment line\n";
1016 out << "[default]\n"; 1016 out << "[default]\n";
  1017 + out << "zero1=[]\n";
  1018 + out << "zero2={}\n";
  1019 + out << "zero3={}\n";
  1020 + out << "nzero={}\n";
  1021 + out << "one=[\"1\"]\n";
1017 out << "two=[\"2\",\"3\"]\n"; 1022 out << "two=[\"2\",\"3\"]\n";
1018 out << "three=[\"1\",\"2\",\"3\"]\n"; 1023 out << "three=[\"1\",\"2\",\"3\"]\n";
1019 } 1024 }
1020 1025
1021 - std::vector<std::string> two, three; 1026 + std::vector<std::string> nzero, zero1, zero2, zero3, one, two, three;
  1027 + app.add_option("--zero1", zero1)->required()->expected(0, 99)->default_str("{}");
  1028 + app.add_option("--zero2", zero2)->required()->expected(0, 99)->default_val(std::vector<std::string>{});
  1029 + // if no default is specified the argument results in an empty string
  1030 + app.add_option("--zero3", zero3)->required()->expected(0, 99);
  1031 + app.add_option("--nzero", nzero)->required();
  1032 + app.add_option("--one", one)->required();
1022 app.add_option("--two", two)->required(); 1033 app.add_option("--two", two)->required();
1023 app.add_option("--three", three)->required(); 1034 app.add_option("--three", three)->required();
1024 1035
1025 run(); 1036 run();
1026 1037
  1038 + CHECK(zero1 == std::vector<std::string>({}));
  1039 + CHECK(zero2 == std::vector<std::string>({}));
  1040 + CHECK(zero3 == std::vector<std::string>({""}));
  1041 + CHECK(nzero == std::vector<std::string>({"{}"}));
  1042 + CHECK(one == std::vector<std::string>({"1"}));
1027 CHECK(two == std::vector<std::string>({"2", "3"})); 1043 CHECK(two == std::vector<std::string>({"2", "3"}));
1028 CHECK(three == std::vector<std::string>({"1", "2", "3"})); 1044 CHECK(three == std::vector<std::string>({"1", "2", "3"}));
1029 } 1045 }
@@ -1038,16 +1054,25 @@ TEST_CASE_METHOD(TApp, &quot;IniVectorCsep&quot;, &quot;[config]&quot;) { @@ -1038,16 +1054,25 @@ TEST_CASE_METHOD(TApp, &quot;IniVectorCsep&quot;, &quot;[config]&quot;) {
1038 std::ofstream out{tmpini}; 1054 std::ofstream out{tmpini};
1039 out << "#this is a comment line\n"; 1055 out << "#this is a comment line\n";
1040 out << "[default]\n"; 1056 out << "[default]\n";
  1057 + out << "zero1=[]\n";
  1058 + out << "zero2=[]\n";
  1059 + out << "one=[1]\n";
1041 out << "two=[2,3]\n"; 1060 out << "two=[2,3]\n";
1042 out << "three=1,2,3\n"; 1061 out << "three=1,2,3\n";
1043 } 1062 }
1044 1063
1045 - std::vector<int> two, three; 1064 + std::vector<int> zero1, zero2, one, two, three;
  1065 + app.add_option("--zero1", zero1)->required()->expected(0, 99)->default_str("{}");
  1066 + app.add_option("--zero2", zero2)->required()->expected(0, 99)->default_val(std::vector<int>{});
  1067 + app.add_option("--one", one)->required();
1046 app.add_option("--two", two)->expected(2)->required(); 1068 app.add_option("--two", two)->expected(2)->required();
1047 app.add_option("--three", three)->required(); 1069 app.add_option("--three", three)->required();
1048 1070
1049 run(); 1071 run();
1050 1072
  1073 + CHECK(zero1 == std::vector<int>({}));
  1074 + CHECK(zero2 == std::vector<int>({}));
  1075 + CHECK(one == std::vector<int>({1}));
1051 CHECK(two == std::vector<int>({2, 3})); 1076 CHECK(two == std::vector<int>({2, 3}));
1052 CHECK(three == std::vector<int>({1, 2, 3})); 1077 CHECK(three == std::vector<int>({1, 2, 3}));
1053 } 1078 }