From 5af7bb8195b37be4ff36d2d301de7d17d8af335a Mon Sep 17 00:00:00 2001 From: Krzysiek Karbowiak Date: Wed, 3 Dec 2025 18:05:06 +0100 Subject: [PATCH 1/5] Disallow positional arguments in mutually exclusive groups --- include/argparse.hpp | 5 +++++ test/unittest/test_argument_parser.cpp | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/include/argparse.hpp b/include/argparse.hpp index d30643a..ebb0137 100644 --- a/include/argparse.hpp +++ b/include/argparse.hpp @@ -1895,6 +1895,11 @@ namespace argparse { m_options.names = std::move(names); m_options.mutually_exclusive_group = group; + + if (is_positional() && group != nullptr) + { + throw 1; + } } ~ArgumentBuilder() diff --git a/test/unittest/test_argument_parser.cpp b/test/unittest/test_argument_parser.cpp index ec69ec7..a3fcfa8 100644 --- a/test/unittest/test_argument_parser.cpp +++ b/test/unittest/test_argument_parser.cpp @@ -350,6 +350,14 @@ TEST_CASE("ArgumentParser supports mutually exclusive groups") CHECK_NOTHROW(parser.add_mutually_exclusive_group()); } +TEST_CASE("Adding a positional argument to a mutually exclusive group results in error") +{ + auto parser = argparse::ArgumentParser(); + auto group = parser.add_mutually_exclusive_group(); + + CHECK_THROWS(group.add_argument("pos")); +} + TEST_CASE("Adding an optional argument to a mutually exclusive group does not result in error") { auto parser = argparse::ArgumentParser(); From 418013b3058eae01e1e67329808f410add9ef087 Mon Sep 17 00:00:00 2001 From: Krzysiek Karbowiak Date: Wed, 3 Dec 2025 18:08:08 +0100 Subject: [PATCH 2/5] Improve error message --- include/argparse.hpp | 2 +- test/unittest/test_argument_parser.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/argparse.hpp b/include/argparse.hpp index ebb0137..64ad975 100644 --- a/include/argparse.hpp +++ b/include/argparse.hpp @@ -1898,7 +1898,7 @@ namespace argparse if (is_positional() && group != nullptr) { - throw 1; + throw option_error("mutually exclusive arguments must be optional"); } } diff --git a/test/unittest/test_argument_parser.cpp b/test/unittest/test_argument_parser.cpp index a3fcfa8..2e23995 100644 --- a/test/unittest/test_argument_parser.cpp +++ b/test/unittest/test_argument_parser.cpp @@ -355,7 +355,7 @@ TEST_CASE("Adding a positional argument to a mutually exclusive group results in auto parser = argparse::ArgumentParser(); auto group = parser.add_mutually_exclusive_group(); - CHECK_THROWS(group.add_argument("pos")); + CHECK_THROWS_WITH_AS(group.add_argument("pos"), "mutually exclusive arguments must be optional", argparse::option_error); } TEST_CASE("Adding an optional argument to a mutually exclusive group does not result in error") From c68ec3ef4ea0e8011e66f3e8a20fdb55244b65ee Mon Sep 17 00:00:00 2001 From: Krzysiek Karbowiak Date: Wed, 3 Dec 2025 18:20:57 +0100 Subject: [PATCH 3/5] Allow non-required positional arguments in mutually exclusive groups --- include/argparse.hpp | 14 ++++++++------ test/unittest/test_argument_parser.cpp | 8 ++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/include/argparse.hpp b/include/argparse.hpp index 64ad975..cd9c47f 100644 --- a/include/argparse.hpp +++ b/include/argparse.hpp @@ -1895,14 +1895,9 @@ namespace argparse { m_options.names = std::move(names); m_options.mutually_exclusive_group = group; - - if (is_positional() && group != nullptr) - { - throw option_error("mutually exclusive arguments must be optional"); - } } - ~ArgumentBuilder() + ~ArgumentBuilder() noexcept(false) { if ((m_options.action == argparse::version) && m_options.help.empty()) { @@ -1911,6 +1906,13 @@ namespace argparse if (is_positional()) { + if (m_options.mutually_exclusive_group != nullptr + && (!std::holds_alternative(*m_options.nargs) + || std::get(*m_options.nargs) != zero_or_one)) + { + throw option_error("mutually exclusive arguments must be optional"); + } + m_arguments.emplace_back(PositionalArgument(std::move(m_options))); } else diff --git a/test/unittest/test_argument_parser.cpp b/test/unittest/test_argument_parser.cpp index 2e23995..fdd5554 100644 --- a/test/unittest/test_argument_parser.cpp +++ b/test/unittest/test_argument_parser.cpp @@ -358,6 +358,14 @@ TEST_CASE("Adding a positional argument to a mutually exclusive group results in CHECK_THROWS_WITH_AS(group.add_argument("pos"), "mutually exclusive arguments must be optional", argparse::option_error); } +TEST_CASE("Adding a non-required positional argument to a mutually exclusive group does not result in error") +{ + auto parser = argparse::ArgumentParser(); + auto group = parser.add_mutually_exclusive_group(); + + CHECK_NOTHROW(group.add_argument("pos").nargs(argparse::zero_or_one)); +} + TEST_CASE("Adding an optional argument to a mutually exclusive group does not result in error") { auto parser = argparse::ArgumentParser(); From 2a29f2d02d9c58141223d38bd5a398a446c9e934 Mon Sep 17 00:00:00 2001 From: Krzysiek Karbowiak Date: Wed, 3 Dec 2025 18:27:52 +0100 Subject: [PATCH 4/5] Allow non-required positional arguments in mutually exclusive groups --- include/argparse.hpp | 3 ++- test/unittest/test_argument_parser.cpp | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/include/argparse.hpp b/include/argparse.hpp index cd9c47f..f6cb546 100644 --- a/include/argparse.hpp +++ b/include/argparse.hpp @@ -1908,7 +1908,8 @@ namespace argparse { if (m_options.mutually_exclusive_group != nullptr && (!std::holds_alternative(*m_options.nargs) - || std::get(*m_options.nargs) != zero_or_one)) + || (std::get(*m_options.nargs) != zero_or_one + && std::get(*m_options.nargs) != zero_or_more))) { throw option_error("mutually exclusive arguments must be optional"); } diff --git a/test/unittest/test_argument_parser.cpp b/test/unittest/test_argument_parser.cpp index fdd5554..e8a9062 100644 --- a/test/unittest/test_argument_parser.cpp +++ b/test/unittest/test_argument_parser.cpp @@ -364,6 +364,7 @@ TEST_CASE("Adding a non-required positional argument to a mutually exclusive gro auto group = parser.add_mutually_exclusive_group(); CHECK_NOTHROW(group.add_argument("pos").nargs(argparse::zero_or_one)); + CHECK_NOTHROW(group.add_argument("pos").nargs(argparse::zero_or_more)); } TEST_CASE("Adding an optional argument to a mutually exclusive group does not result in error") From 5da0db8858bf71e73b0bea05422c4bb46d824287 Mon Sep 17 00:00:00 2001 From: Krzysiek Karbowiak Date: Wed, 3 Dec 2025 19:25:13 +0100 Subject: [PATCH 5/5] Fix condition --- include/argparse.hpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/include/argparse.hpp b/include/argparse.hpp index f6cb546..4ac2468 100644 --- a/include/argparse.hpp +++ b/include/argparse.hpp @@ -1907,9 +1907,10 @@ namespace argparse if (is_positional()) { if (m_options.mutually_exclusive_group != nullptr - && (!std::holds_alternative(*m_options.nargs) - || (std::get(*m_options.nargs) != zero_or_one - && std::get(*m_options.nargs) != zero_or_more))) + && (!m_options.nargs.has_value() + || (!std::holds_alternative(*m_options.nargs) + || (std::get(*m_options.nargs) != zero_or_one + && std::get(*m_options.nargs) != zero_or_more)))) { throw option_error("mutually exclusive arguments must be optional"); }