Commit e29cb3f1b47a973094813ef82c6e2dab84984336
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>
Showing
8 changed files
with
120 additions
and
5 deletions
README.md
| @@ -798,6 +798,8 @@ app.set_config("--config")->expected(1, X); | @@ -798,6 +798,8 @@ app.set_config("--config")->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<Option> { | @@ -1162,7 +1162,7 @@ class Option : public OptionBase<Option> { | ||
| 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<Option> { | @@ -1285,6 +1285,16 @@ class Option : public OptionBase<Option> { | ||
| 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 <typename T, | @@ -304,9 +304,12 @@ template <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 <class AssignTo, | @@ -1208,6 +1211,13 @@ template <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<std ::string> &strings, AssignTo &outp | @@ -1215,6 +1225,9 @@ bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &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, "RequiredOptsDouble", "[app]") { | @@ -939,6 +939,35 @@ TEST_CASE_METHOD(TApp, "RequiredOptsDouble", "[app]") { | ||
| 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, "TOMLStringVector", "[config]") { | @@ -1014,16 +1014,32 @@ TEST_CASE_METHOD(TApp, "TOMLStringVector", "[config]") { | ||
| 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, "IniVectorCsep", "[config]") { | @@ -1038,16 +1054,25 @@ TEST_CASE_METHOD(TApp, "IniVectorCsep", "[config]") { | ||
| 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 | } |