Merge pull request #301 from p-ranav/feature/221_mutex_args

Closes #221
This commit is contained in:
Pranav 2023-11-04 15:54:37 -05:00 committed by GitHub
commit 086c8f3db0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 233 additions and 7 deletions

View File

@ -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!

View File

@ -453,12 +453,13 @@ template <typename T> struct IsChoiceTypeSupported {
};
template <typename StringType>
int get_levenshtein_distance(const StringType &s1, const StringType &s2) {
std::vector<std::vector<int>> dp(s1.size() + 1,
std::vector<int>(s2.size() + 1, 0));
std::size_t get_levenshtein_distance(const StringType &s1,
const StringType &s2) {
std::vector<std::vector<std::size_t>> dp(
s1.size() + 1, std::vector<std::size_t>(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<std::string_view, ValueType> &map,
const std::string_view input) {
std::string_view most_similar{};
int min_distance = std::numeric_limits<int>::max();
std::size_t min_distance = std::numeric_limits<std::size_t>::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 <typename... Targs> Argument &add_argument(Targs... f_args) {
auto &argument = m_parent.add_argument(std::forward<Targs>(f_args)...);
m_elements.push_back(&argument);
return argument;
}
private:
ArgumentParser &m_parent;
bool m_required{false};
std::vector<Argument *> 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 <typename... Targs>
@ -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<Argument>::iterator;
using mutex_group_it = std::vector<MutuallyExclusiveGroup>::iterator;
using argument_parser_it =
std::list<std::reference_wrapper<ArgumentParser>>::iterator;
@ -2030,6 +2119,7 @@ private:
std::list<std::reference_wrapper<ArgumentParser>> m_subparsers;
std::map<std::string_view, argument_parser_it> m_subparser_map;
std::map<std::string_view, bool> m_subparser_used;
std::vector<MutuallyExclusiveGroup> m_mutually_exclusive_groups;
};
} // namespace argparse

View File

@ -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

View File

@ -0,0 +1,102 @@
#ifdef WITH_MODULE
import argparse;
#else
#include <argparse/argparse.hpp>
#endif
#include <doctest.hpp>
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);
}