Skip to content

Commit

Permalink
Merge pull request #301 from p-ranav/feature/221_mutex_args
Browse files Browse the repository at this point in the history
Closes #221
  • Loading branch information
p-ranav authored Nov 4, 2023
2 parents 281f1ab + a986915 commit 086c8f3
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 7 deletions.
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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!
Expand Down
104 changes: 97 additions & 7 deletions include/argparse/argparse.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -1418,6 +1419,19 @@ class ArgumentParser {
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;
Expand Down Expand Up @@ -1455,6 +1469,43 @@ class ArgumentParser {
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>
Expand Down Expand Up @@ -1520,6 +1571,43 @@ class ArgumentParser {
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
Expand Down Expand Up @@ -2006,6 +2094,7 @@ class ArgumentParser {
}

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;

Expand All @@ -2030,6 +2119,7 @@ class ArgumentParser {
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
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
102 changes: 102 additions & 0 deletions test/test_mutually_exclusive_group.cpp
Original file line number Diff line number Diff line change
@@ -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);
}

0 comments on commit 086c8f3

Please sign in to comment.