Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<argon::none_type>("verbose", "v");
log_opts.add_optional_argument<argon::none_type>("quiet", "q");
log_opts.add_optional_argument<argon::none_type>("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).

<br />

Expand Down
66 changes: 53 additions & 13 deletions include/argon/argument_parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
107 changes: 107 additions & 0 deletions tests/source/test_argument_parser_parse_args.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> argv_vec{"program", compound_flag};

const int argc = static_cast<int>(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<std::string> argv_vec{"program", "-log"};

const int argc = static_cast<int>(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<std::string> argv_vec{"program", invalid_flag};

const int argc = static_cast<int>(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(
Expand Down
Loading