You need to sign in before continuing.

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 798  
799 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 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 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 72  
73 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 107 ### Containers of containers
76 108  
77 109 Containers of containers are also supported.
... ...
include/CLI/App.hpp
... ... @@ -2451,8 +2451,9 @@ class App {
2451 2451 }
2452 2452  
2453 2453 if(op->empty()) {
2454   - // Flag parsing
  2454 +
2455 2455 if(op->get_expected_min() == 0) {
  2456 + // Flag parsing
2456 2457 auto res = config_formatter_->to_flag(item);
2457 2458 res = op->get_flag_value(item.name, res);
2458 2459  
... ...
include/CLI/ConfigFwd.hpp
... ... @@ -58,6 +58,9 @@ class Config {
58 58 if(item.inputs.size() == 1) {
59 59 return item.inputs.at(0);
60 60 }
  61 + if(item.inputs.empty()) {
  62 + return "{}";
  63 + }
61 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 1162 add_result(val_str);
1163 1163 // if trigger_on_result_ is set the callback already ran
1164 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 1166 current_option_state_ = option_state::parsing;
1167 1167 } else {
1168 1168 _validate_results(results_);
... ... @@ -1285,6 +1285,16 @@ class Option : public OptionBase&lt;Option&gt; {
1285 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 1300 // Run a result through the Validators
... ...
include/CLI/TypeTools.hpp
... ... @@ -304,9 +304,12 @@ template &lt;typename T,
304 304 is_readable_container<T>::value,
305 305 detail::enabler> = detail::dummy>
306 306 std::string to_string(T &&variable) {
307   - std::vector<std::string> defaults;
308 307 auto cval = variable.begin();
309 308 auto end = variable.end();
  309 + if(cval == end) {
  310 + return std::string("{}");
  311 + }
  312 + std::vector<std::string> defaults;
310 313 while(cval != end) {
311 314 defaults.emplace_back(CLI::detail::to_string(*cval));
312 315 ++cval;
... ... @@ -1208,6 +1211,13 @@ template &lt;class AssignTo,
1208 1211 detail::enabler> = detail::dummy>
1209 1212 bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
1210 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 1221 for(const auto &elem : strings) {
1212 1222 typename AssignTo::value_type out;
1213 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 1225 return false;
1216 1226 }
1217 1227 output.insert(output.end(), std::move(out));
  1228 + if(skip_remaining) {
  1229 + break;
  1230 + }
1218 1231 }
1219 1232 return (!output.empty());
1220 1233 }
... ...
tests/AppTest.cpp
... ... @@ -939,6 +939,35 @@ TEST_CASE_METHOD(TApp, &quot;RequiredOptsDouble&quot;, &quot;[app]&quot;) {
939 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 971 TEST_CASE_METHOD(TApp, "RequiredOptsDoubleShort", "[app]") {
943 972  
944 973 std::vector<std::string> strs;
... ...
tests/ConfigFileTest.cpp
... ... @@ -1014,16 +1014,32 @@ TEST_CASE_METHOD(TApp, &quot;TOMLStringVector&quot;, &quot;[config]&quot;) {
1014 1014 std::ofstream out{tmptoml};
1015 1015 out << "#this is a comment line\n";
1016 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 1022 out << "two=[\"2\",\"3\"]\n";
1018 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 1033 app.add_option("--two", two)->required();
1023 1034 app.add_option("--three", three)->required();
1024 1035  
1025 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 1043 CHECK(two == std::vector<std::string>({"2", "3"}));
1028 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 1054 std::ofstream out{tmpini};
1039 1055 out << "#this is a comment line\n";
1040 1056 out << "[default]\n";
  1057 + out << "zero1=[]\n";
  1058 + out << "zero2=[]\n";
  1059 + out << "one=[1]\n";
1041 1060 out << "two=[2,3]\n";
1042 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 1068 app.add_option("--two", two)->expected(2)->required();
1047 1069 app.add_option("--three", three)->required();
1048 1070  
1049 1071 run();
1050 1072  
  1073 + CHECK(zero1 == std::vector<int>({}));
  1074 + CHECK(zero2 == std::vector<int>({}));
  1075 + CHECK(one == std::vector<int>({1}));
1051 1076 CHECK(two == std::vector<int>({2, 3}));
1052 1077 CHECK(three == std::vector<int>({1, 2, 3}));
1053 1078 }
... ...