diff --git a/CMakeLists.txt b/CMakeLists.txt index 7689b1cd..19bcd720 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ else() endif() project(cpp-ap - VERSION 3.0.0.7 + VERSION 3.0.0.8 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 a353745c..fb9fd682 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.7 +PROJECT_NUMBER = 3.0.0.8 # 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 747201b2..597d9bb6 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,4 +1,4 @@ module( name = "cpp-ap", - version = "3.0.0.7", + version = "3.0.0.8", ) diff --git a/README.md b/README.md index f0ae44d1..0527f171 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,10 @@ Command-line argument parser for C++20 - [Downloading the Library](/docs/tutorial.md#downloading-the-library) - [The Parser Class](/docs/tutorial.md#the-parser-class) - [Adding Arguments](/docs/tutorial.md#adding-arguments) + - [Syntax](/docs/tutorial.md#syntax) + - [Names](/docs/tutorial.md#names) + - [Value Types](/docs/tutorial.md#value-types) + - [Boolean Flags](/docs/tutorial.md#boolean-flags) - [Argument Parameters](/docs/tutorial.md#argument-parameters) - [Common Parameters](/docs/tutorial.md#common-parameters) - [Parameters Specific for Optional Arguments](/docs/tutorial.md#parameters-specific-for-optional-arguments) @@ -63,6 +67,11 @@ Command-line argument parser for C++20 - [Compound Arguments](/docs/tutorial.md#compound-arguments) - [Parsing Known Arguments](/docs/tutorial.md#parsing-known-arguments) - [Retrieving Argument Values](/docs/tutorial.md#retrieving-argument-values) + - [Subparsers](/docs/tutorial.md#subparsers) + - [Creating Subparsers](/docs/tutorial.md#creating-subparsers) + - [Using Multiple Subparsers](/docs/tutorial.md#using-multiple-subparsers) + - [Parsing Arguments with Subparsers](/docs/tutorial.md#parsing-arguments-with-subparsers) + - [Tracking Parser State](/docs/tutorial.md#tracking-parser-state) - [Examples](/docs/tutorial.md#examples) - [Common Utility](/docs/tutorial.md#common-utility) - [Dev notes](/docs/dev_notes.md#dev-notes) diff --git a/cpp-ap-demo b/cpp-ap-demo index 8f11f6fa..bb13a211 160000 --- a/cpp-ap-demo +++ b/cpp-ap-demo @@ -1 +1 @@ -Subproject commit 8f11f6fac0cc59851eda582474015118a1946f8f +Subproject commit bb13a2111f075e388d48e0cc4bba1bf62dfaad45 diff --git a/docs/tutorial.md b/docs/tutorial.md index febc2802..87567155 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -6,6 +6,10 @@ - [Downloading the Library](#downloading-the-library) - [The Parser Class](#the-parser-class) - [Adding Arguments](#adding-arguments) + - [Syntax](#syntax) + - [Names](#names) + - [Value Types](#value-types) + - [Boolean Flags](#boolean-flags) - [Argument Parameters](#argument-parameters) - [Common Parameters](#common-parameters) - [help](#1-help---the-arguments-description-which-will-be-printed-when-printing-the-parser-class-instance) @@ -32,6 +36,11 @@ - [Compound Arguments](#compound-arguments) - [Parsing Known Arguments](#parsing-known-arguments) - [Retrieving Argument Values](#retrieving-argument-values) +- [Subparsers](#subparsers) + - [Creating Subparsers](#creating-subparsers) + - [Using Multiple Subparsers](#using-multiple-subparsers) + - [Parsing Arguments with Subparsers](#parsing-arguments-with-subparsers) + - [Tracking Parser State](#tracking-parser-state) - [Examples](#examples) - [Common Utility](#common-utility) @@ -134,7 +143,7 @@ parser.program_version("alhpa") > > - 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. +> The program name given to the parser cannot be empty and 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`). @@ -165,58 +174,64 @@ parser.program_version("alhpa") ## Adding Arguments -The parser supports both positional and optional arguments. Both argument types are identified by their names. +The parser supports **positional** and **optional** arguments. > [!NOTE] > -> The basic rules of parsing positional and optional arguments are described in the [Parsing arguments](#parsing-arguments) section. +> The general rules for parsing arguments are described in the [Parsing arguments](#parsing-arguments) section. -To add an argument to the parameter's configurations use the following syntax: +### Syntax + +To add an argument, use: ```cpp -parser.add__argument("argument"); +parser.add_positional_argument("name"); +parser.add_optional_argument("name"); ``` -or +For **optional arguments**, you may also specify a secondary (short) name: ```cpp -parser.add__argument("argument", "a"); +parser.add_optional_argument("name", "n") ``` -> [!NOTE] -> -> An argument's name consists of a primary and/or secondary names. The primary name is a longer, more descriptive name, while the secondary name is a shorter/abbreviated name of the argument. -> -> While passing a primary name is required for creating positional arguments, optional arguments can be initialized using only a secondary name as follows: -> -> ```cpp -> parser.add_optional_argument("a", ap::n_secondary); -> parser.add_flag("f", ap::n_secondary); -> ``` +or use only the secondary name: + +```cpp +parser.add_optional_argument("n", ap::n_secondary); +``` + +### Names + +- Positional arguments must have exactly one name (no secondary/short names allowed). +- Optional arguments can have: + - only a primary (long) name, + - only a secondary (short) name, or + - both a primary and a secondary name. + +### Value Types > [!IMPORTANT] +> An argument's value type must be `ap::none_type` **or** satisfy all of the following requirements: > -> An argument's value type must be `ap::none_type` or it must satisfy the following requirements: -> -> - The type is [constructible from](https://en.cppreference.com/w/cpp/concepts/constructible_from) `const std::string&` or the stream extraction operator - `std::istream& operator>>` is defined for the type. -> -> **IMPORTANT:** The argument parser will always use direct initialization from `std::string` and will use the extraction operator only if an argument's value type cannot be initialized from `std::string`. -> -> - The type satisfies the [`std::semiregular`](https://en.cppreference.com/w/cpp/concepts/semiregular.html) concept - is default initializable and copyable. +> - [Constructible from](https://en.cppreference.com/w/cpp/concepts/constructible_from) `const std::string&` or overload `std::istream& operator>>`. +> - The parser will always try direct initialization from std::string first, and only fall back to the extraction operator if direct initialization fails. +> - Satisfy the [`std::semiregular`](https://en.cppreference.com/w/cpp/concepts/semiregular.html) concept (default-initializable and copyable). > [!NOTE] > > - The default value type of any argument is `std::string`. -> > - If the argument's value type is `ap::none_type`, the argument will not accept any values and therefore no value-related parameters can be set for such argument. This includes: > - [nargs](#5-nargs---sets-the-allowed-number-of-values-to-be-parsed-for-an-argument-this-can-be-set-as-a) > - [greedy](#6-greedy---if-this-option-is-set-the-argument-will-consume-all-command-line-values-until-its-upper-nargs-bound-is-reached) > - [choices](#7-choices---a-list-of-valid-argument-values) > - [value actions](#8-value-actions---functions-that-are-called-after-parsing-an-arguments-value) -> - [default_values](#9-default_values---a-list-of-values-which-will-be-used-if-no-values-for-an-argument-have-been-parsed) -> - [implicit_values](#2-implicit_values---a-list-of-values-which-will-be-set-for-an-argument-if-only-its-flag-but-no-values-are-parsed-from-the-command-line) +> - [default values](#9-default_values---a-list-of-values-which-will-be-used-if-no-values-for-an-argument-have-been-parsed) +> - [implicit values](#2-implicit_values---a-list-of-values-which-will-be-set-for-an-argument-if-only-its-flag-but-no-values-are-parsed-from-the-command-line) + +### Boolean Flags -You can also add boolean flags: +Flags are essentialy optional arguments with a boolean value type. ```cpp parser.add_flag("enable_some_option", "eso").help("enables option: some option"); @@ -229,7 +244,7 @@ parser.add_optional_argument("enable_some_option", "eso") */ ``` -Boolean flags store `true` by default but you can specify whether the flag should store `true` or `false` when used: +By default, flags store `true` when parsed from the command-line. You can invert this behavior: ```cpp parser.add_flag("disable_another_option", "dao").help("disables option: another option"); @@ -240,6 +255,7 @@ parser.add_optional_argument("disable_another_option", "dao") .nargs(0) .help("disables option: another option"); */ + ```
@@ -871,6 +887,12 @@ auto& out_opts = parser.add_group("Output Options") .mutually_exclusive(); // but at most one can be chosen ``` +> [!IMPORANT] +> +> If a group is defined as **mutually exclusive** and an argument from this group is used, then the `required` and `nargs` attribute requirements of other arguments from the group **will NOT be verified**. +> +> Consider the example in the section below. Normally the `--output, -o` argument would expect a value to be given in the command-line. However, if the `--print, -p` flag is used, then the `nargs` requirement of the `--output, -o` argument will not be verified, and therefore no exception will be thrown, even though the `nargs` requirement is not satisfied. + ### Complete Example Below is a small program that demonstrates how to use a mutually exclusive group of required arguments: @@ -918,7 +940,7 @@ Output Options: (required, mutually exclusive) ## 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`. +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 the [`std::ranges::forward_range`](https://en.cppreference.com/w/cpp/ranges/forward_range.html) concept and its value type is convertible to `std::string`. The `argument_parser` class also defines the `void parse_args(int argc, char* argv[])` overload, which works directly with the `argc` and `argv` arguments of the `main` function. @@ -932,7 +954,7 @@ The `argument_parser` class also defines the `void parse_args(int argc, char* ar > [!TIP] > -> The `parse_args` function may throw an `ap::argument_parser_exception` (specifically the `ap::parsing_failure` derived exception) if the provided command-line arguments do not match the expected configuration. To simplify error handling, the `argument_parser` class provides a `try_parse_args` methods, which will automatically catch these exceptions, print the error message, and exit with a failure status. +> The `parse_args` function may throw an `ap::argument_parser_exception` if the configuration of the defined arguments is invalid or the parsed command-line arguments do not match the expected configuration. To simplify error handling, the `argument_parser` class provides a `try_parse_args` methods, which will automatically catch these exceptions, print the error message as well as the help message of the deepest used parser (see [Subparsers](#subparsers)), and exit with a failure status. > > Internally, This is equivalent to: > @@ -941,7 +963,7 @@ The `argument_parser` class also defines the `void parse_args(int argc, char* ar > parser.parse_args(...); > } > catch (const ap::argument_parser_exception& err) { -> std::cerr << "[ERROR] : " << err.what() << std::endl << parser << std::endl; +> std::cerr << "[ap::error] " << err.what() << std::endl << parser.resolved_parser() << std::endl; > std::exit(EXIT_FAILURE); > } > ``` @@ -1366,9 +1388,183 @@ You can retrieve the argument's value(s) with:

+
+
+
+ +## Subparsers + +Subparsers allow you to build **hierarchical command-line interfaces**, where a top-level parser delegates parsing to its subcommands. This is particularly useful for creating CLI applications like `git`, where commands such as `git add`, `git commit`, and `git push` each have their own arguments. + +### Creating Subparsers + +```cpp +auto& subparser = parser.add_subparser("subprogram"); +``` + +Each subparser is a separate instance of `ap::argument_parser` and therefore it can have it can have its own parameters, including a description, arguments, argument groups, subparsers, etc. + +For example: + +```cpp +// top-level parser +ap::argument_parser git("ap-git"); +git.program_version({.major = 2u, .minor = 43u, .patch = 0u}) + .program_description("A version control system built with CPP-AP") + .default_arguments(ap::default_argument::o_help, ap::default_argument::o_version); + +// subcommand: status +auto& status = git.add_subparser("status"); +status.default_arguments(ap::default_argument::o_help) + .program_description("Show the working tree status"); +status.add_flag("short", "s") + .help("Give the output in the short-format"); +``` + +This defines `git` and `git status` parsers, each with their own sets of arguments. + +### Using Multiple Subparsers + +You can add as many subparsers as you like, each corresponding to a different command: + +```cpp +auto& init = git.add_subparser("init"); +init.program_description("Create an empty Git repository or reinitialize an existing one"); + +auto& add = git.add_subparser("add"); +add.program_description("Add file contents to the index"); + +auto& commit = git.add_subparser("commit"); +commit.program_description("Record changes to the repository"); + +auto& status = git.add_subparser("status"); +status.program_description("Show the working tree status"); + +auto& push = git.add_subparser("push"); +push.program_description("Update remote refs along with associated objects"); +``` + +All defined subparsers will be included in the parent parser's help message: + +```txt +> ap-git --help +Program: ap-git (v2.43.0) + + A version control system built with CPP-AP + +Commands: + + init : Create an empty Git repository or reinitialize an existing one + add : Add file contents to the index + commit : Record changes to the repository + status : Show the working tree status + push : Update remote refs along with associated objects + +Optional Arguments: + + --help, -h : Display the help message + --version, -v : Dsiplay program version info +``` + +### Parsing Arguments with Subparsers + +When parsing command-line arguments, the parent parser will attempt to match the **first command-line token** against the name of one of its subparsers. + +- If a match is found, the parser delegates the remaining arguments to the matched subparser. +- This process repeats **recursively**, so each subparser may also match one of its own subparsers. +- Parsing stops when no subparser matches the first token of the *current* argument list. At that point, the parser processes its own arguments. + +For example: + +```cpp +ap::argument_parser git("ap-git"); +auto& submodule = git.add_subparser("submodule"); +auto& submodule_init = submodule.add_subparser("init"); +``` + +Running `ap-git submodule init ` will result in `` being parsed by the `submodule_init` parser. + +### Tracking Parser State + +Each parser tracks its state during parsing. The methods described below let you inspect this state: + +- `invoked() -> bool` : Returns `true` if the parser’s name appeared on the command line. + + A parser is *invoked* as soon as the parser is selected during parsing, even if parsing is later delegated to one of its subparsers. + +- `finalized() -> bool` : Returns `true` if the parser has processed its own arguments. + + This is distinct from `invoked()`: a parser can be invoked but not finalized if one of its subparsers handled the arguments instead. + +- `resolved_parser() -> ap::argument_parser&` : Returns a reference to the *deepest invoked parser*. + + sIf no subparser was invoked, this simply returns the current parser. + +
+ +#### Example: Inspecting Parsing States + +```cpp +// define the parser hierarchy +ap::argument_parser git("ap-git"); +auto& submodule = git.add_subparser("submodule"); +auto& submodule_init = submodule.add_subparser("init"); + +// parse arguments +git.try_parse_args(argc, argv); + +// print state for each parser +std::cout << std::boolalpha; + +std::cout << "git : invoked=" << git.invoked() + << ", finalized=" << git.finalized() << '\n'; + +std::cout << "submodule : invoked=" << submodule.invoked() + << ", finalized=" << submodule.finalized() << '\n'; + +std::cout << "submodule_init : invoked=" << submodule_init.invoked() + << ", finalized=" << submodule_init.finalized() << '\n'; + +auto& active = git.resolved_parser(); +std::cout << "\nResolved parser : " << active.name() << " (" << active.program_name() << ")\n"; +``` + +If you run: `./ap-git submodule intit`, you will get the following state: + +```txt +git : invoked=true, finalized=false +submodule : invoked=true, finalized=false +submodule_init : invoked=true, finalized=true + +Resolved parser : init (ap-git submodule init) +``` + +
+
+
+ ## Examples -The library usage examples / demo projects can be found in the [cpp-ap-demo](https://github.com/SpectraL519/cpp-ap-demo) repository. +The library usage examples and demo projects are included in the `cpp-ap-demo` submodule. +To fetch the submodule content after cloning the main repository, run: + +```bash +git submodule update --init --recursive +``` + +For more detailed information about the demo projects, see the [cpp-ap-demo](https://github.com/SpectraL519/cpp-ap-demo) README. + +The following table lists the projects provided in the `cpp-ap-demo` submodule: + +| Project | Description | +| :- | :- | +| [Power Calculator](https://github.com/SpectraL519/cpp-ap-demo/tree/master/power_calculator/) | Calculates the value of a $b^e$ expression for the given base and exponents.
**Demonstrates:** The basic usage of positional and optional arguments. | +| [File Merger](https://github.com/SpectraL519/cpp-ap-demo/tree/master/file_merger/) | Merges multiple text files into a single output file.
**Demonstrates:** The usage of default arguments. | +| [Numbers Converter](https://github.com/SpectraL519/cpp-ap-demo/tree/master/numbers_converter/) | Converts numbers between different bases.
**Demonstrates:** The usage of argument parameters such as *nargs*, *choices*, and *default values*. | +| [Verbosity](https://github.com/SpectraL519/cpp-ap-demo/tree/master/verbosity/) | Prints messages with varying levels of verbosity.
**Demonstrates:** The usage of `none_type` arguments and compound argument flags. | +| [Logging Mode](https://github.com/SpectraL519/cpp-ap-demo/tree/master/logging_mode/) | Logs a message depending on the selected logging mode (`quiet`, `normal`, `verbose`).
**Demonstrates:** The usage of custom argument value types (like enums). | +| [Message Logger](https://github.com/SpectraL519/cpp-ap-demo/tree/master/message_logger/) | Outputs a message to a file, console, or not at all.
**Demonstrates:** The usage of argument groups. | +| [AP-GIT](https://github.com/SpectraL519/cpp-ap-demo/tree/master/ap_git/) | A minimal Git CLI clone with subcommands (`init`, `add`, `commit`, `status`, `push`).
**Demonstrates:** The usage of subparsers for multi-command CLIs and complex argument configurations. |

diff --git a/include/ap/action/predefined.hpp b/include/ap/action/predefined.hpp index 006808ca..ecedc037 100644 --- a/include/ap/action/predefined.hpp +++ b/include/ap/action/predefined.hpp @@ -21,7 +21,7 @@ namespace action { /** * @brief Returns an *on-flag* action which prints the argument parser's help message. - * @param parser The argument parser the help message of which will be printed. + * @param parser Argument parser instance the help message of which will be printed. * @param exit_code The exit code with which `std::exit` will be called (if not `std::nullopt`). * @param os The output stream to which the help message will be printed. */ @@ -33,7 +33,7 @@ inline typename ap::action_type::on_flag::type print_help( return [&parser, &os, exit_code]() { os << parser << std::endl; if (exit_code) - std::exit(exit_code.value()); + std::exit(*exit_code); }; } diff --git a/include/ap/argument.hpp b/include/ap/argument.hpp index 56ac3310..9fcdc472 100644 --- a/include/ap/argument.hpp +++ b/include/ap/argument.hpp @@ -9,7 +9,7 @@ #include "action/predefined.hpp" #include "action/util/helpers.hpp" #include "detail/argument_base.hpp" -#include "detail/argument_descriptor.hpp" +#include "detail/help_builder.hpp" #include "nargs/range.hpp" #include "types.hpp" #include "util/concepts.hpp" @@ -424,41 +424,41 @@ class argument : public detail::argument_base { std::conditional_t; /** - * @brief Creates an descriptor object for the argument. + * @brief Creates a help message builder object for the argument. * @param verbose The verbosity mode value. * @note If the `verbose` parameter is set to `true` all non-default parameters will be included in the output, * @note otherwise only the argument's name and help message will be included. */ - [[nodiscard]] detail::argument_descriptor desc(const bool verbose) const noexcept override { - detail::argument_descriptor desc(this->_name.str(), this->_help_msg); + [[nodiscard]] detail::help_builder help_builder(const bool verbose) const noexcept override { + detail::help_builder bld(this->_name.str(), this->_help_msg); if (not verbose) - return desc; + return bld; - desc.params.reserve(6ull); + bld.params.reserve(6ull); if (this->_required != _default_required) - desc.add_param("required", std::format("{}", this->_required)); + bld.add_param("required", std::format("{}", this->_required)); if (this->is_bypass_required_enabled()) - desc.add_param("bypass required", "true"); + bld.add_param("bypass required", "true"); if (this->_nargs_range != _default_nargs_range) - desc.add_param("nargs", this->_nargs_range); + bld.add_param("nargs", this->_nargs_range); if constexpr (util::c_writable) { if (not this->_choices.empty()) - desc.add_range_param("choices", this->_choices); + bld.add_range_param("choices", this->_choices); if (not this->_default_values.empty()) - desc.add_range_param( + bld.add_range_param( "default value(s)", util::any_range_cast_view(this->_default_values) ); if constexpr (type == argument_type::optional) { if (not this->_implicit_values.empty()) - desc.add_range_param( + bld.add_range_param( "implicit value(s)", util::any_range_cast_view(this->_implicit_values) ); } } - return desc; + return bld; } /// @brief Mark the optional argument as used. diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index 0bd6d044..099f2524 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -66,6 +66,20 @@ enum class default_argument : std::uint8_t { */ o_help, + /** + * @brief An optional argument representing the program's version flag. + * Equivalent to: + * @code{.cpp} + * arg_parser.add_optional_argument("version", "v") + * .action([&arg_parser]() { + * arg_parser.print_version(); + * std::exit(EXIT_SUCCESS); + * }) + * .help("Dsiplay program version info"); + * @endcode + */ + o_version, + /** * @brief A positional argument representing multiple input file paths. * Equivalent to: @@ -170,13 +184,7 @@ class argument_parser { argument_parser(argument_parser&&) = delete; argument_parser& operator=(argument_parser&&) = delete; - 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!"); - } + argument_parser(const std::string_view name) : argument_parser(name, "") {} ~argument_parser() = default; @@ -276,49 +284,36 @@ class argument_parser { /** * @brief Adds a positional argument to the parser's configuration. * @tparam T Type of the argument value. - * @param primary_name The primary name of the argument. + * @param name The name of the argument. * @return Reference to the added positional argument. * @throws ap::invalid_configuration */ template - positional_argument& add_positional_argument(const std::string_view primary_name) { - this->_verify_arg_name_pattern(primary_name); - - const detail::argument_name arg_name(std::make_optional(primary_name)); - if (this->_is_arg_name_used(arg_name)) - throw invalid_configuration::argument_name_used(arg_name); - - 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); + positional_argument& add_positional_argument(const std::string_view name) { + return this->add_positional_argument(this->_gr_positional_args, name); } /** - * @brief Adds a positional argument to the parser's configuration. + * @brief Adds a positional argument to the parser's configuration and binds it to the given group. * @tparam T Type of the argument value. - * @param primary_name The primary name of the argument. - * @param secondary_name The secondary name of the argument. + * @param primary_name The name of the argument. * @return Reference to the added positional argument. * @throws ap::invalid_configuration */ template positional_argument& add_positional_argument( - const std::string_view primary_name, const std::string_view secondary_name + argument_group& group, const std::string_view name ) { - this->_verify_arg_name_pattern(primary_name); - this->_verify_arg_name_pattern(secondary_name); + this->_validate_group(group); + this->_verify_arg_name_pattern(name); - const detail::argument_name arg_name{ - std::make_optional(primary_name), - std::make_optional(secondary_name) - }; + const detail::argument_name arg_name(std::make_optional(name)); if (this->_is_arg_name_used(arg_name)) throw invalid_configuration::argument_name_used(arg_name); auto& new_arg_ptr = this->_positional_args.emplace_back(std::make_shared>(arg_name)); - this->_gr_positional_args._add_argument(new_arg_ptr); + group._add_argument(new_arg_ptr); return static_cast&>(*new_arg_ptr); } @@ -511,6 +506,27 @@ class argument_parser { return *this->_argument_groups.emplace_back(argument_group::create(*this, name)); } + /** + * @brief Adds an subparser with the given name to the parser's configuration. + * @param name Name of the subparser. + * @return Reference to the added subparser. + */ + argument_parser& add_subparser(const std::string_view name) { + const auto subparser_it = std::ranges::find( + this->_subparsers, name, [](const auto& subparser) { return subparser->_name; } + ); + if (subparser_it != this->_subparsers.end()) + throw std::logic_error(std::format( + "A subparser with the given name () already exists in parser '{}'", + (*subparser_it)->_name, + this->_program_name + )); + + return *this->_subparsers.emplace_back( + std::unique_ptr(new argument_parser(name, this->_program_name)) + ); + } + /** * @brief Parses the command-line arguments. * @@ -529,25 +545,20 @@ class argument_parser { } /** - * @todo use std::ranges::forward_range * @brief Parses the command-line arguments. * @tparam AR The argument range type. * @param argv_rng A range of command-line argument values. - * @note `argv_rng` must be a `std::ranges::range` with a value type convertible to `std::string`. + * @note `argv_rng` must be a `std::ranges::forward_range` with a value type convertible to `std::string`. * @throws ap::invalid_configuration, ap::parsing_failure * @attention This overload of the `parse_args` function assumes that the program name argument has already been discarded. */ - template AR> + template AR> void parse_args(const AR& argv_rng) { - this->_validate_argument_configuration(); - - parsing_state state{.curr_arg = nullptr, .curr_pos_arg_it = this->_positional_args.begin()}; - this->_parse_args_impl(this->_tokenize(argv_rng, state), state); + parsing_state state(*this); + this->_parse_args_impl(std::ranges::begin(argv_rng), std::ranges::end(argv_rng), state); if (not state.unknown_args.empty()) throw parsing_failure::argument_deduction_failure(state.unknown_args); - - this->_verify_final_state(); } /** @@ -567,7 +578,6 @@ class argument_parser { } /** - * @todo use std::ranges::forward_range * @brief Parses the command-line arguments and exits on error. * * Calls `parse_args(argv_rng)` in a try-catch block. If an error is thrown, then its @@ -576,16 +586,17 @@ class argument_parser { * * @tparam AR The argument range type. * @param argv_rng A range of command-line argument values. - * @note `argv_rng` must be a `std::ranges::range` with a value type convertible to `std::string`. + * @note `argv_rng` must be a `std::ranges::forward_range` with a value type convertible to `std::string`. * @attention This overload of the `try_parse_args` function assumes that the program name argument has already been discarded. */ - template AR> + template AR> void try_parse_args(const AR& argv_rng) { try { this->parse_args(argv_rng); } catch (const ap::argument_parser_exception& err) { - std::cerr << "[ap::error] " << err.what() << std::endl << *this << std::endl; + std::cerr << "[ap::error] " << err.what() << std::endl + << this->resolved_parser() << std::endl; std::exit(EXIT_FAILURE); } } @@ -613,7 +624,6 @@ class argument_parser { } /** - * @todo use std::ranges::forward_range * @brief Parses the known command-line arguments. * * * An argument is considered "known" if it was defined using the parser's argument declaraion methods: @@ -623,22 +633,14 @@ class argument_parser { * * @tparam AR The argument range type. * @param argv_rng A range of command-line argument values. - * @note `argv_rng` must be a `std::ranges::range` with a value type convertible to `std::string`. + * @note `argv_rng` must be a `std::ranges::forward_range` with a value type convertible to `std::string`. * @throws ap::invalid_configuration, ap::parsing_failure * @attention This overload of the `parse_known_args` function assumes that the program name argument already been discarded. */ - template AR> + template AR> std::vector parse_known_args(const AR& argv_rng) { - this->_validate_argument_configuration(); - - parsing_state state{ - .curr_arg = nullptr, - .curr_pos_arg_it = this->_positional_args.begin(), - .parse_known_only = true - }; - this->_parse_args_impl(this->_tokenize(argv_rng, state), state); - - this->_verify_final_state(); + parsing_state state(*this, true); + this->_parse_args_impl(std::ranges::begin(argv_rng), std::ranges::end(argv_rng), state); return std::move(state.unknown_args); } @@ -660,7 +662,6 @@ class argument_parser { } /** - * @todo use std::ranges::forward_range * @brief Parses known the command-line arguments and exits on error. * * Calls `parse_known_args(argv_rng)` in a try-catch block. If an error is thrown, then its message @@ -669,24 +670,86 @@ class argument_parser { * * @tparam AR The argument range type. * @param argv_rng A range of command-line argument values. - * @note `argv_rng` must be a `std::ranges::range` with a value type convertible to `std::string`. + * @note `argv_rng` must be a `std::ranges::forward_range` with a value type convertible to `std::string`. * @return A vector of unknown argument values. * @attention This overload of the `try_parse_known_args` function assumes that the program name argument has already been discarded. */ - template AR> + template AR> std::vector try_parse_known_args(const AR& argv_rng) { try { return this->parse_known_args(argv_rng); } catch (const ap::argument_parser_exception& err) { - std::cerr << "[ap::error] " << err.what() << std::endl << *this << std::endl; + std::cerr << "[ap::error] " << err.what() << std::endl + << this->resolved_parser() << std::endl; std::exit(EXIT_FAILURE); } } + /// @brief Returns the parser's name. + [[nodiscard]] std::string_view name() const noexcept { + return this->_name; + } + + /** + * @brief Returns the parser's full program name. + * + * - For top-level parsers, this is the same as the parser's name. + * - For subparsers, the name is prefixed with its parent parser names. + * + * @example + * Top-level parser: `git` + * Subparser: `git submodule` + * Nested subparser : `git submodule init` + */ + [[nodiscard]] std::string_view program_name() const noexcept { + return this->_program_name; + } + + /** + * @brief Check whether this parser was invoked. + * @return `true` if the parser was selected when parsing the command-line arguments, `false` otherwise. + * @note A parser is *invoked* as soon as the parser is selected during parsing, even if parsing is later delegated to one of its subparsers. + */ + [[nodiscard]] bool invoked() const noexcept { + return this->_invoked; + } + + /** + * @brief Check whether the parser has finalized parsing its own arguments. + * @return `true` if parsing was completed for the parser, `false` otherwise. + */ + [[nodiscard]] bool finalized() const noexcept { + return this->_finalized; + } + /** + * @brief Returns the *deepest invoked parser*. + * @return Reference to the finalized parser that ultimately processed the arguments. + */ + [[nodiscard]] argument_parser& resolved_parser() noexcept { + const auto used_subparser_it = std::ranges::find_if( + this->_subparsers, [](const auto& subparser) { return subparser->_invoked; } + ); + if (used_subparser_it == this->_subparsers.end()) + return *this; + return (*used_subparser_it)->resolved_parser(); + } + + /** + * @brief Check if a specific argument was used in the command-line. * @param arg_name The name of the argument. - * @return True if the argument has a value, false otherwise. + * @return `true` if the argument was used on the command line, `false` otherwise. + */ + [[nodiscard]] bool is_used(std::string_view arg_name) const noexcept { + const auto arg = this->_get_argument(arg_name); + return arg ? arg->is_used() : false; + } + + /** + * @brief Check if the given argument has a value. + * @param arg_name The name of the argument. + * @return `true` if the argument has a value, `false` otherwise. */ [[nodiscard]] bool has_value(std::string_view arg_name) const noexcept { const auto arg = this->_get_argument(arg_name); @@ -694,8 +757,9 @@ class argument_parser { } /** + * @brief Get the given argument's usage count. * @param arg_name The name of the argument. - * @return The count of times the argument has been used. + * @return The number of times the argument has been used. */ [[nodiscard]] std::size_t count(std::string_view arg_name) const noexcept { const auto arg = this->_get_argument(arg_name); @@ -703,6 +767,7 @@ class argument_parser { } /** + * @brief Get the value of the given argument. * @tparam T Type of the argument value. * @param arg_name The name of the argument. * @return The value of the argument. @@ -724,6 +789,7 @@ class argument_parser { } /** + * @brief Get the value of the given argument, if it has any, or a fallback value, if not. * @tparam T Type of the argument value. * @tparam U The default value type. * @param arg_name The name of the argument. @@ -752,6 +818,7 @@ class argument_parser { } /** + * @brief Get all values of the given argument. * @tparam T Type of the argument values. * @param arg_name The name of the argument. * @return The values of the argument as a vector. @@ -779,7 +846,7 @@ class argument_parser { /** * @brief Prints the argument parser's help message to an output stream. * @param verbose The verbosity mode value. - * @param os Output stream. + * @param os The output stream. */ void print_help(const bool verbose, std::ostream& os = std::cout) const noexcept { os << "Program: " << this->_program_name; @@ -792,10 +859,21 @@ class argument_parser { << std::string(this->_indent_width, ' ') << this->_program_description.value() << '\n'; - for (const auto& group : this->_argument_groups) { - std::cout << '\n'; + this->_print_subparsers(os); + for (const auto& group : this->_argument_groups) this->_print_group(os, *group, verbose); - } + } + + /** + * @brief Prints the argument parser's version info to an output stream. + * + * If no version was spcified for the parser, `unspecified` will be printed. + * + * @param os The output stream. + */ + void print_version(std::ostream& os = std::cout) const noexcept { + os << this->_program_name << " : version " << this->_program_version.value_or("unspecified") + << std::endl; } /** @@ -804,7 +882,7 @@ class argument_parser { * An `os << parser` operation is equivalent to a `parser.print_help(_verbose, os)` call, * where `_verbose` is the inner verbosity mode, which can be set with the @ref verbose function. * - * @param os Output stream. + * @param os The output stream. * @param parser The argument parser to print. * @return The modified output stream. */ @@ -825,20 +903,49 @@ class argument_parser { 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_parser_ptr_t = std::unique_ptr; + using arg_parser_ptr_vec_t = std::vector; using arg_token_vec_t = std::vector; + using arg_token_vec_iter_t = typename arg_token_vec_t::const_iterator; /// @brief A collection of values used during the parsing process. struct parsing_state { + parsing_state(argument_parser& parser, const bool parse_known_only = false) + : curr_arg(nullptr), + curr_pos_arg_it(parser._positional_args.begin()), + parse_known_only(parse_known_only) {} + + /// @brief Update the parser-specific parameters of the state object. + /// @param parser The new parser. + void set_parser(argument_parser& parser) { + this->curr_arg = nullptr; + this->curr_pos_arg_it = parser._positional_args.begin(); + } + arg_ptr_t curr_arg; ///< The currently processed argument. arg_ptr_vec_iter_t curr_pos_arg_it; ///< An iterator pointing to the next positional argument to be processed. + const bool + parse_known_only; ///< A flag indicating whether only known arguments should be parsed. std::vector unknown_args = {}; ///< A vector of unknown argument values. - const bool parse_known_only = - false; ///< A flag indicating whether only known arguments should be parsed. }; + argument_parser(const std::string_view name, const std::string_view parent_name) + : _name(name), + _program_name( + std::format("{}{}{}", parent_name, std::string(not parent_name.empty(), ' '), name) + ), + _gr_positional_args(add_group("Positional Arguments")), + _gr_optional_args(add_group("Optional Arguments")) { + if (name.empty()) // TODO: add test case + throw invalid_configuration("The program name cannot be empty!"); + + if (util::contains_whitespaces(name)) + throw invalid_configuration("The program name cannot contain whitespace characters!"); + } + /** * @brief Verifies the pattern of an argument name and if it's invalid, an error is thrown * @throws ap::invalid_configuration @@ -930,6 +1037,43 @@ class argument_parser { )); } + /** + * @brief Implementation of parsing command-line arguments. + * @tparam AIt The command-line argument value iterator type. + * @note `AIt` must be a `std::forward_iterator` with a value type convertible to `std::string`. + * @param args_begin The begin iterator for the command-line argument value range. + * @param args_end The end iterator for the command-line argument value range. + * @param state The current parsing state. + * @throws ap::invalid_configuration, ap::parsing_failure + */ + template AIt> + void _parse_args_impl(AIt args_begin, const AIt args_end, parsing_state& state) { + this->_invoked = true; + + if (args_begin != args_end) { + // try to match a subparser + const auto subparser_it = + std::ranges::find(this->_subparsers, *args_begin, [](const auto& subparser) { + return subparser->_name; + }); + if (subparser_it != this->_subparsers.end()) { + auto& subparser = **subparser_it; + state.set_parser(subparser); + subparser._parse_args_impl(++args_begin, args_end, state); + return; + } + } + + // process command-line arguments within the current parser + this->_validate_argument_configuration(); + std::ranges::for_each( + this->_tokenize(args_begin, args_end, state), + std::bind_front(&argument_parser::_parse_token, this, std::ref(state)) + ); + this->_verify_final_state(); + this->_finalized = true; + } + /** * @brief Validate whether the definition/configuration of the parser's arguments is correct. * @@ -946,6 +1090,7 @@ class argument_parser { } if (non_required_arg and arg->is_required()) + // TODO: remove static builder in v3 release commit throw invalid_configuration::positional::required_after_non_required( arg->name(), non_required_arg->name() ); @@ -954,20 +1099,22 @@ class argument_parser { /** * @brief Converts the command-line arguments into a list of tokens. - * @tparam AR The command-line argument value range type. - * @param arg_range The command-line argument value range. - * @note `arg_range` must be a `std::ranges::range` with a value type convertible to `std::string`. + * @tparam AIt The command-line argument value iterator type. + * @note `AIt` must be a `std::forward_iterator` with a value type convertible to `std::string`. + * @param args_begin The begin iterator for the command-line argument value range. + * @param args_end The end iterator for the command-line argument value range. * @return A list of preprocessed command-line argument tokens. */ - template AR> - [[nodiscard]] arg_token_vec_t _tokenize(const AR& arg_range, const parsing_state& state) { + template AIt> + [[nodiscard]] arg_token_vec_t _tokenize( + AIt args_begin, const AIt args_end, const parsing_state& state + ) { arg_token_vec_t toks; - - if constexpr (std::ranges::sized_range) - toks.reserve(std::ranges::size(arg_range)); + toks.reserve(static_cast(std::ranges::distance(args_begin, args_end))); std::ranges::for_each( - arg_range, + args_begin, + args_end, std::bind_front(&argument_parser::_tokenize_arg, this, std::ref(state), std::ref(toks)) ); @@ -1126,19 +1273,6 @@ class argument_parser { } } - /** - * @brief Implementation of parsing command-line arguments. - * @param arg_tokens The list of command-line argument tokens. - * @param state The current parsing state. - * @throws ap::parsing_failure - */ - 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)) - ); - } - /** * @brief Parse a single command-line argument token. * @param state The current parsing state. @@ -1226,13 +1360,9 @@ class argument_parser { * @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(); - } - + const bool are_required_args_bypassed = this->_are_required_args_bypassed(); for (const auto& group : this->_argument_groups) - this->_verify_group_requirements(*group); + this->_verify_group_requirements(*group, are_required_args_bypassed); } /** @@ -1252,56 +1382,66 @@ class argument_parser { }); } - /** - * @brief Check if all required positional and optional arguments are used. - * @throws ap::parsing_failure - */ - void _verify_required_args() const { - // TODO: use std::views::join after the transition to C++23 - for (const auto& arg : this->_positional_args) - if (arg->is_required() and not arg->has_value()) - throw parsing_failure::required_argument_not_parsed(arg->name()); - - for (const auto& arg : this->_optional_args) - if (arg->is_required() and not arg->has_value()) - throw parsing_failure::required_argument_not_parsed(arg->name()); - } - - /** - * @brief Check if the number of argument values is within the specified range. - * @throws ap::parsing_failure - */ - void _verify_nvalues() const { - // TODO: use std::views::join after the transition to C++23 - for (const auto& arg : this->_positional_args) - if (const auto nv_ord = arg->nvalues_ordering(); not std::is_eq(nv_ord)) - throw parsing_failure::invalid_nvalues(arg->name(), nv_ord); - - for (const auto& arg : this->_optional_args) - if (const auto nv_ord = arg->nvalues_ordering(); not std::is_eq(nv_ord)) - 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. + * @param are_required_args_bypassed A flag indicating whether required argument bypassing is enabled. * @throws ap::parsing_failure if the requirements are not satistied. */ - void _verify_group_requirements(const argument_group& group) const { + void _verify_group_requirements( + const argument_group& group, const bool are_required_args_bypassed + ) const { + if (group._arguments.empty()) + return; + const auto n_used_args = static_cast( std::ranges::count_if(group._arguments, [](const auto& arg) { return arg->is_used(); }) ); + if (group._mutually_exclusive) { + if (n_used_args > 1ull) + throw parsing_failure(std::format( + "At most one argument from the mutually exclusive group '{}' can be used", + group._name + )); + + const auto used_arg_it = std::ranges::find_if(group._arguments, [](const auto& arg) { + return arg->is_used(); + }); + + if (used_arg_it != group._arguments.end()) { + // only the one used argument has to be validated + this->_verify_argument_requirements(*used_arg_it, are_required_args_bypassed); + return; + } + } + 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 - )); + // all arguments in the group have to be validated + for (const auto& arg : group._arguments) + this->_verify_argument_requirements(arg, are_required_args_bypassed); + } + + /** + * @brief Verifies whether the requirements of the given argument are satisfied. + * @param arg The argument to verify. + * @param are_required_args_bypassed A flag indicating whether required argument bypassing is enabled. + * @throws ap::parsing_failure if the requirements are not satistied. + */ + void _verify_argument_requirements(const arg_ptr_t& arg, const bool are_required_args_bypassed) + const { + if (are_required_args_bypassed) + return; + + if (arg->is_required() and not arg->has_value()) + throw parsing_failure::required_argument_not_parsed(arg->name()); + + if (const auto nv_ord = arg->nvalues_ordering(); not std::is_eq(nv_ord)) + throw parsing_failure::invalid_nvalues(arg->name(), nv_ord); } /** @@ -1325,6 +1465,28 @@ class argument_parser { return nullptr; } + void _print_subparsers(std::ostream& os) const noexcept { + if (this->_subparsers.empty()) + return; + + os << "\nCommands:\n"; + + std::vector builders; + builders.reserve(this->_subparsers.size()); + + for (const auto& subparser : this->_subparsers) + builders.emplace_back(subparser->_name, subparser->_program_description); + + std::size_t max_subparser_name_length = 0ull; + for (const auto& bld : builders) + max_subparser_name_length = std::max(max_subparser_name_length, bld.name.length()); + + for (const auto& bld : builders) + os << '\n' << bld.get_basic(this->_indent_width, max_subparser_name_length); + + os << '\n'; + } + /** * @brief Print the given argument list to an output stream. * @param os The output stream to print to. @@ -1341,7 +1503,7 @@ class argument_parser { if (std::ranges::empty(visible_args)) return; - os << group._name << ":"; + os << '\n' << group._name << ":"; std::vector group_attrs; if (group._required) @@ -1355,38 +1517,44 @@ class argument_parser { if (verbose) { for (const auto& arg : visible_args) - os << '\n' << arg->desc(verbose).get(this->_indent_width) << '\n'; + os << '\n' << arg->help_builder(verbose).get(this->_indent_width) << '\n'; } else { - std::vector descriptors; - descriptors.reserve(group._arguments.size()); + std::vector builders; + builders.reserve(group._arguments.size()); for (const auto& arg : visible_args) - descriptors.emplace_back(arg->desc(verbose)); + builders.emplace_back(arg->help_builder(verbose)); std::size_t max_arg_name_length = 0ull; - for (const auto& desc : descriptors) - max_arg_name_length = std::max(max_arg_name_length, desc.name.length()); + for (const auto& bld : builders) + max_arg_name_length = std::max(max_arg_name_length, bld.name.length()); - for (const auto& desc : descriptors) - os << '\n' << desc.get_basic(this->_indent_width, max_arg_name_length); + for (const auto& bld : builders) + os << '\n' << bld.get_basic(this->_indent_width, max_arg_name_length); os << '\n'; } } - std::string _program_name; - std::optional _program_version; - std::optional _program_description; - bool _verbose = false; - unknown_policy _unknown_policy = unknown_policy::fail; - - 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; + std::string _name; ///< The name of the parser. + std::string + _program_name; ///< The name of the program in the format "... ". + std::optional _program_version; ///< The version of the program. + std::optional _program_description; ///< The description of the program. + bool _verbose = false; ///< Verbosity flag. + unknown_policy _unknown_policy = unknown_policy::fail; ///< Policy for unknown arguments. + + arg_ptr_vec_t _positional_args; ///< The list of positional arguments. + arg_ptr_vec_t _optional_args; ///< The list of optional arguments. + arg_group_ptr_vec_t _argument_groups; ///< The list of argument groups. + argument_group& _gr_positional_args; ///< The positional argument group. + argument_group& _gr_optional_args; ///< The optional argument group. + arg_parser_ptr_vec_t _subparsers; ///< The list of subparsers. + + bool _invoked = + false; ///< A flag indicating whether the parser has been invoked to parse arguments. + bool _finalized = false; ///< A flag indicating whether the parsing process has been finalized. static constexpr std::uint8_t _primary_flag_prefix_length = 2u; static constexpr std::uint8_t _secondary_flag_prefix_length = 1u; @@ -1422,6 +1590,15 @@ inline void add_default_argument( .help("Display the help message"); break; + case default_argument::o_version: + arg_parser.add_optional_argument("version", "v") + .action([&arg_parser]() { + arg_parser.print_version(); + std::exit(EXIT_SUCCESS); + }) + .help("Dsiplay program version info"); + break; + case default_argument::o_input: arg_parser.add_optional_argument("input", "i") .nargs(1ull) diff --git a/include/ap/detail/argument_base.hpp b/include/ap/detail/argument_base.hpp index 6a5caccd..c01a5fcf 100644 --- a/include/ap/detail/argument_base.hpp +++ b/include/ap/detail/argument_base.hpp @@ -9,8 +9,8 @@ #pragma once -#include "argument_descriptor.hpp" #include "argument_name.hpp" +#include "help_builder.hpp" #include #include @@ -55,8 +55,8 @@ class argument_base { 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. - virtual detail::argument_descriptor desc(const bool verbose) const noexcept = 0; + /// @return A help message builder object for the argument. + virtual detail::help_builder help_builder(const bool verbose) const noexcept = 0; /// @brief Mark the argument as used. /// @return `true` if the argument accepts further values, `false` otherwise. diff --git a/include/ap/detail/argument_descriptor.hpp b/include/ap/detail/help_builder.hpp similarity index 95% rename from include/ap/detail/argument_descriptor.hpp rename to include/ap/detail/help_builder.hpp index 026f878d..830a1982 100644 --- a/include/ap/detail/argument_descriptor.hpp +++ b/include/ap/detail/help_builder.hpp @@ -3,8 +3,8 @@ // Licensed under the MIT License. See the LICENSE file in the project root for full license information. /** - * @file ap/detail/argument_descriptor.hpp - * @brief Defines structures for formatting argument descriptions. + * @file ap/detail/help_builder.hpp + * @brief Defines structures for creating and formatting help messages. */ #pragma once @@ -28,14 +28,14 @@ struct parameter_descriptor { std::string value; }; -/// @brief A structure used to represent an argument's description. -class argument_descriptor { +/// @brief A help message builder class. +class help_builder { public: /** * @param name The string representation of the argument's name. * @param help An optional help message string. */ - argument_descriptor(const std::string& name, const std::optional& help) + help_builder(const std::string& name, const std::optional& help = std::nullopt) : name(name), help(help) {} /** diff --git a/include/ap/util/concepts.hpp b/include/ap/util/concepts.hpp index 753f4019..3dc95bb1 100644 --- a/include/ap/util/concepts.hpp +++ b/include/ap/util/concepts.hpp @@ -139,4 +139,27 @@ concept c_range_of = std::ranges::range and c_valid_type>, V, TV>; +/** + * @brief Validates that It is a forward iterator of type T (ignoring the cvref qualifiers). + * @tparam It The iterator type to check. + * @tparam V The expected iterator value type. + * @tparam TV The validation rule (`same` or `convertible`). + * @ingroup util + */ +template +concept c_forward_range_of = + std::ranges::range + and c_valid_type>, V, TV>; + +/** + * @brief Validates that It is a forward iterator of type T (ignoring the cvref qualifiers). + * @tparam It The iterator type to check. + * @tparam V The expected iterator value type. + * @tparam TV The validation rule (`same` or `convertible`). + * @ingroup util + */ +template +concept c_forward_iterator_of = + std::input_iterator and c_valid_type, V, TV>; + } // namespace ap::util diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4c9067d3..a9184b25 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -63,14 +63,14 @@ macro(add_doctest SOURCE_FILE) add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) endmacro() -add_doctest("source/test_argument_descriptor.cpp") +add_doctest("source/test_string_utility.cpp") add_doctest("source/test_argument_name.cpp") -add_doctest("source/test_argument_token.cpp") -add_doctest("source/test_argument_parser_add_argument.cpp") -add_doctest("source/test_argument_parser_info.cpp") -add_doctest("source/test_argument_parser_parse_args.cpp") add_doctest("source/test_nargs_range.cpp") +add_doctest("source/test_help_builder.cpp") +add_doctest("source/test_argument_token.cpp") add_doctest("source/test_none_type_argument.cpp") -add_doctest("source/test_optional_argument.cpp") add_doctest("source/test_positional_argument.cpp") -add_doctest("source/test_string_utility.cpp") +add_doctest("source/test_optional_argument.cpp") +add_doctest("source/test_argument_parser_add_elements.cpp") +add_doctest("source/test_argument_parser_info.cpp") +add_doctest("source/test_argument_parser_parse_args.cpp") diff --git a/tests/include/argument_parser_test_fixture.hpp b/tests/include/argument_parser_test_fixture.hpp index 05e5dd0c..f2e7924a 100644 --- a/tests/include/argument_parser_test_fixture.hpp +++ b/tests/include/argument_parser_test_fixture.hpp @@ -10,6 +10,8 @@ using ap::positional_argument; using ap::detail::argument_name; using ap::detail::argument_token; using ap::util::c_argument_value_type; +using ap::util::c_forward_iterator_of; +using ap::util::type_validator; namespace ap_testing { @@ -64,12 +66,15 @@ struct argument_parser_test_fixture { } [[nodiscard]] std::vector init_argv_vec( - std::size_t n_positional_args, std::size_t n_optional_args + const std::size_t n_positional_args, + const std::size_t n_optional_args, + const bool with_program_name = true ) const { std::vector argv_vec; argv_vec.reserve(static_cast(get_argc(n_positional_args, n_optional_args))); - argv_vec.emplace_back("program"); + if (with_program_name) + argv_vec.emplace_back("program"); for (std::size_t i = 0ull; i < n_positional_args; ++i) argv_vec.emplace_back(init_arg_value(i)); @@ -115,9 +120,7 @@ struct argument_parser_test_fixture { typename F = std::function&)>> void add_positional_args(const std::size_t n, F&& setup_arg = [](positional_argument&) {}) { for (std::size_t i = 0ull; i < n; ++i) - setup_arg(this->sut.add_positional_argument( - init_arg_name_primary(i), init_arg_name_secondary(i) - )); + setup_arg(this->sut.add_positional_argument(init_arg_name_primary(i))); } template < @@ -165,10 +168,6 @@ struct argument_parser_test_fixture { } // argument_parser private member accessors - [[nodiscard]] const std::string& get_program_name() const { - return this->sut._program_name; - } - [[nodiscard]] const std::optional& get_program_description() const { return this->sut._program_description; } @@ -179,13 +178,13 @@ struct argument_parser_test_fixture { // private function callers [[nodiscard]] arg_token_vec_t tokenize(int argc, char* argv[]) { - return this->sut._tokenize(std::span(argv + 1, static_cast(argc - 1)), state); + return this->sut._tokenize(argv + 1, argv + argc, state); } - 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); + template AIt> + void parse_args_impl(AIt args_begin, const AIt args_end) { + this->state.set_parser(sut); + this->sut._parse_args_impl(args_begin, args_end, this->state); } [[nodiscard]] arg_ptr_t get_argument(std::string_view arg_name) const { @@ -193,7 +192,7 @@ struct argument_parser_test_fixture { } ap::argument_parser sut{program_name}; - parsing_state state; + parsing_state state{sut}; static constexpr std::string_view program_name = "program"; }; diff --git a/tests/include/argument_test_fixture.hpp b/tests/include/argument_test_fixture.hpp index 749640dc..8580b376 100644 --- a/tests/include/argument_test_fixture.hpp +++ b/tests/include/argument_test_fixture.hpp @@ -5,8 +5,8 @@ using ap::argument; using ap::argument_type; -using ap::detail::argument_descriptor; using ap::detail::argument_name; +using ap::detail::help_builder; using ap::util::as_string; using ap::util::c_argument_value_type; @@ -107,9 +107,9 @@ struct argument_test_fixture { } template - [[nodiscard]] argument_descriptor get_desc(const argument& arg, const bool verbose) + [[nodiscard]] help_builder get_help_builder(const argument& arg, const bool verbose) const { - return arg.desc(verbose); + return arg.help_builder(verbose); } template diff --git a/tests/source/test_argument_parser_add_argument.cpp b/tests/source/test_argument_parser_add_elements.cpp similarity index 85% rename from tests/source/test_argument_parser_add_argument.cpp rename to tests/source/test_argument_parser_add_elements.cpp index 2d636ba5..ab14f042 100644 --- a/tests/source/test_argument_parser_add_argument.cpp +++ b/tests/source/test_argument_parser_add_elements.cpp @@ -9,7 +9,7 @@ using ap::argument_parser; using ap::default_argument; using ap::invalid_configuration; -struct test_argument_parser_add_argument : public argument_parser_test_fixture { +struct test_argument_parser_add_elements : public argument_parser_test_fixture { const char flag_char = '-'; const std::string_view primary_name_1 = "primary_name_1"; @@ -35,7 +35,7 @@ struct test_argument_parser_add_argument : public argument_parser_test_fixture { }; TEST_CASE_FIXTURE( - test_argument_parser_add_argument, + test_argument_parser_add_elements, "add_{positional,optional}_argument(primary) should throw if the passed argument name is " "invalid" ) { @@ -78,7 +78,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_argument, + test_argument_parser_add_elements, "add_{positional,optional}_argument(primary, secondary) should throw if the primary name is " "invalid" ) { @@ -108,21 +108,21 @@ TEST_CASE_FIXTURE( CAPTURE(reason); CHECK_THROWS_WITH_AS( - sut.add_positional_argument(primary_name, secondary_name_1), + sut.add_positional_argument(primary_name), invalid_configuration::invalid_argument_name(primary_name, reason).what(), invalid_configuration ); CHECK_THROWS_WITH_AS( - sut.add_optional_argument(primary_name, secondary_name_1), + sut.add_optional_argument(primary_name), invalid_configuration::invalid_argument_name(primary_name, reason).what(), invalid_configuration ); } TEST_CASE_FIXTURE( - test_argument_parser_add_argument, - "add_{positional,optional}_argument(primary, secondary) should throw if the secondary name is " + test_argument_parser_add_elements, + "add_optional_argument(primary, secondary) should throw if the secondary name is " "invalid" ) { std::string secondary_name, reason; @@ -150,12 +150,6 @@ TEST_CASE_FIXTURE( CAPTURE(secondary_name); CAPTURE(reason); - CHECK_THROWS_WITH_AS( - sut.add_positional_argument(primary_name_1, secondary_name), - invalid_configuration::invalid_argument_name(secondary_name, reason).what(), - invalid_configuration - ); - CHECK_THROWS_WITH_AS( sut.add_optional_argument(primary_name_1, secondary_name), invalid_configuration::invalid_argument_name(secondary_name, reason).what(), @@ -164,36 +158,24 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_argument, + test_argument_parser_add_elements, "add_positional_argument should throw when adding an argument with a previously used name" ) { - sut.add_positional_argument(primary_name_1, secondary_name_1); + sut.add_positional_argument(primary_name_1); - SUBCASE("adding argument with a unique name") { - CHECK_NOTHROW(sut.add_positional_argument(primary_name_2, secondary_name_2)); - } + // adding argument with a unique name + CHECK_NOTHROW(sut.add_positional_argument(primary_name_2)); - SUBCASE("adding argument with a previously used primary name") { - CHECK_THROWS_WITH_AS( - sut.add_positional_argument(primary_name_1, secondary_name_2), - invalid_configuration::argument_name_used({primary_name_1_opt, secondary_name_2_opt}) - .what(), - invalid_configuration - ); - } - - SUBCASE("adding argument with a previously used secondary name") { - CHECK_THROWS_WITH_AS( - sut.add_positional_argument(primary_name_2, secondary_name_1), - invalid_configuration::argument_name_used({primary_name_2_opt, secondary_name_1_opt}) - .what(), - invalid_configuration - ); - } + // adding argument with a previously used name + CHECK_THROWS_WITH_AS( + sut.add_positional_argument(primary_name_1), + invalid_configuration::argument_name_used({primary_name_1_opt}).what(), + invalid_configuration + ); } TEST_CASE_FIXTURE( - test_argument_parser_add_argument, + test_argument_parser_add_elements, "add_optional_argument should throw when adding an argument with a previously used name" ) { sut.add_optional_argument(primary_name_1, secondary_name_1); @@ -226,7 +208,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_argument, + test_argument_parser_add_elements, "add_flag should return an optional argument reference with flag parameters" ) { const argument_test_fixture arg_fixture; @@ -253,7 +235,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_argument, + test_argument_parser_add_elements, "add_flag should throw when adding and argument with a previously used name" ) { sut.add_flag(primary_name_1, secondary_name_1); @@ -286,7 +268,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_argument, + test_argument_parser_add_elements, "default_arguments should add the specified positional arguments" ) { sut.default_arguments({default_argument::p_input, default_argument::p_output}); @@ -301,7 +283,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_argument, + test_argument_parser_add_elements, "default_arguments should add the specified optional arguments" ) { sut.default_arguments( @@ -343,7 +325,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_argument, + test_argument_parser_add_elements, "add_optional_argument and add_flag should throw if a group does not belong to the parser" ) { argument_parser different_parser("different-program"); @@ -386,3 +368,23 @@ TEST_CASE_FIXTURE( std::logic_error ); } + +TEST_CASE_FIXTURE( + test_argument_parser_add_elements, + "add_subparser should throw if a subparser with the given name already exists" +) { + constexpr std::string_view subparser_name = "subprogram"; + + sut.add_subparser(subparser_name); + + CHECK_THROWS_WITH_AS( + sut.add_subparser(subparser_name), + std::format( + "A subparser with the given name () already exists in parser '{}'", + subparser_name, + sut.name() + ) + .c_str(), + std::logic_error + ); +} diff --git a/tests/source/test_argument_parser_info.cpp b/tests/source/test_argument_parser_info.cpp index 6a37ade7..b8f55c65 100644 --- a/tests/source/test_argument_parser_info.cpp +++ b/tests/source/test_argument_parser_info.cpp @@ -21,9 +21,22 @@ TEST_CASE("argument_parser() should throw if the name contains whitespaces") { } TEST_CASE_FIXTURE( - test_argument_parser_info, "argument_parser() should set the program name member" + test_argument_parser_info, "argument_parser() should set the name and program name members" ) { - CHECK_EQ(get_program_name(), program_name); + CHECK_EQ(sut.name(), program_name); + CHECK_EQ(sut.program_name(), program_name); +} + +TEST_CASE_FIXTURE( + test_argument_parser_info, + "subparser's program name should be a concatenation of the parent parser's name and its own " + "name" +) { + constexpr std::string_view subparser_name = "subprogram"; + + auto& subparser = sut.add_subparser(subparser_name); + CHECK_EQ(subparser.name(), subparser_name); + CHECK_EQ(subparser.program_name(), std::format("{} {}", sut.name(), subparser_name)); } TEST_CASE_FIXTURE( @@ -62,7 +75,9 @@ TEST_CASE_FIXTURE( CHECK_FALSE(stored_program_description); } -TEST_CASE_FIXTURE(test_argument_parser_info, "name() should set the program name member") { +TEST_CASE_FIXTURE( + test_argument_parser_info, "program_description() should set the program description member" +) { sut.program_description(test_description); const auto stored_program_description = get_program_description(); diff --git a/tests/source/test_argument_parser_parse_args.cpp b/tests/source/test_argument_parser_parse_args.cpp index 943f48c8..19dbe5b3 100644 --- a/tests/source/test_argument_parser_parse_args.cpp +++ b/tests/source/test_argument_parser_parse_args.cpp @@ -20,12 +20,7 @@ struct test_argument_parser_parse_args : public argument_parser_test_fixture { const std::string invalid_arg_name = "invalid_arg"; - const std::string positional_primary_name = "positional_arg"; - const std::string positional_secondary_name = "pa"; - - const std::optional positional_primary_name_opt = positional_primary_name; - const std::optional positional_secondary_name_opt = positional_secondary_name; - + const std::string positional_name = "positional_arg"; const std::string optional_primary_name = "optional_arg"; const std::string optional_secondary_name = "oa"; @@ -94,10 +89,10 @@ TEST_CASE_FIXTURE( ) { add_arguments(n_positional_args, n_optional_args); - auto arg_tokens = init_arg_tokens(n_positional_args, n_optional_args); - arg_tokens.erase(std::next(arg_tokens.begin(), static_cast(first_opt_arg_idx))); + auto argv_vec = init_argv_vec(n_positional_args, n_optional_args, false); + argv_vec.erase(std::next(argv_vec.begin(), static_cast(first_opt_arg_idx))); - CHECK_NOTHROW(parse_args_impl(arg_tokens)); + CHECK_NOTHROW(parse_args_impl(argv_vec.begin(), argv_vec.end())); CHECK_NE( std::ranges::find(state.unknown_args, init_arg_value(first_opt_arg_idx)), state.unknown_args.end() @@ -110,9 +105,9 @@ TEST_CASE_FIXTURE( "list" ) { add_arguments(n_positional_args, n_optional_args); - const auto arg_tokens = init_arg_tokens(n_positional_args, n_optional_args); + auto argv_vec = init_argv_vec(n_positional_args, n_optional_args, false); - CHECK_NOTHROW(parse_args_impl(arg_tokens)); + CHECK_NOTHROW(parse_args_impl(argv_vec.begin(), argv_vec.end())); CHECK(state.unknown_args.empty()); } @@ -132,10 +127,12 @@ TEST_CASE_FIXTURE( ) { add_arguments(n_positional_args, n_optional_args); - for (std::size_t i = 0ull; i < n_positional_args; ++i) { - const auto arg_name = init_arg_name(i); - CHECK(get_argument(arg_name.primary.value())); - CHECK(get_argument(arg_name.secondary.value())); + for (std::size_t i = 0ull; i < n_positional_args; ++i) + CHECK(get_argument(init_arg_name_primary(i))); + + for (std::size_t i = first_opt_arg_idx; i < n_args_total; ++i) { + CHECK(get_argument(init_arg_name_primary(i))); + CHECK(get_argument(init_arg_name_secondary(i))); } } @@ -158,7 +155,8 @@ TEST_CASE_FIXTURE( CHECK_THROWS_WITH_AS( sut.parse_args(argc, argv), - parsing_failure::required_argument_not_parsed({init_arg_name(last_pos_arg_idx)}).what(), + parsing_failure::required_argument_not_parsed({init_arg_name_primary(last_pos_arg_idx)}) + .what(), parsing_failure ); @@ -170,16 +168,11 @@ TEST_CASE_FIXTURE( "parse_args should throw if a required positional argument is defined after a non-required " "posisitonal argument" ) { - const auto non_required_arg_name = init_arg_name(0ull); - sut.add_positional_argument( - non_required_arg_name.primary.value(), non_required_arg_name.secondary.value() - ) - .required(false); + const auto non_required_arg_name = init_arg_name_primary(0ull); + sut.add_positional_argument(non_required_arg_name).required(false); - const auto required_arg_name = init_arg_name(1ull); - sut.add_positional_argument( - required_arg_name.primary.value(), required_arg_name.secondary.value() - ); + const auto required_arg_name = init_arg_name_primary(1ull); + sut.add_positional_argument(required_arg_name); const auto argc = get_argc(no_args, no_args); auto argv = init_argv(no_args, no_args); @@ -187,7 +180,7 @@ TEST_CASE_FIXTURE( CHECK_THROWS_WITH_AS( sut.parse_args(argc, argv), invalid_configuration::positional::required_after_non_required( - required_arg_name, non_required_arg_name + {required_arg_name}, {non_required_arg_name} ) .what(), invalid_configuration @@ -603,10 +596,12 @@ TEST_CASE_FIXTURE( REQUIRE_NOTHROW(sut.parse_args(argc, argv)); - for (std::size_t i = 0ull; i < n_args_total; ++i) { - const auto arg_name = init_arg_name(i); - CHECK(sut.has_value(arg_name.primary.value())); - CHECK(sut.has_value(arg_name.secondary.value())); + for (std::size_t i = 0ull; i < n_positional_args; ++i) + CHECK(sut.has_value(init_arg_name_primary(i))); + + for (std::size_t i = first_opt_arg_idx; i < n_args_total; ++i) { + CHECK(sut.has_value(init_arg_name_primary(i))); + CHECK(sut.has_value(init_arg_name_secondary(i))); } free_argv(argc, argv); @@ -636,7 +631,7 @@ TEST_CASE_FIXTURE( test_argument_parser_parse_args, "count should return the number of argument's flag usage" ) { // prepare sut - sut.add_positional_argument(positional_primary_name, positional_secondary_name); + sut.add_positional_argument(positional_name); sut.add_optional_argument(optional_primary_name, optional_secondary_name) .nargs(ap::nargs::any()); @@ -662,7 +657,7 @@ TEST_CASE_FIXTURE( sut.parse_args(argc, argv); // test count - CHECK_EQ(sut.count(positional_primary_name), positional_count); + CHECK_EQ(sut.count(positional_name), positional_count); CHECK_EQ(sut.count(optional_primary_name), optional_count); // free argv @@ -685,10 +680,12 @@ TEST_CASE_FIXTURE( ) { add_arguments(n_positional_args, n_optional_args); - for (std::size_t i = 0ull; i < n_args_total; ++i) { - const auto arg_name = init_arg_name(i); - CHECK_THROWS_AS(discard_result(sut.value(arg_name.primary.value())), std::logic_error); - CHECK_THROWS_AS(discard_result(sut.value(arg_name.secondary.value())), std::logic_error); + for (std::size_t i = 0ull; i < n_positional_args; ++i) + CHECK_THROWS_AS(discard_result(sut.value(init_arg_name_primary(i))), std::logic_error); + + for (std::size_t i = first_opt_arg_idx; i < n_args_total; ++i) { + CHECK_THROWS_AS(discard_result(sut.value(init_arg_name_primary(i))), std::logic_error); + CHECK_THROWS_AS(discard_result(sut.value(init_arg_name_secondary(i))), std::logic_error); } } @@ -729,7 +726,15 @@ TEST_CASE_FIXTURE( REQUIRE_NOTHROW(sut.parse_args(argc, argv)); - for (std::size_t i = 0ull; i < n_args_total; ++i) { + for (std::size_t i = 0ull; i < n_positional_args; ++i) { + const auto arg_name = init_arg_name_primary(i); + const auto arg_value = init_arg_value(i); + + REQUIRE(sut.has_value(arg_name)); + CHECK_EQ(sut.value(arg_name), arg_value); + } + + for (std::size_t i = first_opt_arg_idx; i < n_args_total; ++i) { const auto arg_name = init_arg_name(i); const auto arg_value = init_arg_value(i); @@ -812,7 +817,15 @@ TEST_CASE_FIXTURE( REQUIRE_NOTHROW(sut.parse_args(argc, argv)); - for (std::size_t i = 0ull; i < n_args_total; ++i) { + for (std::size_t i = 0ull; i < n_positional_args; ++i) { + const auto arg_name = init_arg_name_primary(i); + const auto arg_value = init_arg_value(i); + + REQUIRE(sut.has_value(arg_name)); + CHECK_EQ(sut.value_or(arg_name, empty_str), arg_value); + } + + for (std::size_t i = first_opt_arg_idx; i < n_args_total; ++i) { const auto arg_name = init_arg_name(i); const auto arg_value = init_arg_value(i); @@ -856,7 +869,14 @@ TEST_CASE_FIXTURE( add_arguments(n_positional_args, n_optional_args); - for (std::size_t i = 0ull; i < n_args_total; ++i) { + for (std::size_t i = 0ull; i < n_positional_args; ++i) { + const default_value_type fallback_value = 2 * static_cast(i); + CHECK_EQ( + sut.value_or(init_arg_name_primary(i), fallback_value), fallback_value + ); + } + + for (std::size_t i = first_opt_arg_idx; i < n_args_total; ++i) { const auto arg_name = init_arg_name(i); const default_value_type fallback_value = 2 * static_cast(i); @@ -876,7 +896,12 @@ TEST_CASE_FIXTURE( ) { add_arguments(n_positional_args, n_optional_args); - for (std::size_t i = 0ull; i < n_args_total; ++i) { + for (std::size_t i = 0ull; i < n_positional_args; ++i) { + const auto fallback_value = init_arg_value(i); + CHECK_EQ(sut.value_or(init_arg_name_primary(i), fallback_value), fallback_value); + } + + for (std::size_t i = first_opt_arg_idx; i < n_args_total; ++i) { const auto arg_name = init_arg_name(i); const auto fallback_value = init_arg_value(i); @@ -891,10 +916,8 @@ TEST_CASE_FIXTURE( test_argument_parser_parse_args, "values() [positional arguments] should return an empty vector if an argument has no values" ) { - sut.add_positional_argument(positional_primary_name, positional_secondary_name); - - CHECK(sut.values(positional_primary_name).empty()); - CHECK(sut.values(positional_secondary_name).empty()); + sut.add_positional_argument(positional_name); + CHECK(sut.values(positional_name).empty()); } TEST_CASE_FIXTURE( @@ -902,7 +925,7 @@ TEST_CASE_FIXTURE( "values() [positional arguments] should throw when an argument has values but the given type " "is invalid" ) { - sut.add_positional_argument(positional_primary_name, positional_secondary_name); + sut.add_positional_argument(positional_name); // prepare argc & argv const int argc = get_argc(1ull, no_args); @@ -912,12 +935,7 @@ TEST_CASE_FIXTURE( sut.parse_args(argc, argv); CHECK_THROWS_AS( - discard_result(sut.values(positional_primary_name)), - ap::type_error - ); - CHECK_THROWS_AS( - discard_result(sut.values(positional_secondary_name)), - ap::type_error + discard_result(sut.values(positional_name)), ap::type_error ); free_argv(argc, argv); @@ -930,8 +948,7 @@ TEST_CASE_FIXTURE( ) { const std::vector default_values{"default_value_1", "default_value_2"}; - sut.add_positional_argument(positional_primary_name, positional_secondary_name) - .default_values(default_values); + sut.add_positional_argument(positional_name).default_values(default_values); // prepare argc & argv const int argc = get_argc(no_args, no_args); @@ -940,7 +957,7 @@ TEST_CASE_FIXTURE( // parse args sut.parse_args(argc, argv); - const auto& stored_values = sut.values(positional_primary_name); + const auto& stored_values = sut.values(positional_name); REQUIRE_EQ(stored_values.size(), default_values.size()); CHECK_EQ(stored_values, default_values); @@ -953,7 +970,7 @@ TEST_CASE_FIXTURE( "values() [positional arguments] should return a correct vector of values when there is an " "argument with a given name and has parsed values" ) { - sut.add_positional_argument(positional_primary_name, positional_secondary_name).nargs(any()); + sut.add_positional_argument(positional_name).nargs(any()); // prepare argc & argv const std::size_t n_positional_values = 3ull; @@ -970,7 +987,7 @@ TEST_CASE_FIXTURE( // parse args sut.parse_args(argc, argv); - const auto& stored_values = sut.values(positional_primary_name); + const auto& stored_values = sut.values(positional_name); REQUIRE_EQ(stored_values.size(), positional_arg_values.size()); CHECK_EQ(stored_values, positional_arg_values); @@ -1441,3 +1458,37 @@ TEST_CASE_FIXTURE( free_argv(argc, argv); } + +// subparsers + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "an argument parser should delegate parsing remaining arguments if the first command-line " + "argument matches a subparser's name" +) { + const std::string subparser_name = "subprogram"; + auto& subparser = sut.add_subparser(subparser_name); + + const std::string pos_arg_name = "positional"; + const std::string pos_arg_val = "positional-value"; + const std::string opt_arg_name = "optional"; + const std::string opt_arg_val = "optional-value"; + + subparser.add_positional_argument(pos_arg_name); + subparser.add_optional_argument(opt_arg_name); + + const std::vector argv_vec{ + subparser_name, pos_arg_val, std::format("--{}", opt_arg_name), opt_arg_val + }; + + sut.parse_args(argv_vec); + + REQUIRE(sut.invoked()); + CHECK_FALSE(sut.finalized()); + + REQUIRE(subparser.invoked()); + CHECK(subparser.finalized()); + CHECK_EQ(&subparser, &sut.resolved_parser()); + CHECK_EQ(subparser.value(pos_arg_name), pos_arg_val); + CHECK_EQ(subparser.value(opt_arg_name), opt_arg_val); +} diff --git a/tests/source/test_argument_descriptor.cpp b/tests/source/test_help_builder.cpp similarity index 96% rename from tests/source/test_argument_descriptor.cpp rename to tests/source/test_help_builder.cpp index ddc3133f..00290e72 100644 --- a/tests/source/test_argument_descriptor.cpp +++ b/tests/source/test_help_builder.cpp @@ -1,10 +1,10 @@ #include "doctest.h" -#include +#include #include -using sut_type = ap::detail::argument_descriptor; +using sut_type = ap::detail::help_builder; namespace { @@ -16,7 +16,7 @@ constexpr std::size_t align_to = 15ull; } // namespace -TEST_CASE("argument_descriptor should construct with name and optional help correctly") { +TEST_CASE("help_builder should construct with name and optional help correctly") { sut_type no_help(arg_name, std::nullopt); CHECK_EQ(no_help.name, arg_name); CHECK_FALSE(no_help.help.has_value()); diff --git a/tests/source/test_optional_argument.cpp b/tests/source/test_optional_argument.cpp index 5048f030..91880983 100644 --- a/tests/source/test_optional_argument.cpp +++ b/tests/source/test_optional_argument.cpp @@ -78,54 +78,54 @@ TEST_CASE_FIXTURE(argument_test_fixture, "help() should return a massage set for } TEST_CASE_FIXTURE( - argument_test_fixture, "desc(verbose=false) should return an argument_descriptor with no params" + argument_test_fixture, + "help_builder(verbose=false) should return an help_builder with no params" ) { constexpr bool verbose = false; auto sut = sut_type(arg_name); - auto desc = get_desc(sut, verbose); - REQUIRE_EQ(desc.name, get_name(sut).str()); - CHECK_FALSE(desc.help); - CHECK(desc.params.empty()); + auto bld = get_help_builder(sut, verbose); + REQUIRE_EQ(bld.name, get_name(sut).str()); + CHECK_FALSE(bld.help); + CHECK(bld.params.empty()); // with a help msg sut.help(help_msg); - desc = get_desc(sut, verbose); - REQUIRE_EQ(desc.name, get_name(sut).str()); - CHECK(desc.help); - CHECK_EQ(desc.help.value(), help_msg); - CHECK(desc.params.empty()); + bld = get_help_builder(sut, verbose); + REQUIRE_EQ(bld.name, get_name(sut).str()); + CHECK(bld.help); + CHECK_EQ(bld.help.value(), help_msg); + CHECK(bld.params.empty()); } TEST_CASE_FIXTURE( argument_test_fixture, - "desc(verbose=true) should return an argument_descriptor with non-default params" + "help_builder(verbose=true) should return an help_builder with non-default params" ) { constexpr bool verbose = true; auto sut = sut_type(arg_name); - auto desc = get_desc(sut, verbose); - REQUIRE_EQ(desc.name, get_name(sut).str()); - CHECK_FALSE(desc.help); - CHECK(desc.params.empty()); + auto bld = get_help_builder(sut, verbose); + REQUIRE_EQ(bld.name, get_name(sut).str()); + CHECK_FALSE(bld.help); + CHECK(bld.params.empty()); // with a help msg sut.help(help_msg); - desc = get_desc(sut, verbose); - REQUIRE(desc.help); - CHECK_EQ(desc.help.value(), help_msg); - CHECK(desc.params.empty()); + bld = get_help_builder(sut, verbose); + REQUIRE(bld.help); + CHECK_EQ(bld.help.value(), help_msg); + CHECK(bld.params.empty()); // with the required flag enabled sut.required(); - desc = get_desc(sut, verbose); - const auto required_it = - std::ranges::find(desc.params, "required", ¶meter_descriptor::name); - REQUIRE_NE(required_it, desc.params.end()); + bld = get_help_builder(sut, verbose); + const auto required_it = std::ranges::find(bld.params, "required", ¶meter_descriptor::name); + REQUIRE_NE(required_it, bld.params.end()); CHECK_EQ(required_it->value, "true"); // other parameters @@ -136,29 +136,29 @@ TEST_CASE_FIXTURE( sut.implicit_values(implicit_value); // check the descriptor parameters - desc = get_desc(sut, verbose); + bld = get_help_builder(sut, verbose); const auto bypass_required_it = - std::ranges::find(desc.params, "bypass required", ¶meter_descriptor::name); - REQUIRE_NE(bypass_required_it, desc.params.end()); + std::ranges::find(bld.params, "bypass required", ¶meter_descriptor::name); + REQUIRE_NE(bypass_required_it, bld.params.end()); CHECK_EQ(bypass_required_it->value, "true"); - const auto nargs_it = std::ranges::find(desc.params, "nargs", ¶meter_descriptor::name); - REQUIRE_NE(nargs_it, desc.params.end()); + const auto nargs_it = std::ranges::find(bld.params, "nargs", ¶meter_descriptor::name); + REQUIRE_NE(nargs_it, bld.params.end()); CHECK_EQ(nargs_it->value, ap::util::as_string(non_default_range)); - const auto choices_it = std::ranges::find(desc.params, "choices", ¶meter_descriptor::name); - REQUIRE_NE(choices_it, desc.params.end()); + const auto choices_it = std::ranges::find(bld.params, "choices", ¶meter_descriptor::name); + REQUIRE_NE(choices_it, bld.params.end()); CHECK_EQ(choices_it->value, ap::util::join(choices, ", ")); const auto default_value_it = - std::ranges::find(desc.params, "default value(s)", ¶meter_descriptor::name); - REQUIRE_NE(default_value_it, desc.params.end()); + std::ranges::find(bld.params, "default value(s)", ¶meter_descriptor::name); + REQUIRE_NE(default_value_it, bld.params.end()); CHECK_EQ(default_value_it->value, std::to_string(default_value)); const auto implicit_value_it = - std::ranges::find(desc.params, "implicit value(s)", ¶meter_descriptor::name); - REQUIRE_NE(implicit_value_it, desc.params.end()); + std::ranges::find(bld.params, "implicit value(s)", ¶meter_descriptor::name); + REQUIRE_NE(implicit_value_it, bld.params.end()); CHECK_EQ(implicit_value_it->value, std::to_string(implicit_value)); } diff --git a/tests/source/test_positional_argument.cpp b/tests/source/test_positional_argument.cpp index e1e7e5c2..1a403cbc 100644 --- a/tests/source/test_positional_argument.cpp +++ b/tests/source/test_positional_argument.cpp @@ -73,46 +73,47 @@ TEST_CASE_FIXTURE(argument_test_fixture, "help() should return a massage set for } TEST_CASE_FIXTURE( - argument_test_fixture, "desc(verbose=false) should return an argument_descriptor with no params" + argument_test_fixture, + "help_builder(verbose=false) should return an help_builder with no params" ) { constexpr bool verbose = false; auto sut = sut_type(arg_name); - auto desc = get_desc(sut, verbose); - REQUIRE_EQ(desc.name, get_name(sut).str()); - CHECK_FALSE(desc.help); - CHECK(desc.params.empty()); + auto bld = get_help_builder(sut, verbose); + REQUIRE_EQ(bld.name, get_name(sut).str()); + CHECK_FALSE(bld.help); + CHECK(bld.params.empty()); // with a help msg sut.help(help_msg); - desc = get_desc(sut, verbose); - REQUIRE_EQ(desc.name, get_name(sut).str()); - CHECK(desc.help); - CHECK_EQ(desc.help.value(), help_msg); - CHECK(desc.params.empty()); + bld = get_help_builder(sut, verbose); + REQUIRE_EQ(bld.name, get_name(sut).str()); + CHECK(bld.help); + CHECK_EQ(bld.help.value(), help_msg); + CHECK(bld.params.empty()); } TEST_CASE_FIXTURE( argument_test_fixture, - "desc(verbose=true) should return an argument_descriptor with non-default params" + "help_builder(verbose=true) should return an help_builder with non-default params" ) { constexpr bool verbose = true; auto sut = sut_type(arg_name); - auto desc = get_desc(sut, verbose); - REQUIRE_EQ(desc.name, get_name(sut).str()); - CHECK_FALSE(desc.help); - CHECK(desc.params.empty()); + auto bld = get_help_builder(sut, verbose); + REQUIRE_EQ(bld.name, get_name(sut).str()); + CHECK_FALSE(bld.help); + CHECK(bld.params.empty()); // with a help msg sut.help(help_msg); - desc = get_desc(sut, verbose); - REQUIRE(desc.help); - CHECK_EQ(desc.help.value(), help_msg); - CHECK(desc.params.empty()); + bld = get_help_builder(sut, verbose); + REQUIRE(bld.help); + CHECK_EQ(bld.help.value(), help_msg); + CHECK(bld.params.empty()); // other parameters sut.bypass_required(); @@ -121,30 +122,29 @@ TEST_CASE_FIXTURE( sut.default_values(default_value); // check the descriptor parameters - desc = get_desc(sut, verbose); + bld = get_help_builder(sut, verbose); const auto bypass_required_it = - std::ranges::find(desc.params, "bypass required", ¶meter_descriptor::name); - REQUIRE_NE(bypass_required_it, desc.params.end()); + std::ranges::find(bld.params, "bypass required", ¶meter_descriptor::name); + REQUIRE_NE(bypass_required_it, bld.params.end()); CHECK_EQ(bypass_required_it->value, "true"); // automatically set to false with bypass_required - const auto required_it = - std::ranges::find(desc.params, "required", ¶meter_descriptor::name); - REQUIRE_NE(required_it, desc.params.end()); + const auto required_it = std::ranges::find(bld.params, "required", ¶meter_descriptor::name); + REQUIRE_NE(required_it, bld.params.end()); CHECK_EQ(required_it->value, "false"); - const auto nargs_it = std::ranges::find(desc.params, "nargs", ¶meter_descriptor::name); - REQUIRE_NE(nargs_it, desc.params.end()); + const auto nargs_it = std::ranges::find(bld.params, "nargs", ¶meter_descriptor::name); + REQUIRE_NE(nargs_it, bld.params.end()); CHECK_EQ(nargs_it->value, ap::util::as_string(non_default_range)); - const auto choices_it = std::ranges::find(desc.params, "choices", ¶meter_descriptor::name); - REQUIRE_NE(choices_it, desc.params.end()); + const auto choices_it = std::ranges::find(bld.params, "choices", ¶meter_descriptor::name); + REQUIRE_NE(choices_it, bld.params.end()); CHECK_EQ(choices_it->value, ap::util::join(choices, ", ")); const auto default_value_it = - std::ranges::find(desc.params, "default value(s)", ¶meter_descriptor::name); - REQUIRE_NE(default_value_it, desc.params.end()); + std::ranges::find(bld.params, "default value(s)", ¶meter_descriptor::name); + REQUIRE_NE(default_value_it, bld.params.end()); CHECK_EQ(default_value_it->value, std::to_string(default_value)); }