diff --git a/docs/tutorial.md b/docs/tutorial.md index 5d5da2c2..e26ca92a 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -43,6 +43,7 @@ - [3. Positional arguments consume free values](#3-positional-arguments-consume-free-values) - [4. Unknown Argument Flag Handling](#4-unknown-argument-flag-handling) - [Compound Arguments](#compound-arguments) + - [Compound Flags within Argument Groups](#compound-flags-within-argument-groups) - [Parsing Known Arguments](#parsing-known-arguments) - [Retrieving Argument Values](#retrieving-argument-values) - [Using the Parser](#using-the-parser) @@ -1328,9 +1329,29 @@ Numbers: 1, 2, 3 ``` > [!IMPORTANT] -> -> - If there exists an argument whose secondary name matches a possible compound of other arguments, the parser will still treat the flag as a flag of the **single matching argument**, not as multiple flags. -> - The argument parser will try to assign the values following a compound argument flag to the argument represented by the **last character** of the compound flag. +> * If there exists an argument whose secondary name matches a possible compound of other arguments, the parser will still treat the flag as a flag of the **single matching argument**, not as multiple flags. +> * The argument parser will try to assign the values following a compound argument flag to the argument represented by the **last character** of the compound flag. + +#### Compound Flags within Argument Groups + +If you have organized arguments into an [argument group](#argument-groups) that uses a [prefix and/or suffix](#naming-modifiers), you can still use compound flags. To do this, the compound flag must strictly start with the group's prefix and end with the group's suffix. The characters nested between them will be treated as the secondary names of the arguments registered exclusively to that group. + +```cpp +auto& log_opts = parser.add_group("Logging Options").with_prefix("log-").with_suffix("-opt"); + +log_opts.add_optional_argument("verbose", "v"); +log_opts.add_optional_argument("quiet", "q"); +log_opts.add_optional_argument("debug", "d"); + +parser.try_parse_args(argc, argv); + +/* +> ./program -log-vvqd-opt +This is equivalent to passing: -log-v-opt -log-v-opt -log-q-opt -log-d-opt +``` + +> [!NOTE] +> The parser automatically handles prefix overlaps. For instance, if you type `-log` and your group prefix is `log` but no valid group compound can be formed, the parser safely falls back to standard compound parsing (evaluating it as `-l`, `-o`, `-g` if those individual arguments exist).
diff --git a/include/argon/argument_parser.hpp b/include/argon/argument_parser.hpp index ea40c1ee..19589e3e 100644 --- a/include/argon/argument_parser.hpp +++ b/include/argon/argument_parser.hpp @@ -1207,26 +1207,66 @@ class argument_parser { return false; const auto actual_tok_value = this->_strip_flag_prefix(tok); - tok.args.reserve(actual_tok_value.size()); - for (const char c : actual_tok_value) { - const auto opt_arg_it = std::ranges::find_if( - this->_optional_args, - this->_name_match_predicate( - std::string_view(&c, 1ull), detail::argument_name::m_secondary - ) + // Iterate over all argument groups to check for matching affixes + for (const auto& group : this->_argument_groups) { + // The token must be long enough to contain the affixes and at least one compound character + if (actual_tok_value.length() <= group->_prefix.length() + group->_suffix.length()) + continue; + + // The token must correctly start and end with the group's affixes + if (not ( + actual_tok_value.starts_with(group->_prefix) + and actual_tok_value.ends_with(group->_suffix) + )) + continue; + + // Extract the middle characters which represent the actual compound flag + const std::string_view flag = actual_tok_value.substr( + group->_prefix.length(), + actual_tok_value.length() - group->_prefix.length() - group->_suffix.length() + ); + + tok.args.clear(); + tok.args.reserve(flag.length()); + + bool all_match = true; + std::string expected_secondary_name; + expected_secondary_name.reserve( + group->_prefix.length() + 1ull + group->_suffix.length() ); - if (opt_arg_it == this->_optional_args.end()) { - tok.args.clear(); - return false; + for (const char c : flag) { + // Reconstruct the expected secondary name + expected_secondary_name = group->_prefix; + expected_secondary_name += c; + expected_secondary_name += group->_suffix; + + const auto opt_arg_it = std::ranges::find_if( + this->_optional_args, + this->_name_match_predicate( + expected_secondary_name, detail::argument_name::m_secondary + ) + ); + + if (opt_arg_it == this->_optional_args.end()) { + all_match = false; + break; + } + + tok.args.emplace_back(*opt_arg_it); } - tok.args.emplace_back(*opt_arg_it); + // If every reconstructed character matched an argument in this group, it's valid + if (all_match) { + tok.type = detail::argument_token::t_flag_compound; + return true; + } } - tok.type = detail::argument_token::t_flag_compound; - return true; + // If no group matched entirely, clear arguments and fail + tok.args.clear(); + return false; } /** diff --git a/tests/source/test_argument_parser_parse_args.cpp b/tests/source/test_argument_parser_parse_args.cpp index b4bca471..57f773c6 100644 --- a/tests/source/test_argument_parser_parse_args.cpp +++ b/tests/source/test_argument_parser_parse_args.cpp @@ -1191,6 +1191,113 @@ TEST_CASE_FIXTURE( free_argv(argc, argv); } +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "argument_parser should properly handle valid compound flags with group prefixes and suffixes" +) { + const std::string prefix = "log-"; + const std::string suffix = "-opt"; + auto& group = sut.add_group("Log Group").with_prefix(prefix).with_suffix(suffix); + + // add arguments to the group + sut.add_optional_argument(group, "verbose", "v").nargs(argon::nargs::any()); + sut.add_optional_argument(group, "quiet", "q"); + sut.add_optional_argument(group, "debug", "d"); + + std::size_t verbose_count, quiet_count, debug_count; + std::string compound_flag; + + SUBCASE("one usage of each group argument") { + compound_flag = "-log-vqd-opt"; + verbose_count = quiet_count = debug_count = 1ull; + } + SUBCASE("complex usage with repeated flags") { + compound_flag = "-log-vvqvd-opt"; + verbose_count = 3ull; + quiet_count = debug_count = 1ull; + } + + CAPTURE(verbose_count); + CAPTURE(quiet_count); + CAPTURE(debug_count); + CAPTURE(compound_flag); + + std::vector argv_vec{"program", compound_flag}; + + const int argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + // parse args + REQUIRE_NOTHROW(sut.parse_args(argc, argv)); + + // validate argument usage counts + CHECK_EQ(sut.count("log-verbose-opt"), verbose_count); + CHECK_EQ(sut.count("log-quiet-opt"), quiet_count); + CHECK_EQ(sut.count("log-debug-opt"), debug_count); + + // cleanup + free_argv(argc, argv); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "argument_parser should fall back to standard compound parsing if a group prefix overlaps but " + "the token is not a valid group compound" +) { + // Create a group with the prefix "log" + auto& log_group = sut.add_group("Log Group").with_prefix("log"); + sut.add_optional_argument(log_group, "dummy", "d"); + + // Create normal arguments (not in the group) that happen to spell "log" + sut.add_optional_argument("list", "l"); + sut.add_optional_argument("output", "o"); + sut.add_optional_argument("generate", "g"); + + // The token is "-log". + // It starts with the group prefix "log", but leaving an empty middle "" or matching "g" as a middle fails group matching. + // It should safely fall back to the default group and parse as standard '-l', '-o', '-g'. + std::vector argv_vec{"program", "-log"}; + + const int argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + REQUIRE_NOTHROW(sut.parse_args(argc, argv)); + + CHECK_EQ(sut.count("list"), 1ull); + CHECK_EQ(sut.count("output"), 1ull); + CHECK_EQ(sut.count("generate"), 1ull); + CHECK_EQ(sut.count("logdummy"), 0ull); // Ensure the group argument wasn't touched + + free_argv(argc, argv); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "argument_parser should throw if an invalid compound group flag is used" +) { + auto& group = sut.add_group("Log Group").with_prefix("log-"); + sut.add_optional_argument(group, "verbose", "v"); + sut.add_optional_argument(group, "debug", "d"); + + // 'x' is not a valid secondary flag in this group + const std::string invalid_flag = "-log-vdx"; + std::vector argv_vec{"program", invalid_flag}; + + const int argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + CHECK_THROWS_WITH_AS( + sut.parse_args(argc, argv), + parsing_failure::unknown_argument(invalid_flag).what(), + parsing_failure + ); + + CHECK_EQ(sut.count("log-verbose"), 0ull); + CHECK_EQ(sut.count("log-debug"), 0ull); + + free_argv(argc, argv); +} + // greedy arguments TEST_CASE_FIXTURE(