From a2ab16fd7b4ed2e71a4080ae71190649bc47a7ae Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 21 Sep 2025 14:20:21 +0200 Subject: [PATCH 1/6] initial impl --- CMakeLists.txt | 2 +- Doxyfile | 2 +- MODULE.bazel | 2 +- include/ap/argument.hpp | 30 +++-- include/ap/argument_parser.hpp | 184 +++++++++++++-------------- include/ap/detail/argument_base.hpp | 3 + include/ap/detail/argument_token.hpp | 33 ++++- 7 files changed, 139 insertions(+), 117 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b786012e..0f4de373 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ else() endif() project(cpp-ap - VERSION 3.0.0.5 + VERSION 3.0.0.6 DESCRIPTION "Command-line argument parser for C++20" HOMEPAGE_URL "https://github.com/SpectraL519/cpp-ap" LANGUAGES CXX diff --git a/Doxyfile b/Doxyfile index 3c2db87b..057383cc 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = CPP-AP # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 3.0.0.5 +PROJECT_NUMBER = 3.0.0.6 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/MODULE.bazel b/MODULE.bazel index 135ad513..eb14f2e5 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,4 +1,4 @@ module( name = "cpp-ap", - version = "3.0.0.5", + version = "3.0.0.6", ) diff --git a/include/ap/argument.hpp b/include/ap/argument.hpp index 81a74533..4b131d5f 100644 --- a/include/ap/argument.hpp +++ b/include/ap/argument.hpp @@ -123,6 +123,11 @@ class argument : public detail::argument_base { return not this->_required and this->_bypass_required; } + /// @return `true` if the argument is greedy, `false` otherwise. + [[nodiscard]] bool is_greedy() const noexcept override { + return this->_greedy; + } + // attribute setters /** @@ -171,6 +176,16 @@ class argument : public detail::argument_base { return *this; } + /** + * @brief Set the `greedy` attribute of the argument. + * @param g The attribute value. + * @return Reference to the argument instance. + */ + argument& greedy(const bool g = true) noexcept { + this->_greedy = g; + return *this; + } + /** * @brief Set the nargs range for the argument. * @param range The attribute value. @@ -193,8 +208,7 @@ class argument : public detail::argument_base { argument& nargs(const count_type n) noexcept requires(not util::c_is_none) { - this->_nargs_range = nargs::range(n); - return *this; + return this->nargs(nargs::range(n)); } /** @@ -207,8 +221,7 @@ class argument : public detail::argument_base { argument& nargs(const count_type lower, const count_type upper) noexcept requires(not util::c_is_none) { - this->_nargs_range = nargs::range(lower, upper); - return *this; + return this->nargs(nargs::range(lower, upper)); } /** @@ -671,7 +684,7 @@ class argument : public detail::argument_base { // attributes const ap::detail::argument_name _name; ///< The argument's name. std::optional _help_msg; ///< The argument's help message. - nargs::range _nargs_range; ///< The argument's nargs range attribute. + nargs::range _nargs_range; ///< The argument's nargs range attribute value. [[no_unique_address]] value_arg_specific_type> _default_values; ///< The argument's default value list. [[no_unique_address]] value_arg_specific_type>> @@ -683,9 +696,10 @@ class argument : public detail::argument_base { [[no_unique_address]] value_arg_specific_type> _value_actions; ///< The argument's value actions collection. - bool _required : 1; ///< The argument's `required` attribute. - bool _bypass_required : 1 = false; ///< The argument's `bypass_required` attribute. - bool _hidden : 1 = false; ///< The argument's `hidden` attribute. + bool _required : 1; ///< The argument's `required` attribute value. + bool _bypass_required : 1 = false; ///< The argument's `bypass_required` attribute value. + bool _greedy : 1 = false; ///< The argument's `greedy` attribute value. + bool _hidden : 1 = false; ///< The argument's `hidden` attribute value. // parsing result [[no_unique_address]] optional_specific_type diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index 08e5591f..b2642ce8 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -899,15 +899,9 @@ class argument_parser { return; } - // invalid flag - check for compound secondary flag - const auto compound_toks = this->_try_split_compound_flag(tok); - if (not compound_toks.empty()) { // not a valid compound flag - toks.insert(toks.end(), compound_toks.begin(), compound_toks.end()); - return; - } - - // unknown flag - if (state.parse_known_only) { + // not a value token -> flag token + // flag token could not be validated -> unknown flag + if (state.parse_known_only) { // do nothing (will be handled during parsing) toks.emplace_back(std::move(tok)); return; } @@ -943,31 +937,6 @@ class argument_parser { return detail::argument_token::t_value; } - /** - * @brief Builds an argument token from the given value. - * @param arg_value The command-line argument's value to be processed. - * @return An argument token with removed flag prefix (if present) and an adequate token type. - */ - [[nodiscard]] detail::argument_token _build_token(const std::string_view arg_value - ) const noexcept { - if (util::contains_whitespaces(arg_value)) - return {.type = detail::argument_token::t_value, .value = std::string(arg_value)}; - - if (arg_value.starts_with(this->_flag_prefix)) - return { - .type = detail::argument_token::t_flag_primary, - .value = std::string(arg_value.substr(this->_primary_flag_prefix_length)) - }; - - if (arg_value.starts_with(this->_flag_prefix_char)) - return { - .type = detail::argument_token::t_flag_secondary, - .value = std::string(arg_value.substr(this->_secondary_flag_prefix_length)) - }; - - return {.type = detail::argument_token::t_value, .value = std::string(arg_value)}; - } - /** * @brief Check if a flag token is valid based on its value. * @attention Sets the `arg` member of the token if an argument with the given name (token's value) is present. @@ -977,42 +946,57 @@ class argument_parser { [[nodiscard]] bool _validate_flag_token(detail::argument_token& tok) noexcept { const auto opt_arg_it = this->_find_opt_arg(tok); if (opt_arg_it == this->_optional_args.end()) + return this->_validate_compound_flag_token(tok); + + tok.args.emplace_back(*opt_arg_it); + return true; + } + + // TODO: add doc comment + bool _validate_compound_flag_token(detail::argument_token& tok) noexcept { + if (tok.type != detail::argument_token::t_flag_secondary) return false; - tok.arg = *opt_arg_it; + 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 + ) + ); + if (opt_arg_it == this->_optional_args.end()) + return false; + + tok.args.emplace_back(*opt_arg_it); + } + + tok.type = detail::argument_token::t_flag_compound; return true; } /** - * @brief Tries to split a secondary flag token into separate flag token (one for each character of the token's value). - * @param tok The token to be processed. - * @return A vector of new argument tokens. - * @note If ANY of the characters in the token's value does not match an argument, an empty vector will be returned. + * @brief Find an optional argument based on a flag token. + * @param flag_tok An argument_token instance, the value of which will be used to find the argument. + * @return An iterator to the argument's position. + * @note If the `flag_tok.type` is not a valid flag token, then the end iterator will be returned. */ - [[nodiscard]] std::vector _try_split_compound_flag( - const detail::argument_token& tok + [[nodiscard]] arg_ptr_list_iter_t _find_opt_arg(const detail::argument_token& flag_tok ) noexcept { - std::vector compound_toks; - const auto actual_tok_value = this->_strip_flag_prefix(tok); - - compound_toks.reserve(actual_tok_value.size()); - - if (tok.type != detail::argument_token::t_flag_secondary) - return compound_toks; + if (not flag_tok.is_flag_token()) + return this->_optional_args.end(); - for (const char c : actual_tok_value) { - detail::argument_token ctok{ - detail::argument_token::t_flag_secondary, - std::format("{}{}", this->_flag_prefix_char, c) - }; - if (not this->_validate_flag_token(ctok)) { - compound_toks.clear(); - return compound_toks; - } - compound_toks.emplace_back(std::move(ctok)); - } + const auto actual_tok_value = this->_strip_flag_prefix(flag_tok); + const auto match_type = + flag_tok.type == detail::argument_token::t_flag_primary + ? detail::argument_name::m_primary + : detail::argument_name::m_secondary; - return compound_toks; + return std::ranges::find_if( + this->_optional_args, this->_name_match_predicate(actual_tok_value, match_type) + ); } /** @@ -1055,10 +1039,17 @@ class argument_parser { * @throws ap::parsing_failure */ void _parse_token(parsing_state& state, const detail::argument_token& tok) { + if (state.curr_arg and state.curr_arg->is_greedy()) { + this->_set_argument_value(state, tok.value); + return; + } + switch (tok.type) { case detail::argument_token::t_flag_primary: [[fallthrough]]; - case detail::argument_token::t_flag_secondary: { + case detail::argument_token::t_flag_secondary: + [[fallthrough]]; + case detail::argument_token::t_flag_compound: { if (not tok.is_valid_flag_token()) { if (state.parse_known_only) { state.curr_arg.reset(); @@ -1071,10 +1062,13 @@ class argument_parser { } } - if (tok.arg->mark_used()) - state.curr_arg = tok.arg; - else - state.curr_arg.reset(); + for (const auto& arg : tok.args) { + if (arg->mark_used()) + state.curr_arg = arg; + else + state.curr_arg.reset(); + } + break; } @@ -1088,23 +1082,37 @@ class argument_parser { state.curr_arg = *state.curr_pos_arg_it; } - if (not state.curr_arg->set_value(tok.value)) { - // advance to the next positional argument if possible - if (state.curr_arg->is_positional() - and state.curr_pos_arg_it != this->_positional_args.end() - and ++state.curr_pos_arg_it != this->_positional_args.end()) { - state.curr_arg = *state.curr_pos_arg_it; - break; - } - - state.curr_arg.reset(); - } - + this->_set_argument_value(state, tok.value); break; } } } + // TODO: add doc comment + void _set_argument_value(parsing_state& state, const std::string_view value) noexcept { + if (state.curr_arg->set_value(std::string(value))) + return; // argument still accepts values + + if (this->_try_advance_positional(state)) + return; + + state.curr_arg.reset(); + } + + // TODO: add doc comment + [[nodiscard]] bool _try_advance_positional(parsing_state& state) const noexcept { + if (not state.curr_arg->is_positional()) + return false; + + if (state.curr_pos_arg_it == this->_positional_args.end()) + return false; + + if (++state.curr_pos_arg_it == this->_positional_args.end()) + return false; + + return true; + } + /** * @brief Check whether required argument bypassing is enabled * @return true if at least one argument with enabled required argument bypassing is used, false otherwise. @@ -1173,28 +1181,6 @@ class argument_parser { return nullptr; } - /** - * @brief Find an optional argument based on a flag token. - * @param flag_tok An argument_token instance, the value of which will be used to find the argument. - * @return An iterator to the argument's position. - * @note If the `flag_tok.type` is not a valid flag token, then the end iterator will be returned. - */ - [[nodiscard]] arg_ptr_list_iter_t _find_opt_arg(const detail::argument_token& flag_tok - ) noexcept { - if (not flag_tok.is_flag_token()) - return this->_optional_args.end(); - - const auto actual_tok_value = this->_strip_flag_prefix(flag_tok); - const auto match_type = - flag_tok.type == detail::argument_token::t_flag_primary - ? detail::argument_name::m_primary - : detail::argument_name::m_secondary; - - return std::ranges::find_if( - this->_optional_args, this->_name_match_predicate(actual_tok_value, match_type) - ); - } - /** * @brief Print the given argument list to an output stream. * @param os The output stream to print to. diff --git a/include/ap/detail/argument_base.hpp b/include/ap/detail/argument_base.hpp index 0355b5dd..7a76809a 100644 --- a/include/ap/detail/argument_base.hpp +++ b/include/ap/detail/argument_base.hpp @@ -50,6 +50,9 @@ class argument_base { /// @return `true` if the argument is allowed to bypass the required check, `false` otherwise. virtual bool is_bypass_required_enabled() const noexcept = 0; + /// @return `true` if the argument is greedy, `false` otherwise. + virtual bool is_greedy() const noexcept = 0; + protected: /// @param verbose The verbosity mode value. If `true` all non-default parameters will be included in the output. /// @return An argument descriptor object for the argument. diff --git a/include/ap/detail/argument_token.hpp b/include/ap/detail/argument_token.hpp index a532eb1f..3502814c 100644 --- a/include/ap/detail/argument_token.hpp +++ b/include/ap/detail/argument_token.hpp @@ -10,18 +10,21 @@ #include #include +#include namespace ap::detail { /// @brief Structure representing a single command-line argument token. struct argument_token { using arg_ptr_t = std::shared_ptr; ///< Argument pointer type alias. + using arg_ptr_list_t = std::vector; /// @brief The token type discriminator. enum class token_type : std::uint8_t { t_value, ///< Represents a value argument. - t_flag_primary, ///< Represents the primary (--) flag argument. - t_flag_secondary ///< Represents the secondary (-) flag argument. + t_flag_primary, ///< Represents a primary (--) flag argument. + t_flag_secondary, ///< Represents a secondary (-) flag argument. + t_flag_compound ///< Represents a compound flag argument (secondary flag matching multiple arguments). }; using enum token_type; @@ -36,28 +39,44 @@ struct argument_token { /** * @brief Checks whether the `type` member is a flag token type. - * @return true if `type` is either `t_flag_primary` or `t_flag_secondary`, false otherwise. + * @return true if the token has a *flag* type + * @note The token's type is considered a *flag* type if it has one of the following values: + * @note - t_flag_primary + * @note - t_flag_secondary + * @note - t_flag_compound */ [[nodiscard]] bool is_flag_token() const noexcept { - return this->type == t_flag_primary or this->type == t_flag_secondary; + switch (this->type) { + case t_flag_primary: + [[fallthrough]]; + case t_flag_secondary: + [[fallthrough]]; + case t_flag_compound: + return true; + default: + return false; + } } /** * @brief Checks whether the token represents a valid flag. * * A token is considered a valid flag token if: - * 1. The token's type if a valid flag token type (`t_flag_primary` or `t_flag_secondary`) + * 1. The token's type if a valid flag token type: + * - t_flag_primary + * - t_flag_secondary + * - t_flag_compound * 2. The token's `arg` member is set. * * @return true if `type` is either `t_flag_primary` or `t_flag_secondary`, false otherwise. */ [[nodiscard]] bool is_valid_flag_token() const noexcept { - return this->is_flag_token() and this->arg != nullptr; + return this->is_flag_token() and not this->args.empty(); } token_type type; ///< The token's type discrimiator value. std::string value; ///< The actual token's value. - arg_ptr_t arg = nullptr; ///< The corresponding argument + arg_ptr_list_t args; ///< The corresponding argument }; } // namespace ap::detail From c8198163af668aa6e7f15b5e79aba156ace5d4c0 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 21 Sep 2025 14:44:53 +0200 Subject: [PATCH 2/6] some implementation improvements --- include/ap/argument_parser.hpp | 108 +++++++++--------- include/ap/detail/argument_token.hpp | 2 +- include/ap/exceptions.hpp | 4 +- .../include/argument_parser_test_fixture.hpp | 2 +- tests/source/test_argument_token.cpp | 8 +- 5 files changed, 64 insertions(+), 60 deletions(-) diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index b2642ce8..201521d2 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -1034,8 +1034,8 @@ class argument_parser { /** * @brief Parse a single command-line argument token. - * @param curr_arg The currently processed argument. * @param state The current parsing state. + * @param tok The token to be parsed. * @throws ap::parsing_failure */ void _parse_token(parsing_state& state, const detail::argument_token& tok) { @@ -1044,75 +1044,79 @@ class argument_parser { return; } - switch (tok.type) { - case detail::argument_token::t_flag_primary: - [[fallthrough]]; - case detail::argument_token::t_flag_secondary: - [[fallthrough]]; - case detail::argument_token::t_flag_compound: { - if (not tok.is_valid_flag_token()) { - if (state.parse_known_only) { - state.curr_arg.reset(); - state.unknown_args.emplace_back(tok.value); - break; - } - else { - // should never happen as unknown flags are filtered out during tokenization - throw parsing_failure::unknown_argument(tok.value); - } - } + if (tok.is_flag_token()) + this->_parse_flag_token(state, tok); + else + this->_parse_value_token(state, tok); + } - for (const auto& arg : tok.args) { - if (arg->mark_used()) - state.curr_arg = arg; - else - state.curr_arg.reset(); + /** + * @brief Parse a single command-line argument *flag* token. + * @param state The current parsing state. + * @param tok The token to be parsed. + * @throws ap::parsing_failure + */ + void _parse_flag_token(parsing_state& state, const detail::argument_token& tok) { + if (not tok.is_valid_flag_token()) { + if (state.parse_known_only) { + state.curr_arg.reset(); + state.unknown_args.emplace_back(tok.value); + return; } + else { + // should never happen as unknown flags are filtered out during tokenization + throw parsing_failure::unknown_argument(tok.value); + } + } - - break; + for (const auto& arg : tok.args) { + if (arg->mark_used()) + state.curr_arg = arg; + else + state.curr_arg.reset(); } - case detail::argument_token::t_value: { - if (not state.curr_arg) { - if (state.curr_pos_arg_it == this->_positional_args.end()) { - state.unknown_args.emplace_back(tok.value); - break; - } - - state.curr_arg = *state.curr_pos_arg_it; + } + + /** + * @brief Parse a single command-line argument *value* token. + * @param state The current parsing state. + * @param tok The token to be parsed. + * @throws ap::parsing_failure + */ + void _parse_value_token(parsing_state& state, const detail::argument_token& tok) { + if (not state.curr_arg) { + if (state.curr_pos_arg_it == this->_positional_args.end()) { + state.unknown_args.emplace_back(tok.value); + return; } - this->_set_argument_value(state, tok.value); - break; - } + state.curr_arg = *state.curr_pos_arg_it; } + + this->_set_argument_value(state, tok.value); } - // TODO: add doc comment + /** + * @brief Set the value for the currently processed argument. + * @attention This function assumes that the current argument is set (i.e. `state.curr_arg != nullptr`). + * @param state The current parsing state. + * @param value The value to be set for the current argument. + */ void _set_argument_value(parsing_state& state, const std::string_view value) noexcept { if (state.curr_arg->set_value(std::string(value))) return; // argument still accepts values - if (this->_try_advance_positional(state)) + // advance to the next positional argument if possible + if (state.curr_arg->is_positional() + and state.curr_pos_arg_it != this->_positional_args.end() + and ++state.curr_pos_arg_it != this->_positional_args.end()) { + state.curr_arg = *state.curr_pos_arg_it; return; + } state.curr_arg.reset(); } - // TODO: add doc comment - [[nodiscard]] bool _try_advance_positional(parsing_state& state) const noexcept { - if (not state.curr_arg->is_positional()) - return false; - - if (state.curr_pos_arg_it == this->_positional_args.end()) - return false; - - if (++state.curr_pos_arg_it == this->_positional_args.end()) - return false; - - return true; - } - /** * @brief Check whether required argument bypassing is enabled * @return true if at least one argument with enabled required argument bypassing is used, false otherwise. diff --git a/include/ap/detail/argument_token.hpp b/include/ap/detail/argument_token.hpp index 3502814c..8a575fde 100644 --- a/include/ap/detail/argument_token.hpp +++ b/include/ap/detail/argument_token.hpp @@ -76,7 +76,7 @@ struct argument_token { token_type type; ///< The token's type discrimiator value. std::string value; ///< The actual token's value. - arg_ptr_list_t args; ///< The corresponding argument + arg_ptr_list_t args = {}; ///< The corresponding argument }; } // namespace ap::detail diff --git a/include/ap/exceptions.hpp b/include/ap/exceptions.hpp index bcf0c516..b9ceb461 100644 --- a/include/ap/exceptions.hpp +++ b/include/ap/exceptions.hpp @@ -102,12 +102,12 @@ struct parsing_failure : public argument_parser_exception { ) noexcept { if (std::is_lt(ordering)) return parsing_failure( - std::format("Not enough values provided for optional argument [{}]", arg_name.str()) + std::format("Not enough values provided for argument [{}]", arg_name.str()) ); if (std::is_gt(ordering)) return parsing_failure( - std::format("Too many values provided for optional argument [{}]", arg_name.str()) + std::format("Too many values provided for argument [{}]", arg_name.str()) ); return parsing_failure( diff --git a/tests/include/argument_parser_test_fixture.hpp b/tests/include/argument_parser_test_fixture.hpp index 7657cce5..a4a0d4f5 100644 --- a/tests/include/argument_parser_test_fixture.hpp +++ b/tests/include/argument_parser_test_fixture.hpp @@ -155,7 +155,7 @@ struct argument_parser_test_fixture { argument_token flag_tok{argument_token::t_flag_primary, init_arg_flag_primary(arg_idx)}; const auto opt_arg_it = this->sut._find_opt_arg(flag_tok); if (opt_arg_it != this->sut._optional_args.end()) - flag_tok.arg = *opt_arg_it; + flag_tok.args.emplace_back(*opt_arg_it); arg_tokens.push_back(std::move(flag_tok)); arg_tokens.push_back(argument_token{argument_token::t_value, init_arg_value(arg_idx)}); diff --git a/tests/source/test_argument_token.cpp b/tests/source/test_argument_token.cpp index 36717e7d..f88c6adc 100644 --- a/tests/source/test_argument_token.cpp +++ b/tests/source/test_argument_token.cpp @@ -47,8 +47,8 @@ TEST_CASE("is_flag_token should return true if the token's type is either primar CHECK_FALSE(sut_type{t_value, ""}.is_flag_token()); } -TEST_CASE("is_valid_flag_token should return true if the token is a flag token and it's arg member " - "is set") { +TEST_CASE("is_valid_flag_token should return true if the token is a flag token and it's args list " + "is not empty") { CHECK_FALSE(sut_type{t_value, ""}.is_valid_flag_token()); CHECK_FALSE(sut_type{t_flag_primary, ""}.is_valid_flag_token()); @@ -57,6 +57,6 @@ TEST_CASE("is_valid_flag_token should return true if the token is a flag token a std::shared_ptr arg_ptr = std::make_shared>(argument_name{""}); - CHECK(sut_type{t_flag_primary, "", arg_ptr}.is_valid_flag_token()); - CHECK(sut_type{t_flag_secondary, "", arg_ptr}.is_valid_flag_token()); + CHECK(sut_type{t_flag_primary, "", {arg_ptr}}.is_valid_flag_token()); + CHECK(sut_type{t_flag_secondary, "", {arg_ptr}}.is_valid_flag_token()); } From acd67d9436431d7935e8fb385c21a0bca5d15627 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 21 Sep 2025 15:07:55 +0200 Subject: [PATCH 3/6] tests --- include/ap/argument_parser.hpp | 11 ++-- include/ap/detail/argument_token.hpp | 5 +- .../test_argument_parser_parse_args.cpp | 55 +++++++++++++++++++ 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index 201521d2..0d345f7e 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -1023,8 +1023,8 @@ class argument_parser { * @throws ap::parsing_failure */ void _parse_args_impl(const arg_token_list_t& arg_tokens, parsing_state& state) { - if (state.curr_pos_arg_it != this->_positional_args.end()) - state.curr_arg = *state.curr_pos_arg_it; + // if (state.curr_pos_arg_it != this->_positional_args.end()) + // state.curr_arg = *state.curr_pos_arg_it; // process argument tokens std::ranges::for_each( @@ -1108,11 +1108,8 @@ class argument_parser { // advance to the next positional argument if possible if (state.curr_arg->is_positional() - and state.curr_pos_arg_it != this->_positional_args.end() - and ++state.curr_pos_arg_it != this->_positional_args.end()) { - state.curr_arg = *state.curr_pos_arg_it; - return; - } + and state.curr_pos_arg_it != this->_positional_args.end()) + ++state.curr_pos_arg_it; state.curr_arg.reset(); } diff --git a/include/ap/detail/argument_token.hpp b/include/ap/detail/argument_token.hpp index 8a575fde..90ae3406 100644 --- a/include/ap/detail/argument_token.hpp +++ b/include/ap/detail/argument_token.hpp @@ -62,10 +62,7 @@ struct argument_token { * @brief Checks whether the token represents a valid flag. * * A token is considered a valid flag token if: - * 1. The token's type if a valid flag token type: - * - t_flag_primary - * - t_flag_secondary - * - t_flag_compound + * 1. The token's type if a valid flag token type (see @ref is_flag_token) * 2. The token's `arg` member is set. * * @return true if `type` is either `t_flag_primary` or `t_flag_secondary`, false otherwise. diff --git a/tests/source/test_argument_parser_parse_args.cpp b/tests/source/test_argument_parser_parse_args.cpp index 9a66bf5c..b8e5b419 100644 --- a/tests/source/test_argument_parser_parse_args.cpp +++ b/tests/source/test_argument_parser_parse_args.cpp @@ -1176,6 +1176,61 @@ TEST_CASE_FIXTURE( free_argv(argc, argv); } +// greedy arguments + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "greedy arguments should consume all command-line values until their upper nargs bound is " + "reached regardless of the tokens' types" +) { + const std::string greedy_arg_name = "greedy"; + const std::string actual_value = "value"; + + const std::size_t n_greedily_consumed_values = 3ull; + const std::size_t greedy_nargs_bound = 1ull + n_greedily_consumed_values; + const std::size_t opt_begin_idx = 0ull; + + std::vector argv_vec{"program"}; + + SUBCASE("greedy positional argument") { + sut.add_positional_argument(greedy_arg_name).greedy().nargs(greedy_nargs_bound); + argv_vec.emplace_back(actual_value); + } + SUBCASE("greedy optional arg") { + sut.add_optional_argument(greedy_arg_name).greedy().nargs(greedy_nargs_bound); + argv_vec.emplace_back(std::format("--{}", greedy_arg_name)); + argv_vec.emplace_back(actual_value); + } + + CAPTURE(sut); + CAPTURE(argv_vec); + + add_optional_args(n_greedily_consumed_values, opt_begin_idx); + + // add the greedily consumed flags + std::vector expected_greedy_arg_values{actual_value}; + for (std::size_t i = opt_begin_idx; i < n_greedily_consumed_values; ++i) { + expected_greedy_arg_values.emplace_back(init_arg_flag_primary(i)); + argv_vec.emplace_back(init_arg_flag_primary(i)); + } + + // add the normally parsed flags + const std::size_t expected_opt_count = 1ull; + for (std::size_t i = opt_begin_idx; i < n_greedily_consumed_values; ++i) + argv_vec.emplace_back(init_arg_flag_primary(i)); + + 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.values(greedy_arg_name), expected_greedy_arg_values); + for (std::size_t i = opt_begin_idx; i < n_greedily_consumed_values; ++i) + CHECK_EQ(sut.count(init_arg_name_primary(i)), expected_opt_count); + + free_argv(argc, argv); +} + // unknown_arguments_policy TEST_CASE_FIXTURE( From 321bbbbb9776f263baf902985605002f400c76ee Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 21 Sep 2025 15:44:41 +0200 Subject: [PATCH 4/6] docs update --- README.md | 15 ++++++-- docs/tutorial.md | 76 +++++++++++++++++++++++++++++++++++------ include/ap/argument.hpp | 4 ++- 3 files changed, 82 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6cec0ddd..6f928ac0 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,19 @@ Command-line argument parser for C++20 - [The Parser Class](/docs/tutorial.md#the-parser-class) - [Adding Arguments](/docs/tutorial.md#adding-arguments) - [Argument Parameters](/docs/tutorial.md#argument-parameters) - - [Common Parameters](/docs/tutorial.md##common-parameters) - - [Parameters Specific for Optional Arguments](/docs/tutorial.md##parameters-specific-for-optional-arguments) + - [Common Parameters](#common-parameters) + - [help](#1-help---the-arguments-description-which-will-be-printed-when-printing-the-parser-class-instance) + - [hidden](#2-hidden---if-this-option-is-set-for-an-argument-then-it-will-not-be-included-in-the-program-description) + - [required](#3-required---if-this-option-is-set-for-an-argument-and-its-value-is-not-passed-in-the-command-line-an-exception-will-be-thrown) + - [bypass required](#4-bypass_required---if-this-option-is-set-for-an-argument-the-required-option-for-other-arguments-will-be-discarded-if-the-bypassing-argument-is-used-in-the-command-line) + - [nargs](#5-nargs---sets-the-allowed-number-of-values-to-be-parsed-for-an-argument) + - [greedy](#6-greedy---if-this-option-is-set-the-argument-will-consume-all-command-line-values-until-its-upper-nargs-bound-is-reached) + - [choices](#7-choices---a-list-of-valid-argument-values) + - [value actions](#8-value-actions---functions-that-are-called-after-parsing-an-arguments-value) + - [default values](#9-default_values---a-list-of-values-which-will-be-used-if-no-values-for-an-argument-have-been-parsed) + - [Parameters Specific for Optional Arguments](#parameters-specific-for-optional-arguments) + - [on-flag actions](#1-on-flag-actions---functions-that-are-called-immediately-after-parsing-an-arguments-flag) + - [implicit values](#2-implicit_values---a-list-of-values-which-will-be-set-for-an-argument-if-only-its-flag-but-no-values-are-parsed-from-the-command-line) - [Default Arguments](/docs/tutorial.md#default-arguments) - [Parsing Arguments](/docs/tutorial.md#parsing-arguments) - [Basic Argument Parsing Rules](/docs/tutorial.md#basic-argument-parsing-rules) diff --git a/docs/tutorial.md b/docs/tutorial.md index 428c7d15..d1bf0e4c 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -8,7 +8,18 @@ - [Adding Arguments](#adding-arguments) - [Argument Parameters](#argument-parameters) - [Common Parameters](#common-parameters) + - [help](#1-help---the-arguments-description-which-will-be-printed-when-printing-the-parser-class-instance) + - [hidden](#2-hidden---if-this-option-is-set-for-an-argument-then-it-will-not-be-included-in-the-program-description) + - [required](#3-required---if-this-option-is-set-for-an-argument-and-its-value-is-not-passed-in-the-command-line-an-exception-will-be-thrown) + - [bypass required](#4-bypass_required---if-this-option-is-set-for-an-argument-the-required-option-for-other-arguments-will-be-discarded-if-the-bypassing-argument-is-used-in-the-command-line) + - [nargs](#5-nargs---sets-the-allowed-number-of-values-to-be-parsed-for-an-argument) + - [greedy](#6-greedy---if-this-option-is-set-the-argument-will-consume-all-command-line-values-until-its-upper-nargs-bound-is-reached) + - [choices](#7-choices---a-list-of-valid-argument-values) + - [value actions](#8-value-actions---functions-that-are-called-after-parsing-an-arguments-value) + - [default values](#9-default_values---a-list-of-values-which-will-be-used-if-no-values-for-an-argument-have-been-parsed) - [Parameters Specific for Optional Arguments](#parameters-specific-for-optional-arguments) + - [on-flag actions](#1-on-flag-actions---functions-that-are-called-immediately-after-parsing-an-arguments-flag) + - [implicit values](#2-implicit_values---a-list-of-values-which-will-be-set-for-an-argument-if-only-its-flag-but-no-values-are-parsed-from-the-command-line) - [Predefined Parameter Values](#predefined-parameter-values) - [Default Arguments](#default-arguments) - [Parsing Arguments](#parsing-arguments) @@ -189,9 +200,10 @@ parser.add__argument("argument", "a"); > > - If the argument's value type is `ap::none_type`, the argument will not accept any values and therefore no value-related parameters can be set for such argument. This includes: > - [nargs](#5-nargs---sets-the-allowed-number-of-values-to-be-parsed-for-an-argument-this-can-be-set-as-a) -> - [choices](#6-choices---a-list-of-valid-argument-values) -> - [value actions](#7-value-actions---function-performed-after-parsing-an-arguments-value) -> - [default_values](#8-default_values---a-list-of-values-which-will-be-used-if-no-values-for-an-argument-have-been-parsed) +> - [greedy](#6-greedy---if-this-option-is-set-the-argument-will-consume-all-command-line-values-until-its-upper-nargs-bound-is-reached) +> - [choices](#7-choices---a-list-of-valid-argument-values) +> - [value actions](#8-value-actions---functions-that-are-called-after-parsing-an-arguments-value) +> - [default_values](#9-default_values---a-list-of-values-which-will-be-used-if-no-values-for-an-argument-have-been-parsed) > - [implicit_values](#2-implicit_values---a-list-of-values-which-will-be-set-for-an-argument-if-only-its-flag-but-no-values-are-parsed-from-the-command-line) You can also add boolean flags: @@ -343,8 +355,8 @@ Command Result > [!NOTE] > -> - Both positional and optional arguments have the `bypass_required` option disabled. -> - The default value of the value parameter of the `bypass_required(bool)` function is `true` for both positional and optional arguments. +> - Both all arguments have the `bypass_required` option disabled. +> - The default value of the value parameter of the `argument::bypass_required(bool)` method is `true` for all arguments. > [!WARNING] > @@ -377,7 +389,9 @@ os << data << std::endl;
-#### 5. `nargs` - Sets the allowed number of values to be parsed for an argument. This can be set as a: +#### 5. `nargs` - Sets the allowed number of values to be parsed for an argument. + +The `nargs` parameter can be set as: - Specific number: @@ -416,7 +430,49 @@ os << data << std::endl;
-#### 6. `choices` - A list of valid argument values. +#### 6. `greedy` - If this option is set, the argument will consume ALL command-line values until it's upper nargs bound is reached. + +> [!NOTE] +> +> - By default the `greedy` option is disabled for all arguments. +> - The default value of the parameter of the `argument::greedy(bool)` method is true for all arguments. + +> [!TIP] +> +> - Enabling the `greedy` option for an argument only makes sense for arguments with string-like value types. +> - If no explicit `nargs` bound is set for a greedy argument, once it starts being parsed, it will consume all remaining command-line arguments. + +Consider a simple example: + +```cpp +ap::argument_parser parser; +parser.program_name("run-script") + .default_arguments(ap::default_argument::o_help); + +parser.add_positional_argument("script") + .help("The name of the script to run"); +parser.add_optional_argument("args") + .greedy() + .help("Set the execution option"); + +parser.try_parse_args(argc, argv); + +// Application logic here +std::cout << "Executing: " << parser.value("script") << " " << ap::util::join(parser.values("args")) << std::endl; +``` + +Here the program execution should look something like this: + +```txt +> ./run-script remove-comments --args module.py -v --type py +Executing: remove-comments module.py -v --type py +``` + +Notice that even though the `-v` and `--type` command-line arguments have flag prefixes and are not defined in the program, they are not treated as unknown arguments (and therefore no exception is thrown) because the `--args` argument is marked as `greedy` and it consumes these command-line arguments as its values. + +
+ +#### 7. `choices` - A list of valid argument values. ```cpp parser.add_optional_argument("method", "m").choices('a', 'b', 'c'); @@ -433,7 +489,7 @@ parser.add_optional_argument("method", "m").choices('a', 'b', 'c');
-#### 7. Value actions - Function performed after parsing an argument's value. +#### 8. value actions - Functions that are called after parsing an argument's value. Actions are represented as functions, which take the argument's value as an argument. The available action types are: - `observe` actions | `void(const value_type&)` - applied to the parsed value. No value is returned - this action type is used to perform some logic on the parsed value without modifying it. @@ -478,7 +534,7 @@ Actions are represented as functions, which take the argument's value as an argu
-#### 8. `default_values` - A list of values which will be used if no values for an argument have been parsed +#### 9. `default_values` - A list of values which will be used if no values for an argument have been parsed > [!WARNING] > @@ -541,7 +597,7 @@ Command Result Apart from the common parameters listed above, for optional arguments you can also specify the following parameters: -#### 1. On-flag actions - For optional arguments, apart from value actions, you can specify on-flag actions which are executed immediately after parsing an argument's flag. +#### 1. on-flag actions - Functions that are called immediately after parsing an argument's flag. ```cpp void print_debug_info() noexcept { diff --git a/include/ap/argument.hpp b/include/ap/argument.hpp index 4b131d5f..8fcb75e6 100644 --- a/include/ap/argument.hpp +++ b/include/ap/argument.hpp @@ -181,7 +181,9 @@ class argument : public detail::argument_base { * @param g The attribute value. * @return Reference to the argument instance. */ - argument& greedy(const bool g = true) noexcept { + argument& greedy(const bool g = true) noexcept + requires(not util::c_is_none) + { this->_greedy = g; return *this; } From 5201419f70de7a8ac5afeab8cb60029b8a18969d Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 21 Sep 2025 15:46:08 +0200 Subject: [PATCH 5/6] doc refs fix --- README.md | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6f928ac0..bb765a20 100644 --- a/README.md +++ b/README.md @@ -54,19 +54,8 @@ Command-line argument parser for C++20 - [The Parser Class](/docs/tutorial.md#the-parser-class) - [Adding Arguments](/docs/tutorial.md#adding-arguments) - [Argument Parameters](/docs/tutorial.md#argument-parameters) - - [Common Parameters](#common-parameters) - - [help](#1-help---the-arguments-description-which-will-be-printed-when-printing-the-parser-class-instance) - - [hidden](#2-hidden---if-this-option-is-set-for-an-argument-then-it-will-not-be-included-in-the-program-description) - - [required](#3-required---if-this-option-is-set-for-an-argument-and-its-value-is-not-passed-in-the-command-line-an-exception-will-be-thrown) - - [bypass required](#4-bypass_required---if-this-option-is-set-for-an-argument-the-required-option-for-other-arguments-will-be-discarded-if-the-bypassing-argument-is-used-in-the-command-line) - - [nargs](#5-nargs---sets-the-allowed-number-of-values-to-be-parsed-for-an-argument) - - [greedy](#6-greedy---if-this-option-is-set-the-argument-will-consume-all-command-line-values-until-its-upper-nargs-bound-is-reached) - - [choices](#7-choices---a-list-of-valid-argument-values) - - [value actions](#8-value-actions---functions-that-are-called-after-parsing-an-arguments-value) - - [default values](#9-default_values---a-list-of-values-which-will-be-used-if-no-values-for-an-argument-have-been-parsed) - - [Parameters Specific for Optional Arguments](#parameters-specific-for-optional-arguments) - - [on-flag actions](#1-on-flag-actions---functions-that-are-called-immediately-after-parsing-an-arguments-flag) - - [implicit values](#2-implicit_values---a-list-of-values-which-will-be-set-for-an-argument-if-only-its-flag-but-no-values-are-parsed-from-the-command-line) + - [Common Parameters](/docs/tutorial.md#common-parameters) + - [Parameters Specific for Optional Arguments](/docs/tutorial.md#parameters-specific-for-optional-arguments) - [Default Arguments](/docs/tutorial.md#default-arguments) - [Parsing Arguments](/docs/tutorial.md#parsing-arguments) - [Basic Argument Parsing Rules](/docs/tutorial.md#basic-argument-parsing-rules) From 94e0624635607ab6a1823eef0f151c79108acf4b Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 21 Sep 2025 16:27:53 +0200 Subject: [PATCH 6/6] resolved comments --- README.md | 4 ++-- include/ap/argument.hpp | 1 + include/ap/argument_parser.hpp | 29 ++++++++++++++++++++++------- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index bb765a20..2187d65e 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,8 @@ Command-line argument parser for C++20 - [The Parser Class](/docs/tutorial.md#the-parser-class) - [Adding Arguments](/docs/tutorial.md#adding-arguments) - [Argument Parameters](/docs/tutorial.md#argument-parameters) - - [Common Parameters](/docs/tutorial.md#common-parameters) - - [Parameters Specific for Optional Arguments](/docs/tutorial.md#parameters-specific-for-optional-arguments) + - [Common Parameters](/docs/tutorial.md#common-parameters) + - [Parameters Specific for Optional Arguments](/docs/tutorial.md#parameters-specific-for-optional-arguments) - [Default Arguments](/docs/tutorial.md#default-arguments) - [Parsing Arguments](/docs/tutorial.md#parsing-arguments) - [Basic Argument Parsing Rules](/docs/tutorial.md#basic-argument-parsing-rules) diff --git a/include/ap/argument.hpp b/include/ap/argument.hpp index 8fcb75e6..56ac3310 100644 --- a/include/ap/argument.hpp +++ b/include/ap/argument.hpp @@ -180,6 +180,7 @@ class argument : public detail::argument_base { * @brief Set the `greedy` attribute of the argument. * @param g The attribute value. * @return Reference to the argument instance. + * @note The method is enabled only if `value_type` is not `none_type`. */ argument& greedy(const bool g = true) noexcept requires(not util::c_is_none) diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index 0d345f7e..8e5cccd0 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -922,6 +922,15 @@ class argument_parser { } } + /** + * @brief Returns the most appropriate *initial* token type based on a command-line argument's value. + * + * The token's *initial* type is deduced using the following rules: + * - `t_value`: an argument contains whitespace characters or cannot be a flag token + * - `t_flag_primary`: an argument begins with a primary flag prefix (`--`) + * - `t_flag_secondary`: an argument begins with a secondary flag prefix (`-`) + * - `t_flag_compound`: INITIALLY a token can NEVER have a compound flag type (may only be set when a flag token is validated) + */ [[nodiscard]] detail::argument_token::token_type _deduce_token_type( const std::string_view arg_value ) const noexcept { @@ -939,9 +948,9 @@ class argument_parser { /** * @brief Check if a flag token is valid based on its value. - * @attention Sets the `arg` member of the token if an argument with the given name (token's value) is present. + * @attention Sets the `args` member of the token if an argument with the given name (token's value) is present. * @param tok The argument token to validate. - * @return true if the given token represents a valid argument flag. + * @return `true` if the given token represents a valid argument flag. */ [[nodiscard]] bool _validate_flag_token(detail::argument_token& tok) noexcept { const auto opt_arg_it = this->_find_opt_arg(tok); @@ -952,7 +961,13 @@ class argument_parser { return true; } - // TODO: add doc comment + /** + * @brief Check if a flag token is a valid compound argument flag based on its value. + * @attention If the token indeed represents valid compound flag, the token's type is changed to `t_flag_compuund` + * @attention and its `args` list is filled with all the arguments the token represents. + * @param tok The argument token to validate. + * @return `true` if the given token represents a valid compound argument flag. + */ bool _validate_compound_flag_token(detail::argument_token& tok) noexcept { if (tok.type != detail::argument_token::t_flag_secondary) return false; @@ -967,8 +982,11 @@ class argument_parser { std::string_view(&c, 1ull), detail::argument_name::m_secondary ) ); - if (opt_arg_it == this->_optional_args.end()) + + if (opt_arg_it == this->_optional_args.end()) { + tok.args.clear(); return false; + } tok.args.emplace_back(*opt_arg_it); } @@ -1023,9 +1041,6 @@ class argument_parser { * @throws ap::parsing_failure */ void _parse_args_impl(const arg_token_list_t& arg_tokens, parsing_state& state) { - // if (state.curr_pos_arg_it != this->_positional_args.end()) - // state.curr_arg = *state.curr_pos_arg_it; - // process argument tokens std::ranges::for_each( arg_tokens, std::bind_front(&argument_parser::_parse_token, this, std::ref(state))