From 4f0e520499d48b7ecf0e0472e26feddda4a82245 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Mon, 22 Sep 2025 00:09:47 +0200 Subject: [PATCH 1/9] initial design --- include/ap/argument.hpp | 2 + include/ap/argument_group.hpp | 52 +++++++ include/ap/argument_parser.hpp | 135 +++++++++++++++--- include/ap/detail/argument_base.hpp | 4 +- include/ap/detail/argument_token.hpp | 4 +- .../include/argument_parser_test_fixture.hpp | 10 +- 6 files changed, 181 insertions(+), 26 deletions(-) create mode 100644 include/ap/argument_group.hpp diff --git a/include/ap/argument.hpp b/include/ap/argument.hpp index 56ac3310..588f94bc 100644 --- a/include/ap/argument.hpp +++ b/include/ap/argument.hpp @@ -392,6 +392,8 @@ class argument : public detail::argument_base { return *this; } + // argument& in_group() + #ifdef AP_TESTING friend struct ::ap_testing::argument_test_fixture; #endif diff --git a/include/ap/argument_group.hpp b/include/ap/argument_group.hpp new file mode 100644 index 00000000..d1ee5fb4 --- /dev/null +++ b/include/ap/argument_group.hpp @@ -0,0 +1,52 @@ +// Copyright (c) 2023-2025 Jakub Musiał +// This file is part of the CPP-AP project (https://github.com/SpectraL519/cpp-ap). +// Licensed under the MIT License. See the LICENSE file in the project root for full license information. + +#pragma once + +#include "detail/argument_base.hpp" + +#include + +namespace ap { + +class argument_group { +public: + argument_group() = delete; + + argument_group& required(const bool r = true) noexcept { + this->_required = r; + return *this; + } + + argument_group& mutually_exclusive(const bool me = true) noexcept { + this->_mutually_exclusive = me; + return *this; + } + + friend class argument_parser; + +private: + using arg_ptr_t = std::shared_ptr; + using arg_ptr_vec_t = std::vector; + + static std::unique_ptr create(argument_parser& parser, std::string_view name) { + return std::unique_ptr(new argument_group(parser, name)); + } + + argument_group(argument_parser& parser, const std::string_view name) + : _parser(&parser), _name(name) {} + + void _add_argument(arg_ptr_t arg) noexcept { + this->_arguments.emplace_back(std::move(arg)); + } + + argument_parser* _parser; + std::string _name; + arg_ptr_vec_t _arguments; + + bool _required : 1 = false; + bool _mutually_exclusive : 1 = false; +}; + +} // namespace ap diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index 8e5cccd0..7ce47872 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -10,6 +10,7 @@ #pragma once #include "argument.hpp" +#include "argument_group.hpp" #include "detail/argument_token.hpp" #include "types.hpp" @@ -382,6 +383,48 @@ class argument_parser { return static_cast&>(*this->_optional_args.back()); } + /** + * @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 group The argument group to bind the new argument to. + * @param name The 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 ap::invalid_configuration + */ + template + optional_argument& add_optional_argument( + argument_group& group, + const std::string_view name, + const detail::argument_name_discriminator name_discr = n_primary + ) { + this->_validate_group(group); + auto& new_arg = this->add_optional_argument(name, name_discr); + group._add_argument(this->_optional_args.back()); + return new_arg; + } + + /** + * @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 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. + * @return Reference to the added optional argument. + * @throws ap::invalid_configuration + */ + template + optional_argument& add_optional_argument( + argument_group& group, + const std::string_view primary_name, + const std::string_view secondary_name + ) { + this->_validate_group(group); + auto& new_arg = this->add_optional_argument(primary_name, secondary_name); + group._add_argument(this->_optional_args.back()); + return new_arg; + } + /** * @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. @@ -419,6 +462,53 @@ class argument_parser { .nargs(0ull); } + /** + * @brief Adds a boolean flag argument (an optional argument with `value_type = bool`) to the parser's configuration and binds it to the given group. + * @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 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 detail::argument_name_discriminator name_discr = n_primary + ) { + this->_validate_group(group); + auto& new_arg = this->add_flag(name, name_discr); + group._add_argument(this->_optional_args.back()); + return new_arg; + } + + /** + * @brief Adds a boolean flag argument (an optional argument with `value_type = bool`) to the parser's configuration and binds it to the given group. + * @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. + * @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 + ) { + this->_validate_group(group); + auto& new_arg = this->add_flag(primary_name, secondary_name); + group._add_argument(this->_optional_args.back()); + return new_arg; + } + + // TODO: doc comment + argument_group& add_group(const std::string_view name) noexcept { + return *this->_argument_groups.emplace_back(argument_group::create(*this, name)); + } + /** * @brief Parses the command-line arguments. * @@ -742,17 +832,19 @@ class argument_parser { private: using arg_ptr_t = std::shared_ptr; - using arg_ptr_list_t = std::vector; - using arg_ptr_list_iter_t = typename arg_ptr_list_t::iterator; - using const_arg_opt_t = std::optional>; + using arg_ptr_vec_t = std::vector; + using arg_ptr_vec_iter_t = typename arg_ptr_vec_t::iterator; - using arg_token_list_t = std::vector; - using arg_token_list_iter_t = typename arg_token_list_t::const_iterator; + using arg_group_ptr_t = std::unique_ptr; + using arg_group_ptr_vec_t = std::vector; + // using arg_group_vec_iter_t = typename arg_group_vec_t::iterator; + + using arg_token_vec_t = std::vector; /// @brief A collection of values used during the parsing process. struct parsing_state { arg_ptr_t curr_arg; ///< The currently processed argument. - arg_ptr_list_iter_t + arg_ptr_vec_iter_t curr_pos_arg_it; ///< An iterator pointing to the next positional argument to be processed. std::vector unknown_args = {}; ///< A vector of unknown argument values. const bool parse_known_only = @@ -838,6 +930,14 @@ class argument_parser { return false; } + // TODO: doc comment + void _validate_group(const argument_group& group) { + if (group._parser != this) + throw std::logic_error(std::format( + "An argument group '{}' does not belong to the given parser.", group._name + )); + } + /** * @brief Validate whether the definition/configuration of the parser's arguments is correct. * @@ -846,16 +946,16 @@ class argument_parser { */ void _validate_argument_configuration() const { // step 1 - const_arg_opt_t non_required_arg = std::nullopt; + arg_ptr_t non_required_arg = nullptr; for (const auto& arg : this->_positional_args) { if (not arg->is_required()) { - non_required_arg = std::ref(*arg); + non_required_arg = arg; continue; } if (non_required_arg and arg->is_required()) throw invalid_configuration::positional::required_after_non_required( - arg->name(), non_required_arg->get().name() + arg->name(), non_required_arg->name() ); } } @@ -868,8 +968,8 @@ class argument_parser { * @return A list of preprocessed command-line argument tokens. */ template AR> - [[nodiscard]] arg_token_list_t _tokenize(const AR& arg_range, const parsing_state& state) { - arg_token_list_t toks; + [[nodiscard]] arg_token_vec_t _tokenize(const AR& arg_range, const parsing_state& state) { + arg_token_vec_t toks; if constexpr (std::ranges::sized_range) toks.reserve(std::ranges::size(arg_range)); @@ -888,7 +988,7 @@ class argument_parser { * @param arg_value The command-line argument's value to be processed. */ void _tokenize_arg( - const parsing_state& state, arg_token_list_t& toks, const std::string_view arg_value + const parsing_state& state, arg_token_vec_t& toks, const std::string_view arg_value ) { detail::argument_token tok{ .type = this->_deduce_token_type(arg_value), .value = std::string(arg_value) @@ -1001,7 +1101,7 @@ class argument_parser { * @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 + [[nodiscard]] arg_ptr_vec_iter_t _find_opt_arg(const detail::argument_token& flag_tok ) noexcept { if (not flag_tok.is_flag_token()) return this->_optional_args.end(); @@ -1040,7 +1140,7 @@ class argument_parser { * @param state The current parsing state. * @throws ap::parsing_failure */ - void _parse_args_impl(const arg_token_list_t& arg_tokens, parsing_state& state) { + void _parse_args_impl(const arg_token_vec_t& arg_tokens, parsing_state& state) { // process argument tokens std::ranges::for_each( arg_tokens, std::bind_front(&argument_parser::_parse_token, this, std::ref(state)) @@ -1202,7 +1302,7 @@ class argument_parser { * @param os The output stream to print to. * @param args The argument list to print. */ - void _print(std::ostream& os, const arg_ptr_list_t& args, const bool verbose) const noexcept { + void _print(std::ostream& os, const arg_ptr_vec_t& args, const bool verbose) const noexcept { auto visible_args = std::views::filter(args, [](const auto& arg) { return not arg->is_hidden(); }); @@ -1234,8 +1334,9 @@ class argument_parser { bool _verbose = false; unknown_policy _unknown_policy = unknown_policy::fail; - arg_ptr_list_t _positional_args; - arg_ptr_list_t _optional_args; + arg_ptr_vec_t _positional_args; + arg_ptr_vec_t _optional_args; + arg_group_ptr_vec_t _argument_groups; static constexpr std::uint8_t _primary_flag_prefix_length = 2u; static constexpr std::uint8_t _secondary_flag_prefix_length = 1u; diff --git a/include/ap/detail/argument_base.hpp b/include/ap/detail/argument_base.hpp index 7a76809a..6a5caccd 100644 --- a/include/ap/detail/argument_base.hpp +++ b/include/ap/detail/argument_base.hpp @@ -27,8 +27,6 @@ class argument_base { public: virtual ~argument_base() = default; - friend class ::ap::argument_parser; - /// @return `true` if the argument is a positional argument instance, `false` otherwise. virtual bool is_positional() const noexcept = 0; @@ -53,6 +51,8 @@ class argument_base { /// @return `true` if the argument is greedy, `false` otherwise. virtual bool is_greedy() const noexcept = 0; + friend class ::ap::argument_parser; + 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 90ae3406..8328e4df 100644 --- a/include/ap/detail/argument_token.hpp +++ b/include/ap/detail/argument_token.hpp @@ -17,7 +17,7 @@ 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; + using arg_ptr_vec_t = std::vector; /// @brief The token type discriminator. enum class token_type : std::uint8_t { @@ -73,7 +73,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_vec_t args = {}; ///< The corresponding argument }; } // namespace ap::detail diff --git a/tests/include/argument_parser_test_fixture.hpp b/tests/include/argument_parser_test_fixture.hpp index a4a0d4f5..db108a02 100644 --- a/tests/include/argument_parser_test_fixture.hpp +++ b/tests/include/argument_parser_test_fixture.hpp @@ -15,7 +15,7 @@ namespace ap_testing { struct argument_parser_test_fixture { using arg_ptr_t = ap::argument_parser::arg_ptr_t; - using arg_token_list_t = ap::argument_parser::arg_token_list_t; + using arg_token_vec_t = ap::argument_parser::arg_token_vec_t; using parsing_state = ap::argument_parser::parsing_state; @@ -140,10 +140,10 @@ struct argument_parser_test_fixture { add_optional_args(n_optional_args, n_positional_args); } - [[nodiscard]] arg_token_list_t init_arg_tokens( + [[nodiscard]] arg_token_vec_t init_arg_tokens( std::size_t n_positional_args, std::size_t n_optional_args ) { - arg_token_list_t arg_tokens; + arg_token_vec_t arg_tokens; arg_tokens.reserve(get_args_length(n_positional_args, n_optional_args)); for (std::size_t i = 0ull; i < n_positional_args; ++i) @@ -178,11 +178,11 @@ struct argument_parser_test_fixture { } // private function callers - [[nodiscard]] arg_token_list_t tokenize(int argc, char* argv[]) { + [[nodiscard]] arg_token_vec_t tokenize(int argc, char* argv[]) { return this->sut._tokenize(std::span(argv + 1, static_cast(argc - 1)), state); } - void parse_args_impl(const arg_token_list_t& arg_tokens) { + void parse_args_impl(const arg_token_vec_t& arg_tokens) { this->state.curr_arg = nullptr; this->state.curr_pos_arg_it = this->sut._positional_args.begin(); this->sut._parse_args_impl(arg_tokens, this->state); From 54845a1458b3bb53f1e2d9286cf7c514721071c5 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Mon, 22 Sep 2025 13:38:30 +0200 Subject: [PATCH 2/9] aligned help printing to use groups --- include/ap/argument_parser.hpp | 137 ++++++++++++++++++--------------- 1 file changed, 76 insertions(+), 61 deletions(-) diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index 7ce47872..74d682f3 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -168,7 +168,9 @@ class argument_parser { argument_parser(const argument_parser&) = delete; argument_parser& operator=(const argument_parser&) = delete; - argument_parser() = default; + argument_parser() + : _gr_positional_args(add_group("Positional Arguments")), + _gr_optional_args(add_group("Optional Arguments")) {} argument_parser(argument_parser&&) = default; argument_parser& operator=(argument_parser&&) = default; @@ -296,8 +298,10 @@ class argument_parser { if (this->_is_arg_name_used(arg_name)) throw invalid_configuration::argument_name_used(arg_name); - this->_positional_args.emplace_back(std::make_shared>(arg_name)); - return static_cast&>(*this->_positional_args.back()); + auto& new_arg_ptr = + this->_positional_args.emplace_back(std::make_shared>(arg_name)); + this->_gr_positional_args._add_argument(new_arg_ptr); + return static_cast&>(*new_arg_ptr); } /** @@ -322,8 +326,10 @@ class argument_parser { if (this->_is_arg_name_used(arg_name)) throw invalid_configuration::argument_name_used(arg_name); - this->_positional_args.emplace_back(std::make_shared>(arg_name)); - return static_cast&>(*this->_positional_args.back()); + auto& new_arg_ptr = + this->_positional_args.emplace_back(std::make_shared>(arg_name)); + this->_gr_positional_args._add_argument(new_arg_ptr); + return static_cast&>(*new_arg_ptr); } /** @@ -339,21 +345,7 @@ class argument_parser { const std::string_view name, const detail::argument_name_discriminator name_discr = n_primary ) { - this->_verify_arg_name_pattern(name); - - const auto arg_name = - name_discr == n_primary - ? detail:: - argument_name{std::make_optional(name), std::nullopt, this->_flag_prefix_char} - : detail::argument_name{ - std::nullopt, std::make_optional(name), this->_flag_prefix_char - }; - - if (this->_is_arg_name_used(arg_name)) - throw invalid_configuration::argument_name_used(arg_name); - - this->_optional_args.push_back(std::make_shared>(arg_name)); - return static_cast&>(*this->_optional_args.back()); + return this->add_optional_argument(this->_gr_optional_args, name, name_discr); } /** @@ -368,19 +360,9 @@ class argument_parser { optional_argument& add_optional_argument( const std::string_view primary_name, const std::string_view secondary_name ) { - this->_verify_arg_name_pattern(primary_name); - this->_verify_arg_name_pattern(secondary_name); - - const detail::argument_name arg_name( - std::make_optional(primary_name), - std::make_optional(secondary_name), - this->_flag_prefix_char + return this->add_optional_argument( + this->_gr_optional_args, primary_name, secondary_name ); - if (this->_is_arg_name_used(arg_name)) - throw invalid_configuration::argument_name_used(arg_name); - - this->_optional_args.emplace_back(std::make_shared>(arg_name)); - return static_cast&>(*this->_optional_args.back()); } /** @@ -390,7 +372,7 @@ class argument_parser { * @param name The 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 ap::invalid_configuration + * @throws std::logic_error, ap::invalid_configuration */ template optional_argument& add_optional_argument( @@ -399,9 +381,23 @@ class argument_parser { const detail::argument_name_discriminator name_discr = n_primary ) { this->_validate_group(group); - auto& new_arg = this->add_optional_argument(name, name_discr); - group._add_argument(this->_optional_args.back()); - return new_arg; + this->_verify_arg_name_pattern(name); + + const auto arg_name = + name_discr == n_primary + ? detail:: + argument_name{std::make_optional(name), std::nullopt, this->_flag_prefix_char} + : detail::argument_name{ + std::nullopt, std::make_optional(name), this->_flag_prefix_char + }; + + if (this->_is_arg_name_used(arg_name)) + throw invalid_configuration::argument_name_used(arg_name); + + auto& new_arg_ptr = + this->_optional_args.emplace_back(std::make_shared>(arg_name)); + group._add_argument(new_arg_ptr); + return static_cast&>(*new_arg_ptr); } /** @@ -419,10 +415,21 @@ class argument_parser { const std::string_view primary_name, const std::string_view secondary_name ) { - this->_validate_group(group); - auto& new_arg = this->add_optional_argument(primary_name, secondary_name); - group._add_argument(this->_optional_args.back()); - return new_arg; + this->_verify_arg_name_pattern(primary_name); + this->_verify_arg_name_pattern(secondary_name); + + const detail::argument_name arg_name( + std::make_optional(primary_name), + std::make_optional(secondary_name), + this->_flag_prefix_char + ); + if (this->_is_arg_name_used(arg_name)) + throw invalid_configuration::argument_name_used(arg_name); + + auto& new_arg_ptr = + this->_optional_args.emplace_back(std::make_shared>(arg_name)); + group._add_argument(new_arg_ptr); + return static_cast&>(*new_arg_ptr); } /** @@ -477,10 +484,10 @@ class argument_parser { const std::string_view name, const detail::argument_name_discriminator name_discr = n_primary ) { - this->_validate_group(group); - auto& new_arg = this->add_flag(name, name_discr); - group._add_argument(this->_optional_args.back()); - return new_arg; + return this->add_optional_argument(group, name, name_discr) + .default_values(not StoreImplicitly) + .implicit_values(StoreImplicitly) + .nargs(0ull); } /** @@ -498,10 +505,10 @@ class argument_parser { const std::string_view primary_name, const std::string_view secondary_name ) { - this->_validate_group(group); - auto& new_arg = this->add_flag(primary_name, secondary_name); - group._add_argument(this->_optional_args.back()); - return new_arg; + return this->add_optional_argument(group, primary_name, secondary_name) + .default_values(not StoreImplicitly) + .implicit_values(StoreImplicitly) + .nargs(0ull); } // TODO: doc comment @@ -799,14 +806,9 @@ class argument_parser { << std::string(this->_indent_width, ' ') << this->_program_description.value() << '\n'; - if (not this->_positional_args.empty()) { - os << "\nPositional arguments:\n"; - this->_print(os, this->_positional_args, verbose); - } - - if (not this->_optional_args.empty()) { - os << "\nOptional arguments:\n"; - this->_print(os, this->_optional_args, verbose); + for (const auto& group : this->_argument_groups) { + std::cout << '\n'; + this->_print_group(os, *group, verbose); } } @@ -1300,11 +1302,21 @@ class argument_parser { /** * @brief Print the given argument list to an output stream. * @param os The output stream to print to. - * @param args The argument list to print. + * @param group The argument group to print. + * @param verbose A verbosity mode indicator flag. */ - void _print(std::ostream& os, const arg_ptr_vec_t& args, const bool verbose) const noexcept { - auto visible_args = - std::views::filter(args, [](const auto& arg) { return not arg->is_hidden(); }); + void _print_group(std::ostream& os, const argument_group& group, const bool verbose) + const noexcept { + os << group._name << ":\n"; + + if (group._arguments.empty()) { + os << '\n' << std::string(this->_indent_width, ' ') << "No arguments" << '\n'; + return; + } + + auto visible_args = std::views::filter(group._arguments, [](const auto& arg) { + return not arg->is_hidden(); + }); if (verbose) { for (const auto& arg : visible_args) @@ -1312,7 +1324,7 @@ class argument_parser { } else { std::vector descriptors; - descriptors.reserve(args.size()); + descriptors.reserve(group._arguments.size()); for (const auto& arg : visible_args) descriptors.emplace_back(arg->desc(verbose)); @@ -1336,7 +1348,10 @@ class argument_parser { arg_ptr_vec_t _positional_args; arg_ptr_vec_t _optional_args; + arg_group_ptr_vec_t _argument_groups; + argument_group& _gr_positional_args; + argument_group& _gr_optional_args; static constexpr std::uint8_t _primary_flag_prefix_length = 2u; static constexpr std::uint8_t _secondary_flag_prefix_length = 1u; From 2876dcfae405e42f43455800fb88bde2d15dc741 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Mon, 22 Sep 2025 13:53:28 +0200 Subject: [PATCH 3/9] argument group state verification logic --- include/ap/argument_parser.hpp | 45 +++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index 74d682f3..1bc415f5 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -168,13 +168,13 @@ class argument_parser { argument_parser(const argument_parser&) = delete; argument_parser& operator=(const argument_parser&) = delete; + argument_parser(argument_parser&&) = delete; + argument_parser& operator=(argument_parser&&) = delete; + argument_parser() : _gr_positional_args(add_group("Positional Arguments")), _gr_optional_args(add_group("Optional Arguments")) {} - argument_parser(argument_parser&&) = default; - argument_parser& operator=(argument_parser&&) = default; - ~argument_parser() = default; /** @@ -552,10 +552,7 @@ class argument_parser { if (not state.unknown_args.empty()) throw parsing_failure::argument_deduction_failure(state.unknown_args); - if (not this->_are_required_args_bypassed()) { - this->_verify_required_args(); - this->_verify_nvalues(); - } + this->_verify_final_state(); } /** @@ -646,11 +643,7 @@ class argument_parser { }; this->_parse_args_impl(this->_tokenize(argv_rng, state), state); - if (not this->_are_required_args_bypassed()) { - this->_verify_required_args(); - this->_verify_nvalues(); - } - + this->_verify_final_state(); return std::move(state.unknown_args); } @@ -1231,6 +1224,17 @@ class argument_parser { state.curr_arg.reset(); } + // TODO: add doc comment + void _verify_final_state() const noexcept { + if (not this->_are_required_args_bypassed()) { + this->_verify_required_args(); + this->_verify_nvalues(); + } + + for (const auto& group : this->_argument_groups) + this->_verify_argument_group(*group); + } + /** * @brief Check whether required argument bypassing is enabled * @return true if at least one argument with enabled required argument bypassing is used, false otherwise. @@ -1278,6 +1282,23 @@ class argument_parser { throw parsing_failure::invalid_nvalues(arg->name(), nv_ord); } + // TODO: add doc comments + void _verify_argument_group(const argument_group& group) const { + const std::size_t n_used_args = + std::ranges::count_if(group._arguments, [](const auto& arg) { return arg->is_used(); }); + + if (group._required and n_used_args == 0ull) + throw parsing_failure(std::format( + "At least one argument from the required group '{}' must be used", group._name + )); + + if (group._mutually_exclusive and n_used_args > 1ull) + throw parsing_failure(std::format( + "At most one argument from the mutually exclusive group '{}' can be used", + group._name + )); + } + /** * @brief Get the argument with the specified name. * @param arg_name The name of the argument. From 49d18e7ce5da9778bf9b2b964add6bf626fdc58c Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Mon, 22 Sep 2025 14:21:07 +0200 Subject: [PATCH 4/9] error fix --- include/ap/argument_parser.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index 1bc415f5..d2a2d492 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -1225,7 +1225,7 @@ class argument_parser { } // TODO: add doc comment - void _verify_final_state() const noexcept { + void _verify_final_state() const { if (not this->_are_required_args_bypassed()) { this->_verify_required_args(); this->_verify_nvalues(); From 004082a663331ad63eafa13c32326a7bd7537a05 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Mon, 22 Sep 2025 22:58:01 +0200 Subject: [PATCH 5/9] added tests --- include/ap/argument_group.hpp | 2 + include/ap/argument_parser.hpp | 6 +- .../test_argument_parser_add_argument.cpp | 46 ++++++++++ .../test_argument_parser_parse_args.cpp | 90 ++++++++++++++++++- 4 files changed, 141 insertions(+), 3 deletions(-) diff --git a/include/ap/argument_group.hpp b/include/ap/argument_group.hpp index d1ee5fb4..8bf1e724 100644 --- a/include/ap/argument_group.hpp +++ b/include/ap/argument_group.hpp @@ -14,6 +14,8 @@ class argument_group { public: argument_group() = delete; + // TODO: description + argument_group& required(const bool r = true) noexcept { this->_required = r; return *this; diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index d2a2d492..bfcce6c4 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -415,6 +415,7 @@ class argument_parser { const std::string_view primary_name, const std::string_view secondary_name ) { + this->_validate_group(group); this->_verify_arg_name_pattern(primary_name); this->_verify_arg_name_pattern(secondary_name); @@ -1284,8 +1285,9 @@ class argument_parser { // TODO: add doc comments void _verify_argument_group(const argument_group& group) const { - const std::size_t n_used_args = - std::ranges::count_if(group._arguments, [](const auto& arg) { return arg->is_used(); }); + const auto n_used_args = static_cast( + std::ranges::count_if(group._arguments, [](const auto& arg) { return arg->is_used(); }) + ); if (group._required and n_used_args == 0ull) throw parsing_failure(std::format( diff --git a/tests/source/test_argument_parser_add_argument.cpp b/tests/source/test_argument_parser_add_argument.cpp index 23901bf4..957ac11e 100644 --- a/tests/source/test_argument_parser_add_argument.cpp +++ b/tests/source/test_argument_parser_add_argument.cpp @@ -5,6 +5,7 @@ using namespace ap_testing; using ap::argument; +using ap::argument_parser; using ap::default_argument; using ap::invalid_configuration; @@ -340,3 +341,48 @@ TEST_CASE_FIXTURE( REQUIRE(output_arg); CHECK(is_optional(*output_arg)); } + +TEST_CASE_FIXTURE( + test_argument_parser_add_argument, + "add_optional_argument and add_flag should throw if a group does not belong to the parser" +) { + argument_parser different_parser; + + const std::string group_name = "Group From a Different Parser"; + auto& group = different_parser.add_group(group_name); + + const std::string expected_err_msg = + std::format("An argument group '{}' does not belong to the given parser.", group_name); + + CHECK_THROWS_WITH_AS( + sut.add_optional_argument(group, primary_name_1), expected_err_msg.c_str(), std::logic_error + ); + + CHECK_THROWS_WITH_AS( + sut.add_optional_argument(group, secondary_name_1, ap::n_secondary), + expected_err_msg.c_str(), + std::logic_error + ); + + CHECK_THROWS_WITH_AS( + sut.add_optional_argument(group, primary_name_1, secondary_name_1), + expected_err_msg.c_str(), + std::logic_error + ); + + CHECK_THROWS_WITH_AS( + sut.add_flag(group, primary_name_1), expected_err_msg.c_str(), std::logic_error + ); + + CHECK_THROWS_WITH_AS( + sut.add_flag(group, secondary_name_1, ap::n_secondary), + expected_err_msg.c_str(), + std::logic_error + ); + + CHECK_THROWS_WITH_AS( + sut.add_flag(group, primary_name_1, secondary_name_1), + expected_err_msg.c_str(), + std::logic_error + ); +} diff --git a/tests/source/test_argument_parser_parse_args.cpp b/tests/source/test_argument_parser_parse_args.cpp index b8e5b419..943f48c8 100644 --- a/tests/source/test_argument_parser_parse_args.cpp +++ b/tests/source/test_argument_parser_parse_args.cpp @@ -12,7 +12,7 @@ struct test_argument_parser_parse_args : public argument_parser_test_fixture { const std::string_view test_program_name = "test program name"; const std::size_t no_args = 0ull; - const std::size_t n_positional_args = 5ull; + const std::size_t n_positional_args = 3ull; const std::size_t n_optional_args = n_positional_args; const std::size_t n_args_total = n_positional_args + n_optional_args; const std::size_t last_pos_arg_idx = n_positional_args - 1ull; @@ -1353,3 +1353,91 @@ TEST_CASE_FIXTURE( free_argv(argc, argv); } + +// argument groups + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should throw if no arguments from a required group are used" +) { + const std::string req_gr_name = "Required Group"; + auto& req_gr = sut.add_group(req_gr_name).required(); + + for (std::size_t i = 0ull; i < n_optional_args; ++i) + sut.add_optional_argument(req_gr, init_arg_name_primary(i)); + + const int argc = get_argc(no_args, no_args); + auto argv = init_argv(no_args, no_args); + + CHECK_THROWS_WITH_AS( + sut.parse_args(argc, argv), + std::format("At least one argument from the required group '{}' must be used", req_gr_name) + .c_str(), + parsing_failure + ); + + free_argv(argc, argv); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should throw when multiple arguments from a mutually exclusive group are used" +) { + const std::string me_gr_name = "Mutually Exclusive Group"; + auto& me_gr = sut.add_group(me_gr_name).mutually_exclusive(); + + for (std::size_t i = 0ull; i < n_optional_args; ++i) + sut.add_optional_argument(me_gr, init_arg_name_primary(i)); + + const int argc = get_argc(no_args, n_optional_args); + auto argv = init_argv(no_args, n_optional_args); + + CHECK_THROWS_WITH_AS( + sut.parse_args(argc, argv), + std::format( + "At most one argument from the mutually exclusive group '{}' can be used", me_gr_name + ) + .c_str(), + parsing_failure + ); + + free_argv(argc, argv); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should not throw when the group requirements are satisfied" +) { + const std::string req_me_gr_name = "Required & Mutually Exclusive Group"; + auto& req_me_gr = sut.add_group(req_me_gr_name).mutually_exclusive(); + + for (std::size_t i = 0ull; i < n_optional_args; ++i) + sut.add_optional_argument(req_me_gr, init_arg_name_primary(i)); + + // required group: >= 1 argument must be used + // mutually exclusive group: <= 1 argument can be used + // together: exactly one argument can be used + std::size_t used_arg_idx; + SUBCASE("used arg = 0") { + used_arg_idx = 0; + } + SUBCASE("used arg = 1") { + used_arg_idx = 1; + } + SUBCASE("used arg = 2") { + used_arg_idx = 2; + } + CAPTURE(used_arg_idx); + + std::vector argv_vec = { + "program", init_arg_flag_primary(used_arg_idx), init_arg_value(used_arg_idx) + }; + + const int argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + REQUIRE_NOTHROW(sut.parse_args(argc, argv)); + CHECK(sut.count(init_arg_name_primary(used_arg_idx))); + + free_argv(argc, argv); +} From d86a59597af169810ac963323c0ec6415fe6abe2 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Mon, 22 Sep 2025 23:34:32 +0200 Subject: [PATCH 6/9] added doc comments --- include/ap/argument_group.hpp | 74 +++++++++++++++++++++++++++++----- include/ap/argument_parser.hpp | 27 ++++++++++--- 2 files changed, 85 insertions(+), 16 deletions(-) diff --git a/include/ap/argument_group.hpp b/include/ap/argument_group.hpp index 8bf1e724..dc30bb8a 100644 --- a/include/ap/argument_group.hpp +++ b/include/ap/argument_group.hpp @@ -2,6 +2,8 @@ // This file is part of the CPP-AP project (https://github.com/SpectraL519/cpp-ap). // Licensed under the MIT License. See the LICENSE file in the project root for full license information. +/// @file ap/argument_group.hpp + #pragma once #include "detail/argument_base.hpp" @@ -10,17 +12,61 @@ namespace ap { +/** + * @brief Represents a group of arguments. + * + * Groups allow arguments to be organized under a dedicated section in the parser's help message. + * + * A group can be marked as: + * - required: **at least one** argument from the group must be used in the command-line + * - mutually exclusive: **at most one** argument from the group can be used in the command-line + * + * @note - This class is not intended to be constructed directly, but rather through the `add_group` method of @ref ap::argument_parser. + * @note - User defined groups may contain only optional arguments (and flags). + * + * Example usage: + * @code{.cpp} + * ap::argument_parser parser("myprog"); + * auto& out_opts = parser.add_group("Output Options").mutually_exclusive(); + * + * group.add_optional_argument(out_opts, "output", "o") + * .nargs(1) + * .help("Print output to the given file"); + * + * group.add_optional_argument(out_opts, "print", "p") + * .help("Print output to the console"); + * @endcode + * Here `out_opts` is a mutually exclusive group, so using both arguments at the same time would cause an error. + */ class argument_group { public: argument_group() = delete; - // TODO: description - + /** + * @brief Set the `required` attribute of the group. + * + * - If set to true, the parser will require at least one argument from the group to be used in the command-line. + * - If no arguments from the group are used, an exception will be thrown. + * - Argument groups are NOT required by default. + * + * @param r The value to set for the attribute (default: true). + * @return Reference to the group instance. + */ argument_group& required(const bool r = true) noexcept { this->_required = r; return *this; } + /** + * @brief Set the `mutually_exclusive` attribute of the group. + * + * - If set to true, the parser will allow at most one argument from the group to be used in the command-line. + * - If more than one argument from the group is used, an exception will be thrown. + * - Argument groups are NOT mutually exclusive by default. + * + * @param me The value to set for the attribute (default: true). + * @return Reference to the group instance. + */ argument_group& mutually_exclusive(const bool me = true) noexcept { this->_mutually_exclusive = me; return *this; @@ -29,26 +75,34 @@ class argument_group { friend class argument_parser; private: - using arg_ptr_t = std::shared_ptr; - using arg_ptr_vec_t = std::vector; - + using arg_ptr_t = std::shared_ptr; ///< The argument pointer type alias. + using arg_ptr_vec_t = std::vector; ///< The argument pointer list type alias. + + /** + * @brief Factory method to create an argument group. + * @param parser The owning parser. + * @param name Name of the group. + */ static std::unique_ptr create(argument_parser& parser, std::string_view name) { return std::unique_ptr(new argument_group(parser, name)); } + /// 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). void _add_argument(arg_ptr_t arg) noexcept { this->_arguments.emplace_back(std::move(arg)); } - argument_parser* _parser; - std::string _name; - arg_ptr_vec_t _arguments; + 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. - bool _required : 1 = false; - bool _mutually_exclusive : 1 = false; + bool _required : 1 = false; ///< The required attribute value (default: false). + bool _mutually_exclusive : 1 = + false; ///< The mutually exclusive attribute value (default: false). }; } // namespace ap diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index bfcce6c4..7c5cd745 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -512,7 +512,11 @@ class argument_parser { .nargs(0ull); } - // TODO: doc comment + /** + * @brief Adds an argument group with the given name to the parser's configuration. + * @param name Name of the group. + * @return Reference to the added argument group. + */ argument_group& add_group(const std::string_view name) noexcept { return *this->_argument_groups.emplace_back(argument_group::create(*this, name)); } @@ -926,7 +930,11 @@ class argument_parser { return false; } - // TODO: doc comment + /** + * @brief Check if the given group belongs to the parser. + * @param group The group to validate. + * @throws std::logic_error if the group doesn't belong to the parser. + */ void _validate_group(const argument_group& group) { if (group._parser != this) throw std::logic_error(std::format( @@ -1225,7 +1233,10 @@ class argument_parser { state.curr_arg.reset(); } - // TODO: add doc comment + /** + * @brief Verifies the correctness of the parsed command-line arguments. + * @throws ap::parsing_failure if the state of the parsed arguments is invalid. + */ void _verify_final_state() const { if (not this->_are_required_args_bypassed()) { this->_verify_required_args(); @@ -1233,7 +1244,7 @@ class argument_parser { } for (const auto& group : this->_argument_groups) - this->_verify_argument_group(*group); + this->_verify_group_requirements(*group); } /** @@ -1283,8 +1294,12 @@ class argument_parser { throw parsing_failure::invalid_nvalues(arg->name(), nv_ord); } - // TODO: add doc comments - void _verify_argument_group(const argument_group& group) const { + /** + * @brief Verifies whether the requirements of the given argument group are satisfied. + * @param group The argument group to verify. + * @throws ap::parsing_failure if the requirements are not satistied. + */ + void _verify_group_requirements(const argument_group& group) const { const auto n_used_args = static_cast( std::ranges::count_if(group._arguments, [](const auto& arg) { return arg->is_used(); }) ); From 36d35f3e69c98d1542c8b6123c92472077a87c78 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Tue, 23 Sep 2025 22:33:31 +0200 Subject: [PATCH 7/9] docs update; group printing improvement --- README.md | 1 + docs/tutorial.md | 173 ++++++++++++++++++++++++++------- include/ap/argument_parser.hpp | 56 +++++------ 3 files changed, 166 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 2187d65e..f0ae44d1 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Command-line argument parser for C++20 - [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) + - [Argument Groups](/docs/tutorial.md#argument-groups) - [Parsing Arguments](/docs/tutorial.md#parsing-arguments) - [Basic Argument Parsing Rules](/docs/tutorial.md#basic-argument-parsing-rules) - [Compound Arguments](/docs/tutorial.md#compound-arguments) diff --git a/docs/tutorial.md b/docs/tutorial.md index d1bf0e4c..606fb0de 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -22,6 +22,7 @@ - [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) +- [Argument Groups](#argument-groups) - [Parsing Arguments](#parsing-arguments) - [Basic Argument Parsing Rules](#basic-argument-parsing-rules) - [Compound Arguments](#compound-arguments) @@ -118,21 +119,26 @@ If you do not use CMake you can dowload the desired [library release](https://gi To use the argument parser in your code you need to use the `ap::argument_parser` class. -The parameters you can specify for a parser's instance are: - -- The program's name, version and description - used in the parser's configuration output (`std::cout << parser`). -- Verbosity mode - `false` by default; if set to `true` the parser's configuration output will include more detailed info about arguments' parameters in addition to their names and help messages. -- [Arguments](#adding-arguments) - specify the values/options accepted by the program. -- [The unknown argument flags handling policy](#4-unknown-argument-flag-handling). - ```cpp -ap::argument_parser parser; -parser.program_name("Name of the program") - .program_version("alhpa") +ap::argument_parser parser("program"); +parser.program_version("alhpa") .program_description("Description of the program") .verbose(); ``` +> [!IMPORTANT] +> +> - When creating an argument parser instance, you must provide a program name to the constructor. +> +> The program name given to the parser cannot contain whitespace characters. +> +> - Additional parameters you can specify for a parser's instance incldue: +> - The program's version and description - used in the parser's configuration output (`std::cout << parser`). +> - Verbosity mode - `false` by default; if set to `true` the parser's configuration output will include more detailed info about arguments' parameters in addition to their names and help messages. +> - [Arguments](#adding-arguments) - specify the values/options accepted by the program. +> - [Argument Groups](#argument-groups) - organize related optional arguments into sections and optionally enforce usage rules. +> - [The unknown argument flags handling policy](#4-unknown-argument-flag-handling). + > [!TIP] > > You can specify the program version using a string (like in the example above) or using the `ap::version` structure: @@ -256,8 +262,8 @@ parser.add_positional_argument("number", "n") By default all arguments are visible, but this can be modified using the `hidden(bool)` setter as follows: ```cpp -parser.program_name("hidden-test") - .program_description("A simple program") +ap::argument_parser("hidden-test") +parser.program_description("A simple test program for argument hiding") .default_arguments(ap::default_argument::o_help); parser.add_optional_argument("hidden") @@ -272,7 +278,7 @@ parser.try_parse_args(argc, argv); > ./hidden-test --help Program: hidden-test - A simple program + A simple test program for argument hiding Optional arguments: @@ -445,9 +451,8 @@ The `nargs` parameter can be set as: Consider a simple example: ```cpp -ap::argument_parser parser; -parser.program_name("run-script") - .default_arguments(ap::default_argument::o_help); +ap::argument_parser parser("run-script"); +parser.default_arguments(ap::default_argument::o_help); parser.add_positional_argument("script") .help("The name of the script to run"); @@ -807,6 +812,106 @@ parser.default_arguments();

