diff --git a/include/argon/argument_group.hpp b/include/argon/argument_group.hpp index fe1d1b09..b6fe0e00 100644 --- a/include/argon/argument_group.hpp +++ b/include/argon/argument_group.hpp @@ -86,6 +86,32 @@ class argument_group { return *this; } + /** + * @brief Set the `prefix` attribute of the group. + * + * If set, each argument added to the group will have the given value prepended to its name. + * + * @param prefix The value to set for the attribute. + * @return Reference to the group instance. + */ + argument_group& with_prefix(std::string_view prefix) noexcept { + this->_prefix = prefix; + return *this; + } + + /** + * @brief Set the `suffix` attribute of the group. + * + * If set, each argument added to the group will have the given value appended to its name. + * + * @param suffix The value to set for the attribute. + * @return Reference to the group instance. + */ + argument_group& with_suffix(std::string_view suffix) noexcept { + this->_suffix = suffix; + return *this; + } + friend class argument_parser; private: @@ -101,18 +127,25 @@ class argument_group { return std::unique_ptr(new argument_group(parser, name)); } - /// Construct a new argument group with the given name. + /// @brief Construct a new argument group with the given name. argument_group(argument_parser& parser, const std::string_view name) : _parser(&parser), _name(name) {} - /// Add a new argument to this group (called internally by parser). + /// @brief Add a new argument to this group (called internally by parser). void _add_argument(arg_ptr_t arg) noexcept { this->_arguments.emplace_back(std::move(arg)); } + /// @brief Format the argument's base name by adding the group's prefix and suffix. + [[nodiscard]] std::string _format_arg_name(std::string_view arg_base_name) const noexcept { + return std::format("{}{}{}", this->_prefix, arg_base_name, this->_suffix); + } + argument_parser* _parser; ///< Pointer to the owning parser. std::string _name; ///< Name of the group (used in help output). - arg_ptr_vec_t _arguments; ///< A list of arguments that belong to this group. + std::string _prefix = ""; ///< Common argument flag prefix. + std::string _suffix = ""; ///< Common argument flag suffix. + arg_ptr_vec_t _arguments = {}; ///< A list of arguments that belong to this group. bool _hidden : 1 = false; ///< The hidden attribute value (default: false). bool _required : 1 = false; ///< The required attribute value (default: false). diff --git a/include/argon/argument_parser.hpp b/include/argon/argument_parser.hpp index c1548d4c..60baf404 100644 --- a/include/argon/argument_parser.hpp +++ b/include/argon/argument_parser.hpp @@ -246,13 +246,13 @@ class argument_parser { /** * @brief Add default arguments to the argument parser. - * @tparam AR Type of the positional argument discriminator range. + * @tparam ArgvRange Type of the positional argument discriminator range. * @param arg_discriminators A range of default positional argument discriminators. * @note `arg_discriminators` must be a `std::ranges::range` with the `argon::default_argument` value type. * @return Reference to the argument parser. */ - template AR> - argument_parser& default_arguments(const AR& arg_discriminators) noexcept { + template ArgvRange> + argument_parser& default_arguments(const ArgvRange& arg_discriminators) noexcept { for (const auto arg_discriminator : arg_discriminators) detail::add_default_argument(arg_discriminator, *this); return *this; @@ -284,30 +284,32 @@ class argument_parser { /** * @brief Adds a positional argument to the parser's configuration. * @tparam T Type of the argument value. - * @param name The name of the argument. + * @param base_name The base name of the argument. * @return Reference to the added positional argument. * @throws argon::invalid_configuration */ template - positional_argument& add_positional_argument(const std::string_view name) { - return this->add_positional_argument(this->_gr_positional_args, name); + positional_argument& add_positional_argument(const std::string_view base_name) { + return this->add_positional_argument(this->_gr_positional_args, base_name); } /** * @brief Adds a positional argument to the parser's configuration and binds it to the given group. * @tparam T Type of the argument value. - * @param primary_name The name of the argument. + * @param base_name The base name of the argument. * @return Reference to the added positional argument. * @throws argon::invalid_configuration */ template positional_argument& add_positional_argument( - argument_group& group, const std::string_view name + argument_group& group, const std::string_view base_name ) { this->_validate_group(group); - this->_verify_arg_name_pattern(name); - const detail::argument_name arg_name(std::make_optional(name)); + const auto full_name = group._format_arg_name(base_name); + this->_verify_arg_name_pattern(full_name); + + const detail::argument_name arg_name(std::make_optional(full_name)); if (this->_is_arg_name_used(arg_name)) throw invalid_configuration::argument_name_used(arg_name); @@ -320,33 +322,33 @@ class argument_parser { /** * @brief Adds an optional argument to the parser's configuration. * @tparam T Type of the argument value. - * @param name The name of the argument. + * @param base_name The base name of the argument. * @param name_discr The discriminator value specifying whether the given name should be treated as primary or secondary. * @return Reference to the added optional argument. * @throws argon::invalid_configuration */ template optional_argument& add_optional_argument( - const std::string_view name, + const std::string_view base_name, const detail::argument_name_discriminator name_discr = n_primary ) { - return this->add_optional_argument(this->_gr_optional_args, name, name_discr); + return this->add_optional_argument(this->_gr_optional_args, base_name, name_discr); } /** * @brief Adds an optional argument to the parser's configuration. * @tparam T Type of the argument value. - * @param primary_name The primary name of the argument. - * @param secondary_name The secondary name of the argument. + * @param base_primary_name The base primary name of the argument. + * @param base_secondary_name The base secondary name of the argument. * @return Reference to the added optional argument. * @throws argon::invalid_configuration */ template optional_argument& add_optional_argument( - const std::string_view primary_name, const std::string_view secondary_name + const std::string_view base_primary_name, const std::string_view base_secondary_name ) { return this->add_optional_argument( - this->_gr_optional_args, primary_name, secondary_name + this->_gr_optional_args, base_primary_name, base_secondary_name ); } @@ -354,7 +356,7 @@ class argument_parser { * @brief Adds an optional argument to the parser's configuration and binds it to the given group. * @tparam T Type of the argument value. * @param group The argument group to bind the new argument to. - * @param name The name of the argument. + * @param base_name The base name of the argument. * @param name_discr The discriminator value specifying whether the given name should be treated as primary or secondary. * @return Reference to the added optional argument. * @throws std::logic_error, argon::invalid_configuration @@ -362,18 +364,22 @@ class argument_parser { template optional_argument& add_optional_argument( argument_group& group, - const std::string_view name, + const std::string_view base_name, const detail::argument_name_discriminator name_discr = n_primary ) { this->_validate_group(group); - this->_verify_arg_name_pattern(name); + + const auto full_name = group._format_arg_name(base_name); + this->_verify_arg_name_pattern(full_name); const auto arg_name = name_discr == n_primary ? detail:: - argument_name{std::make_optional(name), std::nullopt, this->_flag_prefix_char} + argument_name{std::make_optional(full_name), std::nullopt, this->_flag_prefix_char} : detail::argument_name{ - std::nullopt, std::make_optional(name), this->_flag_prefix_char + std::nullopt, + std::make_optional(full_name), + this->_flag_prefix_char }; if (this->_is_arg_name_used(arg_name)) @@ -389,24 +395,28 @@ class argument_parser { * @brief Adds an optional argument to the parser's configuration and binds it to the given group. * @tparam T Type of the argument value. * @param group The argument group to bind the new argument to. - * @param primary_name The primary name of the argument. - * @param secondary_name The secondary name of the argument. + * @param base_primary_name The base primary name of the argument. + * @param base_secondary_name The base secondary name of the argument. * @return Reference to the added optional argument. * @throws argon::invalid_configuration */ template optional_argument& add_optional_argument( argument_group& group, - const std::string_view primary_name, - const std::string_view secondary_name + const std::string_view base_primary_name, + const std::string_view base_secondary_name ) { this->_validate_group(group); - this->_verify_arg_name_pattern(primary_name); - this->_verify_arg_name_pattern(secondary_name); + + const auto full_primary_name = group._format_arg_name(base_primary_name); + this->_verify_arg_name_pattern(full_primary_name); + + const auto full_secondary_name = group._format_arg_name(base_secondary_name); + this->_verify_arg_name_pattern(full_secondary_name); const detail::argument_name arg_name( - std::make_optional(primary_name), - std::make_optional(secondary_name), + std::make_optional(full_primary_name), + std::make_optional(full_secondary_name), this->_flag_prefix_char ); if (this->_is_arg_name_used(arg_name)) @@ -422,16 +432,16 @@ class argument_parser { * @brief Adds a boolean flag argument (an optional argument with `value_type = bool`) to the parser's configuration. * @tparam StoreImplicitly A boolean value used as the `implicit_values` parameter of the argument. * @note The argument's `default_values` attribute will be set to `not StoreImplicitly`. - * @param name The primary name of the flag. + * @param base_name The primary base name of the flag. * @param name_discr The discriminator value specifying whether the given name should be treated as primary or secondary. * @return Reference to the added boolean flag argument. */ template optional_argument& add_flag( - const std::string_view name, + const std::string_view base_name, const detail::argument_name_discriminator name_discr = n_primary ) { - return this->add_optional_argument(name, name_discr) + return this->add_optional_argument(base_name, name_discr) .default_values(not StoreImplicitly) .implicit_values(StoreImplicitly) .nargs(0ull); @@ -441,15 +451,15 @@ class argument_parser { * @brief Adds a boolean flag argument (an optional argument with `value_type = bool`) to the parser's configuration. * @tparam StoreImplicitly A boolean value used as the `implicit_values` parameter of the argument. * @note The argument's `default_values` attribute will be set to `not StoreImplicitly`. - * @param primary_name The primary name of the flag. - * @param secondary_name The secondary name of the flag. + * @param base_primary_name The base primary name of the flag. + * @param base_secondary_name The base secondary name of the flag. * @return Reference to the added boolean flag argument. */ template optional_argument& add_flag( - const std::string_view primary_name, const std::string_view secondary_name + const std::string_view base_primary_name, const std::string_view base_secondary_name ) { - return this->add_optional_argument(primary_name, secondary_name) + return this->add_optional_argument(base_primary_name, base_secondary_name) .default_values(not StoreImplicitly) .implicit_values(StoreImplicitly) .nargs(0ull); @@ -460,17 +470,17 @@ class argument_parser { * @tparam StoreImplicitly A boolean value used as the `implicit_values` parameter of the argument. * @note The argument's `default_values` attribute will be set to `not StoreImplicitly`. * @param group The argument group to bind the new argument to. - * @param name The primary name of the flag. + * @param base_name The base name of the flag. * @param name_discr The discriminator value specifying whether the given name should be treated as primary or secondary. * @return Reference to the added boolean flag argument. */ template optional_argument& add_flag( argument_group& group, - const std::string_view name, + const std::string_view base_name, const detail::argument_name_discriminator name_discr = n_primary ) { - return this->add_optional_argument(group, name, name_discr) + return this->add_optional_argument(group, base_name, name_discr) .default_values(not StoreImplicitly) .implicit_values(StoreImplicitly) .nargs(0ull); @@ -481,17 +491,17 @@ class argument_parser { * @tparam StoreImplicitly A boolean value used as the `implicit_values` parameter of the argument. * @note The argument's `default_values` attribute will be set to `not StoreImplicitly`. * @param group The argument group to bind the new argument to. - * @param primary_name The primary name of the flag. - * @param secondary_name The secondary name of the flag. + * @param base_primary_name The base primary name of the flag. + * @param base_secondary_name The base secondary name of the flag. * @return Reference to the added boolean flag argument. */ template optional_argument& add_flag( argument_group& group, - const std::string_view primary_name, - const std::string_view secondary_name + const std::string_view base_primary_name, + const std::string_view base_secondary_name ) { - return this->add_optional_argument(group, primary_name, secondary_name) + return this->add_optional_argument(group, base_primary_name, base_secondary_name) .default_values(not StoreImplicitly) .implicit_values(StoreImplicitly) .nargs(0ull); @@ -517,7 +527,7 @@ class argument_parser { ); if (subparser_it != this->_subparsers.end()) throw std::logic_error(std::format( - "A subparser with the given name () already exists in parser '{}'", + "A subparser with the given name ({}) already exists in parser '{}'", (*subparser_it)->_name, this->_program_name )); @@ -546,14 +556,14 @@ class argument_parser { /** * @brief Parses the command-line arguments. - * @tparam AR The argument range type. + * @tparam ArgvRange The argument range type. * @param argv_rng A range of command-line argument values. * @note `argv_rng` must be a `std::ranges::forward_range` with a value type convertible to `std::string`. * @throws argon::invalid_configuration, argon::parsing_failure * @attention This overload of the `parse_args` function assumes that the program name argument has already been discarded. */ - template AR> - void parse_args(const AR& argv_rng) { + template ArgvRange> + void parse_args(const ArgvRange& argv_rng) { parsing_state state(*this); this->_parse_args_impl(std::ranges::begin(argv_rng), std::ranges::end(argv_rng), state); @@ -586,13 +596,13 @@ class argument_parser { * message and the parser are printed to `std::cerr` and the function exists with * `EXIT_FAILURE` status. * - * @tparam AR The argument range type. + * @tparam ArgvRange The argument range type. * @param argv_rng A range of command-line argument values. * @note `argv_rng` must be a `std::ranges::forward_range` with a value type convertible to `std::string`. * @attention This overload of the `try_parse_args` function assumes that the program name argument has already been discarded. */ - template AR> - void try_parse_args(const AR& argv_rng) { + template ArgvRange> + void try_parse_args(const ArgvRange& argv_rng) { try { this->parse_args(argv_rng); } @@ -633,14 +643,14 @@ class argument_parser { * - `add_optional_argument` * - `add_flag` * - * @tparam AR The argument range type. + * @tparam ArgvRange The argument range type. * @param argv_rng A range of command-line argument values. * @note `argv_rng` must be a `std::ranges::forward_range` with a value type convertible to `std::string`. * @throws argon::invalid_configuration, argon::parsing_failure * @attention This overload of the `parse_known_args` function assumes that the program name argument already been discarded. */ - template AR> - std::vector parse_known_args(const AR& argv_rng) { + template ArgvRange> + std::vector parse_known_args(const ArgvRange& argv_rng) { parsing_state state(*this, true); this->_parse_args_impl(std::ranges::begin(argv_rng), std::ranges::end(argv_rng), state); return std::move(state.unknown_args); @@ -670,14 +680,14 @@ class argument_parser { * and the parser are printed to `std::cerr` and the function exists with `EXIT_FAILURE` status. * Otherwise the result of `parse_known_args(argv_rng)` is returned. * - * @tparam AR The argument range type. + * @tparam ArgvRange The argument range type. * @param argv_rng A range of command-line argument values. * @note `argv_rng` must be a `std::ranges::forward_range` with a value type convertible to `std::string`. * @return A vector of unknown argument values. * @attention This overload of the `try_parse_known_args` function assumes that the program name argument has already been discarded. */ - template AR> - std::vector try_parse_known_args(const AR& argv_rng) { + template ArgvRange> + std::vector try_parse_known_args(const ArgvRange& argv_rng) { try { return this->parse_known_args(argv_rng); } @@ -742,9 +752,8 @@ class argument_parser { * @param arg_name The name of the argument. * @return `true` if the argument was used on the command line, `false` otherwise. */ - [[nodiscard]] bool is_used(std::string_view arg_name) const noexcept { - const auto arg = this->_get_argument(arg_name); - return arg ? arg->is_used() : false; + [[nodiscard]] bool is_used(std::string_view arg_name) const { + return this->_get_argument(arg_name)->is_used(); } /** @@ -752,9 +761,8 @@ class argument_parser { * @param arg_name The name of the argument. * @return `true` if the argument has a value, `false` otherwise. */ - [[nodiscard]] bool has_value(std::string_view arg_name) const noexcept { - const auto arg = this->_get_argument(arg_name); - return arg ? arg->has_value() : false; + [[nodiscard]] bool has_value(std::string_view arg_name) const { + return this->_get_argument(arg_name)->has_value(); } /** @@ -762,9 +770,8 @@ class argument_parser { * @param arg_name The name of the argument. * @return The number of times the argument has been used. */ - [[nodiscard]] std::size_t count(std::string_view arg_name) const noexcept { - const auto arg = this->_get_argument(arg_name); - return arg ? arg->count() : 0ull; + [[nodiscard]] std::size_t count(std::string_view arg_name) const { + return this->_get_argument(arg_name)->count(); } /** @@ -777,12 +784,9 @@ class argument_parser { template [[nodiscard]] T value(std::string_view arg_name) const { const auto arg = this->_get_argument(arg_name); - if (not arg) - throw lookup_failure::argument_not_found(arg_name); - const auto& arg_value = arg->value(); try { - return std::any_cast(arg_value); + return std::any_cast(arg->value()); } catch (const std::bad_any_cast&) { throw type_error::invalid_value_type(arg->name()); @@ -801,12 +805,9 @@ class argument_parser { template U> [[nodiscard]] T value_or(std::string_view arg_name, U&& fallback_value) const { const auto arg = this->_get_argument(arg_name); - if (not arg) - throw lookup_failure::argument_not_found(arg_name); try { - const auto& arg_value = arg->value(); - return std::any_cast(arg_value); + return std::any_cast(arg->value()); } catch (const std::logic_error&) { // positional: no value parsed @@ -829,8 +830,6 @@ class argument_parser { template [[nodiscard]] std::vector values(std::string_view arg_name) const { const auto arg = this->_get_argument(arg_name); - if (not arg) - throw lookup_failure::argument_not_found(arg_name); try { std::vector values; @@ -1459,8 +1458,9 @@ class argument_parser { * @brief Get the argument with the specified name. * @param arg_name The name of the argument. * @return The argument with the specified name, if found; otherwise, std::nullopt. + * @throws argon::lookup_failure if an argument with the given name cannot be found. */ - arg_ptr_t _get_argument(std::string_view arg_name) const noexcept { + arg_ptr_t _get_argument(std::string_view arg_name) const { const auto predicate = this->_name_match_predicate(arg_name); if (auto pos_arg_it = std::ranges::find_if(this->_positional_args, predicate); @@ -1473,7 +1473,7 @@ class argument_parser { return *opt_arg_it; } - return nullptr; + throw lookup_failure::argument_not_found(arg_name); } void _print_subparsers(std::ostream& os) const noexcept { @@ -1550,20 +1550,21 @@ class argument_parser { } } - std::string _name; ///< The name of the parser. - std::string - _program_name; ///< The name of the program in the format "... ". - std::optional _program_version; ///< The version of the program. - std::optional _program_description; ///< The description of the program. + std::string _name = ""; ///< The name of the parser. + std::string _program_name = + ""; ///< The name of the program in the format "... ". + std::optional _program_version = std::nullopt; ///< The version of the program. + std::optional _program_description = + std::nullopt; ///< The description of the program. bool _verbose = false; ///< Verbosity flag. unknown_policy _unknown_policy = unknown_policy::fail; ///< Policy for unknown arguments. - arg_ptr_vec_t _positional_args; ///< The list of positional arguments. - arg_ptr_vec_t _optional_args; ///< The list of optional arguments. - arg_group_ptr_vec_t _argument_groups; ///< The list of argument groups. + arg_ptr_vec_t _positional_args = {}; ///< The list of positional arguments. + arg_ptr_vec_t _optional_args = {}; ///< The list of optional arguments. + arg_group_ptr_vec_t _argument_groups = {}; ///< The list of argument groups. argument_group& _gr_positional_args; ///< The positional argument group. argument_group& _gr_optional_args; ///< The optional argument group. - arg_parser_ptr_vec_t _subparsers; ///< The list of subparsers. + arg_parser_ptr_vec_t _subparsers = {}; ///< The list of subparsers. bool _invoked = false; ///< A flag indicating whether the parser has been invoked to parse arguments. diff --git a/tests/include/utility.hpp b/tests/include/utility.hpp index 916dea09..98b2cae3 100644 --- a/tests/include/utility.hpp +++ b/tests/include/utility.hpp @@ -5,7 +5,7 @@ namespace argon_testing { template -void discard_result(T&&) { +void discard(T&&) { // do nothing } diff --git a/tests/source/test_argument_parser_add_elements.cpp b/tests/source/test_argument_parser_args_cfg.cpp similarity index 85% rename from tests/source/test_argument_parser_add_elements.cpp rename to tests/source/test_argument_parser_args_cfg.cpp index b148e651..72ea0109 100644 --- a/tests/source/test_argument_parser_add_elements.cpp +++ b/tests/source/test_argument_parser_args_cfg.cpp @@ -8,10 +8,11 @@ using argon::argument; using argon::argument_parser; using argon::default_argument; using argon::invalid_configuration; +using argon::lookup_failure; -TEST_SUITE_BEGIN("test_argument_parser_add_elements"); +TEST_SUITE_BEGIN("test_argument_parser_args_cfg"); -struct test_argument_parser_add_elements : public argument_parser_test_fixture { +struct test_argument_parser_args_cfg : public argument_parser_test_fixture { const char flag_char = '-'; const std::string_view primary_name_1 = "primary_name_1"; @@ -37,7 +38,7 @@ struct test_argument_parser_add_elements : public argument_parser_test_fixture { }; TEST_CASE_FIXTURE( - test_argument_parser_add_elements, + test_argument_parser_args_cfg, "add_{positional,optional}_argument(primary) should throw if the passed argument name is " "invalid" ) { @@ -80,7 +81,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_elements, + test_argument_parser_args_cfg, "add_{positional,optional}_argument(primary, secondary) should throw if the primary name is " "invalid" ) { @@ -123,7 +124,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_elements, + test_argument_parser_args_cfg, "add_optional_argument(primary, secondary) should throw if the secondary name is " "invalid" ) { @@ -160,7 +161,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_elements, + test_argument_parser_args_cfg, "add_positional_argument should throw when adding an argument with a previously used name" ) { sut.add_positional_argument(primary_name_1); @@ -177,7 +178,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_elements, + test_argument_parser_args_cfg, "add_optional_argument should throw when adding an argument with a previously used name" ) { sut.add_optional_argument(primary_name_1, secondary_name_1); @@ -210,7 +211,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_elements, + test_argument_parser_args_cfg, "add_flag should return an optional argument reference with flag parameters" ) { const argument_test_fixture arg_fixture; @@ -237,7 +238,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_elements, + test_argument_parser_args_cfg, "add_flag should throw when adding and argument with a previously used name" ) { sut.add_flag(primary_name_1, secondary_name_1); @@ -270,8 +271,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_elements, - "default_arguments should add the specified positional arguments" + test_argument_parser_args_cfg, "default_arguments should add the specified positional arguments" ) { sut.default_arguments({default_argument::p_input, default_argument::p_output}); @@ -285,8 +285,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_elements, - "default_arguments should add the specified optional arguments" + test_argument_parser_args_cfg, "default_arguments should add the specified optional arguments" ) { sut.default_arguments( default_argument::o_help, default_argument::o_input, default_argument::o_output @@ -327,7 +326,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_elements, + test_argument_parser_args_cfg, "argument adding functions should throw if a group does not belong to the parser" ) { argument_parser different_parser("different-program"); @@ -376,7 +375,40 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_elements, + test_argument_parser_args_cfg, + "argument adding functions should properly apply the argument group's name modifiers" +) { + const auto group_name = "An Argument Group"; + const auto group_pre = "pre-"; + const auto group_suf = "-suf"; + + auto& group = sut.add_group(group_name).with_prefix(group_pre).with_suffix(group_suf); + + sut.add_positional_argument(group, "positional"); + sut.add_optional_argument(group, "optional"); + sut.add_flag(group, "flag"); + + const auto expected_err_msg = [](const auto& arg_name) -> std::string { + return lookup_failure::argument_not_found(arg_name).what(); + }; + + CHECK_THROWS_WITH_AS( + discard(sut.value("positional")), expected_err_msg("positional").c_str(), lookup_failure + ); + CHECK_THROWS_WITH_AS( + discard(sut.value("optional")), expected_err_msg("optional").c_str(), lookup_failure + ); + CHECK_THROWS_WITH_AS( + discard(sut.value("flag")), expected_err_msg("flag").c_str(), lookup_failure + ); + + CHECK_FALSE(sut.has_value("pre-positional-suf")); + CHECK_FALSE(sut.has_value("pre-optional-suf")); + CHECK_FALSE(sut.value("pre-flag-suf")); +} + +TEST_CASE_FIXTURE( + test_argument_parser_args_cfg, "add_subparser should throw if a subparser with the given name already exists" ) { constexpr std::string_view subparser_name = "subprogram"; @@ -386,7 +418,7 @@ TEST_CASE_FIXTURE( CHECK_THROWS_WITH_AS( sut.add_subparser(subparser_name), std::format( - "A subparser with the given name () already exists in parser '{}'", + "A subparser with the given name ({}) already exists in parser '{}'", subparser_name, sut.name() ) @@ -395,4 +427,4 @@ TEST_CASE_FIXTURE( ); } -TEST_SUITE_END(); // test_argument_parser_add_elements +TEST_SUITE_END(); // test_argument_parser_args_cfg diff --git a/tests/source/test_argument_parser_parse_args.cpp b/tests/source/test_argument_parser_parse_args.cpp index 6f7fc5a1..18cba109 100644 --- a/tests/source/test_argument_parser_parse_args.cpp +++ b/tests/source/test_argument_parser_parse_args.cpp @@ -5,6 +5,7 @@ using namespace argon_testing; using namespace argon::nargs; using argon::invalid_configuration; +using argon::lookup_failure; using argon::parsing_failure; using argon::unknown_policy; @@ -117,10 +118,10 @@ TEST_CASE_FIXTURE( TEST_CASE_FIXTURE( test_argument_parser_parse_args, - "_get_argument should return nullopt if there is no argument with given name present" + "_get_argument should throw if there is no argument with given name present" ) { add_arguments(n_positional_args, n_optional_args); - CHECK_FALSE(get_argument(invalid_arg_name)); + CHECK_THROWS_AS(discard(get_argument(invalid_arg_name)), lookup_failure); } TEST_CASE_FIXTURE( @@ -577,7 +578,7 @@ TEST_CASE_FIXTURE( auto argv = init_argv(n_positional_args, n_optional_args); REQUIRE_NOTHROW(sut.parse_args(argc, argv)); - CHECK_FALSE(sut.has_value(invalid_arg_name)); + CHECK_THROWS_AS(discard(sut.has_value(invalid_arg_name)), lookup_failure); free_argv(argc, argv); } @@ -587,10 +588,12 @@ TEST_CASE_FIXTURE( ) { add_arguments(n_positional_args, n_optional_args); - for (std::size_t i = 0ull; i < n_args_total; ++i) { - const auto arg_name = init_arg_name(i); - CHECK_FALSE(sut.has_value(arg_name.primary.value())); - CHECK_FALSE(sut.has_value(arg_name.secondary.value())); + for (std::size_t i = 0ull; i < n_positional_args; ++i) + CHECK_FALSE(sut.has_value(init_arg_name_primary(i))); + + for (std::size_t i = first_opt_arg_idx; i < n_args_total; ++i) { + CHECK_FALSE(sut.has_value(init_arg_name_primary(i))); + CHECK_FALSE(sut.has_value(init_arg_name_secondary(i))); } } @@ -621,19 +624,21 @@ TEST_CASE_FIXTURE( TEST_CASE_FIXTURE(test_argument_parser_parse_args, "count should return 0 by default") { add_arguments(n_positional_args, n_optional_args); - for (std::size_t i = 0ull; i < n_args_total; ++i) { - const auto arg_name = init_arg_name(i); - CHECK_EQ(sut.count(arg_name.primary.value()), 0ull); - CHECK_EQ(sut.count(arg_name.secondary.value()), 0ull); + for (std::size_t i = 0ull; i < n_positional_args; ++i) + CHECK_EQ(sut.count(init_arg_name_primary(i)), 0ull); + + for (std::size_t i = first_opt_arg_idx; i < n_args_total; ++i) { + CHECK_EQ(sut.count(init_arg_name_primary(i)), 0ull); + CHECK_EQ(sut.count(init_arg_name_secondary(i)), 0ull); } } TEST_CASE_FIXTURE( test_argument_parser_parse_args, - "count should return 0 if there is no argument with given name present" + "count should throw if there is no argument with given name present" ) { add_arguments(n_positional_args, n_optional_args); - CHECK_EQ(sut.count(invalid_arg_name), 0ull); + CHECK_THROWS_AS(discard(sut.count(invalid_arg_name)), lookup_failure); } TEST_CASE_FIXTURE( @@ -680,7 +685,7 @@ TEST_CASE_FIXTURE( "value() should throw if there is no argument with given name present" ) { add_arguments(n_positional_args, n_optional_args); - CHECK_THROWS_AS(discard_result(sut.value(invalid_arg_name)), argon::lookup_failure); + CHECK_THROWS_AS(discard(sut.value(invalid_arg_name)), argon::lookup_failure); } TEST_CASE_FIXTURE( @@ -690,11 +695,11 @@ TEST_CASE_FIXTURE( add_arguments(n_positional_args, n_optional_args); for (std::size_t i = 0ull; i < n_positional_args; ++i) - CHECK_THROWS_AS(discard_result(sut.value(init_arg_name_primary(i))), std::logic_error); + CHECK_THROWS_AS(discard(sut.value(init_arg_name_primary(i))), std::logic_error); for (std::size_t i = first_opt_arg_idx; i < n_args_total; ++i) { - CHECK_THROWS_AS(discard_result(sut.value(init_arg_name_primary(i))), std::logic_error); - CHECK_THROWS_AS(discard_result(sut.value(init_arg_name_secondary(i))), std::logic_error); + CHECK_THROWS_AS(discard(sut.value(init_arg_name_primary(i))), std::logic_error); + CHECK_THROWS_AS(discard(sut.value(init_arg_name_secondary(i))), std::logic_error); } } @@ -716,8 +721,7 @@ TEST_CASE_FIXTURE( REQUIRE(sut.has_value(arg_name.primary.value())); CHECK_THROWS_AS( - discard_result(sut.value(arg_name.primary.value())), - argon::type_error + discard(sut.value(arg_name.primary.value())), argon::type_error ); } @@ -784,9 +788,7 @@ TEST_CASE_FIXTURE( "value_or() should throw if there is no argument with given name present" ) { add_arguments(n_positional_args, n_optional_args); - CHECK_THROWS_AS( - discard_result(sut.value_or(invalid_arg_name, empty_str)), argon::lookup_failure - ); + CHECK_THROWS_AS(discard(sut.value_or(invalid_arg_name, empty_str)), argon::lookup_failure); } TEST_CASE_FIXTURE( @@ -807,8 +809,7 @@ TEST_CASE_FIXTURE( REQUIRE(sut.has_value(arg_name.primary.value())); CHECK_THROWS_AS( - discard_result( - sut.value_or(arg_name.primary.value(), invalid_value_type{}) + discard(sut.value_or(arg_name.primary.value(), invalid_value_type{}) ), argon::type_error ); @@ -947,7 +948,7 @@ TEST_CASE_FIXTURE( sut.parse_args(argc, argv); CHECK_THROWS_AS( - discard_result(sut.values(positional_name)), argon::type_error + discard(sut.values(positional_name)), argon::type_error ); free_argv(argc, argv); @@ -1040,12 +1041,10 @@ TEST_CASE_FIXTURE( sut.parse_args(argc, argv); CHECK_THROWS_AS( - discard_result(sut.values(optional_primary_name)), - argon::type_error + discard(sut.values(optional_primary_name)), argon::type_error ); CHECK_THROWS_AS( - discard_result(sut.values(optional_secondary_name)), - argon::type_error + discard(sut.values(optional_secondary_name)), argon::type_error ); free_argv(argc, argv); @@ -1493,6 +1492,62 @@ TEST_CASE_FIXTURE( CHECK_FALSE(sut.is_used(init_arg_name_primary(i))); } +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, "parse_args should properly handle group prefixes and suffixes" +) { + const std::string prefix = "mod_"; + const std::string suffix = "_mod"; + auto& group = sut.add_group("Modified Group").with_prefix(prefix).with_suffix(suffix); + + const std::string pos_base_name = "pos_arg"; + const std::string opt_base_name = "opt_arg"; + const std::string flag_base_name = "flag"; + + // Add arguments to the group + sut.add_positional_argument(group, pos_base_name); + sut.add_optional_argument(group, opt_base_name); + sut.add_flag(group, flag_base_name); + + // Deducing the expected registered names + const std::string expected_pos_name = prefix + pos_base_name + suffix; + const std::string expected_opt_name = prefix + opt_base_name + suffix; + const std::string expected_flag_name = prefix + flag_base_name + suffix; + + const std::string pos_val = "positional-value"; + const std::string opt_val = "optional-value"; + + std::vector argv_vec{ + "program", + pos_val, + std::format("--{}", expected_opt_name), + opt_val, + std::format("--{}", expected_flag_name), + }; + + const int argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + REQUIRE_NOTHROW(sut.parse_args(argc, argv)); + + // Verify arguments are parsed and accessible by their formatted names + CHECK(sut.has_value(expected_pos_name)); + CHECK_EQ(sut.value(expected_pos_name), pos_val); + + CHECK(sut.has_value(expected_opt_name)); + CHECK_EQ(sut.value(expected_opt_name), opt_val); + CHECK_EQ(sut.count(expected_opt_name), 1ull); + + CHECK(sut.has_value(expected_flag_name)); + CHECK(sut.value(expected_flag_name)); + + // Verify the arguments are NOT accessible by their unformatted base names + CHECK_THROWS_AS(discard(sut.has_value(pos_base_name)), lookup_failure); + CHECK_THROWS_AS(discard(sut.has_value(opt_base_name)), lookup_failure); + CHECK_THROWS_AS(discard(sut.has_value(flag_base_name)), lookup_failure); + + free_argv(argc, argv); +} + // subparsers TEST_CASE_FIXTURE(