diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a60b69f..2910058a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ### Version 1.6: Formatting -Added a new formatting system. You can now set the formatter on Apps. +Added a new formatting system. [#109] You can now set the formatter on Apps. * Added `CLI::Formatter` and `formatter` slot for apps, inherited. * Added `help_all` support (not added by default) @@ -18,11 +18,18 @@ Changes to the help system (most normal users will not notice this): * `format_help` can now be chained -Other small changes: +Other changes: -* Testing (only) now uses submodules. -* Removed `requires` in favor of `needs` (deprecated in last version) -* Better CMake policy handling +* Using `add_set` will now capture L-values for sets, allowing further modification [#113] +* Testing (only) now uses submodules. [#111] +* Removed `requires` in favor of `needs` (deprecated in last version) [#112] +* Better CMake policy handling [#110] + +[#109]: https://github.com/CLIUtils/CLI11/pull/109 +[#110]: https://github.com/CLIUtils/CLI11/pull/110 +[#111]: https://github.com/CLIUtils/CLI11/pull/111 +[#112]: https://github.com/CLIUtils/CLI11/pull/112 +[#113]: https://github.com/CLIUtils/CLI11/issues/113 ### Version 1.5.3: Compiler compatibility This version fixes older AppleClang compilers by removing the optimization for casting. The minimum version of Boost Optional supported has been clarified to be 1.58. CUDA 7.0 NVCC is now supported. @@ -45,7 +52,7 @@ This patch release adds better access to the App progromatically, to assist with [#102]: https://github.com/CLIUtils/CLI11/issues/102 [#104]: https://github.com/CLIUtils/CLI11/pull/104 -[#105]: https://github.com/CLIUtils/CLI11/issues/105 +[#105]: https://github.com/CLIUtils/CLI11/pull/105 [#106]: https://github.com/CLIUtils/CLI11/pull/106 diff --git a/README.md b/README.md index d96ab976..268e3999 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ app.add_set_ignore_case(... // String only App* subcom = app.add_subcommand(name, discription); ``` -An option name must start with a alphabetic character or underscore. For long options, anything but an equals sign or a comma is valid after that. Names are given as a comma separated string, with the dash or dashes. An option or flag can have as many names as you want, and afterward, using `count`, you can use any of the names, with dashes as needed, to count the options. One of the names is allowed to be given without proceeding dash(es); if present the option is a positional option, and that name will be used on help line for its positional form. If you want the default value to print in the help description, pass in `true` for the final parameter for `add_option` or `add_set`. The set options allow your users to pick from a set of predefined options. +An option name must start with a alphabetic character or underscore. For long options, anything but an equals sign or a comma is valid after that. Names are given as a comma separated string, with the dash or dashes. An option or flag can have as many names as you want, and afterward, using `count`, you can use any of the names, with dashes as needed, to count the options. One of the names is allowed to be given without proceeding dash(es); if present the option is a positional option, and that name will be used on help line for its positional form. If you want the default value to print in the help description, pass in `true` for the final parameter for `add_option` or `add_set`. The set options allow your users to pick from a set of predefined options; you can add an existing set if you need to modify the set later, or you can use an initializer list. On a C++14 compiler, you can pass a callback function directly to `.add_flag`, while in C++11 mode you'll need to use `.add_flag_function` if you want a callback function. The function will be given the number of times the flag was passed. You can throw a relevant `CLI::ParseError` to signal a failure. diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 204f9c3a..bb943218 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -500,11 +500,11 @@ class App { } #endif - /// Add set of options (No default) + /// Add set of options (No default, temp refernce, such as an inline set) template Option *add_set(std::string name, - T &member, ///< The selected member of the set - std::set options, ///< The set of possibilities + T &member, ///< The selected member of the set + const std::set &&options, ///< The set of possibilities std::string description = "") { std::string simple_name = CLI::detail::split(name, ',').at(0); @@ -522,11 +522,33 @@ class App { return opt; } - /// Add set of options + /// Add set of options (No default, non-temp refernce, such as an existing set) template Option *add_set(std::string name, - T &member, ///< The selected member of the set - std::set options, ///< The set of posibilities + T &member, ///< The selected member of the set + const std::set &options, ///< The set of possibilities + std::string description = "") { + + std::string simple_name = CLI::detail::split(name, ',').at(0); + CLI::callback_t fun = [&member, &options, simple_name](CLI::results_t res) { + bool retval = detail::lexical_cast(res[0], member); + if(!retval) + throw ConversionError(res[0], simple_name); + return std::find(std::begin(options), std::end(options), member) != std::end(options); + }; + + Option *opt = add_option(name, fun, description, false); + std::string typeval = detail::type_name(); + typeval += " in {" + detail::join(options) + "}"; + opt->set_custom_option(typeval); + return opt; + } + + /// Add set of options (with default, R value, such as an inline set) + template + Option *add_set(std::string name, + T &member, ///< The selected member of the set + const std::set &&options, ///< The set of posibilities std::string description, bool defaulted) { @@ -550,10 +572,38 @@ class App { return opt; } - /// Add set of options, string only, ignore case (no default) + /// Add set of options (with default, L value refernce, such as an existing set) + template + Option *add_set(std::string name, + T &member, ///< The selected member of the set + const std::set &options, ///< The set of posibilities + std::string description, + bool defaulted) { + + std::string simple_name = CLI::detail::split(name, ',').at(0); + CLI::callback_t fun = [&member, &options, simple_name](CLI::results_t res) { + bool retval = detail::lexical_cast(res[0], member); + if(!retval) + throw ConversionError(res[0], simple_name); + return std::find(std::begin(options), std::end(options), member) != std::end(options); + }; + + Option *opt = add_option(name, fun, description, defaulted); + std::string typeval = detail::type_name(); + typeval += " in {" + detail::join(options) + "}"; + opt->set_custom_option(typeval); + if(defaulted) { + std::stringstream out; + out << member; + opt->set_default_str(out.str()); + } + return opt; + } + + /// Add set of options, string only, ignore case (no default, R value) Option *add_set_ignore_case(std::string name, - std::string &member, ///< The selected member of the set - std::set options, ///< The set of possibilities + std::string &member, ///< The selected member of the set + const std::set &&options, ///< The set of possibilities std::string description = "") { std::string simple_name = CLI::detail::split(name, ',').at(0); @@ -578,10 +628,38 @@ class App { return opt; } - /// Add set of options, string only, ignore case + /// Add set of options, string only, ignore case (no default, L value) Option *add_set_ignore_case(std::string name, - std::string &member, ///< The selected member of the set - std::set options, ///< The set of posibilities + std::string &member, ///< The selected member of the set + const std::set &options, ///< The set of possibilities + std::string description = "") { + + std::string simple_name = CLI::detail::split(name, ',').at(0); + CLI::callback_t fun = [&member, &options, simple_name](CLI::results_t res) { + member = detail::to_lower(res[0]); + auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) { + return detail::to_lower(val) == member; + }); + if(iter == std::end(options)) + throw ConversionError(member, simple_name); + else { + member = *iter; + return true; + } + }; + + Option *opt = add_option(name, fun, description, false); + std::string typeval = detail::type_name(); + typeval += " in {" + detail::join(options) + "}"; + opt->set_custom_option(typeval); + + return opt; + } + + /// Add set of options, string only, ignore case (default, R value) + Option *add_set_ignore_case(std::string name, + std::string &member, ///< The selected member of the set + const std::set &&options, ///< The set of posibilities std::string description, bool defaulted) { @@ -609,6 +687,37 @@ class App { return opt; } + /// Add set of options, string only, ignore case (default, L value) + Option *add_set_ignore_case(std::string name, + std::string &member, ///< The selected member of the set + const std::set &options, ///< The set of posibilities + std::string description, + bool defaulted) { + + std::string simple_name = CLI::detail::split(name, ',').at(0); + CLI::callback_t fun = [&member, &options, simple_name](CLI::results_t res) { + member = detail::to_lower(res[0]); + auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) { + return detail::to_lower(val) == member; + }); + if(iter == std::end(options)) + throw ConversionError(member, simple_name); + else { + member = *iter; + return true; + } + }; + + Option *opt = add_option(name, fun, description, defaulted); + std::string typeval = detail::type_name(); + typeval += " in {" + detail::join(options) + "}"; + opt->set_custom_option(typeval); + if(defaulted) { + opt->set_default_str(member); + } + return opt; + } + /// Add a complex number template Option *add_complex(std::string name, diff --git a/tests/AppTest.cpp b/tests/AppTest.cpp index c7edee3f..ab159718 100644 --- a/tests/AppTest.cpp +++ b/tests/AppTest.cpp @@ -992,6 +992,21 @@ TEST_F(TApp, FailSet) { EXPECT_THROW(run(), CLI::ConversionError); } +TEST_F(TApp, FailLValueSet) { + + int choice; + std::set vals{1, 2, 3}; + app.add_set("-q,--quick", choice, vals); + app.add_set("-s,--slow", choice, vals, "", true); + + args = {"--quick=hello"}; + EXPECT_THROW(run(), CLI::ConversionError); + + app.reset(); + args = {"--slow=hello"}; + EXPECT_THROW(run(), CLI::ConversionError); +} + TEST_F(TApp, InSetIgnoreCase) { std::string choice; @@ -1480,3 +1495,72 @@ TEST_F(TApp, CustomDoubleOption) { EXPECT_EQ(custom_opt.first, 12); EXPECT_DOUBLE_EQ(custom_opt.second, 1.5); } + +// #113 +TEST_F(TApp, AddRemoveSetItems) { + std::set items{"TYPE1", "TYPE2", "TYPE3", "TYPE4", "TYPE5"}; + + std::string type1, type2; + app.add_set("--type1", type1, items); + app.add_set("--type2", type2, items, "", true); + + args = {"--type1", "TYPE1", "--type2", "TYPE2"}; + + run(); + EXPECT_EQ(type1, "TYPE1"); + EXPECT_EQ(type2, "TYPE2"); + + items.insert("TYPE6"); + items.insert("TYPE7"); + + items.erase("TYPE1"); + items.erase("TYPE2"); + + app.reset(); + args = {"--type1", "TYPE6", "--type2", "TYPE7"}; + run(); + EXPECT_EQ(type1, "TYPE6"); + EXPECT_EQ(type2, "TYPE7"); + + app.reset(); + args = {"--type1", "TYPE1"}; + EXPECT_THROW(run(), CLI::ConversionError); + + app.reset(); + args = {"--type2", "TYPE2"}; + EXPECT_THROW(run(), CLI::ConversionError); +} + +TEST_F(TApp, AddRemoveSetItemsNoCase) { + std::set items{"TYPE1", "TYPE2", "TYPE3", "TYPE4", "TYPE5"}; + + std::string type1, type2; + app.add_set_ignore_case("--type1", type1, items); + app.add_set_ignore_case("--type2", type2, items, "", true); + + args = {"--type1", "TYPe1", "--type2", "TyPE2"}; + + run(); + EXPECT_EQ(type1, "TYPE1"); + EXPECT_EQ(type2, "TYPE2"); + + items.insert("TYPE6"); + items.insert("TYPE7"); + + items.erase("TYPE1"); + items.erase("TYPE2"); + + app.reset(); + args = {"--type1", "TyPE6", "--type2", "tYPE7"}; + run(); + EXPECT_EQ(type1, "TYPE6"); + EXPECT_EQ(type2, "TYPE7"); + + app.reset(); + args = {"--type1", "TYPe1"}; + EXPECT_THROW(run(), CLI::ConversionError); + + app.reset(); + args = {"--type2", "TYpE2"}; + EXPECT_THROW(run(), CLI::ConversionError); +}