From 39988ec62d5b22b01c2887d1ee5ee5f60a050368 Mon Sep 17 00:00:00 2001 From: Pranav Srinivas Kumar Date: Sat, 4 Nov 2023 14:57:01 -0500 Subject: [PATCH 1/9] Initial commit for implementing MutuallyExclusiveGroup --- include/argparse/argparse.hpp | 60 ++++++++++++++++++++++++++ test/CMakeLists.txt | 1 + test/test_mutually_exclusive_group.cpp | 21 +++++++++ 3 files changed, 82 insertions(+) create mode 100644 test/test_mutually_exclusive_group.cpp diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index fa31ef0..a66c8ed 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -52,6 +52,7 @@ SOFTWARE. #include #include #include +#include #include #include #include @@ -1455,6 +1456,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.insert(&argument); + return argument; + } + + private: + ArgumentParser &m_parent; + bool m_required{false}; + std::unordered_set 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 +1558,24 @@ 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() + "'"); + } + } + } } /* Call parse_known_args_internal - which does all the work @@ -2006,6 +2062,7 @@ private: } using argument_it = std::list::iterator; + using mutex_group_it = std::vector::iterator; using argument_parser_it = std::list>::iterator; @@ -2030,6 +2087,9 @@ private: std::list> m_subparsers; std::map m_subparser_map; std::map m_subparser_used; + std::vector + m_mutually_exclusive_groups; /// TODO: Add this to the copy/move + /// constructors }; } // 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..e83e4f7 --- /dev/null +++ b/test/test_mutually_exclusive_group.cpp @@ -0,0 +1,21 @@ +#ifdef WITH_MODULE +import argparse; +#else +#include +#endif +#include + +using doctest::test_suite; + +TEST_CASE("User-supplied argument" * test_suite("is_used")) { + 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 '--first VAR' not allowed with '--second VAR'", + std::runtime_error); +} \ No newline at end of file From eea95c0e3a0ebe1c206f7e11fae904838be1ecd8 Mon Sep 17 00:00:00 2001 From: Pranav Srinivas Kumar Date: Sat, 4 Nov 2023 15:19:10 -0500 Subject: [PATCH 2/9] Added mutex args to copy constructor, changed to ordered set for data structure --- include/argparse/argparse.hpp | 21 ++++++++++++++++----- test/test_mutually_exclusive_group.cpp | 22 ++++++++++++++++++++-- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index a66c8ed..d4aec0e 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -46,13 +46,13 @@ SOFTWARE. #include #include #include +#include #include #include #include #include #include #include -#include #include #include #include @@ -1419,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 (auto &g : other.m_mutually_exclusive_groups) { + MutuallyExclusiveGroup group(*this, g.m_required); + for (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.insert(&(*it->second)); + } + m_mutually_exclusive_groups.push_back(std::move(group)); + } } ~ArgumentParser() = default; @@ -1485,7 +1498,7 @@ public: private: ArgumentParser &m_parent; bool m_required{false}; - std::unordered_set m_elements{}; + std::set m_elements{}; }; MutuallyExclusiveGroup &add_mutually_exclusive_group(bool required = false) { @@ -2087,9 +2100,7 @@ private: std::list> m_subparsers; std::map m_subparser_map; std::map m_subparser_used; - std::vector - m_mutually_exclusive_groups; /// TODO: Add this to the copy/move - /// constructors + std::vector m_mutually_exclusive_groups; }; } // namespace argparse diff --git a/test/test_mutually_exclusive_group.cpp b/test/test_mutually_exclusive_group.cpp index e83e4f7..493f58a 100644 --- a/test/test_mutually_exclusive_group.cpp +++ b/test/test_mutually_exclusive_group.cpp @@ -7,7 +7,8 @@ import argparse; using doctest::test_suite; -TEST_CASE("User-supplied argument" * test_suite("is_used")) { +TEST_CASE("Create mutually exclusive group with 2 arguments" * + test_suite("mutex_args")) { argparse::ArgumentParser program("test"); auto &group = program.add_mutually_exclusive_group(); @@ -16,6 +17,23 @@ TEST_CASE("User-supplied argument" * test_suite("is_used")) { REQUIRE_THROWS_WITH_AS( program.parse_args({"test", "--first", "1", "--second", "2"}), - "Argument '--first VAR' not allowed with '--second VAR'", + "Argument '--second VAR' not allowed with '--first VAR'", + 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); } \ No newline at end of file From 8f70dde82e942cf0d4e7332b6e8ae752b540cb0f Mon Sep 17 00:00:00 2001 From: Pranav Srinivas Kumar Date: Sat, 4 Nov 2023 15:20:47 -0500 Subject: [PATCH 3/9] Added unit test for mutex_args with three arguments --- test/test_mutually_exclusive_group.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/test_mutually_exclusive_group.cpp b/test/test_mutually_exclusive_group.cpp index 493f58a..b1162e3 100644 --- a/test/test_mutually_exclusive_group.cpp +++ b/test/test_mutually_exclusive_group.cpp @@ -36,4 +36,19 @@ TEST_CASE( 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); } \ No newline at end of file From 7bbde0defbdcf776c608fe1296623032fa7ebb89 Mon Sep 17 00:00:00 2001 From: Pranav Srinivas Kumar Date: Sat, 4 Nov 2023 15:24:07 -0500 Subject: [PATCH 4/9] Added unit test for 2 mutex_groups --- test/test_mutually_exclusive_group.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/test_mutually_exclusive_group.cpp b/test/test_mutually_exclusive_group.cpp index b1162e3..28c6bec 100644 --- a/test/test_mutually_exclusive_group.cpp +++ b/test/test_mutually_exclusive_group.cpp @@ -51,4 +51,21 @@ TEST_CASE("Create mutually exclusive group with 3 arguments" * 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 From de4239483dd55a95fd781a652b8bc8be0aa95235 Mon Sep 17 00:00:00 2001 From: Pranav Srinivas Kumar Date: Sat, 4 Nov 2023 15:36:01 -0500 Subject: [PATCH 5/9] Added logic and unit tests for the required flag in mutex_args --- README.md | 18 +++++++++++++++ include/argparse/argparse.hpp | 19 ++++++++++++++++ test/test_mutually_exclusive_group.cpp | 31 ++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/README.md b/README.md index 64b9236..9abd975 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,23 @@ 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' +``` + ### 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 d4aec0e..d32d2e8 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -1588,6 +1588,25 @@ public: 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"); + } } } diff --git a/test/test_mutually_exclusive_group.cpp b/test/test_mutually_exclusive_group.cpp index 28c6bec..93d6215 100644 --- a/test/test_mutually_exclusive_group.cpp +++ b/test/test_mutually_exclusive_group.cpp @@ -21,6 +21,37 @@ TEST_CASE("Create mutually exclusive group with 2 arguments" * 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")) { From e0a095571f59f19fecd814781d9a733e3a13ea02 Mon Sep 17 00:00:00 2001 From: Pranav Srinivas Kumar Date: Sat, 4 Nov 2023 15:41:48 -0500 Subject: [PATCH 6/9] Updated README to include mutex_args feature --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 9abd975..0a89e96 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,21 @@ 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! From b43c0a7e8344a08e8c805c43433599310fc9c740 Mon Sep 17 00:00:00 2001 From: Pranav Srinivas Kumar Date: Sat, 4 Nov 2023 15:46:41 -0500 Subject: [PATCH 7/9] Addressed clang-tidy issues --- include/argparse/argparse.hpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index d32d2e8..efbf3c4 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -454,12 +454,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) { @@ -1420,9 +1421,9 @@ public: m_subparser_used.insert_or_assign(it->get().m_program_name, false); } - for (auto &g : other.m_mutually_exclusive_groups) { + for (const auto &g : other.m_mutually_exclusive_groups) { MutuallyExclusiveGroup group(*this, g.m_required); - for (auto &arg : g.m_elements) { + 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") From ecccae530c85974d4686f4037181f574116de5ae Mon Sep 17 00:00:00 2001 From: Pranav Srinivas Kumar Date: Sat, 4 Nov 2023 15:48:13 -0500 Subject: [PATCH 8/9] Using size_t for levenshtein distance --- include/argparse/argparse.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index efbf3c4..1d4938b 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -481,10 +481,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; From a9869150fd7a1f9ca5aaaa8457cbd52142dd6454 Mon Sep 17 00:00:00 2001 From: Pranav Srinivas Kumar Date: Sat, 4 Nov 2023 15:51:22 -0500 Subject: [PATCH 9/9] Changed from using std::set to std::vector for mutex_args elements --- include/argparse/argparse.hpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index 1d4938b..bf8de35 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -46,7 +46,6 @@ SOFTWARE. #include #include #include -#include #include #include #include @@ -1429,7 +1428,7 @@ public: // 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.insert(&(*it->second)); + group.m_elements.push_back(&(*it->second)); } m_mutually_exclusive_groups.push_back(std::move(group)); } @@ -1492,14 +1491,14 @@ public: template Argument &add_argument(Targs... f_args) { auto &argument = m_parent.add_argument(std::forward(f_args)...); - m_elements.insert(&argument); + m_elements.push_back(&argument); return argument; } private: ArgumentParser &m_parent; bool m_required{false}; - std::set m_elements{}; + std::vector m_elements{}; }; MutuallyExclusiveGroup &add_mutually_exclusive_group(bool required = false) {