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/README.md b/README.md index 6cec0ddd..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/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 81a74533..56ac3310 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,19 @@ 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. + * @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) + { + this->_greedy = g; + return *this; + } + /** * @brief Set the nargs range for the argument. * @param range The attribute value. @@ -193,8 +211,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 +224,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 +687,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 +699,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..8e5cccd0 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; } @@ -928,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 { @@ -943,76 +946,75 @@ 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. + * @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); if (opt_arg_it == this->_optional_args.end()) - return false; + return this->_validate_compound_flag_token(tok); - tok.arg = *opt_arg_it; + tok.args.emplace_back(*opt_arg_it); 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 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. */ - [[nodiscard]] std::vector _try_split_compound_flag( - const detail::argument_token& tok - ) noexcept { - std::vector compound_toks; - const auto actual_tok_value = this->_strip_flag_prefix(tok); - - compound_toks.reserve(actual_tok_value.size()); - + bool _validate_compound_flag_token(detail::argument_token& tok) noexcept { if (tok.type != detail::argument_token::t_flag_secondary) - return compound_toks; + 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) { - 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; + 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()) { + tok.args.clear(); + return false; } - compound_toks.emplace_back(std::move(ctok)); + + tok.args.emplace_back(*opt_arg_it); } - return compound_toks; + tok.type = detail::argument_token::t_flag_compound; + return true; + } + + /** + * @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) + ); } /** @@ -1039,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)) @@ -1050,59 +1049,84 @@ 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) { - switch (tok.type) { - case detail::argument_token::t_flag_primary: - [[fallthrough]]; - case detail::argument_token::t_flag_secondary: { - 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 (state.curr_arg and state.curr_arg->is_greedy()) { + this->_set_argument_value(state, tok.value); + return; + } + + if (tok.is_flag_token()) + this->_parse_flag_token(state, tok); + else + this->_parse_value_token(state, tok); + } + + /** + * @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); + } + } - if (tok.arg->mark_used()) - state.curr_arg = tok.arg; + for (const auto& arg : tok.args) { + if (arg->mark_used()) + state.curr_arg = arg; else state.curr_arg.reset(); - - break; } - 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; - } - - 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(); + /** + * @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; } - break; - } + state.curr_arg = *state.curr_pos_arg_it; } + + this->_set_argument_value(state, tok.value); + } + + /** + * @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 + + // advance to the next positional argument if possible + if (state.curr_arg->is_positional() + and state.curr_pos_arg_it != this->_positional_args.end()) + ++state.curr_pos_arg_it; + + state.curr_arg.reset(); } /** @@ -1173,28 +1197,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..90ae3406 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,41 @@ 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 (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. */ [[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 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_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( 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()); }