diff --git a/README.md b/README.md index 64b9236..0a89e96 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ * [Deciding if the value was given by the user](#deciding-if-the-value-was-given-by-the-user) * [Joining values of repeated optional arguments](#joining-values-of-repeated-optional-arguments) * [Repeating an argument to increase a value](#repeating-an-argument-to-increase-a-value) + * [Mutually Exclusive Group](#mutually-exclusive-group) * [Negative Numbers](#negative-numbers) * [Combining Positional and Optional Arguments](#combining-positional-and-optional-arguments) * [Printing Help](#printing-help) @@ -280,6 +281,38 @@ program.parse_args(argc, argv); // Example: ./main -VVVV std::cout << "verbose level: " << verbosity << std::endl; // verbose level: 4 ``` +#### Mutually Exclusive Group + +Create a mutually exclusive group using `program.add_mutually_exclusive_group(required = false)`. `argparse`` will make sure that only one of the arguments in the mutually exclusive group was present on the command line: + +```cpp +auto &group = program.add_mutually_exclusive_group(); +group.add_argument("--first"); +group.add_argument("--second"); +``` + +with the following usage will yield an error: + +```console +foo@bar:/home/dev/$ ./main --first 1 --second 2 +Argument '--second VAR' not allowed with '--first VAR' +``` + +The `add_mutually_exclusive_group()` function also accepts a `required` argument, to indicate that at least one of the mutually exclusive arguments is required: + +```cpp +auto &group = program.add_mutually_exclusive_group(true); +group.add_argument("--first"); +group.add_argument("--second"); +``` + +with the following usage will yield an error: + +```console +foo@bar:/home/dev/$ ./main +One of the arguments '--first VAR' or '--second VAR' is required +``` + ### Negative Numbers Optional arguments start with ```-```. Can ```argparse``` handle negative numbers? The answer is yes! diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index fa31ef0..bf8de35 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -453,12 +453,13 @@ template struct IsChoiceTypeSupported { }; template -int get_levenshtein_distance(const StringType &s1, const StringType &s2) { - std::vector> dp(s1.size() + 1, - std::vector(s2.size() + 1, 0)); +std::size_t get_levenshtein_distance(const StringType &s1, + const StringType &s2) { + std::vector> dp( + s1.size() + 1, std::vector(s2.size() + 1, 0)); - for (int i = 0; i <= s1.size(); ++i) { - for (int j = 0; j <= s2.size(); ++j) { + for (std::size_t i = 0; i <= s1.size(); ++i) { + for (std::size_t j = 0; j <= s2.size(); ++j) { if (i == 0) { dp[i][j] = j; } else if (j == 0) { @@ -479,10 +480,10 @@ std::string_view get_most_similar_string(const std::map &map, const std::string_view input) { std::string_view most_similar{}; - int min_distance = std::numeric_limits::max(); + std::size_t min_distance = std::numeric_limits::max(); for (const auto &entry : map) { - int distance = get_levenshtein_distance(entry.first, input); + std::size_t distance = get_levenshtein_distance(entry.first, input); if (distance < min_distance) { min_distance = distance; most_similar = entry.first; @@ -1418,6 +1419,19 @@ public: m_subparser_map.insert_or_assign(it->get().m_program_name, it); m_subparser_used.insert_or_assign(it->get().m_program_name, false); } + + for (const auto &g : other.m_mutually_exclusive_groups) { + MutuallyExclusiveGroup group(*this, g.m_required); + for (const auto &arg : g.m_elements) { + // Find argument in argument map and add reference to it + // in new group + // argument_it = other.m_argument_map.find("name") + auto first_name = arg->m_names[0]; + auto it = m_argument_map.find(first_name); + group.m_elements.push_back(&(*it->second)); + } + m_mutually_exclusive_groups.push_back(std::move(group)); + } } ~ArgumentParser() = default; @@ -1455,6 +1469,43 @@ public: return *argument; } + class MutuallyExclusiveGroup { + friend class ArgumentParser; + + public: + MutuallyExclusiveGroup() = delete; + + explicit MutuallyExclusiveGroup(ArgumentParser &parent, + bool required = false) + : m_parent(parent), m_required(required), m_elements({}) {} + + MutuallyExclusiveGroup(const MutuallyExclusiveGroup &other) = delete; + MutuallyExclusiveGroup & + operator=(const MutuallyExclusiveGroup &other) = delete; + + MutuallyExclusiveGroup(MutuallyExclusiveGroup &&other) noexcept + : m_parent(other.m_parent), m_required(other.m_required), + m_elements(std::move(other.m_elements)) { + other.m_elements.clear(); + } + + template Argument &add_argument(Targs... f_args) { + auto &argument = m_parent.add_argument(std::forward(f_args)...); + m_elements.push_back(&argument); + return argument; + } + + private: + ArgumentParser &m_parent; + bool m_required{false}; + std::vector m_elements{}; + }; + + MutuallyExclusiveGroup &add_mutually_exclusive_group(bool required = false) { + m_mutually_exclusive_groups.emplace_back(*this, required); + return m_mutually_exclusive_groups.back(); + } + // Parameter packed add_parents method // Accepts a variadic number of ArgumentParser objects template @@ -1520,6 +1571,43 @@ public: for ([[maybe_unused]] const auto &[unused, argument] : m_argument_map) { argument->validate(); } + + // Check each mutually exclusive group and make sure + // there are no constraint violations + for (const auto &group : m_mutually_exclusive_groups) { + auto mutex_argument_used{false}; + Argument *mutex_argument_it{nullptr}; + for (Argument *arg : group.m_elements) { + if (!mutex_argument_used && arg->m_is_used) { + mutex_argument_used = true; + mutex_argument_it = arg; + } else if (mutex_argument_used && arg->m_is_used) { + // Violation + throw std::runtime_error("Argument '" + arg->get_usage_full() + + "' not allowed with '" + + mutex_argument_it->get_usage_full() + "'"); + } + } + + if (!mutex_argument_used && group.m_required) { + // at least one argument from the group is + // required + std::string argument_names{}; + std::size_t i = 0; + std::size_t size = group.m_elements.size(); + for (Argument *arg : group.m_elements) { + if (i + 1 == size) { + // last + argument_names += "'" + arg->get_usage_full() + "' "; + } else { + argument_names += "'" + arg->get_usage_full() + "' or "; + } + i += 1; + } + throw std::runtime_error("One of the arguments " + argument_names + + "is required"); + } + } } /* Call parse_known_args_internal - which does all the work @@ -2006,6 +2094,7 @@ private: } using argument_it = std::list::iterator; + using mutex_group_it = std::vector::iterator; using argument_parser_it = std::list>::iterator; @@ -2030,6 +2119,7 @@ private: std::list> m_subparsers; std::map m_subparser_map; std::map m_subparser_used; + std::vector m_mutually_exclusive_groups; }; } // namespace argparse diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 718e14a..8e942be 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -41,6 +41,7 @@ file(GLOB ARGPARSE_TEST_SOURCES test_invalid_arguments.cpp test_is_used.cpp test_issue_37.cpp + test_mutually_exclusive_group.cpp test_negative_numbers.cpp test_optional_arguments.cpp test_parent_parsers.cpp diff --git a/test/test_mutually_exclusive_group.cpp b/test/test_mutually_exclusive_group.cpp new file mode 100644 index 0000000..93d6215 --- /dev/null +++ b/test/test_mutually_exclusive_group.cpp @@ -0,0 +1,102 @@ +#ifdef WITH_MODULE +import argparse; +#else +#include +#endif +#include + +using doctest::test_suite; + +TEST_CASE("Create mutually exclusive group with 2 arguments" * + test_suite("mutex_args")) { + argparse::ArgumentParser program("test"); + + auto &group = program.add_mutually_exclusive_group(); + group.add_argument("--first"); + group.add_argument("--second"); + + REQUIRE_THROWS_WITH_AS( + program.parse_args({"test", "--first", "1", "--second", "2"}), + "Argument '--second VAR' not allowed with '--first VAR'", + std::runtime_error); +} + +TEST_CASE( + "Create mutually exclusive group with 2 arguments with required flag" * + test_suite("mutex_args")) { + argparse::ArgumentParser program("test"); + + auto &group = program.add_mutually_exclusive_group(true); + group.add_argument("--first"); + group.add_argument("--second"); + + REQUIRE_THROWS_WITH_AS( + program.parse_args({"test"}), + "One of the arguments '--first VAR' or '--second VAR' is required", + std::runtime_error); +} + +TEST_CASE( + "Create mutually exclusive group with 3 arguments with required flag" * + test_suite("mutex_args")) { + argparse::ArgumentParser program("test"); + + auto &group = program.add_mutually_exclusive_group(true); + group.add_argument("--first"); + group.add_argument("--second"); + group.add_argument("--third"); + + REQUIRE_THROWS_WITH_AS(program.parse_args({"test"}), + "One of the arguments '--first VAR' or '--second VAR' " + "or '--third VAR' is required", + std::runtime_error); +} + +TEST_CASE( + "Create mutually exclusive group with 2 arguments, then copy the parser" * + test_suite("mutex_args")) { + argparse::ArgumentParser program("test"); + + auto &group = program.add_mutually_exclusive_group(); + group.add_argument("--first"); + group.add_argument("--second"); + + auto program_copy(program); + + REQUIRE_THROWS_WITH_AS( + program_copy.parse_args({"test", "--first", "1", "--second", "2"}), + "Argument '--second VAR' not allowed with '--first VAR'", + std::runtime_error); +} + +TEST_CASE("Create mutually exclusive group with 3 arguments" * + test_suite("mutex_args")) { + argparse::ArgumentParser program("test"); + + auto &group = program.add_mutually_exclusive_group(); + group.add_argument("--first"); + group.add_argument("--second"); + group.add_argument("--third"); + + REQUIRE_THROWS_WITH_AS( + program.parse_args({"test", "--first", "1", "--third", "2"}), + "Argument '--third VAR' not allowed with '--first VAR'", + std::runtime_error); +} + +TEST_CASE("Create two mutually exclusive groups" * test_suite("mutex_args")) { + argparse::ArgumentParser program("test"); + + auto &group_1 = program.add_mutually_exclusive_group(); + group_1.add_argument("--first"); + group_1.add_argument("--second"); + group_1.add_argument("--third"); + + auto &group_2 = program.add_mutually_exclusive_group(); + group_2.add_argument("-a"); + group_2.add_argument("-b"); + + REQUIRE_THROWS_WITH_AS( + program.parse_args({"test", "--first", "1", "-a", "2", "-b", "3"}), + "Argument '-b VAR' not allowed with '-a VAR'", std::runtime_error); +} \ No newline at end of file