+## Argument Groups + +Argument groups provide a way to organize related optional arguments into logical sections. They make the command-line interface easier to read in help messages, and can enforce rules such as **mutual exclusivity** or **required usage**. + +By default, every parser comes with two predefined groups: + +- **Positional Arguments** – contains all arguments added via `add_positional_argument`. +- **Optional Arguments** – contains all arguments added via `add_optional_argument` or `add_flag` without explicitly specifying an argument group. + +User-defined groups can only contain optional arguments (including flags). This allows you to structure your command-line interface into meaningful sections such as "Input Options", "Output Options", or "Debug Settings". + + +### Creating New Groups + +A new group can be created by calling the `add_group` method of an argument parser: + +```cpp +ap::argument_parser parser("myprog"); +auto& out_opts = parser.add_group("Output Options"); +``` + +The group’s name will appear as a dedicated section in the help message and arguments added to this group will be listed under `Output Options` instead of the default `Optional Arguments` section. + +> [!NOTE] +> +> If a group has no visible arguments, it will not be included in the parser's help message output at all. + +### Adding Arguments to Groups + +Arguments are added to a group by passing the group reference as the first parameter to the `add_optional_argument` and `add_flag` functions: + +```cpp +parser.add_optional_argument(out_opts, "output", "o") + .nargs(1) + .help("Print output to the given file"); + +parser.add_flag(out_opts, "print", "p") + .help("Print output to the console"); +``` + +### Group Attributes + +User-defined groups can be configured with special attributes that change how the parser enforces their usage: + +- `required()` – at least one argument from the group must be provided by the user, otherwise parsing will fail. +- `mutually_exclusive()` – at most one argument from the group can be provided; using more than one at the same time results in an error. + +Both attributes are **off by default**, and they can be combined (e.g., a group can require that exactly one argument is chosen). + +```cpp +auto& out_opts = parser.add_group("Output Options") + .required() // at least one option is required + .mutually_exclusive(); // but at most one can be chosen +``` + +### Complete Example + +Below is a small program that demonstrates how to use a mutually exclusive group of required arguments: + +```cpp +#include + +int main(int argc, char* argv[]) { + ap::argument_parser parser("myprog"); + parser.default_arguments(ap::default_argument::o_help); + + // create the argument group + auto& out_opts = parser.add_group("Output Options") + .required() + .mutually_exclusive(); + + // add arguments to the custom group + parser.add_optional_argument(out_opts, "output", "o") + .nargs(1) + .help("Print output to a given file"); + + parser.add_flag(out_opts, "print", "p") + .help("Print output to the console"); + + parser.try_parse_args(argc, argv); + + return 0; +} +``` + +When invoked with the `--help` flag, the above program produces a help message that clearly shows the group and its rules: + +``` +Program: myprog + +Output Options: (required, mutually exclusive) + + --output, -o : Print output to a given file + --print, -p : Print output to the console +``` + +
+
+
+ ## Parsing Arguments To parse the command-line arguments use the `void argument_parser::parse_args(const AR& argv)` method, where `AR` must be a type that satisfies `std::ranges::range` and its value type is convertible to `std::string`. @@ -845,19 +950,18 @@ The simple example below demonstrates how (in terms of the program's structure) int main(int argc, char* argv[]) { // create the parser class instance - ap::argument_parser parser; + ap::argument_parser parser("some-program"); - // define the parser's attributes - parser.program_name("some-program") - .program_description("The program does something with command-line arguments"); + // define the parser's attributes and default arguments + parser.program_version({0u, 0u, 0u}) + .program_description("The program does something with command-line arguments") + .default_arguments(ap::default_argument::o_help); // define the program arguments parser.add_positional_argument("positional").help("A positional argument"); parser.add_optional_argument("optional", "o").help("An optional argument"); parser.add_flag("flag", "f").help("A boolean flag"); - parser.default_arguments(ap::default_argument::o_help); - // parse command-line arguments parser.try_parse_args(argc, argv); @@ -1009,10 +1113,9 @@ This behavior can be modified using the `unknown_arguments_policy` method of the #include int main(int argc, char* argv[]) { - ap::argument_parser parser; + ap::argument_parser parser("unknown-policy-test"); - parser.program_name("test") - .program_description("A simple test program") + parser.program_description("A simple test program for unknwon argument handling policies") .default_arguments(ap::default_argument::o_help) // set the unknown argument flags handling policy .unknown_arguments_policy(ap::unknown_policy::); @@ -1031,12 +1134,12 @@ int main(int argc, char* argv[]) { The available policies are: - `ap::unknown_policy::fail` (default) - throws an exception if an unknown argument flag is encountered: - ```bash - > ./test --known --unknown + ```txt + > ./unknown-policy-test --known --unknown [ap::error] Unknown argument [--unknown]. - Program: test + Program: unknown-policy-test - A simple test program + A simple test program for unknwon argument handling policies Optional arguments: @@ -1046,23 +1149,23 @@ The available policies are: - `ap::unknown_policy::warn` - prints a warning message to the standard error stream and continues parsing the remaining arguments: - ```bash - > ./test --known --unknown + ```txt + > ./unknown-policy-test --known --unknown [ap::warning] Unknown argument '--unknown' will be ignored. known = ``` - `ap::unknown_policy::ignore` - ignores unknown argument flags and continues parsing the remaining arguments: - ```shell - ./test --known --unknown + ```txt + ./unknown-policy-test --known --unknown known = ``` - `ap::unknown_policy::as_values` - treats unknown argument flags as values: - ```shell - > ./test --known --unknown + ```txt + > ./unknown-policy-test --known --unknown known = --unknown ``` diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index 7c5cd745..a822b0c9 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -171,25 +171,16 @@ class argument_parser { argument_parser(argument_parser&&) = delete; argument_parser& operator=(argument_parser&&) = delete; - argument_parser() - : _gr_positional_args(add_group("Positional Arguments")), - _gr_optional_args(add_group("Optional Arguments")) {} - - ~argument_parser() = default; - - /** - * @brief Set the program name. - * @param name The name of the program. - * @return Reference to the argument parser. - */ - argument_parser& program_name(std::string_view name) { + argument_parser(const std::string_view name) + : _program_name(name), + _gr_positional_args(add_group("Positional Arguments")), + _gr_optional_args(add_group("Optional Arguments")) { if (util::contains_whitespaces(name)) throw invalid_configuration("The program name cannot contain whitespace characters!"); - - this->_program_name.emplace(name); - return *this; } + ~argument_parser() = default; + /** * @brief Set the program version. * @param version The version of the program. @@ -792,12 +783,10 @@ class argument_parser { * @param os Output stream. */ void print_help(const bool verbose, std::ostream& os = std::cout) const noexcept { - if (this->_program_name) { - os << "Program: " << this->_program_name.value(); - if (this->_program_version) - os << " (" << this->_program_version.value() << ')'; - os << '\n'; - } + os << "Program: " << this->_program_name; + if (this->_program_version) + os << " (" << this->_program_version.value() << ')'; + os << '\n'; if (this->_program_description) os << '\n' @@ -1342,20 +1331,29 @@ class argument_parser { * @param os The output stream to print to. * @param group The argument group to print. * @param verbose A verbosity mode indicator flag. + * @attention If a group has no visible arguments, nothing will be printed. */ void _print_group(std::ostream& os, const argument_group& group, const bool verbose) const noexcept { - os << group._name << ":\n"; - - if (group._arguments.empty()) { - os << '\n' << std::string(this->_indent_width, ' ') << "No arguments" << '\n'; - return; - } - auto visible_args = std::views::filter(group._arguments, [](const auto& arg) { return not arg->is_hidden(); }); + if (std::ranges::empty(visible_args)) + return; + + os << group._name << ":"; + + std::vector group_attrs; + if (group._required) + group_attrs.emplace_back("required"); + if (group._mutually_exclusive) + group_attrs.emplace_back("mutually exclusive"); + if (not group_attrs.empty()) + os << " (" << util::join(group_attrs) << ')'; + + os << '\n'; + if (verbose) { for (const auto& arg : visible_args) os << '\n' << arg->desc(verbose).get(this->_indent_width) << '\n'; @@ -1378,7 +1376,7 @@ class argument_parser { } } - std::optional _program_name; + std::string _program_name; std::optional _program_version; std::optional _program_description; bool _verbose = false; From 2864f2fcfe24688b9806be989d0002c3509c6db8 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Tue, 23 Sep 2025 22:41:10 +0200 Subject: [PATCH 8/9] tests alignment; demo update --- cpp-ap-demo | 2 +- include/ap/argument_parser.hpp | 5 ++-- .../include/argument_parser_test_fixture.hpp | 6 ++-- .../test_argument_parser_add_argument.cpp | 2 +- tests/source/test_argument_parser_info.cpp | 29 +++++-------------- 5 files changed, 16 insertions(+), 28 deletions(-) diff --git a/cpp-ap-demo b/cpp-ap-demo index 1c50d9e0..8f11f6fa 160000 --- a/cpp-ap-demo +++ b/cpp-ap-demo @@ -1 +1 @@ -Subproject commit 1c50d9e003ab9f14aafac5afd389d6580d898a21 +Subproject commit 8f11f6fac0cc59851eda582474015118a1946f8f diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index a822b0c9..ae92dd34 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -139,9 +139,8 @@ void add_default_argument(const default_argument, argument_parser&) noexcept; * * int main(int argc, char* argv[]) { * // Create the argument parser instance - * ap::argument_parser parser; - * parser.program_name("fcopy") - * .program_version({ .major = 1, .minor = 0, .patch = 0 }) + * ap::argument_parser parser("fcopy"); + * parser.program_version({ .major = 1, .minor = 0, .patch = 0 }) * .program_description("A simple file copy utility.") * .default_arguments( * ap::default_argument::o_help, diff --git a/tests/include/argument_parser_test_fixture.hpp b/tests/include/argument_parser_test_fixture.hpp index db108a02..05e5dd0c 100644 --- a/tests/include/argument_parser_test_fixture.hpp +++ b/tests/include/argument_parser_test_fixture.hpp @@ -165,7 +165,7 @@ struct argument_parser_test_fixture { } // argument_parser private member accessors - [[nodiscard]] const std::optional& get_program_name() const { + [[nodiscard]] const std::string& get_program_name() const { return this->sut._program_name; } @@ -192,8 +192,10 @@ struct argument_parser_test_fixture { return this->sut._get_argument(arg_name); } - ap::argument_parser sut; + ap::argument_parser sut{program_name}; parsing_state state; + + static constexpr std::string_view program_name = "program"; }; } // namespace ap_testing diff --git a/tests/source/test_argument_parser_add_argument.cpp b/tests/source/test_argument_parser_add_argument.cpp index 957ac11e..2d636ba5 100644 --- a/tests/source/test_argument_parser_add_argument.cpp +++ b/tests/source/test_argument_parser_add_argument.cpp @@ -346,7 +346,7 @@ TEST_CASE_FIXTURE( test_argument_parser_add_argument, "add_optional_argument and add_flag should throw if a group does not belong to the parser" ) { - argument_parser different_parser; + argument_parser different_parser("different-program"); const std::string group_name = "Group From a Different Parser"; auto& group = different_parser.add_group(group_name); diff --git a/tests/source/test_argument_parser_info.cpp b/tests/source/test_argument_parser_info.cpp index 7ae7a66e..6a37ade7 100644 --- a/tests/source/test_argument_parser_info.cpp +++ b/tests/source/test_argument_parser_info.cpp @@ -3,46 +3,33 @@ using namespace ap_testing; +using ap::argument_parser; using ap::invalid_configuration; struct test_argument_parser_info : public argument_parser_test_fixture { - const std::string test_name = "test-program-name"; const std::string test_description = "test program description"; const ap::version test_version{1u, 2u, 3u}; const std::string test_str_version = "alpha"; }; -TEST_CASE_FIXTURE( - test_argument_parser_info, "parser's program name member should be nullopt by default" -) { - const auto stored_program_name = get_program_name(); - CHECK_FALSE(stored_program_name); -} - -TEST_CASE_FIXTURE( - test_argument_parser_info, "program_name() should throw if the name contains whitespaces" -) { +TEST_CASE("argument_parser() should throw if the name contains whitespaces") { CHECK_THROWS_WITH_AS( - sut.program_name("invalid name"), + argument_parser("invalid name"), "The program name cannot contain whitespace characters!", invalid_configuration ); } -TEST_CASE_FIXTURE(test_argument_parser_info, "program_name() should set the program name member") { - sut.program_name(test_name); - - const auto stored_program_name = get_program_name(); - - REQUIRE(stored_program_name); - CHECK_EQ(stored_program_name.value(), test_name); +TEST_CASE_FIXTURE( + test_argument_parser_info, "argument_parser() should set the program name member" +) { + CHECK_EQ(get_program_name(), program_name); } TEST_CASE_FIXTURE( test_argument_parser_info, "parser's program version member should be nullopt by default" ) { - const auto stored_program_version = get_program_version(); - CHECK_FALSE(stored_program_version); + CHECK_FALSE(get_program_version()); } TEST_CASE_FIXTURE( From e2b38dfdbee9a9d3716aa9d714bfd09f4f40c0ae Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Tue, 23 Sep 2025 23:12:02 +0200 Subject: [PATCH 9/9] resolved comments --- CMakeLists.txt | 2 +- Doxyfile | 2 +- MODULE.bazel | 2 +- docs/tutorial.md | 4 ++++ include/ap/argument.hpp | 2 -- include/ap/argument_parser.hpp | 8 ++++---- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f4de373..7689b1cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ else() endif() project(cpp-ap - VERSION 3.0.0.6 + VERSION 3.0.0.7 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 057383cc..a353745c 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.6 +PROJECT_NUMBER = 3.0.0.7 # 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 eb14f2e5..747201b2 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,4 +1,4 @@ module( name = "cpp-ap", - version = "3.0.0.6", + version = "3.0.0.7", ) diff --git a/docs/tutorial.md b/docs/tutorial.md index 606fb0de..febc2802 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -23,6 +23,10 @@ - [Predefined Parameter Values](#predefined-parameter-values) - [Default Arguments](#default-arguments) - [Argument Groups](#argument-groups) + - [Creating New Groups](#creating-new-groups) + - [Adding Arguments to Groups](#adding-arguments-to-groups) + - [Group Attributes](#group-attributes) + - [Complete Example](#complete-example) - [Parsing Arguments](#parsing-arguments) - [Basic Argument Parsing Rules](#basic-argument-parsing-rules) - [Compound Arguments](#compound-arguments) diff --git a/include/ap/argument.hpp b/include/ap/argument.hpp index 588f94bc..56ac3310 100644 --- a/include/ap/argument.hpp +++ b/include/ap/argument.hpp @@ -392,8 +392,6 @@ class argument : public detail::argument_base { return *this; } - // argument& in_group() - #ifdef AP_TESTING friend struct ::ap_testing::argument_test_fixture; #endif diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index ae92dd34..0bd6d044 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -323,7 +323,7 @@ class argument_parser { } /** - * @brief Adds a positional argument to the parser's configuration. + * @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 name_discr The discriminator value specifying whether the given name should be treated as primary or secondary. @@ -339,7 +339,7 @@ class argument_parser { } /** - * @brief Adds a positional argument to the parser's configuration. + * @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. @@ -356,7 +356,7 @@ class argument_parser { } /** - * @brief Adds a positional argument to the parser's configuration and binds it to the given group. + * @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. @@ -391,7 +391,7 @@ class argument_parser { } /** - * @brief Adds a positional argument to the parser's configuration and binds it to the given group. + * @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.