diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index 588666c..f54d4c3 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -428,7 +428,7 @@ public: } template - auto action(F &&callable, Args &&... bound_args) + auto action(F &&callable, Args &&...bound_args) -> std::enable_if_t, Argument &> { using action_type = std::conditional_t< @@ -509,10 +509,12 @@ public: m_num_args_range = NArgsRange{0, 1}; break; case nargs_pattern::any: - m_num_args_range = NArgsRange{0, (std::numeric_limits::max)()}; + m_num_args_range = + NArgsRange{0, (std::numeric_limits::max)()}; break; case nargs_pattern::at_least_one: - m_num_args_range = NArgsRange{1, (std::numeric_limits::max)()}; + m_num_args_range = + NArgsRange{1, (std::numeric_limits::max)()}; break; } return *this; @@ -523,6 +525,80 @@ public: return nargs(nargs_pattern::any); } + void add_choice(const std::string &choice) { + if (!m_choices.has_value()) { + /// create it + m_choices = std::vector{}; + } + m_choices.value().push_back(choice); + } + + Argument &choices() { + if (!m_choices.has_value()) { + throw std::runtime_error("Zero choices provided"); + } + return *this; + } + + template + Argument &choices(const std::string &first, T &...rest) { + add_choice(first); + choices(rest...); + + return *this; + } + + template Argument &choices(const char *first, T &...rest) { + add_choice(first); + choices(rest...); + + return *this; + } + + void find_default_value_in_choices_or_throw() const { + + const auto &choices = m_choices.value(); + + if (m_default_value.has_value()) { + if (std::find(choices.begin(), choices.end(), m_default_value_repr) == + choices.end()) { + // provided arg not in list of allowed choices + // report error + + std::string choices_as_csv = + std::accumulate(choices.begin(), choices.end(), std::string(), + [](const std::string &a, const std::string &b) { + return a + (a.empty() ? "" : ", ") + b; + }); + + throw std::runtime_error( + std::string{"Invalid default value "} + m_default_value_repr + + " - allowed options: {" + choices_as_csv + "}"); + } + } + } + + template + void find_value_in_choices_or_throw(Iterator it) const { + + const auto &choices = m_choices.value(); + + if (std::find(choices.begin(), choices.end(), *it) == choices.end()) { + // provided arg not in list of allowed choices + // report error + + std::string choices_as_csv = + std::accumulate(choices.begin(), choices.end(), std::string(), + [](const std::string &a, const std::string &b) { + return a + (a.empty() ? "" : ", ") + b; + }); + + throw std::runtime_error(std::string{"Invalid argument "} + + details::repr(*it) + " - allowed options: {" + + choices_as_csv + "}"); + } + } + template Iterator consume(Iterator start, Iterator end, std::string_view used_name = {}) { @@ -532,6 +608,14 @@ public: m_is_used = true; m_used_name = used_name; + if (m_choices.has_value()) { + // Check each value in (start, end) and make sure + // it is in the list of allowed choices/options + for (auto it = start; it != end; ++it) { + find_value_in_choices_or_throw(it); + } + } + const auto num_args_max = m_num_args_range.get_max(); const auto num_args_min = m_num_args_range.get_min(); std::size_t dist = 0; @@ -602,6 +686,12 @@ public: throw_nargs_range_validation_error(); } } + + if (m_choices.has_value()) { + // Make sure the default value (if provided) + // is in the list of choices + find_default_value_in_choices_or_throw(); + } } std::string get_inline_usage() const { @@ -738,8 +828,7 @@ public: using ValueType = typename T::value_type; auto lhs = get(); return std::equal(std::begin(lhs), std::end(lhs), std::begin(rhs), - std::end(rhs), - [](const auto &a, const auto &b) { + std::end(rhs), [](const auto &a, const auto &b) { return std::any_cast(a) == b; }); } @@ -1064,6 +1153,7 @@ private: std::any m_default_value; std::string m_default_value_repr; std::any m_implicit_value; + std::optional> m_choices{std::nullopt}; using valued_action = std::function; using void_action = std::function; std::variant m_action{ @@ -1152,16 +1242,11 @@ public: } explicit operator bool() const { - auto arg_used = std::any_of(m_argument_map.cbegin(), - m_argument_map.cend(), - [](auto &it) { - return it.second->m_is_used; - }); - auto subparser_used = std::any_of(m_subparser_used.cbegin(), - m_subparser_used.cend(), - [](auto &it) { - return it.second; - }); + auto arg_used = std::any_of(m_argument_map.cbegin(), m_argument_map.cend(), + [](auto &it) { return it.second->m_is_used; }); + auto subparser_used = + std::any_of(m_subparser_used.cbegin(), m_subparser_used.cend(), + [](auto &it) { return it.second; }); return m_is_parsed && (arg_used || subparser_used); } @@ -1186,7 +1271,7 @@ public: // Parameter packed add_parents method // Accepts a variadic number of ArgumentParser objects template - ArgumentParser &add_parents(const Targs &... f_args) { + ArgumentParser &add_parents(const Targs &...f_args) { for (const ArgumentParser &parent_parser : {std::ref(f_args)...}) { for (const auto &argument : parent_parser.m_positional_arguments) { auto it = m_positional_arguments.insert( @@ -1215,8 +1300,7 @@ public: /* Getter for arguments and subparsers. * @throws std::logic_error in case of an invalid argument or subparser name */ - template - T& at(std::string_view name) { + template T &at(std::string_view name) { if constexpr (std::is_same_v) { return (*this)[name]; } else { @@ -1692,7 +1776,8 @@ private: } std::size_t max_size = 0; for ([[maybe_unused]] const auto &[unused, argument] : m_argument_map) { - max_size = std::max(max_size, argument->get_arguments_length()); + max_size = + std::max(max_size, argument->get_arguments_length()); } for ([[maybe_unused]] const auto &[command, unused] : m_subparser_map) { max_size = std::max(max_size, command.size()); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 2be4acc..87104e9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -29,6 +29,7 @@ file(GLOB ARGPARSE_TEST_SOURCES test_append.cpp test_as_container.cpp test_bool_operator.cpp + test_choices.cpp test_compound_arguments.cpp test_container_arguments.cpp test_const_correct.cpp diff --git a/test/test_append.cpp b/test/test_append.cpp index 0114f71..8a68c65 100644 --- a/test/test_append.cpp +++ b/test/test_append.cpp @@ -5,8 +5,8 @@ import argparse; #endif #include -#include #include +#include using doctest::test_suite; diff --git a/test/test_as_container.cpp b/test/test_as_container.cpp index fb6e87c..37f725e 100644 --- a/test/test_as_container.cpp +++ b/test/test_as_container.cpp @@ -28,8 +28,8 @@ TEST_CASE("Get argument with .at()" * test_suite("as_container")) { SUBCASE("with unknown argument") { program.parse_args({"test"}); - REQUIRE_THROWS_WITH_AS(program.at("--folder"), - "No such argument: --folder", std::logic_error); + REQUIRE_THROWS_WITH_AS(program.at("--folder"), "No such argument: --folder", + std::logic_error); } } @@ -44,7 +44,8 @@ TEST_CASE("Get subparser with .at()" * test_suite("as_container")) { SUBCASE("and its argument") { program.parse_args({"test", "walk", "4km/h"}); REQUIRE(&(program.at("walk")) == &walk_cmd); - REQUIRE(&(program.at("walk").at("speed")) == &speed); + REQUIRE(&(program.at("walk").at("speed")) == + &speed); REQUIRE(program.at("walk").is_used("speed")); } diff --git a/test/test_bool_operator.cpp b/test/test_bool_operator.cpp index aada150..a34b5d6 100644 --- a/test/test_bool_operator.cpp +++ b/test/test_bool_operator.cpp @@ -8,8 +8,7 @@ import argparse; using doctest::test_suite; -TEST_CASE("ArgumentParser in bool context" * - test_suite("argument_parser")) { +TEST_CASE("ArgumentParser in bool context" * test_suite("argument_parser")) { argparse::ArgumentParser program("test"); program.add_argument("cases").remaining(); @@ -39,7 +38,7 @@ TEST_CASE("With subparsers in bool context" * test_suite("argument_parser")) { } TEST_CASE("Parsers remain false with unknown arguments" * - test_suite("argument_parser")) { + test_suite("argument_parser")) { argparse::ArgumentParser program("test"); argparse::ArgumentParser cmd_build("build"); @@ -59,7 +58,7 @@ TEST_CASE("Parsers remain false with unknown arguments" * } TEST_CASE("Multi-level parsers match subparser bool" * - test_suite("argument_parser")) { + test_suite("argument_parser")) { argparse::ArgumentParser program("test"); argparse::ArgumentParser cmd_cook("cook"); diff --git a/test/test_choices.cpp b/test/test_choices.cpp new file mode 100644 index 0000000..9c17d23 --- /dev/null +++ b/test/test_choices.cpp @@ -0,0 +1,70 @@ +#ifdef WITH_MODULE +import argparse; +#else +#include +#endif + +#include + +using doctest::test_suite; + +TEST_CASE("Parse argument that is provided zero choices" * + test_suite("choices")) { + argparse::ArgumentParser program("test"); + REQUIRE_THROWS_WITH_AS(program.add_argument("color").choices(), + "Zero choices provided", std::runtime_error); +} + +TEST_CASE("Parse argument that is in the fixed number of allowed choices" * + test_suite("choices")) { + argparse::ArgumentParser program("test"); + program.add_argument("color").choices("red", "green", "blue"); + + program.parse_args({"test", "red"}); +} + +TEST_CASE("Parse argument that is in the fixed number of allowed choices, with " + "invalid default" * + test_suite("choices")) { + argparse::ArgumentParser program("test"); + program.add_argument("color").default_value("yellow").choices("red", "green", + "blue"); + + REQUIRE_THROWS_WITH_AS( + program.parse_args({"test"}), + "Invalid default value \"yellow\" - allowed options: {red, green, blue}", + std::runtime_error); +} + +TEST_CASE("Parse invalid argument that is not in the fixed number of allowed " + "choices" * + test_suite("choices")) { + argparse::ArgumentParser program("test"); + program.add_argument("color").choices("red", "green", "blue"); + + REQUIRE_THROWS_WITH_AS( + program.parse_args({"test", "red2"}), + "Invalid argument \"red2\" - allowed options: {red, green, blue}", + std::runtime_error); +} + +TEST_CASE( + "Parse multiple arguments that are in the fixed number of allowed choices" * + test_suite("choices")) { + argparse::ArgumentParser program("test"); + program.add_argument("color").nargs(2).choices("red", "green", "blue"); + + program.parse_args({"test", "red", "green"}); +} + +TEST_CASE("Parse multiple arguments one of which is not in the fixed number of " + "allowed choices" * + test_suite("choices")) { + argparse::ArgumentParser program("test"); + program.add_argument("color").nargs(2).choices("red", "green", "blue"); + + REQUIRE_THROWS_WITH_AS( + program.parse_args({"test", "red", "green2"}), + "Invalid argument \"green2\" - allowed options: {red, green, blue}", + std::runtime_error); +} \ No newline at end of file diff --git a/test/test_default_args.cpp b/test/test_default_args.cpp index 61fbabc..4b813fb 100644 --- a/test/test_default_args.cpp +++ b/test/test_default_args.cpp @@ -5,9 +5,9 @@ import argparse; #endif #include +#include #include #include -#include using doctest::test_suite; @@ -30,7 +30,7 @@ TEST_CASE("Do not exit on default arguments" * test_suite("default_args")) { argparse::ArgumentParser parser("test", "1.0", argparse::default_arguments::all, false); std::stringstream buf; - std::streambuf* saved_cout_buf = std::cout.rdbuf(buf.rdbuf()); + std::streambuf *saved_cout_buf = std::cout.rdbuf(buf.rdbuf()); parser.parse_args({"test", "--help"}); std::cout.rdbuf(saved_cout_buf); REQUIRE(parser.is_used("--help")); diff --git a/test/test_equals_form.cpp b/test/test_equals_form.cpp index 6d95c3e..a4b89a4 100644 --- a/test/test_equals_form.cpp +++ b/test/test_equals_form.cpp @@ -6,8 +6,8 @@ import argparse; #include #include -#include #include +#include using doctest::test_suite; diff --git a/test/test_get.cpp b/test/test_get.cpp index d2144a1..13db01c 100644 --- a/test/test_get.cpp +++ b/test/test_get.cpp @@ -42,7 +42,7 @@ TEST_CASE("Implicit argument" * test_suite("ArgumentParser::get")) { TEST_CASE("Mismatched type for argument" * test_suite("ArgumentParser::get")) { argparse::ArgumentParser program("test"); - program.add_argument("-s", "--stuff"); // as default type, a std::string + program.add_argument("-s", "--stuff"); // as default type, a std::string REQUIRE_NOTHROW(program.parse_args({"test", "-s", "321"})); REQUIRE_THROWS_AS(program.get("--stuff"), std::bad_any_cast); } diff --git a/test/test_help.cpp b/test/test_help.cpp index ec5b6bd..db4f49f 100644 --- a/test/test_help.cpp +++ b/test/test_help.cpp @@ -5,8 +5,8 @@ import argparse; #endif #include -#include #include +#include using doctest::test_suite; @@ -82,22 +82,19 @@ TEST_CASE("Users can replace default -h/--help" * test_suite("help")) { TEST_CASE("Multiline help message alignment") { // '#' is used at the beginning of each help message line to simplify testing. - // It is important to ensure that this character doesn't appear elsewhere in the test case. - // Default arguments (e.g., -h/--help, -v/--version) are not included in this test. + // It is important to ensure that this character doesn't appear elsewhere in + // the test case. Default arguments (e.g., -h/--help, -v/--version) are not + // included in this test. argparse::ArgumentParser program("program"); - program.add_argument("INPUT1") - .help( - "#This is the first line of help message.\n" - "#And this is the second line of help message." - ); - program.add_argument("program_input2") - .help("#There is only one line."); + program.add_argument("INPUT1").help( + "#This is the first line of help message.\n" + "#And this is the second line of help message."); + program.add_argument("program_input2").help("#There is only one line."); program.add_argument("-p", "--prog_input3") .help( -R"(#Lorem ipsum dolor sit amet, consectetur adipiscing elit. + R"(#Lorem ipsum dolor sit amet, consectetur adipiscing elit. #Sed ut perspiciatis unde omnis iste natus error sit voluptatem -#accusantium doloremque laudantium, totam rem aperiam...)" - ); +#accusantium doloremque laudantium, totam rem aperiam...)"); program.add_argument("--verbose").default_value(false).implicit_value(true); std::ostringstream stream; @@ -107,7 +104,8 @@ R"(#Lorem ipsum dolor sit amet, consectetur adipiscing elit. auto help_message_start = std::string::npos; std::string line; while (std::getline(iss, line)) { - // Find the position of '#', which indicates the start of the help message line + // Find the position of '#', which indicates the start of the help message + // line auto pos = line.find('#'); if (pos == std::string::npos) { diff --git a/test/test_parse_known_args.cpp b/test/test_parse_known_args.cpp index d5a939c..909621f 100644 --- a/test/test_parse_known_args.cpp +++ b/test/test_parse_known_args.cpp @@ -5,8 +5,8 @@ import argparse; #endif #include -#include #include +#include using doctest::test_suite; diff --git a/test/test_repr.cpp b/test/test_repr.cpp index f7330bc..d924a72 100644 --- a/test/test_repr.cpp +++ b/test/test_repr.cpp @@ -6,8 +6,8 @@ import argparse.details; #endif #include -#include #include +#include #include using doctest::test_suite; diff --git a/test/test_subparsers.cpp b/test/test_subparsers.cpp index 9e7c29f..7f261b5 100644 --- a/test/test_subparsers.cpp +++ b/test/test_subparsers.cpp @@ -6,8 +6,8 @@ import argparse; #include #include -#include #include +#include using doctest::test_suite; @@ -213,8 +213,8 @@ TEST_CASE("Check is_subcommand_used after parse" * test_suite("subparsers")) { argparse::ArgumentParser command_2("clean"); command_2.add_argument("--fullclean") - .default_value(false) - .implicit_value(true); + .default_value(false) + .implicit_value(true); argparse::ArgumentParser program("test"); program.add_subparser(command_1);