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/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/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/docs/tutorial.md b/docs/tutorial.md index d1bf0e4c..febc2802 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -22,6 +22,11 @@ - [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) + - [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) @@ -118,21 +123,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 +266,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 +282,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 +455,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 +816,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 +954,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 +1117,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 +1138,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 +1153,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_group.hpp b/include/ap/argument_group.hpp new file mode 100644 index 00000000..dc30bb8a --- /dev/null +++ b/include/ap/argument_group.hpp @@ -0,0 +1,108 @@ +// 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. + +/// @file ap/argument_group.hpp + +#pragma once + +#include "detail/argument_base.hpp" + +#include + +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; + + /** + * @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; + } + + friend class argument_parser; + +private: + 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; ///< 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; ///< 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 8e5cccd0..0bd6d044 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" @@ -138,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, @@ -167,26 +167,19 @@ class argument_parser { argument_parser(const argument_parser&) = delete; argument_parser& operator=(const argument_parser&) = delete; - argument_parser() = default; + argument_parser(argument_parser&&) = delete; + argument_parser& operator=(argument_parser&&) = delete; - argument_parser(argument_parser&&) = default; - argument_parser& operator=(argument_parser&&) = default; - - ~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. @@ -295,8 +288,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); } /** @@ -321,12 +316,14 @@ 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); } /** - * @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. @@ -338,6 +335,42 @@ class argument_parser { const std::string_view name, const detail::argument_name_discriminator name_discr = n_primary ) { + return this->add_optional_argument(this->_gr_optional_args, 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. + * @return Reference to the added optional argument. + * @throws ap::invalid_configuration + */ + template + optional_argument& add_optional_argument( + const std::string_view primary_name, const std::string_view secondary_name + ) { + return this->add_optional_argument( + this->_gr_optional_args, primary_name, secondary_name + ); + } + + /** + * @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 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, 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); this->_verify_arg_name_pattern(name); const auto arg_name = @@ -351,13 +384,16 @@ class argument_parser { 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()); + 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); } /** - * @brief Adds a positional argument to the parser's configuration. + * @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. * @return Reference to the added optional argument. @@ -365,8 +401,11 @@ class argument_parser { */ template optional_argument& add_optional_argument( - const std::string_view primary_name, const std::string_view secondary_name + argument_group& group, + 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); @@ -378,8 +417,10 @@ class argument_parser { 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()); + 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,6 +460,57 @@ 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 + ) { + return this->add_optional_argument(group, name, name_discr) + .default_values(not StoreImplicitly) + .implicit_values(StoreImplicitly) + .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 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 + ) { + return this->add_optional_argument(group, primary_name, secondary_name) + .default_values(not StoreImplicitly) + .implicit_values(StoreImplicitly) + .nargs(0ull); + } + + /** + * @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)); + } + /** * @brief Parses the command-line arguments. * @@ -455,10 +547,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(); } /** @@ -549,11 +638,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); } @@ -697,26 +782,19 @@ 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' << 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); } } @@ -742,17 +820,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 +918,18 @@ class argument_parser { return false; } + /** + * @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( + "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 +938,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 +960,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 +980,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 +1093,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 +1132,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)) @@ -1129,6 +1221,20 @@ class argument_parser { state.curr_arg.reset(); } + /** + * @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(); + this->_verify_nvalues(); + } + + for (const auto& group : this->_argument_groups) + this->_verify_group_requirements(*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. @@ -1176,6 +1282,28 @@ class argument_parser { throw parsing_failure::invalid_nvalues(arg->name(), nv_ord); } + /** + * @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(); }) + ); + + 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. @@ -1200,11 +1328,30 @@ 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. + * @attention If a group has no visible arguments, nothing will be printed. */ - void _print(std::ostream& os, const arg_ptr_list_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 { + 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) @@ -1212,7 +1359,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)); @@ -1228,14 +1375,18 @@ class argument_parser { } } - std::optional _program_name; + std::string _program_name; std::optional _program_version; std::optional _program_description; 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; + 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; 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..05e5dd0c 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) @@ -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; } @@ -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); @@ -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 23901bf4..2d636ba5 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("different-program"); + + 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_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( 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); +}