diff --git a/CHANGELOG.md b/CHANGELOG.md index ea9cc7bed..d5dbb6316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ ## Version 1.1 (in progress) + +* Added simple support for enumerations, allow non-printable objects [#12](https://github.com/CLIUtils/CLI11/issues/12) * Added `app.parse_order()` with original parse order ([#13](https://github.com/CLIUtils/CLI11/issues/13), [#16](https://github.com/CLIUtils/CLI11/pull/16)) * Added `prefix_command()`, which is like `allow_extras` but instantly stops and returns. ([#8](https://github.com/CLIUtils/CLI11/issues/8), [#17](https://github.com/CLIUtils/CLI11/pull/17)) * Removed Windows error ([#10](https://github.com/CLIUtils/CLI11/issues/10), [#20](https://github.com/CLIUtils/CLI11/pull/20)) * Some improvements to CMake, detect Python and no dependencies on Python 2 (like Python 3) ([#18](https://github.com/CLIUtils/CLI11/issues/18), [#21](https://github.com/CLIUtils/CLI11/pull/21)) - ## Version 1.0 * Cleanup using `clang-tidy` and `clang-format` * Small improvements to Timers, easier to subclass Error diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 1b6f2abc5..68124dbcc 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -19,3 +19,4 @@ add_cli_exe(subcommands subcommands.cpp) add_cli_exe(groups groups.cpp) add_cli_exe(inter_argument_order inter_argument_order.cpp) add_cli_exe(prefix_command prefix_command.cpp) +add_cli_exe(enum enum.cpp) diff --git a/examples/enum.cpp b/examples/enum.cpp new file mode 100644 index 000000000..ce97db393 --- /dev/null +++ b/examples/enum.cpp @@ -0,0 +1,23 @@ +#include + +enum Level : std::int32_t { + High, + Medium, + Low +}; + +int main(int argc, char** argv) { + CLI::App app; + + Level level; + app.add_set("-l,--level", level, {High, Medium, Low}, "Level settings") + ->set_type_name("enum/Level in {High=0, Medium=1, Low=2}"); + + try { + app.parse(argc, argv); + } catch (CLI::Error const& e) { + app.exit(e); + } + return 0; +} + diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 594b607d3..02863cf15 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -225,13 +225,14 @@ class App { } else throw OptionAlreadyAdded(myopt.get_name()); } + + - /// Add option for non-vectors + /// Add option for non-vectors (duplicate copy needed without defaulted to avoid `iostream << value`) template ::value, detail::enabler> = detail::dummy> Option *add_option(std::string name, T &variable, ///< The variable to set - std::string description = "", - bool defaulted = false) { + std::string description = "") { CLI::callback_t fun = [&variable](CLI::results_t res) { if(res.size() != 1) @@ -239,6 +240,24 @@ class App { return detail::lexical_cast(res[0], variable); }; + Option *opt = add_option(name, fun, description, false); + opt->set_custom_option(detail::type_name()); + return opt; + } + + /// Add option for non-vectors with a default print + template ::value, detail::enabler> = detail::dummy> + Option *add_option(std::string name, + T &variable, ///< The variable to set + std::string description, + bool defaulted) { + + CLI::callback_t fun = [&variable](CLI::results_t res) { + if(res.size() != 1) + return false; + return detail::lexical_cast(res[0], variable); + }; + Option *opt = add_option(name, fun, description, defaulted); opt->set_custom_option(detail::type_name()); if(defaulted) { @@ -248,13 +267,34 @@ class App { } return opt; } - + + /// Add option for vectors (no default) + template + Option *add_option(std::string name, + std::vector &variable, ///< The variable vector to set + std::string description = "") { + + CLI::callback_t fun = [&variable](CLI::results_t res) { + bool retval = true; + variable.clear(); + for(const auto &a : res) { + variable.emplace_back(); + retval &= detail::lexical_cast(a, variable.back()); + } + return (!variable.empty()) && retval; + }; + + Option *opt = add_option(name, fun, description, false); + opt->set_custom_option(detail::type_name(), -1, true); + return opt; + } + /// Add option for vectors template Option *add_option(std::string name, std::vector &variable, ///< The variable vector to set - std::string description = "", - bool defaulted = false) { + std::string description, + bool defaulted) { CLI::callback_t fun = [&variable](CLI::results_t res) { bool retval = true; @@ -323,13 +363,12 @@ class App { return opt; } - /// Add set of options + /// Add set of options (No default) template Option *add_set(std::string name, T &member, ///< The selected member of the set std::set options, ///< The set of posibilities - std::string description = "", - bool defaulted = false) { + std::string description = "") { CLI::callback_t fun = [&member, options](CLI::results_t res) { if(res.size() != 1) { @@ -341,6 +380,31 @@ class App { 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 + template + Option *add_set(std::string name, + T &member, ///< The selected member of the set + std::set options, ///< The set of posibilities + std::string description, + bool defaulted) { + + CLI::callback_t fun = [&member, options](CLI::results_t res) { + if(res.size() != 1) { + return false; + } + bool retval = detail::lexical_cast(res[0], member); + if(!retval) + return false; + 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) + "}"; @@ -353,12 +417,11 @@ class App { return opt; } - /// Add set of options, string only, ignore case + /// Add set of options, string only, ignore case (no default) 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 description = "", - bool defaulted = false) { + std::string description = "") { CLI::callback_t fun = [&member, options](CLI::results_t res) { if(res.size() != 1) { @@ -376,6 +439,37 @@ class App { } }; + 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 + 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 description, + bool defaulted) { + + CLI::callback_t fun = [&member, options](CLI::results_t res) { + if(res.size() != 1) { + return false; + } + 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)) + return false; + else { + member = *iter; + return true; + } + }; + Option *opt = add_option(name, fun, description, defaulted); std::string typeval = detail::type_name(); typeval += " in {" + detail::join(options) + "}"; diff --git a/include/CLI/TypeTools.hpp b/include/CLI/TypeTools.hpp index 475e2fa22..e43d1e2c4 100644 --- a/include/CLI/TypeTools.hpp +++ b/include/CLI/TypeTools.hpp @@ -74,8 +74,10 @@ constexpr const char *type_name() { // Lexical cast -/// Integers -template ::value, detail::enabler> = detail::dummy> +/// Integers / enums +template ::value + || std::is_enum::value + , detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { try { output = static_cast(std::stoll(input)); @@ -103,7 +105,9 @@ bool lexical_cast(std::string input, T &output) { /// String and similar template < typename T, - enable_if_t::value && !std::is_integral::value, detail::enabler> = detail::dummy> + enable_if_t::value + && !std::is_integral::value + && !std::is_enum::value, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { output = input; return true; diff --git a/tests/AppTest.cpp b/tests/AppTest.cpp index 75d3a452e..dd9a36847 100644 --- a/tests/AppTest.cpp +++ b/tests/AppTest.cpp @@ -203,6 +203,35 @@ TEST_F(TApp, DefaultOpts) { EXPECT_EQ("9", s); } +TEST_F(TApp, EnumTest) { + enum Level : std::int32_t { + High, + Medium, + Low + }; + Level level = Level::Low; + app.add_option("--level", level); + + args = {"--level", "1"}; + run(); + EXPECT_EQ(level, Level::Medium); +} + +TEST_F(TApp, NewEnumTest) { + enum class Level2 : std::int32_t { + High, + Medium, + Low + }; + Level2 level = Level2::Low; + app.add_option("--level", level); + + args = {"--level", "1"}; + run(); + EXPECT_EQ(level, Level2::Medium); +} + + TEST_F(TApp, RequiredFlags) { app.add_flag("-a")->required(); app.add_flag("-b")->mandatory(); // Alternate term @@ -391,6 +420,47 @@ TEST_F(TApp, InSet) { EXPECT_THROW(run(), CLI::ConversionError); } +TEST_F(TApp, InSetWithDefault) { + + std::string choice = "one"; + app.add_set("-q,--quick", choice, {"one", "two", "three"}, "", true); + + run(); + EXPECT_EQ("one", choice); + app.reset(); + + args = {"--quick", "two"}; + + run(); + EXPECT_EQ("two", choice); + + app.reset(); + + args = {"--quick", "four"}; + EXPECT_THROW(run(), CLI::ConversionError); +} + + +TEST_F(TApp, InCaselessSetWithDefault) { + + std::string choice = "one"; + app.add_set_ignore_case("-q,--quick", choice, {"one", "two", "three"}, "", true); + + run(); + EXPECT_EQ("one", choice); + app.reset(); + + args = {"--quick", "tWo"}; + + run(); + EXPECT_EQ("two", choice); + + app.reset(); + + args = {"--quick", "four"}; + EXPECT_THROW(run(), CLI::ConversionError); +} + TEST_F(TApp, InIntSet) { int choice; @@ -462,6 +532,20 @@ TEST_F(TApp, VectorFixedString) { EXPECT_EQ(answer, strvec); } + +TEST_F(TApp, VectorDefaultedFixedString) { + std::vector strvec{"one"}; + std::vector answer{"mystring", "mystring2", "mystring3"}; + + CLI::Option *opt = app.add_option("-s,--string", strvec, "", true)->expected(3); + EXPECT_EQ(3, opt->get_expected()); + + args = {"--string", "mystring", "mystring2", "mystring3"}; + run(); + EXPECT_EQ((size_t)3, app.count("--string")); + EXPECT_EQ(answer, strvec); +} + TEST_F(TApp, VectorUnlimString) { std::vector strvec; std::vector answer{"mystring", "mystring2", "mystring3"}; @@ -808,3 +892,43 @@ TEST_F(TApp, CheckSubcomFail) { EXPECT_THROW(CLI::detail::AppFriend::parse_subcommand(&app, args), CLI::HorribleError); } + +// Added to test defaults on dual method +TEST_F(TApp, OptionWithDefaults) { + int someint=2; + app.add_option("-a", someint, "", true); + + args = {"-a1", "-a2"}; + + EXPECT_THROW(run(), CLI::ConversionError); +} + +// Added to test defaults on dual method +TEST_F(TApp, SetWithDefaults) { + int someint=2; + app.add_set("-a", someint, {1,2,3,4}, "", true); + + args = {"-a1", "-a2"}; + + EXPECT_THROW(run(), CLI::ConversionError); +} + +// Added to test defaults on dual method +TEST_F(TApp, SetWithDefaultsConversion) { + int someint=2; + app.add_set("-a", someint, {1,2,3,4}, "", true); + + args = {"-a", "hi"}; + + EXPECT_THROW(run(), CLI::ConversionError); +} + +// Added to test defaults on dual method +TEST_F(TApp, SetWithDefaultsIC) { + std::string someint="ho"; + app.add_set_ignore_case("-a", someint, {"Hi", "Ho"}, "", true); + + args = {"-aHi", "-aHo"}; + + EXPECT_THROW(run(), CLI::ConversionError); +}