From f80b52236d5a5a986c7ce68eebfa09fa6537bc61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Musia=C5=82?= <111433005+SpectraL519@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:33:38 +0200 Subject: [PATCH 01/10] YT-CPPAP-54: Argument parsing logic refactor - Modified the argument parsing logic: Positional arguments consume free values at arbitrary positions in the command-line argument list instead of strictly at the beginning - Added value validation for program name and version setters: values cannot contain whitespaces --- CMakeLists.txt | 2 +- Doxyfile | 2 +- MODULE.bazel | 2 +- README.md | 2 +- cpp-ap-demo | 2 +- docs/tutorial.md | 331 ++++++++++-------- include/ap/argument/optional.hpp | 10 + include/ap/argument/positional.hpp | 10 + include/ap/argument_parser.hpp | 142 ++++---- include/ap/detail/argument_base.hpp | 18 +- include/ap/exceptions.hpp | 4 +- include/ap/version.hpp | 6 +- scripts/check_version.py | 17 +- tests/source/test_argument_parser_info.cpp | 27 +- .../test_argument_parser_parse_args.cpp | 73 +++- 15 files changed, 405 insertions(+), 243 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2055e2d1..cd24e702 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ else() endif() project(cpp-ap - VERSION 2.7.0 + VERSION 3.0.0.0 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 585f767e..7ed49dc7 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 = 2.7.0 +PROJECT_NUMBER = 3.0.0.0 # 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 c7bf023c..f7dac7e6 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,4 +1,4 @@ module( name = "cpp-ap", - version = "2.7.0", + version = "3.0.0.0", ) diff --git a/README.md b/README.md index e793034b..d2193c54 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Command-line argument parser for C++20 - [Argument Parameters](/docs/tutorial.md#argument-parameters) - [Default Arguments](/docs/tutorial.md#default-arguments) - [Parsing Arguments](/docs/tutorial.md#parsing-arguments) - - [Argument Parsing Rules](/docs/tutorial.md#argument-parsing-rules) + - [Basic Argument Parsing Rules](/docs/tutorial.md#basic-argument-parsing-rules) - [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) diff --git a/cpp-ap-demo b/cpp-ap-demo index 76d1d7a4..901f7d0f 160000 --- a/cpp-ap-demo +++ b/cpp-ap-demo @@ -1 +1 @@ -Subproject commit 76d1d7a49ca14ed7595739c118c07b6d98160f83 +Subproject commit 901f7d0f81f4839d38b82163f4b91d602fad2f8e diff --git a/docs/tutorial.md b/docs/tutorial.md index 850d8975..d98a1728 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -10,7 +10,7 @@ - [Predefined Parameter Values](#predefined-parameter-values) - [Default Arguments](#default-arguments) - [Parsing Arguments](#parsing-arguments) - - [Argument Parsing Rules](#argument-parsing-rules) + - [Basic Argument Parsing Rules](#basic-argument-parsing-rules) - [Compound Arguments](#compound-arguments) - [Parsing Known Arguments](#parsing-known-arguments) - [Retrieving Argument Values](#retrieving-argument-values) @@ -140,7 +140,7 @@ parser.program_name("Name of the program") ## Adding Arguments -The parser supports both positional and optional arguments. Both argument types are identified by their names represented as strings. Arguments can be defined with only a primary name or with a primary and a secondary (short) name. +The parser supports both positional and optional arguments. Both argument types are identified by their names represented as strings. Arguments are identified using their names. > [!NOTE] > @@ -158,6 +158,17 @@ or parser.add__argument("argument", "a"); ``` +> [!NOTE] +> +> An argument consists of a primary and/or secondary name. 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); +> ``` + > [!IMPORTANT] > > The library supports any argument value types which meet the following requirements: @@ -167,10 +178,8 @@ parser.add__argument("argument", "a"); > **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. - -> [!NOTE] > -> The default value type of any argument is `std::string`. +> **NOTE:** The default value type of any argument is `std::string`. You can also add boolean flags: @@ -198,15 +207,6 @@ parser.add_optional_argument("disable_another_option", "dao") */ ``` -> [!NOTE] -> -> While passing a primary name is required for creating positional arguments, optional arguments (and flags) 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); -> ``` -


@@ -255,7 +255,7 @@ Optional arguments: #### 3. `required` - If this option is set for an argument and it's value is not passed in the command-line, an exception will be thrown. -> [!NOTE] +> [!IMPORTANT] > > - By default positional arguments are set to be required, while optional arguments have this option disabled by default. > - The default value of the value parameter of the `required(bool)` function is `true` for both positional and optional arguments. @@ -726,38 +726,9 @@ The `argument_parser` class also defines the `void parse_args(int argc, char* ar > parse_args(std::span(argv + 1, argc - 1)); > ``` -> [!WARNING] -> -> By default the `argument_parser` class treats *all\** command-line arguments beggining with a `--` or `-` prefix as optional argument flags and if the flag's value does not match any of the specified arguments, then such flag is considered *unknown* and an exception will be thrown. -> -> > [*all\**] If a command-line argument begins with a flag prefix, but contains whitespaces (e.g. `"--flag value"`), then it is treated as a value and not a flag. -> -> This behavior can be altered so that the unknown argument flags will be treated as values, not flags. -> -> Example: -> ```cpp -> parser.add_optional_argument("option", "o"); -> parser.try_parse_args(argc, argv); -> std::cout << "option: " << parser.value("option"); -> -> /* -> ./program --option --unknown-flag -> option: --unknown-flag -> ``` -> -> To do this add the following in you `CMakeLists.txt` file: -> ```cmake -> target_compile_definitions(cpp-ap PRIVATE AP_UNKNOWN_FLAGS_AS_VALUES) -> ``` -> or simply add: -> ```cpp -> #define AP_UNKNOWN_FLAGS_AS_VALUES -> ``` -> before the `#include ` statement. - > [!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 `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` (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. > > Internally, This is equivalent to: > @@ -771,142 +742,192 @@ The `argument_parser` class also defines the `void parse_args(int argc, char* ar > } > ``` +The simple example below demonstrates how (in terms of the program's structure) the argument parsing should look like. + ```cpp -// power.cpp +// include the main library header #include -#include -#include - int main(int argc, char* argv[]) { // create the parser class instance ap::argument_parser parser; - parser.program_name("power calculator") - .program_description("Calculates the value of an expression: base ^ exponent"); - // add arguments - parser.add_positional_argument("base").help("the exponentation base value"); - parser.add_optional_argument("exponent", "e") - .nargs(ap::nargs::any()) - .help("the exponent value"); + // define the parser's attributes + parser.program_name("some-program") + .program_description("The program does something with command-line arguments"); + + // 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_optional_arguments({ap::argument::default_optional::help}); // parse command-line arguments parser.try_parse_args(argc, argv); - // check if any values for the `exponent` argument have been parsed - if (not parser.has_value("exponent")) { - std::cout << "no exponent values given" << std::endl; - std::exit(EXIT_SUCCESS); - } - - const double base = parser.value("base"); - const std::vector exponent_values = parser.values("exponent"); - - for (const int exponent : exponent_values) { - std::cout << base << " ^ " << exponent << " = " << std::pow(base, exponent) << std::endl; - } + // use the program's arguments + std::cout << "positional: " << parser.value("positional") << std::endl + << "optional: " << join(parser.values("optional")) << std::endl + << "flag: " << std::boolalpha << parser.value("flag") << std::endl; return 0; } +``` + +### Basic Argument Parsing Rules -// compiled with: -// g++ -o power power.cpp -I -std=c++20 +#### 1. Optional arguments are parsed only with a flag + +An optional argument is recognized only when its primary or secondary flag appears in the command-line input. For example: + +```cpp +parser.add_optional_argument("optional", "o"); ``` -### Argument Parsing Rules +Here, the argument is parsed only if either `--optional` (primary flag) or `-o` (secondary flag) is present. If neither flag is given, the argument is ignored. -- Positional arguments are parsed first, in the order they were defined in and without a flag. +> [!IMPORTANT] +> +> The parser will try to assign the values following such flag to the specified argument until: +> +> - A different argument flag is encountered: +> +> ```cpp +> // program.cpp +> parser.add_optional_argument("first", "f"); +> parser.add_optional_argument("second", "s"); +> +> parser.try_parse_args(argc, argv); +> +> std::cout << "first: " << join(parser.values("first")) << std::endl +> << "second: " << join(parser.values("second")) << std::endl; +> +> /* Example execution: +> > ./program --first value1 value2 --second value3 value4 +> first: value1, value2 +> second: value3, value4 +> ``` +> +> - The upper bound of the argument's [nargs](#1-nargs---sets-the-allowed-number-of-values-to-be-parsed-for-an-argument-this-can-be-set-as-a) parameter is reached: +> +> **NOTE:** By default an optional argument accepts an arbitrary number of values (the number of values has no upper bound). +> +> ```cpp +> parser.add_optional_argument("numbers", "n") +> .nargs(ap::nargs::up_to(3)) +> .help("A list of numbers"); +> ``` +> ```txt +> > ./program --numbers 1 2 3 4 5 +> [ERROR] : Failed to deduce the argument for values [4, 5] +> Program: program +> +> An example program +> +> Optional arguments: +> +> --help, -h : Display the help message +> --numbers, -n : A list of numbers +> ``` - In the example above the first command-line argument must be the value for the `positional` argument: +
- ```shell - ./power 2 - no exponent values given - ``` +#### 2. Positional arguments are parsed in the order of definition - ```shell - ./power - [ERROR] : No values parsed for a required argument [base] - Program: power calculator +Positional arguments are assigned values in the same order they are defined in the program. They are parsed from the command-line input **excluding any values that have already been consumed by optional arguments**. This means positional arguments no longer need to appear at the beginning of the argument list. - Calculates the value of an expression: base ^ exponent +For example: - Positional arguments: +```cpp +parser.add_positional_argument("positional1"); +parser.add_positional_argument("positional2"); - base : the exponentation base value +parser.try_parse_args(argc, argv); - Optional arguments: +std::cout << "positional1: " << parser.value("positional1") << std::endl + << "positional2: " << parser.value("positional2") << std::endl; - --exponent, -e : the exponent value - --help, -h : Display the help message - ``` +/* Example execution: +> ./program value1 value2 +positional1: value1 +positional2: value2 +``` > [!IMPORTANT] > -> For each positional argument there must be **exactly one value**. +> - All positional arguments expect **at most one value**. +> - A positional argument's value doesn't have to be preset in the command-line only if the argument is defined as **not** [required](#3-required---if-this-option-is-set-for-an-argument-and-its-value-is-not-passed-in-the-command-line-an-exception-will-be-thrown). -- Optional arguments are parsed only with a flag. The values passed after an argument flag will be treated as the values of the last optional argument that preceeds them. If no argument flag preceeds a value argument, then it will be treated as an **unknown** value. +
- ```shell - ./power 2 --exponent 1 2 3 # equivalent to: ./power 2 -e 1 2 3 - 2 ^ 1 = 2 - 2 ^ 2 = 4 - 2 ^ 3 = 8 - ``` +#### 3. Positional arguments consume free values - You can use the flag for each command-line value: +A positional argument consumes only those values that cannot be assigned to optional arguments. This allows positional arguments to appear after optional arguments in the command-line input. - ```shell - ./power 2 -e 1 -e 2 -e 3 - ``` +```cpp +parser.add_positional_argument("positional1"); +parser.add_positional_argument("positional2"); +parser.add_optional_argument("optional").nargs(1); // limit the number of arguments - Not using a flag will result in an error: +parser.try_parse_args(argc, argv); - ```shell - ./power 2 1 2 3 - [ERROR] : Failed to deduce the argument for values [1, 2, 3] - Program: power calculator +std::cout << "positional1: " << parser.value("positional1") << std::endl + << "positional2: " << parser.value("positional2") << std::endl + << "optional: " << parser.value("optional") << std::endl; - Calculates the value of an expression: base ^ exponent +/* Example executions: +> ./program pos1-value pos2-value --optional opt-value +positional1: pos1-value +positional2: pos2-value +optional: opt-value + +> ./program --optional opt-value pos1-value pos2-value +positional1: pos1-value +positional2: pos2-value +optional: opt-value + +> ./program pos1-value --optional opt-value pos2-value +positional1: pos1-value +positional2: pos2-value +optional: opt-value +``` - Positional arguments: +> [!TIP] +> +> Because of the optional arguments accept an arbitrary number of arguments by default, it is a good practice to set the [nargs](#1-nargs---sets-the-allowed-number-of-values-to-be-parsed-for-an-argument-this-can-be-set-as-a) parameter for optional arguments (where it makes sense). - base : the exponentation base value +
- Optional arguments: +#### 4. Unrecognized argument flag handling - --exponent, -e : the exponent value - --help, -h : Display the help message - ``` +By default the `argument_parser` class treats *all\** command-line arguments beggining with a `--` or `-` prefix as optional argument flags and if the flag's value does not match any of the specified arguments, then such flag is considered *unknown* and an exception will be thrown. -> [!WARNING] -> -> If an optional argument has the `nargs` parameter set with an upper bound, then the values that succeed this argument's flag will be assigned to this argument only until the specified upper bound is reached. Further values will be treated as **unknown** values. -> -> **Example:** -> -> ```cpp -> parser.add_optional_argument("exponent", "e").nargs(ap::nargs::up_to(3)) -> ``` -> ```shell -> ./power 2 -e 1 2 3 4 5 -> [ERROR] : Failed to deduce the argument for values [4, 5] -> Program: power calculator -> -> Calculates the value of an expression: base ^ exponent -> -> Positional arguments: -> -> base : the exponentation base value -> -> Optional arguments: -> -> --exponent, -e : the exponent value -> --help, -h : Display the help message -> ``` +> [*all\**] If a command-line argument begins with a flag prefix, but contains whitespaces (e.g. `"--flag value"`), then it is treated as a value and not a flag. + +This behavior can be altered so that the unknown argument flags will be treated as values, not flags. For example: + +```cpp +parser.add_optional_argument("option", "o"); +parser.try_parse_args(argc, argv); +std::cout << "option: " << parser.value("option"); + +/* +./program --option --unknown-flag +option: --unknown-flag +``` + +To do this add the following in your `CMakeLists.txt` file: +```cmake +target_compile_definitions(cpp-ap PRIVATE AP_UNKNOWN_FLAGS_AS_VALUES) +``` +or simply add: +```cpp +#define AP_UNKNOWN_FLAGS_AS_VALUES +``` +before the `#include ` statement. +

### Compound Arguments @@ -960,7 +981,7 @@ parser.add_optional_argument("recognized", "r") .nargs(ap::nargs::up_to(2)) .help("A recognized optional argument"); -parser.try_parse_args(argc, argv); +parser.parse_args(argc, argv); std::cout << "recognized = " << join(parser.values("recognized")) << std::endl; @@ -998,7 +1019,7 @@ std::cout << "recognized = " << join(parser.values("recognized")) << std::endl << "unkown = " << join(unknown_args) << std::endl; /* Example execution: -./program value0 --recognized value1 value2 value3 --unrecognized value +> ./program value0 --recognized value1 value2 value3 --unrecognized value recognized = value1, value2 unkown = value0, value3, --unrecognized, value ``` @@ -1007,25 +1028,34 @@ Now all the values, that caused an exception for the `parse_args` example, are c > [!IMPORTANT] > -> If a parser encounters an unrecognized argument flag during *known* args parsing, then the flag will be collected and the currently processed optional argument will be reset. That means that any value following an unrecognized flag will be treated as an unknown argument as well. Let's consider an example: +> If a parser encounters an unrecognized argument flag during *known* args parsing, then the flag will be collected and the currently processed optional argument will be reset. That means that any value following an unrecognized flag will be used to parse positional arguments or treated as an unknown argument as well (if there are no unparsed positional arguments). Let's consider an example: > > ```cpp +> parser.add_positional_argument("positional") +> .help("A positinal argument"); > parser.add_optional_argument("recognized", "r") -> .nargs(ap::nargs::any()) // don't restrict the number of arguments +> .nargs(ap::nargs::any()) > .help("A recognized optional argument"); > > const auto unknown_args = parser.parse_known_args(argc, argv); > -> std::cout << "recognized = " << join(parser.values("recognized")) << std::endl +> std::cout << "positional = " << parser.value("positional") << std::endl +> << "recognized = " << join(parser.values("recognized")) << std::endl > << "unkown = " << join(unknown_args) << std::endl; > > /* Example execution: -> ./program value0 --recognized value1 value2 value3 --unrecognized value --recognized value4 -> recognized = value1, value2, value3, value4 -> unkown = value0, --unrecognized, value +> > ./program --recognized value1 value2 value3 --unrecognized value4 value5 --recognized value6 +> positional = value4 +> recognized = value1, value2, value3, value6 +> unkown = --unrecognized, value5 +> +> > ./program value0 --recognized value1 value2 value3 --unrecognized value4 --recognized value5 +> positional = value0 +> recognized = value1, value2, value3, value5 +> unkown = --unrecognized, value4 > ``` > -> Here `value` is treated as an unknown argument even though the `recognized` optional argument still accepts values and only after a different flag is encountered the parser stops collecting the values to the unknown arguments list. +> Here `value` is treated either as the `positional` argument's value or as an unknown argument (depending on the input arguments) even though the `recognized` optional argument still accepts values and only after the `--recognized` argument flag is encountered the parser continues collecting values for this argument. > > **NOTE:** If the `AP_UNKNOWN_FLAGS_AS_VALUES` is set, the unrecognized argument flags will be treated as values during parsing and therefore they **may** not be collected as unknown arguments, depending on the argument's configuration and the command-line argument list. @@ -1060,11 +1090,18 @@ You can retrieve the argument's value with: Additionally for optional arguments, you can use: ```cpp -(const) std::vector values = parser.values("argument_name"); +(const) std::vector values = parser.values("argument_name"); // (3) ``` which returns a `vector` containing all values parsed for the given argument. +> [!NOTE] +> +> The argument value getter functions might throw an exception if: +> - An argument with the given name does not exist +> - The argument does not contain any values - parsed or predefined (only getter `(1)`) +> - The specified `value_type` does not match the value type of the argument +


diff --git a/include/ap/argument/optional.hpp b/include/ap/argument/optional.hpp index 006de541..c295984e 100644 --- a/include/ap/argument/optional.hpp +++ b/include/ap/argument/optional.hpp @@ -215,6 +215,16 @@ class optional : public detail::argument_base { action::detail::value_action_variant_type; ///< The argument's value action type. using flag_action_type = typename action_type::on_flag::type; + /// @brief `true` if the argument is an instance of `positional`, `false` otherwise. + [[nodiscard]] bool is_positional() const noexcept override { + return false; + } + + /// @brief `true` if the argument is an instance of `optional`, `false` otherwise. + virtual bool is_optional() const noexcept override { + return true; + } + /** * @param verbose The verbosity mode value. * @return An argument descriptor object for the argument. diff --git a/include/ap/argument/positional.hpp b/include/ap/argument/positional.hpp index df82deb5..29c67769 100644 --- a/include/ap/argument/positional.hpp +++ b/include/ap/argument/positional.hpp @@ -164,6 +164,16 @@ class positional : public detail::argument_base { using value_action_type = action::detail::value_action_variant_type; ///< The argument's value action type. + /// @brief `true` if the argument is an instance of `positional`, `false` otherwise. + [[nodiscard]] bool is_positional() const noexcept override { + return true; + } + + /// @brief `true` if the argument is an instance of `optional`, `false` otherwise. + virtual bool is_optional() const noexcept override { + return false; + } + /** * @param verbose The verbosity mode value. * @return An argument descriptor object for the argument. diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index 1297be76..ddf8d5fd 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -59,7 +59,10 @@ class argument_parser { * @param name The name of the program. * @return Reference to the argument parser. */ - argument_parser& program_name(std::string_view name) noexcept { + argument_parser& program_name(std::string_view name) { + if (detail::contains_whitespaces(name)) + throw invalid_configuration("The program name cannot contain whitespace characters!"); + this->_program_name.emplace(name); return *this; } @@ -79,7 +82,11 @@ class argument_parser { * @param version The version of the program. * @return Reference to the argument parser. */ - argument_parser& program_version(std::string_view version) noexcept { + argument_parser& program_version(std::string_view version) { + if (detail::contains_whitespaces(version)) + throw invalid_configuration("The program version cannot contain whitespace characters!" + ); + this->_program_version.emplace(version); return *this; } @@ -642,7 +649,7 @@ class argument_parser { using const_arg_opt_t = std::optional>; using arg_token_list_t = std::vector; - using arg_token_list_iterator_t = typename arg_token_list_t::const_iterator; + using arg_token_list_iter_t = typename arg_token_list_t::const_iterator; /** * @brief Verifies the pattern of an argument name and if it's invalid, an error is thrown @@ -888,93 +895,97 @@ class argument_parser { /** * @brief Implementation of parsing command-line arguments. * @param arg_tokens The list of command-line argument tokens. + * @param handle_unknown A flag specifying whether unknown arguments should be handled or collected. * @throws ap::parsing_failure - * \todo Use `c_range_of` instead of `arg_token_list_t` directly. */ void _parse_args_impl( const arg_token_list_t& arg_tokens, std::vector& unknown_args, const bool handle_unknown = true ) { - arg_token_list_iterator_t token_it = arg_tokens.begin(); - this->_parse_positional_args(token_it, arg_tokens.end()); - this->_parse_optional_args(token_it, arg_tokens.end(), unknown_args, handle_unknown); - } + // set the current argument indicators + arg_ptr_opt_t curr_arg_opt = std::nullopt; + arg_ptr_list_iter_t curr_positional_arg_it = this->_positional_args.begin(); - /** - * @brief Parse positional arguments based on command-line input. - * @param token_it Iterator for iterating through command-line argument tokens. - * @param tokens_end The token list end iterator. - */ - void _parse_positional_args( - arg_token_list_iterator_t& token_it, const arg_token_list_iterator_t& tokens_end - ) noexcept { - for (const auto& pos_arg : this->_positional_args) { - if (token_it == tokens_end) - return; + if (curr_positional_arg_it != this->_positional_args.end()) + curr_arg_opt.emplace(*curr_positional_arg_it); - if (token_it->type != detail::argument_token::t_value) - return; - - pos_arg->set_value(token_it->value); - ++token_it; - } + // process argument tokens + std::ranges::for_each( + arg_tokens, + std::bind_front( + &argument_parser::_parse_token, + this, + std::ref(curr_arg_opt), + std::ref(curr_positional_arg_it), + std::ref(unknown_args), + handle_unknown + ) + ); } /** - * @brief Parse optional arguments based on command-line input. - * @param token_it Iterator for iterating through command-line argument tokens. - * @param tokens_end The token list end iterator. - * @param unknown_args Reference to the vector into which the dangling values shall be collected. + * @brief Parse a single command-line argument token. + * @param curr_arg_opt The currently processed argument. + * @param curr_positional_arg_it An iterator pointing to the current positional argument. + * @param unknown_args The unknown arguments collection. + * @param handle_unknown A flag specifying whether unknown arguments should be handled or collected. + * @param tok The argument token to be processed. * @throws ap::parsing_failure */ - void _parse_optional_args( - arg_token_list_iterator_t& token_it, - const arg_token_list_iterator_t& tokens_end, + void _parse_token( + arg_ptr_opt_t& curr_arg_opt, + arg_ptr_list_iter_t& curr_positional_arg_it, std::vector& unknown_args, - const bool handle_unknown = true + const bool handle_unknown, + const detail::argument_token& tok ) { - arg_ptr_opt_t curr_opt_arg; - - while (token_it != tokens_end) { - switch (token_it->type) { - case detail::argument_token::t_flag_primary: - [[fallthrough]]; - case detail::argument_token::t_flag_secondary: { - if (not token_it->is_valid_flag_token()) { - if (handle_unknown) { - throw parsing_failure::unknown_argument( - this->_unstripped_token_value(*token_it) - ); - } - else { - unknown_args.emplace_back(this->_unstripped_token_value(*token_it)); - curr_opt_arg.reset(); - break; - } + switch (tok.type) { + case detail::argument_token::t_flag_primary: + [[fallthrough]]; + case detail::argument_token::t_flag_secondary: { + if (not tok.is_valid_flag_token()) { + if (handle_unknown) { + throw parsing_failure::unrecognized_argument(this->_unstripped_token_value(tok) + ); + } + else { + curr_arg_opt.reset(); + unknown_args.emplace_back(this->_unstripped_token_value(tok)); + break; } + } - if (token_it->arg->get()->mark_used()) - curr_opt_arg = token_it->arg; - else - curr_opt_arg.reset(); + if (tok.arg->get()->mark_used()) + curr_arg_opt = tok.arg; + else + curr_arg_opt.reset(); - break; - } - case detail::argument_token::t_value: { - if (not curr_opt_arg) { - unknown_args.emplace_back(token_it->value); + break; + } + case detail::argument_token::t_value: { + if (not curr_arg_opt) { + if (curr_positional_arg_it == this->_positional_args.end()) { + unknown_args.emplace_back(tok.value); break; } - if (not curr_opt_arg->get()->set_value(token_it->value)) - curr_opt_arg.reset(); - - break; + curr_arg_opt.emplace(*curr_positional_arg_it); } + + if (auto& curr_arg = *curr_arg_opt->get(); not curr_arg.set_value(tok.value)) { + if (curr_arg.is_positional() + and curr_positional_arg_it != this->_positional_args.end() + and ++curr_positional_arg_it != this->_positional_args.end()) { + curr_arg_opt.emplace(*curr_positional_arg_it); + break; + } + + curr_arg_opt.reset(); } - ++token_it; + break; + } } } @@ -1149,7 +1160,6 @@ inline void add_default_argument( case argument::default_optional::help: arg_parser.add_flag("help", "h") .action(action::print_config(arg_parser, EXIT_SUCCESS)) - .nargs(0ull) .help("Display the help message"); break; diff --git a/include/ap/detail/argument_base.hpp b/include/ap/detail/argument_base.hpp index eb811111..ea59fdb0 100644 --- a/include/ap/detail/argument_base.hpp +++ b/include/ap/detail/argument_base.hpp @@ -38,16 +38,16 @@ class argument_base { return this->_name; } - /// @return Optional help message for the positional argument. - [[nodiscard]] const std::optional& help() const noexcept { - return this->_help_msg; - } - /// @return `true` if the argument is hidden, `false` otherwise [[nodiscard]] bool is_hidden() const noexcept { return this->_hidden; } + /// @return Optional help message for the positional argument. + [[nodiscard]] const std::optional& help() const noexcept { + return this->_help_msg; + } + /// @return `true` if the argument is required, `false` otherwise [[nodiscard]] bool is_required() const noexcept { return this->_required; @@ -61,6 +61,14 @@ class argument_base { return not this->_required and this->_bypass_required; } + // pure virtual methods + + /// @brief `true` if the argument is an instance of `positional`, `false` otherwise. + virtual bool is_positional() const noexcept = 0; + + /// @brief `true` if the argument is an instance of `optional`, `false` otherwise. + virtual bool is_optional() const noexcept = 0; + /** * @param verbose The verbosity mode value. * @return An argument descriptor object for the argument. diff --git a/include/ap/exceptions.hpp b/include/ap/exceptions.hpp index f0ad81ac..42073cf3 100644 --- a/include/ap/exceptions.hpp +++ b/include/ap/exceptions.hpp @@ -57,8 +57,8 @@ struct invalid_configuration : public argument_parser_exception { struct parsing_failure : public argument_parser_exception { explicit parsing_failure(const std::string& message) : argument_parser_exception(message) {} - static parsing_failure unknown_argument(const std::string_view arg_name) noexcept { - return parsing_failure(std::format("Unknown argument [{}].", arg_name)); + static parsing_failure unrecognized_argument(const std::string_view arg_name) noexcept { + return parsing_failure(std::format("Unrecognized argument [{}].", arg_name)); } static parsing_failure value_already_set(const detail::argument_name& arg_name) noexcept { diff --git a/include/ap/version.hpp b/include/ap/version.hpp index da4fea0f..58d4f1fa 100644 --- a/include/ap/version.hpp +++ b/include/ap/version.hpp @@ -11,9 +11,9 @@ namespace ap { /// @brief A helper structure used to represent a program's version. struct version { - std::uint32_t major; ///< The major version number. - std::uint32_t minor; ///< The minor version number. - std::uint32_t patch; ///< The patch number. + std::uint32_t major = 1u; ///< The major version number. + std::uint32_t minor = 0u; ///< The minor version number. + std::uint32_t patch = 0u; ///< The patch number. /// @brief Converts the structure into a string in the `v{major}.{minor}.{path}` format [[nodiscard]] std::string str() const noexcept { diff --git a/scripts/check_version.py b/scripts/check_version.py index 01d23f76..4a92e18e 100644 --- a/scripts/check_version.py +++ b/scripts/check_version.py @@ -5,28 +5,31 @@ from collections.abc import Iterable +VERSION_REGEX = r"(\d+\.\d+(?:\.\d+)*)" + + def get_cmake_version(cmake_path: Path) -> str: text = cmake_path.read_text() - match = re.search(r'project\s*\([^\)]*VERSION\s+(\d+\.\d+\.\d+)', text, re.IGNORECASE) + match = re.search(rf'project\s*\([^\)]*VERSION\s+{VERSION_REGEX}', text, re.IGNORECASE) if match: return match.group(1) - raise ValueError(f"[CMake] Could not find project version in {cmake_path}") + raise ValueError(f"[CMake] Could not find a valid project version in {cmake_path}") def get_doxy_version(doxyfile_path: Path) -> str: text = doxyfile_path.read_text() - match = re.search(r'^\s*PROJECT_NUMBER\s*=\s*("?)([\d\.]+)\1', text, re.MULTILINE) + match = re.search(rf'^\s*PROJECT_NUMBER\s*=\s*("?){VERSION_REGEX}\1', text, re.MULTILINE) if match: - return match.group(2) - raise ValueError(f"[Doxygen] Could not find PROJECT_NUMBER in {doxyfile_path}") + return match.group(2) # group(2) because group(1) is the optional quote + raise ValueError(f"[Doxygen] Could not find a valid PROJECT_NUMBER in {doxyfile_path}") def get_bazel_version(bazel_module_file_path: Path) -> str: text = bazel_module_file_path.read_text() - match = re.search(r'\bversion\s*=\s*"(\d+\.\d+\.\d+)"', text) + match = re.search(rf'\bversion\s*=\s*"{VERSION_REGEX}"', text) if match: return match.group(1) - raise ValueError(f"[Bazel] Could not find module version in {bazel_module_file_path}") + raise ValueError(f"[Bazel] Could not find a valid module version in {bazel_module_file_path}") def all_equal(items: Iterable) -> bool: diff --git a/tests/source/test_argument_parser_info.cpp b/tests/source/test_argument_parser_info.cpp index 393e6042..7ae7a66e 100644 --- a/tests/source/test_argument_parser_info.cpp +++ b/tests/source/test_argument_parser_info.cpp @@ -3,8 +3,10 @@ using namespace ap_testing; +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_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"; @@ -17,7 +19,17 @@ TEST_CASE_FIXTURE( CHECK_FALSE(stored_program_name); } -TEST_CASE_FIXTURE(test_argument_parser_info, "name() should set the program name member") { +TEST_CASE_FIXTURE( + test_argument_parser_info, "program_name() should throw if the name contains whitespaces" +) { + CHECK_THROWS_WITH_AS( + sut.program_name("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(); @@ -33,6 +45,17 @@ TEST_CASE_FIXTURE( CHECK_FALSE(stored_program_version); } +TEST_CASE_FIXTURE( + test_argument_parser_info, + "program_version() should throw if the version string contains whitespaces" +) { + CHECK_THROWS_WITH_AS( + sut.program_version("invalid version"), + "The program version cannot contain whitespace characters!", + invalid_configuration + ); +} + TEST_CASE_FIXTURE(test_argument_parser_info, "version() should set the program version member") { sut.program_version(test_version); auto stored_program_version = get_program_version(); diff --git a/tests/source/test_argument_parser_parse_args.cpp b/tests/source/test_argument_parser_parse_args.cpp index 2b8cd1a3..ae039669 100644 --- a/tests/source/test_argument_parser_parse_args.cpp +++ b/tests/source/test_argument_parser_parse_args.cpp @@ -12,8 +12,8 @@ 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 = 6ull; - const std::size_t n_optional_args = 4ull; + const std::size_t n_positional_args = 5ull; + 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; const std::size_t first_opt_arg_idx = n_positional_args; @@ -265,7 +265,8 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_parse_args, "parse_args should throw when an unknown argument flag is used" + test_argument_parser_parse_args, + "parse_args should throw when an unrecognized argument flag is used" ) { add_arguments(no_args, no_args); @@ -279,7 +280,7 @@ TEST_CASE_FIXTURE( CHECK_THROWS_WITH_AS( sut.parse_args(argc, argv), - parsing_failure::unknown_argument(unknown_arg_name).what(), + parsing_failure::unrecognized_argument(unknown_arg_name).what(), parsing_failure ); @@ -320,7 +321,7 @@ TEST_CASE_FIXTURE( CHECK_THROWS_WITH_AS( sut.parse_args(argc, argv), - parsing_failure::unknown_argument(invalid_flag).what(), + parsing_failure::unrecognized_argument(invalid_flag).what(), parsing_failure ); @@ -427,6 +428,66 @@ TEST_CASE_FIXTURE( free_argv(argc, argv); } +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, "parse_args should consume free values at various positions" +) { + add_positional_args(n_positional_args); + add_optional_args(n_optional_args, n_positional_args, [](auto& arg) { arg.nargs(1ull); }); + + std::vector argv_vec{"program"}; + + SUBCASE("at the beginning") { + for (std::size_t i = 0ull; i < n_positional_args; ++i) + argv_vec.emplace_back(init_arg_value(i)); + + for (std::size_t i = n_positional_args; i < n_args_total; ++i) { + argv_vec.emplace_back(init_arg_flag_primary(i)); + argv_vec.emplace_back(init_arg_value(i)); + } + } + + SUBCASE("mixed with optional args (front)") { + for (std::size_t i = 0ull; i < n_positional_args; ++i) { + argv_vec.emplace_back(init_arg_value(i)); + argv_vec.emplace_back(init_arg_flag_primary(i + n_positional_args)); + argv_vec.emplace_back(init_arg_value(i + n_positional_args)); + } + } + + SUBCASE("mixed with optional args (back)") { + for (std::size_t i = 0ull; i < n_positional_args; ++i) { + argv_vec.emplace_back(init_arg_flag_primary(i + n_positional_args)); + argv_vec.emplace_back(init_arg_value(i + n_positional_args)); + argv_vec.emplace_back(init_arg_value(i)); + } + } + + SUBCASE("at the end") { + for (std::size_t i = n_positional_args; i < n_args_total; ++i) { + argv_vec.emplace_back(init_arg_flag_primary(i)); + argv_vec.emplace_back(init_arg_value(i)); + } + + for (std::size_t i = 0ull; i < n_positional_args; ++i) + argv_vec.emplace_back(init_arg_value(i)); + } + + CAPTURE(argv_vec); + + const auto argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + sut.parse_args(argc, argv); + + for (std::size_t i = 0ull; i < n_positional_args; ++i) { + const auto arg_name = init_arg_name_primary(i); + REQUIRE(sut.has_value(arg_name)); + CHECK_EQ(sut.value(arg_name), init_arg_value(i)); + } + + free_argv(argc, argv); +} + // parse_known_args TEST_CASE_FIXTURE( @@ -978,7 +1039,7 @@ TEST_CASE_FIXTURE( // parse args CHECK_THROWS_WITH_AS( sut.parse_args(argc, argv), - parsing_failure::unknown_argument(invalid_flag).what(), + parsing_failure::unrecognized_argument(invalid_flag).what(), parsing_failure ); From 68bad2f34eabe617da608475fad991723b3c7a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Musia=C5=82?= <111433005+SpectraL519@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:21:37 +0200 Subject: [PATCH 02/10] YT-CPPAP-55: Argument types refactor 1. Combined the `argument::positional` and `argument::optional` classes into one: `argument` - `ArgT` is a discriminator value specifying whether the argument is positional or optional - Added generic argument type aliases: `positional_argument` and `optional_argument` 2. Aligned the positional argument implementation to handle the `nargs` parameter 3. Introduced an `none_type` type: - Used as a fallback type for conditional class members or as an argument value type. - Modified the `c_argument_value_type` concept to allow the usage of `none_type` 4. Combined the default argument discriminator types into one: - Previously: `argument::default_positional` and `argument::default_optional` - Currently: `default_argument` - Combined the default argument setter methods of `argument_parser` to match the new structure 5. Modified the `nargs::range` class to always store concrete lower/upper bounds instead of using `std::optional` --- CMakeLists.txt | 2 +- Doxyfile | 2 +- MODULE.bazel | 2 +- README.md | 2 + cpp-ap-demo | 2 +- docs/tutorial.md | 264 ++++---- include/ap/action/detail/utility.hpp | 11 +- ...{predefined_actions.hpp => predefined.hpp} | 2 +- .../ap/action/{specifiers.hpp => types.hpp} | 2 +- include/ap/argument.hpp | 623 ++++++++++++++++++ include/ap/argument/default.hpp | 22 - include/ap/argument/optional.hpp | 376 ----------- include/ap/argument/positional.hpp | 300 --------- include/ap/argument_parser.hpp | 137 ++-- include/ap/detail/argument_base.hpp | 98 +-- include/ap/detail/argument_descriptor.hpp | 2 +- include/ap/detail/argument_name.hpp | 2 +- include/ap/detail/argument_token.hpp | 2 +- include/ap/detail/concepts.hpp | 16 +- include/ap/detail/str_utility.hpp | 2 +- include/ap/detail/typing_utility.hpp | 2 + include/ap/exceptions.hpp | 3 +- include/ap/nargs/range.hpp | 146 ++-- include/ap/{version.hpp => types.hpp} | 10 + tests/CMakeLists.txt | 1 + .../include/argument_parser_test_fixture.hpp | 18 +- tests/include/argument_test_fixture.hpp | 131 ++++ .../optional_argument_test_fixture.hpp | 127 ---- .../positional_argument_test_fixture.hpp | 110 ---- tests/include/utility.hpp | 7 +- .../test_argument_parser_add_argument.cpp | 21 +- .../test_argument_parser_parse_args.cpp | 133 +++- ...ser_parse_args_unknown_flags_as_values.cpp | 3 - tests/source/test_argument_token.cpp | 7 +- tests/source/test_nargs_range.cpp | 141 ++-- tests/source/test_none_type_argument.cpp | 49 ++ tests/source/test_optional_argument.cpp | 211 +++--- tests/source/test_positional_argument.cpp | 284 ++++---- 38 files changed, 1633 insertions(+), 1640 deletions(-) rename include/ap/action/{predefined_actions.hpp => predefined.hpp} (99%) rename include/ap/action/{specifiers.hpp => types.hpp} (98%) create mode 100644 include/ap/argument.hpp delete mode 100644 include/ap/argument/default.hpp delete mode 100644 include/ap/argument/optional.hpp delete mode 100644 include/ap/argument/positional.hpp rename include/ap/{version.hpp => types.hpp} (79%) create mode 100644 tests/include/argument_test_fixture.hpp delete mode 100644 tests/include/optional_argument_test_fixture.hpp delete mode 100644 tests/include/positional_argument_test_fixture.hpp create mode 100644 tests/source/test_none_type_argument.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index cd24e702..d02740bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ else() endif() project(cpp-ap - VERSION 3.0.0.0 + VERSION 3.0.0.1 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 7ed49dc7..e91c4729 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.0 +PROJECT_NUMBER = 3.0.0.1 # 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 f7dac7e6..64894bff 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,4 +1,4 @@ module( name = "cpp-ap", - version = "3.0.0.0", + version = "3.0.0.1", ) diff --git a/README.md b/README.md index d2193c54..09f34eac 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ Command-line argument parser for C++20 - [The Parser Class](/docs/tutorial.md#the-parser-class) - [Adding Arguments](/docs/tutorial.md#adding-arguments) - [Argument Parameters](/docs/tutorial.md#argument-parameters) + - [Common Parameters](/docs/tutorial.md##common-parameters) + - [Parameters Specific for Optional Arguments](/docs/tutorial.md##parameters-specific-for-optional-arguments) - [Default Arguments](/docs/tutorial.md#default-arguments) - [Parsing Arguments](/docs/tutorial.md#parsing-arguments) - [Basic Argument Parsing Rules](/docs/tutorial.md#basic-argument-parsing-rules) diff --git a/cpp-ap-demo b/cpp-ap-demo index 901f7d0f..9cd984da 160000 --- a/cpp-ap-demo +++ b/cpp-ap-demo @@ -1 +1 @@ -Subproject commit 901f7d0f81f4839d38b82163f4b91d602fad2f8e +Subproject commit 9cd984da2520f9115479afd39d52b7e6065c94b5 diff --git a/docs/tutorial.md b/docs/tutorial.md index d98a1728..f4c98793 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -7,6 +7,8 @@ - [The Parser Class](#the-parser-class) - [Adding Arguments](#adding-arguments) - [Argument Parameters](#argument-parameters) + - [Common Parameters](#common-parameters) + - [Parameters Specific for Optional Arguments](#parameters-specific-for-optional-arguments) - [Predefined Parameter Values](#predefined-parameter-values) - [Default Arguments](#default-arguments) - [Parsing Arguments](#parsing-arguments) @@ -140,7 +142,7 @@ parser.program_name("Name of the program") ## Adding Arguments -The parser supports both positional and optional arguments. Both argument types are identified by their names represented as strings. Arguments are identified using their names. +The parser supports both positional and optional arguments. Both argument types are identified by their names. > [!NOTE] > @@ -160,7 +162,7 @@ parser.add__argument("argument", "a"); > [!NOTE] > -> An argument consists of a primary and/or secondary name. The primary name is a longer, more descriptive name, while the secondary name is a shorter/abbreviated name of the argument. +> 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: > @@ -171,15 +173,24 @@ parser.add__argument("argument", "a"); > [!IMPORTANT] > -> The library supports any argument value types which meet 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. + +> [!NOTE] +> +> - The default value type of any argument is `std::string`. > -> **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) +> - [choices](#6-choices---a-list-of-valid-argument-values) +> - [value actions](#7-value-actions---function-performed-after-parsing-an-arguments-value) +> - [default_value](#8-default_value---the-default-value-for-an-argument-which-will-be-used-if-no-values-for-this-argument-are-parsed) +> - [implicit_value](#2-implicit_value---a-value-which-will-be-set-for-an-argument-if-only-its-flag-is-parsed-from-the-command-line-but-no-values-follow) You can also add boolean flags: @@ -224,6 +235,8 @@ parser.add_positional_argument("number", "n") .help("a positive integer value"); ``` +
+ #### 2. `hidden` - If this option is set for an argument, then it will not be included in the program description. By default all arguments are visible, but this can be modified using the `hidden(bool)` setter as follows: @@ -231,7 +244,7 @@ By default all arguments are visible, but this can be modified using the `hidden ```cpp parser.program_name("hidden-test") .program_description("A simple program") - .default_optional_arguments({ap::argument::default_optional::help}); + .default_arguments({ap::default_argument::o_help}); parser.add_optional_argument("hidden") .hidden() @@ -253,6 +266,8 @@ Optional arguments: --visible : A simple visible argument ``` +
+ #### 3. `required` - If this option is set for an argument and it's value is not passed in the command-line, an exception will be thrown. > [!IMPORTANT] @@ -320,6 +335,8 @@ Command Result */ ``` +
+ #### 4. `bypass_required` - If this option is set for an argument, the `required` option for other arguments will be discarded if the bypassing argument is used in the command-line. > [!NOTE] @@ -356,59 +373,46 @@ std::ofstream os(parser.value("output")); os << data << std::endl; ``` -#### 5. `default_value` - The default value for an argument which will be used if no values for this argument are parsed - -> [!WARNING] -> -> For both positional and optional arguments, setting the `default_value` parameter disables the `required` option. - -```cpp -// example: positional arguments -parser.add_positional_argument("input"); -parser.add_positional_argument("output").default_value("output.txt"); +
-parser.parse_args(argc, argv); +#### 5. `nargs` - Sets the allowed number of values to be parsed for an argument. This can be set as a: -// `input` is guaranteed to have a value if parsing was successfull -const auto data = read_data(parser.value("input")); +- Specific number: -// `output` is guaranteed to have a value even if one was not specified in the command-line -std::ofstream os(parser.value("output")); -os << data << std::endl; + ```cpp + parser.add_optional_argument("input", "i").nargs(1); + ``` -/* -Command Result ------------------------------------------------------------------------------------------ -./program Parsing error (no value for the input argument) -./program input.txt Parsing success; Printing data to `output.txt` -./program input.txt myfile.txt Parsing success; Printing data to the `myfile.txt` file -``` +- Fully bound range: -```cpp -// example: optional arguments -parser.add_optional_argument("input", "i").required(); -parser.add_optional_argument("output", "o").default_value("output.txt"); + ```cpp + parser.add_optional_argument("input", "i").nargs(1, 3); + ``` -parser.parse_args(argc, argv); +- Partially bound range: -// `input` is guaranteed to have a value if parsing was successfull -const auto data = read_data(parser.value("input")); + ```cpp + parser.add_optional_argument("input", "i").nargs(ap::nargs::at_least(1)); // n >= 1 + parser.add_optional_argument("input", "i").nargs(ap::nargs::more_than(1)); // n > 1 + parser.add_optional_argument("input", "i").nargs(ap::nargs::less_than(5)); // n < 5 + parser.add_optional_argument("input", "i").nargs(ap::nargs::up_to(5)); // n <= 5 + ``` -// `output` is guaranteed to have a value even if one was not specified in the command-line -std::ofstream os(parser.value("output")); -os << data << std::endl; +- Unbound range: -/* -Command Result ------------------------------------------------------------------------------------------------ -./program Parsing error (no value for the input argument) -./program --input input.txt Parsing success; Printing data to `output.txt` -./program -i input.txt -o myfile.txt Parsing success; Printing data to the `myfile.txt` file -``` + ```cpp + parser.add_optional_argument("input", "i").nargs(ap::nargs::any()); + ``` -> [!TIP] +> [!IMPORTANT] > -> The setter of the `default_value` parameter accepts any type that is convertible to the argument's value type. +> The default `nargs` parameter value is: +> +> - `ap::nargs::range(1ull)` for positional arguments +> +> - `ap::nargs::any()` for optional arguments + +
#### 6. `choices` - A list of valid argument values. @@ -423,6 +427,8 @@ parser.add_optional_argument("method", "m").choices({'a', 'b', 'c'}); > > The `choices` function can be used only if the argument's `value_type` is equality comparable (defines the `==` operator). +
+ #### 7. Value actions - Function performed after parsing an argument's value. Actions are represented as functions, which take the argument's value as an argument. The available action types are: @@ -466,44 +472,88 @@ Actions are represented as functions, which take the argument's value as an argu > > A single argument can have multiple value actions. Instead of writing complex logic in one action, consider composing several simple, focused actions for better readability and reusability. -
+
-### Parameters Specific for Optional Arguments +#### 8. `default_value` - The default value for an argument which will be used if no values for this argument are parsed -Apart from the common parameters listed above, for optional arguments you can also specify the following parameters: +> [!WARNING] +> +> For both positional and optional arguments, setting the `default_value` parameter disables the `required` option. -#### 1. `nargs` - Sets the allowed number of values to be parsed for an argument. This can be set as a: +```cpp +// example: positional arguments +parser.add_positional_argument("input"); +parser.add_positional_argument("output").default_value("output.txt"); -- Specific number: +parser.parse_args(argc, argv); - ```cpp - parser.add_optional_argument("input", "i").nargs(1); - ``` +// `input` is guaranteed to have a value if parsing was successfull +const auto data = read_data(parser.value("input")); -- Fully bound range: +// `output` is guaranteed to have a value even if one was not specified in the command-line +std::ofstream os(parser.value("output")); +os << data << std::endl; - ```cpp - parser.add_optional_argument("input", "i").nargs(1, 3); - ``` +/* +Command Result +----------------------------------------------------------------------------------------- +./program Parsing error (no value for the input argument) +./program input.txt Parsing success; Printing data to `output.txt` +./program input.txt myfile.txt Parsing success; Printing data to the `myfile.txt` file +``` -- Partially bound range: +```cpp +// example: optional arguments +parser.add_optional_argument("input", "i").required(); +parser.add_optional_argument("output", "o").default_value("output.txt"); - ```cpp - parser.add_optional_argument("input", "i").nargs(ap::nargs::at_least(1)); // n >= 1 - parser.add_optional_argument("input", "i").nargs(ap::nargs::more_than(1)); // n > 1 - parser.add_optional_argument("input", "i").nargs(ap::nargs::less_than(5)); // n < 5 - parser.add_optional_argument("input", "i").nargs(ap::nargs::up_to(5)); // n <= 5 - ``` +parser.parse_args(argc, argv); -- Unbound range: +// `input` is guaranteed to have a value if parsing was successfull +const auto data = read_data(parser.value("input")); - ```cpp - parser.add_optional_argument("input", "i").nargs(ap::nargs::any()); - ``` +// `output` is guaranteed to have a value even if one was not specified in the command-line +std::ofstream os(parser.value("output")); +os << data << std::endl; -> [!IMPORTANT] +/* +Command Result +----------------------------------------------------------------------------------------------- +./program Parsing error (no value for the input argument) +./program --input input.txt Parsing success; Printing data to `output.txt` +./program -i input.txt -o myfile.txt Parsing success; Printing data to the `myfile.txt` file +``` + +> [!TIP] > -> The default `nargs` parameter value is `ap::nargs::any()`. +> The setter of the `default_value` parameter accepts any type that is convertible to the argument's value type. + +
+
+ +### Parameters Specific for Optional Arguments + +Apart from the common parameters listed above, for optional arguments you can also specify the following parameters: + +#### 1. On-flag actions - For optional arguments, apart from value actions, you can specify on-flag actions which are executed immediately after parsing an argument's flag. + +```cpp +void print_debug_info() noexcept { +#ifdef NDEBUG + std::cout << "Running in release mode.\n"; +#else + std::cout << "Running in debug mode.\n"; +#endif + std::exit(EXIT_SUCCESS); +}; + +parser.add_optional_argument("--debug-info") + .action(print_debug_info); +``` + +Here the `print_debug_info` function will be called right after parsing the `--debug-info` flag and the program will exit, even if there are more arguments after this flag. + +
#### 2. `implicit_value` - A value which will be set for an argument if only it's flag is parsed from the command-line but no values follow. @@ -534,24 +584,6 @@ Command Result > - The `implicit_value` parameter is extremely useful when combined with default value (e.g. in case of boolean flags - see [Adding Arguments](#adding-arguments)). > - The setter of the `implicit_value` parameter accepts any type that is convertible to the argument's value type. -#### 4. On-flag actions - For optional arguments, apart from value actions, you can specify on-flag actions which are executed immediately after parsing an argument's flag. - -```cpp -void print_debug_info() noexcept { -#ifdef NDEBUG - std::cout << "Running in release mode.\n"; -#else - std::cout << "Running in debug mode.\n"; -#endif - std::exit(EXIT_SUCCESS); -}; - -parser.add_optional_argument("--debug-info") - .action(print_debug_info); -``` - -Here the `print_debug_info` function will be called right after parsing the `--debug-info` flag and the program will exit, even if there are more arguments after this flag. -


@@ -636,20 +668,16 @@ Here the `print_debug_info` function will be called right after parsing the `--d The `CPP-AP` library defines several default arguments, which can be added to the parser's configuration as follows. ```cpp -parser.default_positional_arguments({...}); -// here `...` represents a collection of ap::argument::default_positional values - -parser.default_positional_arguments({...}); -// here `...` represents a collection of ap::argument::default_optional values +parser.default_arguments(); ``` > [!NOTE] > -> These functions work with `std::initializer_list` and all other `std::ranges::range` types with the correct value type - `ap::argument::default_{positional/optional}` +> - The `default_arguments` function takes as parameter (``) either a `std::initializer_list` or a type satisfying the [`std::ranges::range`](https://en.cppreference.com/w/cpp/ranges/range.html) concept with the `ap::default_argument` value type. The available default arguments are: -- `default_positional::input`: +- `p_input`: ```cpp // equivalent to: @@ -658,14 +686,14 @@ The available default arguments are: .help("Input file path"); ``` -- `default_positional::output`: +- `p_output`: ```cpp // equivalent to: parser.add_positional_argument("output").help("Output file path"); ``` -- `default_optional::help`: +- `o_help`: ```cpp // equivalent to: @@ -674,7 +702,7 @@ The available default arguments are: .help("Display the help message"); ``` -- `default_optional::input` and `default_optional::multi_input`: +- `o_input` and `o_multi_input`: ```cpp // input - equivalent to: @@ -692,7 +720,7 @@ The available default arguments are: .help("Input files paths"); ``` -- `default_optional::output` and `default_optional::multi_output`: +- `o_output` and `o_multi_output`: ```cpp // output - equivalent to: @@ -761,7 +789,7 @@ int main(int argc, char* argv[]) { parser.add_optional_argument("optional", "o").help("An optional argument"); parser.add_flag("flag", "f").help("A boolean flag"); - parser.default_optional_arguments({ap::argument::default_optional::help}); + parser.default_arguments({ap::default_argument::o_help}); // parse command-line arguments parser.try_parse_args(argc, argv); @@ -1069,37 +1097,35 @@ Now all the values, that caused an exception for the `parse_args` example, are c ## Retrieving Argument Values -You can retrieve the argument's value with: +You can retrieve the argument's value(s) with: ```cpp -(const) auto value = parser.value("argument_name"); // (1) -(const) auto value = parser.value_or("argument_name", fallback_value); // (2) +(const) value_type value = parser.value("argument_name"); // (1) +(const) value_type value = parser.value_or("argument_name", fallback_value); // (2) +(const) std::vector values = parser.values("argument_name"); // (3) ``` -1. This will return the value parsed for the given argument. +1. Returns the given argument's value. - For optional arguments this will return the argument's predefined value if no value has been parsed. Additionaly, if more than one value has been parsed for an optional argument, this function will return the first parsed value. + - Returns the argument's parsed value if it has one. + - If more than one value has been parsed for the argument, this function will return the first parsed value. + - Returns the argument's predefined value if no value has been parsed for the argument. -2. When a value has been parsed for the argument, the behavior is the same as in case **(1)**. Otherwise, this will return `value_type{std::forward(fallback_value)}` (where `U` is the deducted type of `fallback_value`), if: +2. Returns the given argument's value or the specified fallback value if the argument has no values. - - There is no value parsed for a positional argument - - There is no parsed values and no predefined values for an optional arrument + - If the argument has a value (parsed or predefind), the behavior is the same as in case **(1)**. + - If the argument has no values, this will return `value_type{std::forward(fallback_value)}` (where `U` is the deduced type of `fallback_value`). -
- -Additionally for optional arguments, you can use: - -```cpp -(const) std::vector values = parser.values("argument_name"); // (3) -``` +3. Returns a vector of the given argument's values. -which returns a `vector` containing all values parsed for the given argument. + - If the argument has any values (parsed or predefined), they will be returned as a `std::vector`. + - If th argument has no values an empty vector will be returned. > [!NOTE] > > The argument value getter functions might throw an exception if: > - An argument with the given name does not exist -> - The argument does not contain any values - parsed or predefined (only getter `(1)`) +> - The argument does not contain any values - parsed or predefined (only getter function `(1)`) > - The specified `value_type` does not match the value type of the argument
diff --git a/include/ap/action/detail/utility.hpp b/include/ap/action/detail/utility.hpp index 03483363..ebec8951 100644 --- a/include/ap/action/detail/utility.hpp +++ b/include/ap/action/detail/utility.hpp @@ -3,13 +3,13 @@ // Licensed under the MIT License. See the LICENSE file in the project root for full license information. /** - * @file utility.hpp + * @file ap/action/detail/utility.hpp * @brief Defines general action-related utility. */ #pragma once -#include "ap/action/specifiers.hpp" +#include "ap/action/types.hpp" #include #include @@ -24,6 +24,13 @@ template concept c_value_action_specifier = ap::detail::c_one_of; +/** + * @brief The concept is satisfied when `AS` is a valid *on-flag* action action specifier. + * @tparam AS The action specifier type. + */ +template +concept c_flag_action_specifier = ap::detail::c_one_of; + /** * @brief The concept is satisfied when `AS` is a valid action action specifier. * @tparam AS The action specifier type. diff --git a/include/ap/action/predefined_actions.hpp b/include/ap/action/predefined.hpp similarity index 99% rename from include/ap/action/predefined_actions.hpp rename to include/ap/action/predefined.hpp index 405b4071..c6bfee1d 100644 --- a/include/ap/action/predefined_actions.hpp +++ b/include/ap/action/predefined.hpp @@ -2,7 +2,7 @@ // 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 predefined_actions.hpp +/// @file ap/action/predefined.hpp #pragma once diff --git a/include/ap/action/specifiers.hpp b/include/ap/action/types.hpp similarity index 98% rename from include/ap/action/specifiers.hpp rename to include/ap/action/types.hpp index b7079ac2..b1b601c1 100644 --- a/include/ap/action/specifiers.hpp +++ b/include/ap/action/types.hpp @@ -3,7 +3,7 @@ // Licensed under the MIT License. See the LICENSE file in the project root for full license information. /** - * @file specifiers.hpp + * @file ap/action/types.hpp * @brief Defies the action specifier types. */ diff --git a/include/ap/argument.hpp b/include/ap/argument.hpp new file mode 100644 index 00000000..924ccba7 --- /dev/null +++ b/include/ap/argument.hpp @@ -0,0 +1,623 @@ +// 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.hpp + +#pragma once + +#include "action/detail/utility.hpp" +#include "action/predefined.hpp" +#include "detail/argument_base.hpp" +#include "detail/argument_descriptor.hpp" +#include "detail/concepts.hpp" +#include "nargs/range.hpp" +#include "types.hpp" + +#ifdef AP_TESTING + +namespace ap_testing { +struct argument_test_fixture; +} // namespace ap_testing + +#endif + +namespace ap { + +/// @brief A discriminator type used to specify the type of an argument within the @ref ap::argument class. +enum class argument_type : bool { positional, optional }; + +/** + * @brief Represents a command-line argument, either positional or optional. + * + * This class defines the behaviour of command-line arguments - both positional and optional, + * depending on the given type discriminator. + * + * @note This class is not intended to be constructed directly, but rather throught the + * @note - `add_positional_argument` + * @note - `add_optional_argument` + * @note - `add_flag` + * @note methods of @ref ap::argument_parser. + * @attention Some member functions are conditionally enabled/disabled depending on the argument type and value type. + * + * Example usage: + * @code + * ap::argument_parser parser; + * parser.add_positional_argument("input", "i") + * .help("An input file path"); + * parser.add_optional_argument("output", "o") + * .default_value("out.txt") + * .help("An output file path"); + * @endcode + * + * @tparam ArgT The argument type, either @ref ap::argument_type::positional or @ref ap::argument_type::optional. + * @tparam T The value type accepted by the argument (defaults to std::string). + */ +template +class argument : public detail::argument_base { +public: + using value_type = T; ///< The argument's value type alias. + using count_type = nargs::count_type; ///< The argument's count type alias. + + static constexpr argument_type type = ArgT; ///< The argument's type discriminator. + + argument() = delete; + + /** + * @brief Positional argument constructor. + * @param name The name of the positional argument. + * @note The constructor is enabled only if `type` is `argument_type::positional`. + */ + argument(const detail::argument_name& name) + requires(type == argument_type::positional) + : _name(name), _nargs_range(_default_nargs_range_actual), _required(_default_required) {} + + /** + * @brief Optional argument constructor. + * @param name The name of the optional argument. + * @note The constructor is enabled only if `type` is `argument_type::optional`. + */ + argument(const detail::argument_name& name) + requires(type == argument_type::optional) + : _name(name), + _nargs_range(_default_nargs_range_actual), + _required(_default_required), + _count(0ull) {} + + /// @brief Checks if the argument is positional. + /// @return `true` if the argument's `type` is `argument_type::positional`, `false` otherwise. + [[nodiscard]] bool is_positional() const noexcept override { + return type == argument_type::positional; + } + + /// @brief Checks if the argument is optional. + /// @return `true` if the argument's `type` is `argument_type::optional`, `false` otherwise. + [[nodiscard]] bool is_optional() const noexcept override { + return type == argument_type::optional; + } + + /// @return Reference the name of the positional argument. + [[nodiscard]] const ap::detail::argument_name& name() const noexcept override { + return this->_name; + } + + /// @return Optional help message for the positional argument. + [[nodiscard]] const std::optional& help() const noexcept override { + return this->_help_msg; + } + + /// @return `true` if the argument is hidden, `false` otherwise + [[nodiscard]] bool is_hidden() const noexcept override { + return this->_hidden; + } + + /// @return `true` if the argument is required, `false` otherwise + [[nodiscard]] bool is_required() const noexcept override { + return this->_required; + } + + /// @return `true` if required argument bypassing is enabled for the argument, `false` otherwise. + /// @note Required argument bypassing is enabled only if the argument is not required. + [[nodiscard]] bool is_bypass_required_enabled() const noexcept override { + return not this->_required and this->_bypass_required; + } + + // attribute setters + + /** + * @brief Set the help message for the argument. + * @param help_msg The help message to set. + * @return Reference to the argument instance. + */ + argument& help(std::string_view help_msg) noexcept { + this->_help_msg = help_msg; + return *this; + } + + /** + * @brief Set the `hidden` attribute for the argument. + * @param h The attribute value. + * @return Reference to the argument instance. + */ + argument& hidden(const bool h = true) noexcept { + this->_hidden = h; + return *this; + } + + /** + * @brief Set the `required` attribute of the argument + * @param r The attribute value. + * @return Reference to the argument instance. + * @attention Setting the `required` attribute to `true` disables the `bypass_required` attribute. + */ + argument& required(const bool r = true) noexcept { + this->_required = r; + if (this->_required) + this->_bypass_required = false; + return *this; + } + + /** + * @brief Enable/disable bypassing the `required` attribute for the argument. + * @param br The attribute value. + * @return Reference to the argument instance. + * @attention Setting the `bypass_required` option to `true` disables the `required` attribute. + */ + argument& bypass_required(const bool br = true) noexcept { + this->_bypass_required = br; + if (this->_bypass_required) + this->_required = false; + return *this; + } + + /** + * @brief Set the nargs range for the argument. + * @param range The attribute value. + * @return Reference to the argument instance. + * @note The method is enabled only if `value_type` is not `none_type`. + */ + argument& nargs(const nargs::range& range) noexcept + requires(not detail::c_is_none) + { + this->_nargs_range = range; + return *this; + } + + /** + * @brief Set the nargs range for the argument. + * @param n The exact bound for the nargs range attribute. + * @return Reference to the argument instance. + * @note The method is enabled only if `value_type` is not `none_type`. + */ + argument& nargs(const count_type n) noexcept + requires(not detail::c_is_none) + { + this->_nargs_range = nargs::range(n); + return *this; + } + + /** + * @brief Set the nargs range for the optional argument. + * @param lower The lower bound for the nargs range attribute. + * @param upper The upper bound for the nargs range attribute. + * @return Reference to the argument instance. + * @note The method is enabled only if `value_type` is not `none_type`. + */ + argument& nargs(const count_type lower, const count_type upper) noexcept + requires(not detail::c_is_none) + { + this->_nargs_range = nargs::range(lower, upper); + return *this; + } + + /** + * @brief Set the *value* action for the argument. + * @tparam AS The action specifier type (see @ref ap/action/type.hpp). + * @tparam F The type of the action function. + * @param action The action callable. + * @return Reference to the argument instance. + * @note The method is enabled only if: + * @note - `value_type` is not `none_type`. + * @note - `AS` is a valid value action specifier: `action_type::observe`, `action_type::transform`, `action_type::modify`. + */ + template + argument& action(F&& action) noexcept + requires(not detail::c_is_none) + { + using callable_type = action::detail::callable_type; + this->_value_actions.emplace_back(std::forward(action)); + return *this; + } + + /** + * @brief Set the *on-flag* action for the argument. + * @tparam AS The action specifier type (see @ref ap/action/types.hpp). + * @tparam F The type of the action function. + * @param action The action callable. + * @return Reference to the argument instance. + * @note The method is enabled only for optional arguments and if `AS` is `action_type::on_flag`. + */ + template + argument& action(F&& action) noexcept + requires(type == argument_type::optional) + { + this->_flag_actions.emplace_back(std::forward(action)); + return *this; + } + + /** + * @brief Set the choices for the argument. + * @tparam CR The choices range type. + * @param choices The range of valid choices for the argument. + * @return Reference to the argument instance. + * @note The method is enabled only if: + * @note - `value_type` must not be `none_type` and must be equality comparable + * @note - `CR` must be a range such that its value type is convertible to the argument's `value_type` + */ + template CR> + argument& choices(const CR& choices) noexcept + requires(not detail::c_is_none and std::equality_comparable) + { + for (const auto& choice : choices) + this->_choices.emplace_back(choice); + return *this; + } + + /** + * @brief Set the choices for the argument. + * @param choices The list of valid choices for the argument. + * @return Reference to the argument instance. + * @note The method is enabled only if `value_type` is not `none_type` and is equality comparable. + */ + argument& choices(std::initializer_list choices) noexcept + requires(not detail::c_is_none and std::equality_comparable) + { + return this->choices<>(choices); + } + + /** + * @brief Set the default value for the argument. + * @param default_value The attribute value. + * @return Reference to the argument instance. + * @attention Setting the default value sets the `required` attribute to `false`. + * @note The method is enabled only if `value_type` is not `none_type`. + */ + argument& default_value(const std::convertible_to auto& default_value) noexcept + requires(not detail::c_is_none) + { + this->_default_value = std::make_any(default_value); + this->_required = false; + return *this; + } + + /** + * @brief Set the implicit value for the optional argument. + * @param implicit_value The implicit value to set. + * @return Reference to the optional argument instance. + * @note The method is enabled only for optional arguments and if `value_type` is not `none_type`. + */ + argument& implicit_value(const std::convertible_to auto& implicit_value) noexcept + requires(not detail::c_is_none and type == argument_type::optional) + { + this->_implicit_value = std::make_any(implicit_value); + return *this; + } + +#ifdef AP_TESTING + friend struct ::ap_testing::argument_test_fixture; +#endif + +private: + /// @brief The argument's value action type alias. + using value_action_type = action::detail::value_action_variant_type; + + /// @brief The argument's flag action type alias. + using flag_action_type = typename action_type::on_flag::type; + + /// @brief The argument's value-argument-specific type alias. + /// @tparam _T The actual type used if the argument's `value_type` is not `none_type`. + template + using value_arg_specific_type = std::conditional_t< + detail::c_is_none, + none_type, + _T>; ///< Type alias for value-argument-specific types. + + /// @brief The argument's positional-argument-specific type alias. + /// @tparam _T The actual type used if the argument's `type` is `argument_type::positional`. + template + using positional_specific_type = + std::conditional_t; + + /// @brief The argument's optional-argument-specific type alias. + /// @tparam _T The actual type used if the argument's `type` is `argument_type::optional`. + template + using optional_specific_type = + std::conditional_t; + + /** + * @brief Creates an descriptor 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); + + if (not verbose) + return desc; + + desc.params.reserve(6ull); + if (this->_required != _default_required) + desc.add_param("required", std::format("{}", this->_required)); + if (this->is_bypass_required_enabled()) + desc.add_param("bypass required", "true"); + if (this->_nargs_range != _default_nargs_range) + desc.add_param("nargs", this->_nargs_range); + if constexpr (detail::c_writable) { + if (not this->_choices.empty()) + desc.add_range_param("choices", this->_choices); + if (this->_default_value.has_value()) + desc.add_param("default value", std::any_cast(this->_default_value)); + if constexpr (type == argument_type::optional) { + if (this->_implicit_value.has_value()) + desc.add_param( + "implicit value", std::any_cast(this->_implicit_value) + ); + } + } + + return desc; + } + + /// @brief Mark the optional argument as used. + /// @return `true` if the argument accepts further values, `false` otherwise. + bool mark_used() override { + if constexpr (type == argument_type::optional) { + ++this->_count; + for (const auto& action : this->_flag_actions) + action(); + } + + return this->_accepts_further_values(); + } + + /// @return `true` if the argument is used, `false` otherwise. + [[nodiscard]] bool is_used() const noexcept override { + return this->count() > 0ull; + } + + /** + * @return The number of times the argument has been used. + * @note - For positional arguments, the count is either `0` (not used) or `1` (used). + * @note - For optional arguments, the count reflects the number of times the argument's flag has been used. + */ + [[nodiscard]] std::size_t count() const noexcept override { + if constexpr (type == argument_type::optional) + return this->_count; + else + return static_cast(this->has_parsed_values()); + } + + /** + * @brief Set the value for the optional argument. + * @param str_value The string value to use. + * @return `true` if the argument accepts further values, `false` otherwise. + * @throws ap::parsing_failure + */ + bool set_value(const std::string& str_value) override { + return this->_set_value_impl(str_value); + } + + /// @return `true` if the argument has a value, `false` otherwise. + /// @note An argument is considered to have a value if it has parsed values or predefined values (default/implicit). + [[nodiscard]] bool has_value() const noexcept override { + return this->has_parsed_values() or this->_has_predefined_values_impl(); + } + + /// @return `true` if parsed values are available for the argument, `false` otherwise. + [[nodiscard]] bool has_parsed_values() const noexcept override { + return not this->_values.empty(); + } + + /// @return `true` if the argument has predefined values, `false` otherwise. + [[nodiscard]] bool has_predefined_values() const noexcept override { + return this->_has_predefined_values_impl(); + } + + /// @return The ordering relationship of the argument's values and its nargs range attribute. + [[nodiscard]] std::weak_ordering nvalues_ordering() const noexcept override { + if (this->_values.empty() and this->_has_predefined_values_impl()) + return std::weak_ordering::equivalent; + + return this->_values.size() <=> this->_nargs_range; + } + + /** + * @return Reference to the stored value of the argument. + * @note If multiple values are available, the first one is returned. + * @throws std::logic_error if no values are available. + */ + [[nodiscard]] const std::any& value() const override { + if (not this->_values.empty()) + return this->_values.front(); + + if constexpr (detail::c_is_none) + throw std::logic_error( + std::format("No values parsed for argument '{}'.", this->_name.str()) + ); + else + return this->_predefined_value(); + } + + /// @return Reference to the vector of parsed values for the argument. + [[nodiscard]] const std::vector& values() const override { + return this->_values; + } + + /// @return `true` if the argument has a predefined value, `false` otherwise. + [[nodiscard]] bool _has_predefined_values_impl() const noexcept + requires(detail::c_is_none) + { + return false; + } + + /** + * @return `true` if the argument has a predefined value, `false` otherwise. + * @note The method is enabled only if `value_type` is not `none_type`. + * @note - For positional arguments, a predefined value exists if a default value is set. + * @note - For optional arguments, a predefined value exists if either a default value is set or if the argument has been used and an implicit value is set. + */ + [[nodiscard]] bool _has_predefined_values_impl() const noexcept + requires(not detail::c_is_none) + { + if constexpr (type == argument_type::positional) + return this->_default_value.has_value(); + else + return this->_default_value.has_value() + or (this->is_used() and this->_implicit_value.has_value()); + } + + /** + * @return Reference to the predefined value of the argument. + * @throws std::logic_error if no predefined value is available. + * @note The method is enabled only if `value_type` is not `none_type`. + * @note - For positional arguments, the default value is returned. + * @note - For optional arguments, if the argument has been used, the implicit value is returned, otherwise the default value is returned. + */ + [[nodiscard]] const std::any& _predefined_value() const + requires(not detail::c_is_none) + { + if constexpr (type == argument_type::optional) { + if (this->is_used()) { + if (not this->_implicit_value.has_value()) + throw(std::logic_error(std::format( + "No implicit value specified for argument '{}'.", this->_name.str() + ))); + + return this->_implicit_value; + } + } + + if (not this->_default_value.has_value()) + throw(std::logic_error( + std::format("No default value specified for argument '{}'.", this->_name.str()) + )); + + return this->_default_value; + } + + /// @return `true` if the argument accepts further values, `false` otherwise. + [[nodiscard]] bool _accepts_further_values() const noexcept { + return not std::is_gt(this->_values.size() + 1ull <=> this->_nargs_range); + } + + /// @return `true` if the given value is a valid choice for the argument, `false` otherwise. + /// @todo Use std::ranges::contains after the switch to C++23 + [[nodiscard]] bool _is_valid_choice(const value_type& value) const noexcept + requires(not detail::c_is_none) + { + return this->_choices.empty() + or std::ranges::find(this->_choices, value) != this->_choices.end(); + } + + /** + * @brief The implementation of the `set_value` method for none-type arguments. + * @param str_value The string value to set. + * @throws ap::parsing_failure + * @attention Always throws! (`set_value` should never be called for a none-type argument). + */ + bool _set_value_impl(const std::string& str_value) + requires(detail::c_is_none) + { + throw parsing_failure(std::format( + "Cannot set values for a none-type argument '{}' (value: '{}')", + this->_name.str(), + str_value + )); + } + + /** + * @brief The implementation of the `set_value` method for non-none-type arguments. + * @return `true` if the argument accepts further values, `false` otherwise. + * @param str_value The string value to set. + * @throws ap::parsing_failure if: + * @throws - the argument does not accept further values (nargs limit exceeded). + * @throws - the value cannot be parsed to the argument's `value_type`. + * @throws - the value is not a valid choice for the argument (if choices are defined). + * @note The method is enabled only if `value_type` is not `none_type`. + */ + bool _set_value_impl(const std::string& str_value) + requires(not detail::c_is_none) + { + if (not this->_accepts_further_values()) + throw parsing_failure::invalid_nvalues(this->_name, std::weak_ordering::greater); + + value_type value; + if constexpr (detail::c_trivially_readable) { + value = value_type(str_value); + } + else { + if (not (std::istringstream(str_value) >> value)) + throw parsing_failure::invalid_value(this->_name, str_value); + } + + if (not this->_is_valid_choice(value)) + throw parsing_failure::invalid_choice(this->_name, str_value); + + const auto apply_visitor = action::detail::apply_visitor{value}; + for (const auto& action : this->_value_actions) + std::visit(apply_visitor, action); + + this->_values.emplace_back(std::move(value)); + return this->_accepts_further_values(); + } + + // attributes + const ap::detail::argument_name _name; ///< The argument's name. + std::optional _help_msg; ///< The argument's help message. + nargs::range _nargs_range; ///< The argument's nargs range attribute. + [[no_unique_address]] value_arg_specific_type + _default_value; ///< The argument's default value. + [[no_unique_address]] value_arg_specific_type> + _implicit_value; ///< The optional argument's implicit value. + [[no_unique_address]] value_arg_specific_type> + _choices; ///< The argument's valid choices collection. + [[no_unique_address]] optional_specific_type> + _flag_actions; ///< The optional argument's flag actions collection. + [[no_unique_address]] value_arg_specific_type> + _value_actions; ///< The argument's value actions collection. + + bool _required : 1; ///< The argument's `required` attribute. + bool _bypass_required : 1 = false; ///< The argument's `bypass_required` attribute. + bool _hidden : 1 = false; ///< The argument's `hidden` attribute. + + // parsing result + [[no_unique_address]] optional_specific_type + _count; ///< The argument's value count. + std::vector _values; ///< The argument's parsed values. + + // default attribute values + static constexpr bool _default_required = (type == argument_type::positional); + static constexpr nargs::range _default_nargs_range = + (type == argument_type::positional) ? nargs::range(1ull) : nargs::any(); + static constexpr nargs::range _default_nargs_range_actual = + detail::c_is_none ? nargs::range(0ull) : _default_nargs_range; +}; + +/** + * @brief Positional argument alias. + * @tparam T The value type accepted by the argument (defaults to std::string). + * @see ap::argument + */ +template +using positional_argument = argument; + +/** + * @brief Optional argument alias. + * @tparam T The value type accepted by the argument (defaults to std::string). + * @see ap::argument + */ +template +using optional_argument = argument; + +} // namespace ap diff --git a/include/ap/argument/default.hpp b/include/ap/argument/default.hpp deleted file mode 100644 index cc6a4cc8..00000000 --- a/include/ap/argument/default.hpp +++ /dev/null @@ -1,22 +0,0 @@ -// 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 default.hpp - * @brief Defines the default argument discriminator types. - */ - -#pragma once - -#include - -namespace ap::argument { - -/// @brief Enum class representing positional arguments. -enum class default_positional : uint8_t { input, output }; - -/// @brief Enum class representing optional arguments. -enum class default_optional : uint8_t { help, input, output, multi_input, multi_output }; - -} // namespace ap::argument diff --git a/include/ap/argument/optional.hpp b/include/ap/argument/optional.hpp deleted file mode 100644 index c295984e..00000000 --- a/include/ap/argument/optional.hpp +++ /dev/null @@ -1,376 +0,0 @@ -// 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 optional.hpp - -#pragma once - -#include "ap/action/detail/utility.hpp" -#include "ap/action/predefined_actions.hpp" -#include "ap/detail/argument_base.hpp" -#include "ap/detail/argument_descriptor.hpp" -#include "ap/detail/concepts.hpp" -#include "ap/nargs/range.hpp" - -#ifdef AP_TESTING - -namespace ap_testing { -struct optional_argument_test_fixture; -} // namespace ap_testing - -#endif - -namespace ap::argument { - -/** - * @brief The optioanl argument class. - * @tparam T The argument's value type. - */ -template -class optional : public detail::argument_base { -public: - using value_type = T; ///< The argument's value type. - using count_type = nargs::range::count_type; ///< The argument's value count type. - - optional() = delete; - - /** - * @brief Constructor for optional argument with the `name` identifier. - * @param name The `name` identifier of the optional argument. - */ - optional(const detail::argument_name& name) : argument_base(name) {} - - ~optional() = default; - - /** - * @brief Equality comparison operator for optional argument. - * @param other The optional argument to compare with. - * @return Equality of comparison. - */ - bool operator==(const optional& other) const noexcept { - return this->_name == other._name; - } - - /** - * @brief Set the help message for the optional argument. - * @param help_msg The help message to set. - * @return Reference to the optional argument. - */ - optional& help(std::string_view help_msg) noexcept { - this->_help_msg = help_msg; - return *this; - } - - /** - * @brief Set the `hidden` attribute for the positional argument. - * @param h The attribute value. - * @return Reference to the positional argument. - */ - optional& hidden(const bool h = true) noexcept { - this->_hidden = h; - return *this; - } - - /** - * @brief Set the `required` attribute of the optional argument - * @param r The attribute value. - * @return Reference to the optional argument. - * @attention Setting the `required` attribute to true disables the `bypass_required` attribute. - */ - optional& required(const bool r = true) noexcept { - this->_required = r; - if (this->_required) - this->_bypass_required = false; - return *this; - } - - /** - * @brief Enable/disable bypassing the `required` attribute for the optional argument. - * @param br The attribute value. - * @return Reference to the optional argument. - * @attention Setting the `bypass_required` option to true disables the `required` attribute. - */ - optional& bypass_required(const bool br = true) noexcept { - this->_bypass_required = br; - if (this->_bypass_required) - this->_required = false; - return *this; - } - - /** - * @brief Set the nargs range for the optional argument. - * @param range The nargs range to set. - * @return Reference to the optional argument. - */ - optional& nargs(const nargs::range& range) noexcept { - this->_nargs_range = range; - return *this; - } - - /** - * @brief Set the nargs range for the optional argument. - * @param n The exact bound for nargs range. - * @return Reference to the optional argument. - */ - optional& nargs(const count_type n) noexcept { - this->_nargs_range = nargs::range(n); - return *this; - } - - /** - * @brief Set the nargs range for the optional argument. - * @param lower_bound The lower bound for nargs range. - * @param upper_bound The upper bound for nargs range. - * @return Reference to the optional argument. - */ - optional& nargs(const count_type lower_bound, const count_type upper_bound) noexcept { - this->_nargs_range = nargs::range(lower_bound, upper_bound); - return *this; - } - - /** - * @brief Set the action for the optional argument. - * @tparam AS The action specifier type (see @ref ap/action/specifiers.hpp). - * @tparam F The type of the action function. - * @param action The action function to set. - * @return Reference to the optional argument. - */ - template - optional& action(F&& action) noexcept { - if constexpr (action::detail::c_value_action_specifier) { - using callable_type = action::detail::callable_type; - this->_value_actions.emplace_back(std::forward(action)); - } - else { - this->_flag_actions.emplace_back(std::forward(action)); - } - - return *this; - } - - /** - * @brief Set the choices for the optional argument. - * @tparam CR The choices range type. - * @param choices The range of valid choices for the argument. - * @return Reference to the optional argument. - * @note `value_type` must be equality comparable. - * @note `CR` must be a range such that its value type is convertible to `value_type`. - */ - template CR> - optional& choices(const CR& choices) noexcept - requires(std::equality_comparable) - { - for (const auto& choice : choices) - this->_choices.emplace_back(choice); - return *this; - } - - /** - * @brief Set the choices for the optional argument. - * @param choices The list of valid choices for the argument. - * @return Reference to the optional argument. - * @note `value_type` must be equality comparable. - */ - optional& choices(std::initializer_list choices) noexcept - requires(std::equality_comparable) - { - return this->choices<>(choices); - } - - /** - * @brief Set the default value for the optional argument. - * @param default_value The default value to set. - * @return Reference to the optional argument. - * @attention Setting the default value disables the `required` attribute. - */ - optional& default_value(const std::convertible_to auto& default_value) noexcept { - this->_default_value = std::make_any(default_value); - this->_required = false; - return *this; - } - - /** - * @brief Set the implicit value for the optional argument. - * @param implicit_value The implicit value to set. - * @return Reference to the optional argument. - */ - optional& implicit_value(const std::convertible_to auto& implicit_value) noexcept { - this->_implicit_value = std::make_any(implicit_value); - return *this; - } - - /// @brief Friend class declaration for access by argument_parser. - friend class ::ap::argument_parser; - -#ifdef AP_TESTING - /** - * @brief Friend struct declaration for testing purposes. - */ - friend struct ::ap_testing::optional_argument_test_fixture; -#endif - -private: - using value_action_type = - action::detail::value_action_variant_type; ///< The argument's value action type. - using flag_action_type = typename action_type::on_flag::type; - - /// @brief `true` if the argument is an instance of `positional`, `false` otherwise. - [[nodiscard]] bool is_positional() const noexcept override { - return false; - } - - /// @brief `true` if the argument is an instance of `optional`, `false` otherwise. - virtual bool is_optional() const noexcept override { - return true; - } - - /** - * @param verbose The verbosity mode value. - * @return An argument descriptor object for the argument. - */ - [[nodiscard]] detail::argument_descriptor desc(const bool verbose) const noexcept override { - detail::argument_descriptor desc(this->_name.str(), this->_help_msg); - - if (not verbose) - return desc; - - desc.params.reserve(6); - if (this->_required) - desc.add_param("required", "true"); - if (this->bypass_required_enabled()) - desc.add_param("bypass required", "true"); - if (this->_nargs_range.is_bound()) - desc.add_param("nargs", this->_nargs_range); - if constexpr (detail::c_writable) { - if (not this->_choices.empty()) - desc.add_range_param("choices", this->_choices); - if (this->_default_value.has_value()) - desc.add_param("default value", std::any_cast(this->_default_value)); - if (this->_implicit_value.has_value()) - desc.add_param("implicit value", std::any_cast(this->_implicit_value)); - } - - return desc; - } - - /// @brief Mark the optional argument as used. - bool mark_used() override { - ++this->_count; - for (const auto& action : this->_flag_actions) - action(); - return this->_accepts_further_values(); - } - - /// @return True if the optional argument is used, false otherwise. - [[nodiscard]] bool is_used() const noexcept override { - return this->_count > 0; - } - - /// @return The number of times the optional argument attribute has been used. - [[nodiscard]] std::size_t count() const noexcept override { - return this->_count; - } - - /** - * @brief Set the value for the optional argument. - * @param str_value The string value to set. - * @return Reference to the optional argument. - * @throws ap::parsing_failure - */ - bool set_value(const std::string& str_value) override { - if (not this->_accepts_further_values()) - throw parsing_failure::invalid_nvalues(this->_name, std::weak_ordering::greater); - - value_type value; - if constexpr (detail::c_trivially_readable) { - value = value_type(str_value); - } - else { - if (not (std::istringstream(str_value) >> value)) - throw parsing_failure::invalid_value(this->_name, str_value); - } - - if (not detail::is_valid_choice(value, this->_choices)) - throw parsing_failure::invalid_choice(this->_name, str_value); - - const auto apply_visitor = action::detail::apply_visitor{value}; - for (const auto& action : this->_value_actions) - std::visit(apply_visitor, action); - - this->_values.emplace_back(std::move(value)); - return this->_accepts_further_values(); - } - - /// @return True if the optional argument has a value, false otherwise. - [[nodiscard]] bool has_value() const noexcept override { - return this->has_parsed_values() or this->_has_predefined_value(); - } - - /// @return True if parsed values are available for the optional argument, false otherwise. - [[nodiscard]] bool has_parsed_values() const noexcept override { - return not this->_values.empty(); - } - - /// @return ordering relationship of optional argument range. - [[nodiscard]] std::weak_ordering nvalues_ordering() const noexcept override { - if (this->_values.empty() and this->_has_predefined_value()) - return std::weak_ordering::equivalent; - - return this->_nargs_range.ordering(this->_values.size()); - } - - /// @return Reference to the stored value of the optional argument. - [[nodiscard]] const std::any& value() const override { - return this->_values.empty() ? this->_predefined_value() : this->_values.front(); - } - - /// @return Reference to the vector of parsed values for the optional argument. - [[nodiscard]] const std::vector& values() const override { - return this->_values; - } - - /// @return True if the optional argument has a predefined value, false otherwise. - [[nodiscard]] bool _has_predefined_value() const noexcept { - return this->_default_value.has_value() - or (this->is_used() and this->_implicit_value.has_value()); - } - - /** - * @return Reference to the predefined value of the optional argument. - * @throws std::logic_error - */ - [[nodiscard]] const std::any& _predefined_value() const { - if (this->is_used()) { - if (not this->_implicit_value.has_value()) - throw(std::logic_error( - std::format("No implicit value specified for argument `{}`.", this->_name.str()) - )); - - return this->_implicit_value; - } - - if (not this->_default_value.has_value()) - throw(std::logic_error( - std::format("No default value specified for argument `{}`.", this->_name.str()) - )); - - return this->_default_value; - } - - [[nodiscard]] bool _accepts_further_values() const noexcept { - return not std::is_gt(this->_nargs_range.ordering(this->_values.size() + 1ull)); - } - - nargs::range _nargs_range = nargs::any(); - std::any _default_value; - std::any _implicit_value; - std::vector _choices; - std::vector _flag_actions; - std::vector _value_actions; - - std::size_t _count = 0ull; - std::vector _values; -}; - -} // namespace ap::argument diff --git a/include/ap/argument/positional.hpp b/include/ap/argument/positional.hpp deleted file mode 100644 index 29c67769..00000000 --- a/include/ap/argument/positional.hpp +++ /dev/null @@ -1,300 +0,0 @@ -// 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 positional.hpp - -#pragma once - -#include "ap/action/detail/utility.hpp" -#include "ap/action/predefined_actions.hpp" -#include "ap/detail/argument_base.hpp" -#include "ap/detail/concepts.hpp" - -#ifdef AP_TESTING - -namespace ap_testing { -struct positional_argument_test_fixture; -} // namespace ap_testing - -#endif - -namespace ap::argument { - -/** - * @brief The positional argument class. - * @tparam T The argument's value type. - */ -template -class positional : public detail::argument_base { -public: - using value_type = T; ///< The argument's value type. - - positional() = delete; - - /** - * @brief Constructor for positional argument with the `name` identifier. - * @param name The `name` identifier of the positional argument. - */ - positional(const detail::argument_name& name) : argument_base(name, true) {} - - ~positional() = default; - - /** - * @brief Equality operator for positional argument. - * @param other Another positional argument for comparison. - * @return Result of equality - */ - bool operator==(const positional& other) const noexcept { - return this->_name == other._name; - } - - /** - * @brief Set the help message for the positional argument. - * @param help_msg The help message to set. - * @return Reference to the positional argument. - */ - positional& help(std::string_view help_msg) noexcept { - this->_help_msg = help_msg; - return *this; - } - - /** - * @brief Set the `hidden` attribute for the positional argument. - * @param h The attribute value. - * @return Reference to the positional argument. - */ - positional& hidden(const bool h = true) noexcept { - this->_hidden = h; - return *this; - } - - /** - * @brief Set the `required` attribute of the positional argument - * @param r The attribute value. - * @return Reference to the positional argument. - * @attention Setting the `required` attribute to true disables the `bypass_required` attribute. - */ - positional& required(const bool r = true) noexcept { - this->_required = r; - if (this->_required) - this->_bypass_required = false; - return *this; - } - - /** - * @brief Enable/disable bypassing the `required` attributeattribute for the positional argument. - * @param br The attribute value. - * @return Reference to the positional argument. - * @attention Setting the `bypass_required` attribute to true disables the `required` attribute. - */ - positional& bypass_required(const bool br = true) noexcept { - this->_bypass_required = br; - if (this->_bypass_required) - this->_required = false; - return *this; - } - - /** - * @brief Set the choices for the positional argument. - * @tparam CR The choices range type. - * @param choices The range of valid choices for the argument. - * @return Reference to the positional argument. - * @note `value_type` must be equality comparable. - * @note `CR` must be a range such that its value type is convertible to `value_type`. - */ - template CR> - positional& choices(const CR& choices) noexcept - requires(std::equality_comparable) - { - for (const auto& choice : choices) - this->_choices.emplace_back(choice); - return *this; - } - - /** - * @brief Set the choices for the positional argument. - * @param choices The list of valid choices for the argument. - * @return Reference to the positional argument. - * @note `value_type` must be equality comparable. - */ - positional& choices(std::initializer_list choices) noexcept - requires(std::equality_comparable) - { - return this->choices<>(choices); - } - - /** - * @brief Set the default value for the positional argument. - * @param default_value The default value to set. - * @return Reference to the positional argument. - * @attention Setting the default value disables the `required` attribute. - */ - positional& default_value(const std::convertible_to auto& default_value) noexcept { - this->_default_value = std::make_any(default_value); - this->_required = false; - return *this; - } - - /** - * @brief Set the action for the positional argument. - * @tparam AS The value action specifier type (valued_action or void_action). - * @tparam F The type of the action function. - * @param action The action function to set. - * @return Reference to the positional argument. - */ - template F> - positional& action(F&& action) noexcept { - using callable_type = action::detail::callable_type; - this->_value_actions.emplace_back(std::forward(action)); - return *this; - } - - /// @brief Friend class declaration for access by argument_parser. - friend class ::ap::argument_parser; - -#ifdef AP_TESTING - /** - * @brief Friend struct declaration for testing purposes. - */ - friend struct ::ap_testing::positional_argument_test_fixture; -#endif - -private: - using value_action_type = - action::detail::value_action_variant_type; ///< The argument's value action type. - - /// @brief `true` if the argument is an instance of `positional`, `false` otherwise. - [[nodiscard]] bool is_positional() const noexcept override { - return true; - } - - /// @brief `true` if the argument is an instance of `optional`, `false` otherwise. - virtual bool is_optional() const noexcept override { - return false; - } - - /** - * @param verbose The verbosity mode value. - * @return An argument descriptor object for the argument. - */ - [[nodiscard]] detail::argument_descriptor desc(const bool verbose) const noexcept override { - detail::argument_descriptor desc(this->_name.str(), this->_help_msg); - - if (not verbose) - return desc; - - if (not this->_required) - desc.add_param("required", "false"); - if (this->bypass_required_enabled()) - desc.add_param("bypass required", "true"); - if constexpr (detail::c_writable) { - if (not this->_choices.empty()) - desc.add_range_param("choices", this->_choices); - if (this->_default_value.has_value()) - desc.add_param("default value", std::any_cast(this->_default_value)); - } - - return desc; - } - - /** - * @brief Mark the positional argument as used. - * @remark No logic is performed for positional arguments - */ - bool mark_used() override { - return false; - } - - /// @return True if the positional argument is used, false otherwise. - [[nodiscard]] bool is_used() const noexcept override { - return this->_value.has_value(); - } - - /// @return 1 if a value has been parsed for the positional argument, 0 otherwise. - [[nodiscard]] std::size_t count() const noexcept override { - return static_cast(this->has_parsed_values()); - } - - /** - * @brief Set the value for the positional argument. - * @param str_value The string representation of the value. - * @return Reference to the positional argument. - * @throws ap::parsing_failure - */ - bool set_value(const std::string& str_value) override { - if (this->_value.has_value()) - throw parsing_failure::value_already_set(this->_name); - - value_type value; - if constexpr (detail::c_trivially_readable) { - value = value_type(str_value); - } - else { - if (not (std::istringstream(str_value) >> value)) - throw parsing_failure::invalid_value(this->_name, str_value); - } - - if (not detail::is_valid_choice(value, this->_choices)) - throw parsing_failure::invalid_choice(this->_name, str_value); - - const auto apply_visitor = action::detail::apply_visitor{value}; - for (const auto& action : this->_value_actions) - std::visit(apply_visitor, action); - - this->_value = value; - return false; - } - - /// @return True if the positional argument has a value, false otherwise. - [[nodiscard]] bool has_value() const noexcept override { - return this->has_parsed_values() or this->_default_value.has_value(); - } - - /// @return True if the positional argument has parsed values, false otherwise. - [[nodiscard]] bool has_parsed_values() const noexcept override { - return this->_value.has_value(); - } - - /// @return Ordering relationship of positional argument range. - [[nodiscard]] std::weak_ordering nvalues_ordering() const noexcept override { - if (not this->_required) - return std::weak_ordering::equivalent; - - return this->has_value() ? std::weak_ordering::equivalent : std::weak_ordering::less; - } - - /** - * @brief Get the stored value of the positional argument. - * @throws std::logic_error - */ - [[nodiscard]] const std::any& value() const override { - if (this->has_parsed_values()) - return this->_value; - - if (this->_default_value.has_value()) - return this->_default_value; - - throw std::logic_error( - std::format("No value parsed for the `{}` positional argument.", this->_name.str()) - ); - } - - /** - * @return Reference to the vector of parsed values for the positional argument. - * @throws std::logic_error - */ - [[nodiscard]] const std::vector& values() const override { - throw std::logic_error( - std::format("Positional argument `{}` has only 1 value.", this->_name.str()) - ); - } - - std::any _default_value; - std::vector _choices; - std::vector _value_actions; - - std::any _value; ///< Stored value of the positional argument. -}; - -} // namespace ap::argument diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index ddf8d5fd..a34bc2f5 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -3,18 +3,16 @@ // Licensed under the MIT License. See the LICENSE file in the project root for full license information. /** - * @file argument_parser.hpp + * @file ap/argument_parser.hpp * @brief Main library header file. Defines the `argument_parser` class. */ #pragma once -#include "argument/default.hpp" -#include "argument/optional.hpp" -#include "argument/positional.hpp" +#include "argument.hpp" #include "detail/argument_token.hpp" #include "detail/concepts.hpp" -#include "version.hpp" +#include "types.hpp" #include #include @@ -34,14 +32,26 @@ namespace ap { class argument_parser; +enum class default_argument { + p_input, + p_output, + o_help, + o_input, + o_output, + o_multi_input, + o_multi_output +}; + namespace detail { -void add_default_argument(const argument::default_positional, argument_parser&) noexcept; -void add_default_argument(const argument::default_optional, argument_parser&) noexcept; +void add_default_argument(const default_argument, argument_parser&) noexcept; } // namespace detail -/// @brief Main argument parser class. +/** + * @brief The main argument parser class. + * This class is responsible for the configuration and parsing of command-line arguments. + */ class argument_parser { public: argument_parser(const argument_parser&) = delete; @@ -118,8 +128,8 @@ class argument_parser { * @param arg_discriminator_range A range of default positional argument discriminators. * @return Reference to the argument parser. */ - template AR> - argument_parser& default_positional_arguments(const AR& arg_discriminator_range) noexcept { + template AR> + argument_parser& default_arguments(const AR& arg_discriminator_range) noexcept { for (const auto arg_discriminator : arg_discriminator_range) detail::add_default_argument(arg_discriminator, *this); return *this; @@ -130,34 +140,10 @@ class argument_parser { * @param arg_discriminator_list A list of default positional argument discriminators. * @return Reference to the argument parser. */ - argument_parser& default_positional_arguments( - const std::initializer_list arg_discriminator_list - ) noexcept { - return this->default_positional_arguments<>(arg_discriminator_list); - } - - /** - * @brief Set default optional arguments. - * @tparam AR Type of the optional argument discriminator range. - * @param arg_discriminator_range A range of default optional argument discriminators. - * @return Reference to the argument parser. - */ - template AR> - argument_parser& default_optional_arguments(const AR& arg_discriminator_range) noexcept { - for (const auto arg_discriminator : arg_discriminator_range) - detail::add_default_argument(arg_discriminator, *this); - return *this; - } - - /** - * @brief Set default optional arguments. - * @param arg_discriminator_list A list of default optional argument discriminators. - * @return Reference to the argument parser. - */ - argument_parser& default_optional_arguments( - const std::initializer_list arg_discriminator_list + argument_parser& default_arguments( + const std::initializer_list arg_discriminator_list ) noexcept { - return this->default_optional_arguments<>(arg_discriminator_list); + return this->default_arguments<>(arg_discriminator_list); } /** @@ -166,19 +152,17 @@ class argument_parser { * @param primary_name The primary name of the argument. * @return Reference to the added positional argument. * @throws ap::invalid_configuration - * - * \todo Check forbidden characters (after adding the assignment character). */ template - argument::positional& add_positional_argument(const std::string_view primary_name) { + 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); - this->_positional_args.emplace_back(std::make_unique>(arg_name)); - return static_cast&>(*this->_positional_args.back()); + this->_positional_args.emplace_back(std::make_unique>(arg_name)); + return static_cast&>(*this->_positional_args.back()); } /** @@ -188,11 +172,9 @@ class argument_parser { * @param secondary_name The secondary name of the argument. * @return Reference to the added positional argument. * @throws ap::invalid_configuration - * - * \todo Check forbidden characters (after adding the assignment character). */ template - argument::positional& add_positional_argument( + positional_argument& add_positional_argument( const std::string_view primary_name, const std::string_view secondary_name ) { this->_verify_arg_name_pattern(primary_name); @@ -205,8 +187,8 @@ 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_unique>(arg_name)); - return static_cast&>(*this->_positional_args.back()); + this->_positional_args.emplace_back(std::make_unique>(arg_name)); + return static_cast&>(*this->_positional_args.back()); } /** @@ -216,11 +198,9 @@ class argument_parser { * @param name_discr The discriminator value specifying whether the given name should be treated as primary or secondary. * @return Reference to the added optional argument. * @throws ap::invalid_configuration - * - * \todo Check forbidden characters (after adding the assignment character). */ template - argument::optional& add_optional_argument( + optional_argument& add_optional_argument( const std::string_view name, const detail::argument_name_discriminator name_discr = n_primary ) { @@ -237,8 +217,8 @@ 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_unique>(arg_name)); - return static_cast&>(*this->_optional_args.back()); + this->_optional_args.push_back(std::make_unique>(arg_name)); + return static_cast&>(*this->_optional_args.back()); } /** @@ -248,11 +228,9 @@ class argument_parser { * @param secondary_name The secondary name of the argument. * @return Reference to the added optional argument. * @throws ap::invalid_configuration - * - * \todo Check forbidden characters (after adding the assignment character). */ template - argument::optional& add_optional_argument( + optional_argument& add_optional_argument( const std::string_view primary_name, const std::string_view secondary_name ) { this->_verify_arg_name_pattern(primary_name); @@ -266,8 +244,8 @@ 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_unique>(arg_name)); - return static_cast&>(*this->_optional_args.back()); + this->_optional_args.emplace_back(std::make_unique>(arg_name)); + return static_cast&>(*this->_optional_args.back()); } /** @@ -278,7 +256,7 @@ class argument_parser { * @return Reference to the added boolean flag argument. */ template - argument::optional& add_flag( + optional_argument& add_flag( const std::string_view name, const detail::argument_name_discriminator name_discr = n_primary ) { @@ -296,7 +274,7 @@ class argument_parser { * @return Reference to the added boolean flag argument. */ template - argument::optional& add_flag( + optional_argument& add_flag( const std::string_view primary_name, const std::string_view secondary_name ) { return this->add_optional_argument(primary_name, secondary_name) @@ -308,7 +286,7 @@ class argument_parser { /** * @brief Parses the command-line arguments. * - * * Equivalent to: + * Equivalent to: * ```cpp * parse_args(std::span(argv + 1, static_cast(argc - 1))) * ``` @@ -573,7 +551,8 @@ class argument_parser { const auto& arg = arg_opt->get(); try { - if (not arg.has_parsed_values() and arg.has_value()) + if (arg.has_predefined_values()) + // currently an argument may have only one predefined value return std::vector{std::any_cast(arg.value())}; std::vector values; @@ -998,11 +977,11 @@ class argument_parser { return std::ranges::any_of( this->_positional_args, [](const arg_ptr_t& arg) { - return arg->is_used() and arg->bypass_required_enabled(); + return arg->is_used() and arg->is_bypass_required_enabled(); } ) or std::ranges::any_of(this->_optional_args, [](const arg_ptr_t& arg) { - return arg->is_used() and arg->bypass_required_enabled(); + return arg->is_used() and arg->is_bypass_required_enabled(); }); } @@ -1133,37 +1112,26 @@ namespace detail { * @param arg_parser The argument parser to which the argument will be added. */ inline void add_default_argument( - const argument::default_positional arg_discriminator, argument_parser& arg_parser + const default_argument arg_discriminator, argument_parser& arg_parser ) noexcept { switch (arg_discriminator) { - case argument::default_positional::input: + case default_argument::p_input: arg_parser.add_positional_argument("input") .action(action::check_file_exists()) .help("Input file path"); break; - case argument::default_positional::output: + case default_argument::p_output: arg_parser.add_positional_argument("output").help("Output file path"); break; - } -} -/** - * @brief Adds a predefined/default optional argument to the parser. - * @param arg_discriminator The default argument discriminator. - * @param arg_parser The argument parser to which the argument will be added. - */ -inline void add_default_argument( - const argument::default_optional arg_discriminator, argument_parser& arg_parser -) noexcept { - switch (arg_discriminator) { - case argument::default_optional::help: + case default_argument::o_help: arg_parser.add_flag("help", "h") .action(action::print_config(arg_parser, EXIT_SUCCESS)) .help("Display the help message"); break; - case argument::default_optional::input: + case default_argument::o_input: arg_parser.add_optional_argument("input", "i") .required() .nargs(1ull) @@ -1171,11 +1139,14 @@ inline void add_default_argument( .help("Input file path"); break; - case argument::default_optional::output: - arg_parser.add_optional_argument("output", "o").required().nargs(1).help("Output file path"); + case default_argument::o_output: + arg_parser.add_optional_argument("output", "o") + .required() + .nargs(1ull) + .help("Output file path"); break; - case argument::default_optional::multi_input: + case default_argument::o_multi_input: arg_parser.add_optional_argument("input", "i") .required() .nargs(ap::nargs::at_least(1ull)) @@ -1183,7 +1154,7 @@ inline void add_default_argument( .help("Input files paths"); break; - case argument::default_optional::multi_output: + case default_argument::o_multi_output: arg_parser.add_optional_argument("output", "o") .required() .nargs(ap::nargs::at_least(1ull)) diff --git a/include/ap/detail/argument_base.hpp b/include/ap/detail/argument_base.hpp index ea59fdb0..0355b5dd 100644 --- a/include/ap/detail/argument_base.hpp +++ b/include/ap/detail/argument_base.hpp @@ -3,7 +3,7 @@ // Licensed under the MIT License. See the LICENSE file in the project root for full license information. /** - * @file argument_base.hpp + * @file ap/detail/argument_base.hpp * @brief Defines the base argument class and common utility. */ @@ -29,56 +29,34 @@ class argument_base { friend class ::ap::argument_parser; -protected: - argument_base(const argument_name& name, const bool required = false) - : _name(name), _required(required) {} - - /// @return Reference the name of the positional argument. - [[nodiscard]] const ap::detail::argument_name& name() const noexcept { - return this->_name; - } - - /// @return `true` if the argument is hidden, `false` otherwise - [[nodiscard]] bool is_hidden() const noexcept { - return this->_hidden; - } - - /// @return Optional help message for the positional argument. - [[nodiscard]] const std::optional& help() const noexcept { - return this->_help_msg; - } - - /// @return `true` if the argument is required, `false` otherwise - [[nodiscard]] bool is_required() const noexcept { - return this->_required; - } - - /** - * @return `true` if required argument bypassing is enabled for the argument, `false` otherwise. - * @note Required argument bypassing is enabled only when both `required` and `bypass_required` flags are set to `true`. - */ - [[nodiscard]] bool bypass_required_enabled() const noexcept { - return not this->_required and this->_bypass_required; - } - - // pure virtual methods - - /// @brief `true` if the argument is an instance of `positional`, `false` otherwise. + /// @return `true` if the argument is a positional argument instance, `false` otherwise. virtual bool is_positional() const noexcept = 0; - /// @brief `true` if the argument is an instance of `optional`, `false` otherwise. + /// @return `true` if the argument is an optional argument instance, `false` otherwise. virtual bool is_optional() const noexcept = 0; - /** - * @param verbose The verbosity mode value. - * @return An argument descriptor object for the argument. - */ + /// @return Returns the argument's name. + virtual const argument_name& name() const noexcept = 0; + + /// @return Returns the argument's help message. + virtual const std::optional& help() const noexcept = 0; + + /// @return `true` if the argument is hidden from help output, `false` otherwise. + virtual bool is_hidden() const noexcept = 0; + + /// @return `true` if the argument is required, `false` otherwise. + virtual bool is_required() const noexcept = 0; + + /// @return `true` if the argument is allowed to bypass the required check, `false` otherwise. + virtual bool is_bypass_required_enabled() const noexcept = 0; + +protected: + /// @param verbose The verbosity mode value. If `true` all non-default parameters will be included in the output. + /// @return An argument descriptor object for the argument. virtual detail::argument_descriptor desc(const bool verbose) const noexcept = 0; - /** - * @brief Mark the argument as used. - * @return `true` if the argument accepts further values, `false` otherwise. - */ + /// @brief Mark the argument as used. + /// @return `true` if the argument accepts further values, `false` otherwise. virtual bool mark_used() = 0; /// @return `true` if the argument has been used, `false` otherwise. @@ -87,11 +65,9 @@ class argument_base { /// @return The number of times an argument has been used. virtual std::size_t count() const noexcept = 0; - /** - * @brief Set the value for the argument. - * @param value The string representation of the value. - * @return `true` if the argument accepts further values, `false` otherwise. - */ + /// @brief Set the value for the argument. + /// @param value The string representation of the value. + /// @return `true` if the argument accepts further values, `false` otherwise. virtual bool set_value(const std::string& value) = 0; /// @return `true` if the argument has a value, `false` otherwise. @@ -100,6 +76,9 @@ class argument_base { /// @return `true` if the argument has parsed values., `false` otherwise. virtual bool has_parsed_values() const noexcept = 0; + /// @return `true` if the argument has predefined values, `false` otherwise. + virtual bool has_predefined_values() const noexcept = 0; + /// @return The ordering relationship of argument range. virtual std::weak_ordering nvalues_ordering() const noexcept = 0; @@ -108,26 +87,7 @@ class argument_base { /// @return Reference to the vector of parsed values of the argument. virtual const std::vector& values() const = 0; - - const ap::detail::argument_name _name; - std::optional _help_msg; - - bool _required : 1; - bool _bypass_required : 1 = false; - bool _hidden : 1 = false; }; -/** - * @brief Checks if the provided choice is valid for the given set of choices. - * @param value A value, the validity of which is to be checked. - * @param choices The set against which the choice validity will be checked. - * @return `true` if the choice is valid, `false` otherwise. - * \todo replace with `std::ranges::contains` after transition to C++23 - */ -template -[[nodiscard]] bool is_valid_choice(const T& value, const std::vector& choices) noexcept { - return choices.empty() or std::ranges::find(choices, value) != choices.end(); -} - } // namespace detail } // namespace ap diff --git a/include/ap/detail/argument_descriptor.hpp b/include/ap/detail/argument_descriptor.hpp index 60928558..f8097def 100644 --- a/include/ap/detail/argument_descriptor.hpp +++ b/include/ap/detail/argument_descriptor.hpp @@ -3,7 +3,7 @@ // Licensed under the MIT License. See the LICENSE file in the project root for full license information. /** - * @file argument_descriptor.hpp + * @file ap/detail/argument_descriptor.hpp * @brief Defines structures for formatting argument descriptions. */ diff --git a/include/ap/detail/argument_name.hpp b/include/ap/detail/argument_name.hpp index 5f32c30f..6fadf31a 100644 --- a/include/ap/detail/argument_name.hpp +++ b/include/ap/detail/argument_name.hpp @@ -2,7 +2,7 @@ // 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 argument_name.hpp +/// @file ap/detail/argument_name.hpp #pragma once diff --git a/include/ap/detail/argument_token.hpp b/include/ap/detail/argument_token.hpp index 117799eb..e1fbf346 100644 --- a/include/ap/detail/argument_token.hpp +++ b/include/ap/detail/argument_token.hpp @@ -2,7 +2,7 @@ // 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 argument_token.hpp +/// @file ap/detail/argument_token.hpp #pragma once diff --git a/include/ap/detail/concepts.hpp b/include/ap/detail/concepts.hpp index d7d234eb..484b2d8a 100644 --- a/include/ap/detail/concepts.hpp +++ b/include/ap/detail/concepts.hpp @@ -3,17 +3,26 @@ // Licensed under the MIT License. See the LICENSE file in the project root for full license information. /** - * @file concepts.hpp + * @file ap/detail/concepts.hpp * @brief Provides the general concept definitions. */ #pragma once +#include "ap/types.hpp" + #include #include namespace ap::detail { +/** + * @brief The concept is satisfied when `T` is @ref ap::none_type. + * @tparam T Type to check. + */ +template +concept c_is_none = std::same_as; + /** * @brief The concept is satisfied when `T` overloads the std::istream operator `>>`. * @tparam T Type to check. @@ -47,7 +56,9 @@ concept c_arithmetic = std::is_arithmetic_v; * @tparam T Type to check. */ template -concept c_argument_value_type = std::semiregular and (c_trivially_readable or c_readable); +concept c_argument_value_type = + std::same_as + or (std::semiregular and (c_trivially_readable or c_readable)); /** * @brief Validates that `T` is the same as one of the types defined by `Types`. @@ -126,5 +137,4 @@ concept c_sized_range_of = std::ranges::sized_range and c_valid_type>, V, TV>; - } // namespace ap::detail diff --git a/include/ap/detail/str_utility.hpp b/include/ap/detail/str_utility.hpp index d3dab32e..f730bbc0 100644 --- a/include/ap/detail/str_utility.hpp +++ b/include/ap/detail/str_utility.hpp @@ -3,7 +3,7 @@ // Licensed under the MIT License. See the LICENSE file in the project root for full license information. /** - * @file str_utility.hpp + * @file ap/detail/str_utility.hpp * @brief Provides common string utility functions. */ diff --git a/include/ap/detail/typing_utility.hpp b/include/ap/detail/typing_utility.hpp index ae1c3dbd..e985a875 100644 --- a/include/ap/detail/typing_utility.hpp +++ b/include/ap/detail/typing_utility.hpp @@ -2,6 +2,8 @@ // This file is part of the CPP-AP project (https://github.com/SpectraL519/cpp-ap). // Licensed under the MIT License. See the LICENSE file in the project root for full license information. +/// @file ap/detail/typing_utility.hpp + #pragma once #include diff --git a/include/ap/exceptions.hpp b/include/ap/exceptions.hpp index 42073cf3..e1b72482 100644 --- a/include/ap/exceptions.hpp +++ b/include/ap/exceptions.hpp @@ -2,7 +2,7 @@ // 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 exceptions.hpp +/// @file ap/exceptions.hpp #pragma once @@ -117,7 +117,6 @@ struct parsing_failure : public argument_parser_exception { }; /// @brief Exception type used for type-related errors. -/// \todo Use demangled type names struct type_error : public argument_parser_exception { explicit type_error(const std::string& message) : argument_parser_exception(message) {} diff --git a/include/ap/nargs/range.hpp b/include/ap/nargs/range.hpp index 9e2b317c..7e66655e 100644 --- a/include/ap/nargs/range.hpp +++ b/include/ap/nargs/range.hpp @@ -3,52 +3,66 @@ // Licensed under the MIT License. See the LICENSE file in the project root for full license information. /** - * @file range.hpp + * @file ap/nargs/range.hpp * @brief Defines the `nargs::range` class and it's builder functions. */ #pragma once -#include +#include +#include #include namespace ap::nargs { +using count_type = std::size_t; + +constexpr count_type min_bound = std::numeric_limits::min(); +constexpr count_type max_bound = std::numeric_limits::max(); + /// @brief Argument's number of values managing class. class range { public: - using count_type = std::size_t; - - /// @brief Default constructor: creates range [1, 1]. - range() : _lower_bound(_default_bound), _upper_bound(_default_bound) {} + /// @brief Default constructor: creates an unbound range. + constexpr range() = default; /** * @brief Exact count constructor: creates range [n, n]. * @param n Expected value count. */ - explicit range(const count_type n) : _lower_bound(n), _upper_bound(n) {} + explicit constexpr range(const count_type n) : _lower_bound(n), _upper_bound(n) {} /** - * @brief Concrete range constructor: creates range [lower_bound, upper_bound]. - * @param lower_bound The lower bound. - * @param upper_bound The upper bound. + * @brief Concrete range constructor: creates range [lower, upper]. + * @param lower The lower bound. + * @param upper The upper bound. */ - range(const count_type lower_bound, const count_type upper_bound) - : _lower_bound(lower_bound), _upper_bound(upper_bound) {} + constexpr range(const count_type lower, const count_type upper) + : _lower_bound(lower), _upper_bound(upper) { + if (upper < lower) + throw std::logic_error( + std::format("Invalid range bounds: lower = {}, upper = {}", lower, upper) + ); + } - range(const range&) = default; - range(range&&) = default; + [[nodiscard]] constexpr bool has_explicit_lower_bound() const noexcept { + return this->_lower_bound > min_bound; + } - range& operator=(const range&) = default; - range& operator=(range&&) = default; + [[nodiscard]] constexpr bool has_explicit_upper_bound() const noexcept { + return this->_upper_bound < max_bound; + } - ~range() = default; + [[nodiscard]] constexpr bool is_explicitly_bound() const noexcept { + return this->has_explicit_lower_bound() or this->has_explicit_upper_bound(); + } - /// @brief Returns `true` if at least one bound (lower, upper) is set. Otherwise returns `false` - [[nodiscard]] bool is_bound() const noexcept { - return this->_lower_bound.has_value() or this->_upper_bound.has_value(); + [[nodiscard]] constexpr bool is_exactly_bound() const noexcept { + return this->_lower_bound == this->_upper_bound; } + constexpr bool operator==(const range& other) const = default; + /** * @brief Determines the ordering of the count against a range instance. * @@ -56,93 +70,69 @@ class range { * - `less` if `n < lower`, * - `equivalent` if `n >= lower` and `n <= upper`, * - `greater` if `n > upper`. - * If either `lower` or `upper` limits are not set (std::nullopt), - * then the corresponding conditions are dropped. * * @param n The value count to order. * @return Ordering relationship between the count and the range. */ - [[nodiscard]] std::weak_ordering ordering(const range::count_type n) const noexcept { - if (not (this->_lower_bound.has_value() or this->_upper_bound.has_value())) - return std::weak_ordering::equivalent; - - if (this->_lower_bound.has_value() and this->_upper_bound.has_value()) { - if (n < this->_lower_bound.value()) - return std::weak_ordering::less; + [[nodiscard]] friend constexpr std::weak_ordering operator<=>( + const count_type n, const range& r + ) noexcept { + if (n < r._lower_bound) + return std::weak_ordering::less; - if (n > this->_upper_bound.value()) - return std::weak_ordering::greater; + if (n > r._upper_bound) + return std::weak_ordering::greater; - return std::weak_ordering::equivalent; - } - - if (this->_lower_bound.has_value()) - return (n < this->_lower_bound.value()) - ? std::weak_ordering::less - : std::weak_ordering::equivalent; - - return (n > this->_upper_bound.value()) - ? std::weak_ordering::greater - : std::weak_ordering::equivalent; + return std::weak_ordering::equivalent; } friend std::ostream& operator<<(std::ostream& os, const range& r) noexcept { - if (r._lower_bound.has_value() and r._lower_bound == r._upper_bound) { - os << r._lower_bound.value(); + if (not r.is_explicitly_bound()) { + os << "unbound"; return os; } - if (not r._lower_bound.has_value() and not r._upper_bound.has_value()) { - os << "unbound"; + if (r.is_exactly_bound()) { + os << r._upper_bound; return os; } - os << "[" << r._lower_bound.value_or(0ull) << ", "; - if (r._upper_bound.has_value()) - os << r._upper_bound.value() << "]"; + os << "[" << r._lower_bound << ", "; + if (r.has_explicit_upper_bound()) + os << r._upper_bound << "]"; else os << "inf)"; return os; } - friend range at_least(const count_type) noexcept; - friend range more_than(const count_type) noexcept; - friend range less_than(const count_type) noexcept; - friend range up_to(const count_type) noexcept; - friend range any() noexcept; + friend constexpr range at_least(const count_type) noexcept; + friend constexpr range more_than(const count_type) noexcept; + friend constexpr range less_than(const count_type) noexcept; + friend constexpr range up_to(const count_type) noexcept; + friend constexpr range any() noexcept; private: - /** - * @brief Private constructor: creates a possibly unbound range - * @param lower_bound The optional lower bound of the range. - * @param upper_bound The optional upper bound of the range. - */ - range(const std::optional lower_bound, const std::optional upper_bound) - : _lower_bound(lower_bound), _upper_bound(upper_bound) {} - - std::optional _lower_bound; - std::optional _upper_bound; - - static constexpr count_type _default_bound = 1ull; + count_type _lower_bound = min_bound; + count_type _upper_bound = max_bound; }; /** - * @brief `range` class builder function. Creates a range [n, inf]. + * @brief `range` class builder function. Creates a range [n, inf). * @param n The lower bound. * @return Built `range` class instance. */ -[[nodiscard]] inline range at_least(const range::count_type n) noexcept { - return range(n, std::nullopt); +[[nodiscard]] constexpr range at_least(const count_type n) noexcept { + return range(n, max_bound); } /** - * @brief `range` class builder function. Creates a range [n + 1, inf]. + * @brief `range` class builder function. Creates a range [n + 1, inf). * @param n The lower bound. * @return Built `range` class instance. */ -[[nodiscard]] inline range more_than(const range::count_type n) noexcept { - return range(n + 1, std::nullopt); +[[nodiscard]] constexpr range more_than(const count_type n) noexcept { + return range(n + 1ull, max_bound); } /** @@ -150,8 +140,8 @@ class range { * @param n The upper bound * @return Built `range` class instance. */ -[[nodiscard]] inline range less_than(const range::count_type n) noexcept { - return range(std::nullopt, n - 1); +[[nodiscard]] constexpr range less_than(const count_type n) noexcept { + return range(min_bound, n - 1ull); } /** @@ -159,16 +149,16 @@ class range { * @param n The upper bound * @return Built `range` class instance. */ -[[nodiscard]] inline range up_to(const range::count_type n) noexcept { - return range(std::nullopt, n); +[[nodiscard]] constexpr range up_to(const count_type n) noexcept { + return range(min_bound, n); } /** * @brief `range` class builder function. Creates a range [0, inf]. * @return Built `range` class instance. */ -[[nodiscard]] inline range any() noexcept { - return range(std::nullopt, std::nullopt); +[[nodiscard]] constexpr range any() noexcept { + return range(min_bound, max_bound); } } // namespace ap::nargs diff --git a/include/ap/version.hpp b/include/ap/types.hpp similarity index 79% rename from include/ap/version.hpp rename to include/ap/types.hpp index 58d4f1fa..f640302c 100644 --- a/include/ap/version.hpp +++ b/include/ap/types.hpp @@ -2,13 +2,23 @@ // 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/types.hpp + #pragma once +#include #include #include namespace ap { +/** + * @brief A type representing the absence of a value. + * This type is used for arguments that should not store any values + * or as a fallback type for conditionally defined types. + */ +struct none_type {}; + /// @brief A helper structure used to represent a program's version. struct version { std::uint32_t major = 1u; ///< The major version number. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9a7ffc30..3c465b4f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -73,6 +73,7 @@ add_doctest("source/test_argument_parser_parse_args_unknown_flags_as_values.cpp" COMPILE_DEFINITIONS "AP_UNKNOWN_FLAGS_AS_VALUES" ) add_doctest("source/test_nargs_range.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_str_utility.cpp") diff --git a/tests/include/argument_parser_test_fixture.hpp b/tests/include/argument_parser_test_fixture.hpp index e87d5297..5a613785 100644 --- a/tests/include/argument_parser_test_fixture.hpp +++ b/tests/include/argument_parser_test_fixture.hpp @@ -5,8 +5,8 @@ #include #include -using ap::argument::optional; -using ap::argument::positional; +using ap::optional_argument; +using ap::positional_argument; using ap::detail::argument_name; using ap::detail::argument_token; using ap::detail::c_argument_value_type; @@ -104,17 +104,23 @@ struct argument_parser_test_fixture { ); } - template &)>> - void add_positional_args(const std::size_t n, F&& setup_arg = [](positional&) {}) { + template < + c_argument_value_type T = std::string, + 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( sut.add_positional_argument(init_arg_name_primary(i), init_arg_name_secondary(i)) ); } - template &)>> + template < + c_argument_value_type T = std::string, + typename F = std::function&)>> void add_optional_args( - const std::size_t n, const std::size_t begin_idx, F&& setup_arg = [](optional&) {} + const std::size_t n, + const std::size_t begin_idx, + F&& setup_arg = [](optional_argument&) {} ) { for (std::size_t i = 0ull; i < n; ++i) setup_arg(sut.add_optional_argument( diff --git a/tests/include/argument_test_fixture.hpp b/tests/include/argument_test_fixture.hpp new file mode 100644 index 00000000..f10f0e41 --- /dev/null +++ b/tests/include/argument_test_fixture.hpp @@ -0,0 +1,131 @@ +#pragma once + +#include +#include + +using ap::argument; +using ap::argument_type; +using ap::detail::argument_descriptor; +using ap::detail::argument_name; +using ap::detail::as_string; +using ap::detail::c_argument_value_type; + +namespace ap_testing { + +struct argument_test_fixture { + template + using value_type = typename argument::value_type; + + template + bool mark_used(argument& arg) const { + return arg.mark_used(); + } + + template + bool is_used(const argument& arg) const { + return arg.is_used(); + } + + template + std::size_t get_count(const argument& arg) const { + return arg.count(); + } + + template + bool set_required(argument& arg, const bool r) const { + return arg._required = r; + } + + template + bool set_bypass_required(argument& arg, const bool br) const { + return arg._bypass_required = br; + } + + template + bool set_value(argument& arg, const T& value) const { + return set_value(arg, as_string(value)); + } + + template + bool set_value(argument& arg, const std::string& str_value) const { + return arg.set_value(str_value); + } + + template + void set_value_force(argument& arg, const T& value) const { + set_value_force(arg, as_string(value)); + } + + template + void set_value_force(argument& arg, const std::string& str_value) const { + arg._values.emplace_back(str_value); + } + + template + void reset_values(argument& arg) const { + arg._values.clear(); + } + + template + [[nodiscard]] bool has_value(const argument& arg) const { + return arg.has_value(); + } + + template + [[nodiscard]] bool has_parsed_values(const argument& arg) const { + return arg.has_parsed_values(); + } + + template + [[nodiscard]] bool has_predefined_values(const argument& arg) const { + return arg.has_predefined_values(); + } + + template + [[nodiscard]] std::weak_ordering nvalues_ordering(const argument& arg) const { + return arg.nvalues_ordering(); + } + + template + [[nodiscard]] const std::any& get_value(const argument& arg) const { + return arg.value(); + } + + template + [[nodiscard]] const std::vector& get_values(const argument& arg) const { + return arg.values(); + } + + template + [[nodiscard]] const argument_name& get_name(const argument& arg) const { + return arg.name(); + } + + template + [[nodiscard]] const std::optional& get_help(const argument& arg) const { + return arg._help_msg; + } + + template + [[nodiscard]] argument_descriptor get_desc(const argument& arg, const bool verbose) + const { + return arg.desc(verbose); + } + + template + [[nodiscard]] bool is_hidden(const argument& arg) const { + return arg.is_hidden(); + } + + template + [[nodiscard]] bool is_required(const argument& arg) const { + return arg.is_required(); + } + + template + [[nodiscard]] bool is_bypass_required_enabled(const argument& arg) const { + return arg.is_bypass_required_enabled(); + } +}; + +} // namespace ap_testing diff --git a/tests/include/optional_argument_test_fixture.hpp b/tests/include/optional_argument_test_fixture.hpp deleted file mode 100644 index a6ddbdf9..00000000 --- a/tests/include/optional_argument_test_fixture.hpp +++ /dev/null @@ -1,127 +0,0 @@ -#pragma once - -#include -#include - -using ap::argument::optional; -using ap::detail::argument_descriptor; -using ap::detail::argument_name; -using ap::detail::as_string; -using ap::detail::c_argument_value_type; - -namespace ap_testing { - -struct optional_argument_test_fixture { - optional_argument_test_fixture() = default; - ~optional_argument_test_fixture() = default; - - template - using value_type = typename optional::value_type; - - template - bool mark_used(optional& arg) const { - return arg.mark_used(); - } - - template - bool is_used(const optional& arg) const { - return arg.is_used(); - } - - template - std::size_t get_count(const optional& arg) const { - return arg.count(); - } - - template - bool set_required(optional& arg, const bool r) const { - return arg._required = r; - } - - template - bool set_bypass_required(optional& arg, const bool br) const { - return arg._bypass_required = br; - } - - template - bool set_value(optional& arg, const T& value) const { - return set_value(arg, as_string(value)); - } - - template - bool set_value(optional& arg, const std::string& str_value) const { - return arg.set_value(str_value); - } - - template - void set_value_force(optional& arg, const T& value) const { - set_value_force(arg, as_string(value)); - } - - template - void set_value_force(optional& arg, const std::string& str_value) const { - arg._values.emplace_back(str_value); - } - - template - void reset_value(optional& arg) const { - arg._values.clear(); - } - - template - [[nodiscard]] bool has_value(const optional& arg) const { - return arg.has_value(); - } - - template - [[nodiscard]] bool has_parsed_values(const optional& arg) const { - return arg.has_parsed_values(); - } - - template - [[nodiscard]] std::weak_ordering nvalues_ordering(const optional& arg) const { - return arg.nvalues_ordering(); - } - - template - [[nodiscard]] const std::any& get_value(const optional& arg) const { - return arg.value(); - } - - template - [[nodiscard]] const std::vector& get_values(const optional& arg) const { - return arg.values(); - } - - template - [[nodiscard]] const argument_name& get_name(const optional& arg) const { - return arg.name(); - } - - template - [[nodiscard]] const std::optional& get_help(const optional& arg) const { - return arg._help_msg; - } - - template - [[nodiscard]] argument_descriptor get_desc(const optional& arg, const bool verbose) const { - return arg.desc(verbose); - } - - template - [[nodiscard]] bool is_hidden(const optional& arg) const { - return arg.is_hidden(); - } - - template - [[nodiscard]] bool is_required(const optional& arg) const { - return arg.is_required(); - } - - template - [[nodiscard]] bool is_bypass_required_enabled(const optional& arg) const { - return arg.bypass_required_enabled(); - } -}; - -} // namespace ap_testing diff --git a/tests/include/positional_argument_test_fixture.hpp b/tests/include/positional_argument_test_fixture.hpp deleted file mode 100644 index 7f82db36..00000000 --- a/tests/include/positional_argument_test_fixture.hpp +++ /dev/null @@ -1,110 +0,0 @@ -#pragma once - -#include - -using ap::argument::positional; -using ap::detail::argument_descriptor; -using ap::detail::argument_name; -using ap::detail::c_argument_value_type; - -namespace ap_testing { - -struct positional_argument_test_fixture { - positional_argument_test_fixture() = default; - ~positional_argument_test_fixture() = default; - - template - using value_type = typename positional::value_type; - - template - [[nodiscard]] const argument_name& get_name(const positional& arg) const { - return arg.name(); - } - - template - [[nodiscard]] const std::optional& get_help(const positional& arg) const { - return arg._help_msg; - } - - template - [[nodiscard]] argument_descriptor get_desc(const positional& arg, const bool verbose) const { - return arg.desc(verbose); - } - - template - [[nodiscard]] bool is_hidden(const positional& arg) const { - return arg.is_hidden(); - } - - template - [[nodiscard]] bool is_required(const positional& arg) const { - return arg.is_required(); - } - - template - [[nodiscard]] bool is_bypass_required_enabled(const positional& arg) const { - return arg.bypass_required_enabled(); - } - - template - [[nodiscard]] bool is_used(const positional& arg) const { - return arg.is_used(); - } - - template - [[nodiscard]] std::size_t get_count(const positional& arg) const { - return arg.count(); - } - - template - [[nodiscard]] bool has_value(const positional& arg) const { - return arg.has_value(); - } - - template - [[nodiscard]] bool has_parsed_values(const positional& arg) const { - return arg.has_parsed_values(); - } - - template - bool set_required(positional& arg, const bool r) const { - return arg._required = r; - } - - template - bool set_bypass_required(positional& arg, const bool br) const { - return arg._bypass_required = br; - } - - template - bool set_value(positional& arg, const T& value) const { - return arg.set_value(std::to_string(value)); - } - - template - bool set_value(positional& arg, const std::string& str_value) const { - return arg.set_value(str_value); - } - - template - void reset_value(positional& arg) const { - arg._value.reset(); - } - - template - [[nodiscard]] std::weak_ordering nvalues_ordering(const positional& arg) const { - return arg.nvalues_ordering(); - } - - template - [[nodiscard]] const std::any& get_value(const positional& arg) const { - return arg.value(); - } - - template - const std::vector& get_values(const positional& arg) const { - return arg.values(); - } -}; - -} // namespace ap_testing diff --git a/tests/include/utility.hpp b/tests/include/utility.hpp index a1a1d8b6..8553f895 100644 --- a/tests/include/utility.hpp +++ b/tests/include/utility.hpp @@ -1,7 +1,6 @@ #pragma once -#include -#include +#include namespace ap_testing { @@ -12,12 +11,12 @@ void discard_result(T&&) { template bool is_positional(const ap::detail::argument_base& arg) { - return dynamic_cast*>(&arg); + return dynamic_cast*>(&arg); } template bool is_optional(const ap::detail::argument_base& arg) { - return dynamic_cast*>(&arg); + return dynamic_cast*>(&arg); } } // namespace ap_testing diff --git a/tests/source/test_argument_parser_add_argument.cpp b/tests/source/test_argument_parser_add_argument.cpp index cb2a66d0..043b652a 100644 --- a/tests/source/test_argument_parser_add_argument.cpp +++ b/tests/source/test_argument_parser_add_argument.cpp @@ -1,10 +1,11 @@ #include "argument_parser_test_fixture.hpp" +#include "argument_test_fixture.hpp" #include "doctest.h" -#include "optional_argument_test_fixture.hpp" #include "utility.hpp" using namespace ap_testing; -using namespace ap::argument; +using ap::argument; +using ap::default_argument; using ap::invalid_configuration; struct test_argument_parser_add_argument : public argument_parser_test_fixture { @@ -227,7 +228,7 @@ TEST_CASE_FIXTURE( test_argument_parser_add_argument, "add_flag should return an optional argument reference with flag parameters" ) { - const optional_argument_test_fixture opt_arg_fixture; + const argument_test_fixture arg_fixture; SUBCASE("StoreImplicitly = true") { auto& argument = sut.add_flag(primary_name_1, secondary_name_1); @@ -235,7 +236,7 @@ TEST_CASE_FIXTURE( REQUIRE(is_optional(argument)); CHECK_FALSE(sut.value(primary_name_1)); - opt_arg_fixture.mark_used(argument); + arg_fixture.mark_used(argument); CHECK(sut.value(primary_name_1)); } @@ -245,7 +246,7 @@ TEST_CASE_FIXTURE( REQUIRE(is_optional(argument)); CHECK(sut.value(primary_name_1)); - opt_arg_fixture.mark_used(argument); + arg_fixture.mark_used(argument); CHECK_FALSE(sut.value(primary_name_1)); } } @@ -285,9 +286,9 @@ TEST_CASE_FIXTURE( TEST_CASE_FIXTURE( test_argument_parser_add_argument, - "default_positional_arguments should add the specified arguments" + "default_arguments should add the specified positional arguments" ) { - sut.default_positional_arguments({default_positional::input, default_positional::output}); + sut.default_arguments({default_argument::p_input, default_argument::p_output}); const auto input_arg = get_argument("input"); REQUIRE(input_arg); @@ -300,10 +301,10 @@ TEST_CASE_FIXTURE( TEST_CASE_FIXTURE( test_argument_parser_add_argument, - "default_optional_arguments should add the specified arguments" + "default_arguments should add the specified optional arguments" ) { - sut.default_optional_arguments( - {default_optional::help, default_optional::input, default_optional::output} + sut.default_arguments( + {default_argument::o_help, default_argument::o_input, default_argument::o_output} ); std::string help_flag; diff --git a/tests/source/test_argument_parser_parse_args.cpp b/tests/source/test_argument_parser_parse_args.cpp index ae039669..787ee3fa 100644 --- a/tests/source/test_argument_parser_parse_args.cpp +++ b/tests/source/test_argument_parser_parse_args.cpp @@ -3,7 +3,6 @@ #include "utility.hpp" using namespace ap_testing; -using namespace ap::argument; using namespace ap::nargs; using ap::invalid_configuration; using ap::parsing_failure; @@ -881,38 +880,22 @@ TEST_CASE_FIXTURE( } } -// values +// values: optional arguments TEST_CASE_FIXTURE( test_argument_parser_parse_args, - "values() should throw when calling with a positional argument's name" -) { - sut.add_positional_argument(positional_primary_name, positional_secondary_name); - - CHECK_THROWS_AS(discard_result(sut.values(positional_primary_name)), std::logic_error); - CHECK_THROWS_AS(discard_result(sut.values(positional_secondary_name)), std::logic_error); -} - -TEST_CASE_FIXTURE( - test_argument_parser_parse_args, - "values() should return an empty vector if an argument has no values" + "values() [optional arguments] should return an empty vector if an argument has no values" ) { sut.add_optional_argument(optional_primary_name, optional_secondary_name); - SUBCASE("calling with argument's primary name") { - const auto& values = sut.values(optional_primary_name); - CHECK(values.empty()); - } - - SUBCASE("calling with argument's secondary name") { - const auto& values = sut.values(optional_secondary_name); - CHECK(values.empty()); - } + CHECK(sut.values(optional_primary_name).empty()); + CHECK(sut.values(optional_secondary_name).empty()); } TEST_CASE_FIXTURE( test_argument_parser_parse_args, - "values() should throw when an argument has values but the given type is invalid" + "values() [optional arguments] should throw when an argument has values but the given type is " + "invalid" ) { sut.add_optional_argument(optional_primary_name, optional_secondary_name).nargs(at_least(1)); @@ -943,8 +926,8 @@ TEST_CASE_FIXTURE( TEST_CASE_FIXTURE( test_argument_parser_parse_args, - "values() should return a vector containing a predefined value of an optional argument if no " - "values for an argument have been parsed" + "values() [optional arguments] should return a vector containing a predefined value of an " + "argument if no values for an argument have been parsed" ) { const std::string default_value = "default_value"; const std::string implicit_value = "implicit_value"; @@ -987,8 +970,8 @@ TEST_CASE_FIXTURE( TEST_CASE_FIXTURE( test_argument_parser_parse_args, - "values() should return a correct vector of values when there is an argument with " - "a given name and has parsed values" + "values() [optional arguments] should return a correct vector of values when there is an " + "argument with a given name and has parsed values" ) { sut.add_optional_argument(optional_primary_name, optional_secondary_name).nargs(at_least(1)); @@ -1013,8 +996,100 @@ TEST_CASE_FIXTURE( const auto& stored_values = sut.values(optional_primary_name); REQUIRE_EQ(stored_values.size(), optional_arg_values.size()); - for (std::size_t i = 0ull; i < stored_values.size(); ++i) - CHECK_EQ(stored_values[i], optional_arg_values[i]); + CHECK_EQ(stored_values, optional_arg_values); + + free_argv(argc, argv); +} + +// values: positional arguments + +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()); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "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); + + // prepare argc & argv + const int argc = get_argc(1ull, no_args); + auto argv = init_argv(1ull, no_args); + + // parse args + 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 + ); + + free_argv(argc, argv); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "values() [positional arguments] should return a vector containing a predefined value of an " + "argument if no values for an argument have been parsed" +) { + const std::string default_value = "default_value"; + + sut.add_positional_argument(positional_primary_name, positional_secondary_name) + .default_value(default_value); + + // prepare argc & argv + const int argc = get_argc(no_args, no_args); + auto argv = init_argv(no_args, no_args); + + // parse args + sut.parse_args(argc, argv); + + const auto& stored_values = sut.values(positional_primary_name); + + REQUIRE_EQ(stored_values.size(), 1); + CHECK_EQ(stored_values.front(), default_value); + + free_argv(argc, argv); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "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()); + + // prepare argc & argv + const std::size_t n_positional_values = 3ull; + std::vector positional_arg_values; + for (std::size_t i = 0ull; i < n_positional_values; ++i) + positional_arg_values.emplace_back(std::format("positional_value_{}", i + 1ull)); + + std::vector argv_vec{"program"}; + argv_vec.insert(argv_vec.end(), positional_arg_values.begin(), positional_arg_values.end()); + + const int argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + // parse args + sut.parse_args(argc, argv); + + const auto& stored_values = sut.values(positional_primary_name); + + REQUIRE_EQ(stored_values.size(), positional_arg_values.size()); + CHECK_EQ(stored_values, positional_arg_values); free_argv(argc, argv); } diff --git a/tests/source/test_argument_parser_parse_args_unknown_flags_as_values.cpp b/tests/source/test_argument_parser_parse_args_unknown_flags_as_values.cpp index d890c1ac..851848b4 100644 --- a/tests/source/test_argument_parser_parse_args_unknown_flags_as_values.cpp +++ b/tests/source/test_argument_parser_parse_args_unknown_flags_as_values.cpp @@ -3,9 +3,6 @@ #include "utility.hpp" using namespace ap_testing; -using namespace ap::argument; -using namespace ap::nargs; -using ap::invalid_configuration; using ap::parsing_failure; struct test_argument_parser_parse_args_unknown_flags_as_values diff --git a/tests/source/test_argument_token.cpp b/tests/source/test_argument_token.cpp index 3a269b09..b05a9c49 100644 --- a/tests/source/test_argument_token.cpp +++ b/tests/source/test_argument_token.cpp @@ -1,11 +1,11 @@ #include "doctest.h" -#include +#include #include using sut_type = ap::detail::argument_token; using enum sut_type::token_type; -using ap::argument::optional; +using ap::optional_argument; using ap::detail::argument_base; using ap::detail::argument_name; @@ -54,7 +54,8 @@ TEST_CASE("is_valid_flag_token should return true if the token is a flag token a CHECK_FALSE(sut_type{t_flag_primary, ""}.is_valid_flag_token()); CHECK_FALSE(sut_type{t_flag_secondary, ""}.is_valid_flag_token()); - std::unique_ptr arg_ptr = std::make_unique>(argument_name{""}); + std::unique_ptr arg_ptr = + std::make_unique>(argument_name{""}); const typename sut_type::arg_ptr_opt_t arg_ptr_opt{std::ref(arg_ptr)}; CHECK(sut_type{t_flag_primary, "", arg_ptr_opt}.is_valid_flag_token()); diff --git a/tests/source/test_nargs_range.cpp b/tests/source/test_nargs_range.cpp index 365f9a54..c8daeb31 100644 --- a/tests/source/test_nargs_range.cpp +++ b/tests/source/test_nargs_range.cpp @@ -8,53 +8,88 @@ using namespace ap::nargs; namespace { -constexpr range::count_type exact_bound = 1ull; -constexpr range::count_type lower_bound = 3ull; -constexpr range::count_type upper_bound = 9ull; -constexpr range::count_type mid = (lower_bound + upper_bound) / 2ull; - -constexpr range::count_type min_bound = std::numeric_limits::min(); -constexpr range::count_type max_bound = std::numeric_limits::max(); +constexpr count_type exact_bound = 1ull; +constexpr count_type lower_bound = 3ull; +constexpr count_type upper_bound = 9ull; +constexpr count_type mid = (lower_bound + upper_bound) / 2ull; } // namespace -TEST_CASE("is_bound should return true only if at least one bound is set") { - CHECK_FALSE(any().is_bound()); +TEST_CASE("has_explicit_lower_bound should return true only if the lower bound is explicitly set") { + CHECK_FALSE(any().is_explicitly_bound()); + CHECK_FALSE(up_to(upper_bound).has_explicit_lower_bound()); + + CHECK(at_least(lower_bound).has_explicit_lower_bound()); + CHECK(range(exact_bound).has_explicit_lower_bound()); + CHECK(range(lower_bound, upper_bound).has_explicit_lower_bound()); +} + +TEST_CASE("has_explicit_upper_bound should return true only if the upper bound is explicitly set") { + CHECK_FALSE(any().has_explicit_upper_bound()); + CHECK_FALSE(at_least(lower_bound).has_explicit_upper_bound()); + + CHECK(up_to(upper_bound).has_explicit_upper_bound()); + CHECK(range(exact_bound).has_explicit_upper_bound()); + CHECK(range(lower_bound, upper_bound).has_explicit_upper_bound()); +} + +TEST_CASE("is_explicitly_bound should return true only if at least one bound is explicitly set") { + CHECK_FALSE(any().is_explicitly_bound()); + + CHECK(at_least(lower_bound).is_explicitly_bound()); + CHECK(up_to(upper_bound).is_explicitly_bound()); + CHECK(range(exact_bound).is_explicitly_bound()); + CHECK(range(lower_bound, upper_bound).is_explicitly_bound()); +} - CHECK(at_least(lower_bound).is_bound()); - CHECK(up_to(upper_bound).is_bound()); - CHECK(range(exact_bound).is_bound()); - CHECK(range(lower_bound, upper_bound).is_bound()); +TEST_CASE("range instances should be considered equal only if they have the same bounds") { + CHECK_EQ(any(), any()); + CHECK_EQ(at_least(lower_bound), at_least(lower_bound)); + CHECK_EQ(more_than(lower_bound), more_than(lower_bound)); + CHECK_EQ(less_than(upper_bound), less_than(upper_bound)); + CHECK_EQ(up_to(upper_bound), up_to(upper_bound)); + CHECK_EQ(range(lower_bound, upper_bound), range(lower_bound, upper_bound)); + CHECK_EQ(range(exact_bound), range(exact_bound)); + + CHECK_NE(at_least(lower_bound), any()); + CHECK_NE(more_than(lower_bound), any()); + CHECK_NE(less_than(lower_bound), any()); + CHECK_NE(up_to(lower_bound), any()); + CHECK_NE(range(lower_bound, upper_bound), any()); + CHECK_NE(range(lower_bound, upper_bound), range(lower_bound, upper_bound + 1ull)); + CHECK_NE(range(lower_bound, upper_bound), range(lower_bound - 1ull, upper_bound)); + CHECK_NE(range(exact_bound), any()); + CHECK_NE(range(exact_bound), range(lower_bound, upper_bound)); } -TEST_CASE("ordering should return true for default range only when n is 1") { - const auto sut = range(); +TEST_CASE("operator<=> should return eq for default range only when n is 1") { + const auto sut = range(exact_bound); - REQUIRE(std::is_eq(sut.ordering(exact_bound))); + CHECK(std::is_eq(exact_bound <=> sut)); - REQUIRE(std::is_lt(sut.ordering(exact_bound - 1))); - REQUIRE(std::is_gt(sut.ordering(exact_bound + 1))); + CHECK(std::is_lt(exact_bound - 1 <=> sut)); + CHECK(std::is_gt(exact_bound + 1 <=> sut)); } -TEST_CASE("ordering should return true if n is in range") { +TEST_CASE("operator<=> should return eq if n is in range") { SUBCASE("range is [n]") { const auto sut = range(mid); - REQUIRE(std::is_eq(sut.ordering(mid))); + CHECK(std::is_eq(mid <=> sut)); - REQUIRE(std::is_lt(sut.ordering(mid - 1))); - REQUIRE(std::is_gt(sut.ordering(mid + 1))); + CHECK(std::is_lt(mid - 1 <=> sut)); + CHECK(std::is_gt(mid + 1 <=> sut)); } SUBCASE("range is [lower, upper]") { const auto sut = range(lower_bound, upper_bound); - REQUIRE(std::is_eq(sut.ordering(lower_bound))); - REQUIRE(std::is_eq(sut.ordering(upper_bound))); - REQUIRE(std::is_eq(sut.ordering(mid))); + CHECK(std::is_eq(lower_bound <=> sut)); + CHECK(std::is_eq(upper_bound <=> sut)); + CHECK(std::is_eq(mid <=> sut)); - REQUIRE(std::is_lt(sut.ordering(lower_bound - 1))); - REQUIRE(std::is_gt(sut.ordering(upper_bound + 1))); + CHECK(std::is_lt(lower_bound - 1 <=> sut)); + CHECK(std::is_gt(upper_bound + 1 <=> sut)); } } @@ -62,55 +97,55 @@ TEST_CASE("range builders should return correct range objects") { SUBCASE("at_least") { const auto sut = at_least(lower_bound); - REQUIRE(std::is_eq(sut.ordering(lower_bound))); - REQUIRE(std::is_eq(sut.ordering(upper_bound))); - REQUIRE(std::is_eq(sut.ordering(max_bound))); + CHECK(std::is_eq(lower_bound <=> sut)); + CHECK(std::is_eq(upper_bound <=> sut)); + CHECK(std::is_eq(max_bound <=> sut)); - REQUIRE(std::is_lt(sut.ordering(lower_bound - 1))); - REQUIRE(std::is_lt(sut.ordering(min_bound))); + CHECK(std::is_lt(lower_bound - 1 <=> sut)); + CHECK(std::is_lt(min_bound <=> sut)); } SUBCASE("more_than") { const auto sut = more_than(lower_bound); - REQUIRE(std::is_eq(sut.ordering(lower_bound + 1))); - REQUIRE(std::is_eq(sut.ordering(upper_bound))); - REQUIRE(std::is_eq(sut.ordering(max_bound))); + CHECK(std::is_eq(lower_bound + 1 <=> sut)); + CHECK(std::is_eq(upper_bound <=> sut)); + CHECK(std::is_eq(max_bound <=> sut)); - REQUIRE(std::is_lt(sut.ordering(lower_bound))); - REQUIRE(std::is_lt(sut.ordering(min_bound))); + CHECK(std::is_lt(lower_bound <=> sut)); + CHECK(std::is_lt(min_bound <=> sut)); } SUBCASE("less_than") { const auto sut = less_than(upper_bound); - REQUIRE(std::is_eq(sut.ordering(upper_bound - 1))); - REQUIRE(std::is_eq(sut.ordering(lower_bound))); - REQUIRE(std::is_eq(sut.ordering(min_bound))); + CHECK(std::is_eq(upper_bound - 1 <=> sut)); + CHECK(std::is_eq(lower_bound <=> sut)); + CHECK(std::is_eq(min_bound <=> sut)); - REQUIRE(std::is_gt(sut.ordering(upper_bound))); - REQUIRE(std::is_gt(sut.ordering(max_bound))); + CHECK(std::is_gt(upper_bound <=> sut)); + CHECK(std::is_gt(max_bound <=> sut)); } SUBCASE("up_to") { const auto sut = up_to(upper_bound); - REQUIRE(std::is_eq(sut.ordering(upper_bound))); - REQUIRE(std::is_eq(sut.ordering(lower_bound))); - REQUIRE(std::is_eq(sut.ordering(min_bound))); + CHECK(std::is_eq(upper_bound <=> sut)); + CHECK(std::is_eq(lower_bound <=> sut)); + CHECK(std::is_eq(min_bound <=> sut)); - REQUIRE(std::is_gt(sut.ordering(upper_bound + 1))); - REQUIRE(std::is_gt(sut.ordering(max_bound))); + CHECK(std::is_gt(upper_bound + 1 <=> sut)); + CHECK(std::is_gt(max_bound <=> sut)); } SUBCASE("any") { const auto sut = any(); - REQUIRE(std::is_eq(sut.ordering(min_bound))); - REQUIRE(std::is_eq(sut.ordering(exact_bound))); - REQUIRE(std::is_eq(sut.ordering(lower_bound))); - REQUIRE(std::is_eq(sut.ordering(mid))); - REQUIRE(std::is_eq(sut.ordering(upper_bound))); - REQUIRE(std::is_eq(sut.ordering(max_bound))); + CHECK(std::is_eq(min_bound <=> sut)); + CHECK(std::is_eq(exact_bound <=> sut)); + CHECK(std::is_eq(lower_bound <=> sut)); + CHECK(std::is_eq(mid <=> sut)); + CHECK(std::is_eq(upper_bound <=> sut)); + CHECK(std::is_eq(max_bound <=> sut)); } } diff --git a/tests/source/test_none_type_argument.cpp b/tests/source/test_none_type_argument.cpp new file mode 100644 index 00000000..57861c8c --- /dev/null +++ b/tests/source/test_none_type_argument.cpp @@ -0,0 +1,49 @@ +#include "argument_test_fixture.hpp" +#include "doctest.h" + +using namespace ap_testing; + +using ap::optional_argument; +using ap::parsing_failure; +using ap::detail::argument_name; + +namespace { + +constexpr std::string_view primary_name = "test"; +const auto primary_name_opt = std::make_optional(primary_name); + +constexpr std::string_view secondary_name = "t"; +const auto secondary_name_opt = std::make_optional(secondary_name); + +const argument_name arg_name(primary_name_opt, secondary_name_opt); + +using sut_value_type = ap::none_type; +using sut_type = optional_argument; + +const std::string some_value = "some-value"; + +} // namespace + +TEST_CASE_FIXTURE( + argument_test_fixture, "mark_used should always return false (further values not accepted)" +) { + auto sut = sut_type(arg_name); + CHECK_FALSE(mark_used(sut)); +} + +TEST_CASE_FIXTURE( + argument_test_fixture, "set_value should always throw and no values should be stored" +) { + auto sut = sut_type(arg_name); + + CHECK_THROWS_WITH_AS( + set_value(sut, some_value), + std::format( + "Cannot set values for a none-type argument '{}' (value: '{}')", + arg_name.str(), + some_value + ) + .c_str(), + parsing_failure + ); +} diff --git a/tests/source/test_optional_argument.cpp b/tests/source/test_optional_argument.cpp index 749ec1a4..a26f0221 100644 --- a/tests/source/test_optional_argument.cpp +++ b/tests/source/test_optional_argument.cpp @@ -1,13 +1,13 @@ +#include "argument_test_fixture.hpp" #include "doctest.h" -#include "optional_argument_test_fixture.hpp" #include using namespace ap_testing; using namespace ap::nargs; +using ap::optional_argument; using ap::parsing_failure; -using ap::argument::optional; using ap::detail::argument_name; using ap::detail::parameter_descriptor; @@ -28,7 +28,7 @@ constexpr std::string_view help_msg = "test help msg"; using sut_value_type = int; using invalid_value_type = double; -using sut_type = optional; +using sut_type = optional_argument; const std::string empty_str = ""; const std::string invalid_value_str = "invalid_value"; @@ -44,9 +44,7 @@ const range non_default_range = range{1ull, choices.size()}; } // namespace -TEST_CASE_FIXTURE( - optional_argument_test_fixture, "name() should return the proper argument_name instance" -) { +TEST_CASE_FIXTURE(argument_test_fixture, "name() should return the proper argument_name instance") { SUBCASE("initialized with the primary name only") { const auto sut = sut_type(arg_name_primary); const auto name = get_name(sut); @@ -64,14 +62,12 @@ TEST_CASE_FIXTURE( } } -TEST_CASE_FIXTURE(optional_argument_test_fixture, "help() should return nullopt by default") { +TEST_CASE_FIXTURE(argument_test_fixture, "help() should return nullopt by default") { const auto sut = sut_type(arg_name_primary); CHECK_FALSE(get_help(sut)); } -TEST_CASE_FIXTURE( - optional_argument_test_fixture, "help() should return a massage set for the argument" -) { +TEST_CASE_FIXTURE(argument_test_fixture, "help() should return a massage set for the argument") { auto sut = sut_type(arg_name_primary); sut.help(help_msg); @@ -82,8 +78,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - optional_argument_test_fixture, - "desc(verbose=false) should return an argument_descriptor with no params" + argument_test_fixture, "desc(verbose=false) should return an argument_descriptor with no params" ) { constexpr bool verbose = false; @@ -104,7 +99,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - optional_argument_test_fixture, + argument_test_fixture, "desc(verbose=true) should return an argument_descriptor with non-default params" ) { constexpr bool verbose = true; @@ -168,7 +163,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - optional_argument_test_fixture, + argument_test_fixture, "is_hidden() should return false by default or the value passed in the attribute setter" ) { auto sut = sut_type(arg_name_primary); @@ -178,40 +173,41 @@ TEST_CASE_FIXTURE( CHECK(is_hidden(sut)); } -TEST_CASE_FIXTURE(optional_argument_test_fixture, "is_required() should return false by default") { +TEST_CASE_FIXTURE(argument_test_fixture, "is_required() should return false by default") { auto sut = sut_type(arg_name_primary); - CHECK_FALSE(is_required(sut)); + CHECK_FALSE(sut.is_required()); } TEST_CASE_FIXTURE( - optional_argument_test_fixture, + argument_test_fixture, "is_required() should return the value set using the `required` param setter" ) { auto sut = sut_type(arg_name_primary); sut.required(); - CHECK(is_required(sut)); + CHECK(sut.is_required()); sut.required(false); - CHECK_FALSE(is_required(sut)); + CHECK_FALSE(sut.is_required()); } TEST_CASE_FIXTURE( - optional_argument_test_fixture, + argument_test_fixture, "bypass_required() should return the value set using the `bypass_required` param setter" ) { auto sut = sut_type(arg_name_primary); sut.bypass_required(true); - CHECK(is_bypass_required_enabled(sut)); + CHECK(sut.is_bypass_required_enabled()); sut.bypass_required(false); - CHECK_FALSE(is_bypass_required_enabled(sut)); + CHECK_FALSE(sut.is_bypass_required_enabled()); } TEST_CASE_FIXTURE( - optional_argument_test_fixture, - "bypass_required_enabled() should return true only if the `required` flag is set to false and " + argument_test_fixture, + "is_bypass_required_enabled() should return true only if the `required` flag is set to false " + "and " "the `bypass_required` flags is set to true" ) { auto sut = sut_type(arg_name_primary); @@ -219,53 +215,52 @@ TEST_CASE_FIXTURE( // disabled set_required(sut, false); set_bypass_required(sut, false); - CHECK_FALSE(is_bypass_required_enabled(sut)); + CHECK_FALSE(sut.is_bypass_required_enabled()); set_required(sut, true); set_bypass_required(sut, false); - CHECK_FALSE(is_bypass_required_enabled(sut)); + CHECK_FALSE(sut.is_bypass_required_enabled()); set_required(sut, true); set_bypass_required(sut, true); - CHECK_FALSE(is_bypass_required_enabled(sut)); + CHECK_FALSE(sut.is_bypass_required_enabled()); // enabled set_required(sut, false); set_bypass_required(sut, true); - CHECK(is_bypass_required_enabled(sut)); + CHECK(sut.is_bypass_required_enabled()); } TEST_CASE_FIXTURE( - optional_argument_test_fixture, + argument_test_fixture, "required(true) should disable `bypass_required` option and bypass_required(true) should " "disable the `required` option" ) { auto sut = sut_type(arg_name_primary); - REQUIRE_FALSE(is_required(sut)); - REQUIRE_FALSE(is_bypass_required_enabled(sut)); + REQUIRE_FALSE(sut.is_required()); + REQUIRE_FALSE(sut.is_bypass_required_enabled()); sut.bypass_required(); - CHECK(is_bypass_required_enabled(sut)); - CHECK_FALSE(is_required(sut)); + CHECK(sut.is_bypass_required_enabled()); + CHECK_FALSE(sut.is_required()); sut.required(); - CHECK(is_required(sut)); - CHECK_FALSE(is_bypass_required_enabled(sut)); + CHECK(sut.is_required()); + CHECK_FALSE(sut.is_bypass_required_enabled()); sut.bypass_required(); - CHECK(is_bypass_required_enabled(sut)); - CHECK_FALSE(is_required(sut)); + CHECK(sut.is_bypass_required_enabled()); + CHECK_FALSE(sut.is_required()); } -TEST_CASE_FIXTURE(optional_argument_test_fixture, "is_used() should return false by default") { +TEST_CASE_FIXTURE(argument_test_fixture, "is_used() should return false by default") { const auto sut = sut_type(arg_name_primary); CHECK_FALSE(is_used(sut)); } TEST_CASE_FIXTURE( - optional_argument_test_fixture, - "is_used() should return true if the argument's flag has been used" + argument_test_fixture, "is_used() should return true if the argument's flag has been used" ) { auto sut = sut_type(arg_name_primary); REQUIRE_FALSE(is_used(sut)); @@ -274,13 +269,13 @@ TEST_CASE_FIXTURE( CHECK(is_used(sut)); } -TEST_CASE_FIXTURE(optional_argument_test_fixture, "count() should return 0 by default") { +TEST_CASE_FIXTURE(argument_test_fixture, "count() should return 0 by default") { const auto sut = sut_type(arg_name_primary); CHECK_EQ(get_count(sut), 0ull); } TEST_CASE_FIXTURE( - optional_argument_test_fixture, + argument_test_fixture, "is_used() should return the number of times the argument's flag has been used " "[number of mark_used() function calls]" ) { @@ -293,9 +288,7 @@ TEST_CASE_FIXTURE( CHECK_EQ(get_count(sut), count); } -TEST_CASE_FIXTURE( - optional_argument_test_fixture, "argument flag usage should trigger the on-flag actions" -) { +TEST_CASE_FIXTURE(argument_test_fixture, "argument flag usage should trigger the on-flag actions") { auto sut = sut_type(arg_name_primary); const auto throw_action = []() { throw std::runtime_error("no reason"); }; @@ -304,19 +297,19 @@ TEST_CASE_FIXTURE( CHECK_THROWS_AS(mark_used(sut), std::runtime_error); } -TEST_CASE_FIXTURE(optional_argument_test_fixture, "has_value() should return false by default") { +TEST_CASE_FIXTURE(argument_test_fixture, "has_value() should return false by default") { const auto sut = sut_type(arg_name_primary); CHECK_FALSE(has_value(sut)); } -TEST_CASE_FIXTURE(optional_argument_test_fixture, "has_value() should return true if value is set") { +TEST_CASE_FIXTURE(argument_test_fixture, "has_value() should return true if value is set") { auto sut = sut_type(arg_name_primary); set_value(sut, arbitrary_value); CHECK(has_value(sut)); } TEST_CASE_FIXTURE( - optional_argument_test_fixture, + argument_test_fixture, "has_value() should return true if a default value is set and value() should return the " "default value" ) { @@ -328,7 +321,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - optional_argument_test_fixture, + argument_test_fixture, "has_value() should return false if only the implicit value is set but the argument is not used" ) { auto sut = sut_type(arg_name_primary); @@ -338,7 +331,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - optional_argument_test_fixture, + argument_test_fixture, "has_value() should return true if the implicit value is set and the agument is used" ) { auto sut = sut_type(arg_name_primary); @@ -350,15 +343,13 @@ TEST_CASE_FIXTURE( CHECK_EQ(std::any_cast(get_value(sut)), implicit_value); } -TEST_CASE_FIXTURE( - optional_argument_test_fixture, "has_parsed_values() should return false by default" -) { +TEST_CASE_FIXTURE(argument_test_fixture, "has_parsed_values() should return false by default") { const auto sut = sut_type(arg_name_primary); CHECK_FALSE(has_parsed_values(sut)); } TEST_CASE_FIXTURE( - optional_argument_test_fixture, + argument_test_fixture, "has_parsed_values() should return false regardles of the " "default_value and implicit_value parameters" ) { @@ -381,35 +372,72 @@ TEST_CASE_FIXTURE( } } -TEST_CASE_FIXTURE( - optional_argument_test_fixture, "has_parsed_values() should true if the value is set" -) { +TEST_CASE_FIXTURE(argument_test_fixture, "has_parsed_values() should true if the value is set") { auto sut = sut_type(arg_name_primary); set_value(sut, arbitrary_value); CHECK(has_parsed_values(sut)); } +TEST_CASE_FIXTURE(argument_test_fixture, "has_predefined_values() should return false by default") { + const auto sut = sut_type(arg_name_primary); + CHECK_FALSE(has_predefined_values(sut)); +} + TEST_CASE_FIXTURE( - optional_argument_test_fixture, "value() should throw if the argument's value has not been set" + argument_test_fixture, "has_predefined_values() should return true if the default value is set" ) { auto sut = sut_type(arg_name_primary); + sut.default_value(default_value); - REQUIRE_FALSE(has_value(sut)); - CHECK_THROWS_AS(static_cast(get_value(sut)), std::logic_error); + CHECK(has_predefined_values(sut)); } TEST_CASE_FIXTURE( - optional_argument_test_fixture, "value() should return the argument's value if it has been set" + argument_test_fixture, + "has_predefined_values() should return false if the implicit value is set and the argument is " + "not used" ) { auto sut = sut_type(arg_name_primary); - set_value(sut, arbitrary_value); + sut.implicit_value(implicit_value); - REQUIRE(has_value(sut)); - CHECK_EQ(std::any_cast(get_value(sut)), arbitrary_value); + CHECK_FALSE(has_predefined_values(sut)); +} + +TEST_CASE_FIXTURE( + argument_test_fixture, + "has_predefined_values() should return true if the implicit value is set and the argument is " + "used" +) { + auto sut = sut_type(arg_name_primary); + sut.implicit_value(implicit_value); + + mark_used(sut); + + CHECK(has_predefined_values(sut)); +} + +TEST_CASE_FIXTURE( + argument_test_fixture, + "has_predefined_values() should return true if both the default and implicit values are set" +) { + auto sut = sut_type(arg_name_primary); + sut.default_value(default_value); + sut.implicit_value(implicit_value); + + CHECK(has_predefined_values(sut)); +} + +TEST_CASE_FIXTURE( + argument_test_fixture, "value() should throw if the argument's value has not been set" +) { + auto sut = sut_type(arg_name_primary); + + REQUIRE_FALSE(has_value(sut)); + CHECK_THROWS_AS(static_cast(get_value(sut)), std::logic_error); } TEST_CASE_FIXTURE( - optional_argument_test_fixture, + argument_test_fixture, "value() should return the default value if one has been provided and argument is not used" ) { auto sut = sut_type(arg_name_primary); @@ -420,7 +448,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - optional_argument_test_fixture, + argument_test_fixture, "value() should return the implicit value if one has been provided and argument is used" ) { auto sut = sut_type(arg_name_primary); @@ -433,7 +461,19 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - optional_argument_test_fixture, + argument_test_fixture, "value() should return the argument's value if it has been set" +) { + auto sut = sut_type(arg_name_primary); + sut.default_value(default_value); + sut.implicit_value(implicit_value); + set_value(sut, arbitrary_value); + + REQUIRE(has_value(sut)); + CHECK_EQ(std::any_cast(get_value(sut)), arbitrary_value); +} + +TEST_CASE_FIXTURE( + argument_test_fixture, "set_value(any) should throw when the given string cannot be converted to an instance of " "value_type" ) { @@ -458,7 +498,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - optional_argument_test_fixture, + argument_test_fixture, "set_value(any) should throw when the choices set does not contain the parsed value" ) { auto sut = sut_type(arg_name_primary); @@ -473,33 +513,19 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - optional_argument_test_fixture, "set_value(any) should accept any number of values by default" + argument_test_fixture, + "set_value(any) should throw when adding the given value would result in exceeding the maximum " + "number of values specified by nargs" ) { - auto sut = sut_type(arg_name_primary); - sut.nargs(non_default_range); + auto sut = sut_type(arg_name_primary).nargs(non_default_range); - for (const auto value : choices) { + for (const auto value : choices) REQUIRE_NOTHROW(set_value(sut, value)); - } const auto stored_values = get_values(sut); - REQUIRE_EQ(stored_values.size(), choices.size()); - for (std::size_t i = 0; i < stored_values.size(); ++i) { + for (std::size_t i = 0; i < stored_values.size(); ++i) REQUIRE_EQ(std::any_cast(stored_values[i]), choices[i]); - } -} - -TEST_CASE_FIXTURE( - optional_argument_test_fixture, - "set_value(any) should throw when adding the given value would result in exceeding the maximum " - "number of values specified by nargs" -) { - auto sut = sut_type(arg_name_primary).nargs(non_default_range); - - for (const auto value : choices) { - REQUIRE_NOTHROW(set_value(sut, value)); - } CHECK_THROWS_WITH_AS( set_value(sut, arbitrary_value), @@ -509,7 +535,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - optional_argument_test_fixture, "set_value(any) should perform the specified value action" + argument_test_fixture, "set_value(any) should perform the specified value action" ) { auto sut = sut_type(arg_name_primary); @@ -552,15 +578,14 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - optional_argument_test_fixture, - "nvalues_ordering() should return equivalent for default nargs (any)" + argument_test_fixture, "nvalues_ordering() should return equivalent for default nargs (any)" ) { const auto sut = sut_type(arg_name_primary); CHECK(std::is_eq(nvalues_ordering(sut))); } TEST_CASE_FIXTURE( - optional_argument_test_fixture, + argument_test_fixture, "nvalues_ordering() should return equivalent if a default value has been set" ) { auto sut = sut_type(arg_name_primary); @@ -572,7 +597,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - optional_argument_test_fixture, + argument_test_fixture, "nvalues_ordering() should return equivalent only when the number of values " "is in the specified range" ) { diff --git a/tests/source/test_positional_argument.cpp b/tests/source/test_positional_argument.cpp index 70d76a41..3aceaf01 100644 --- a/tests/source/test_positional_argument.cpp +++ b/tests/source/test_positional_argument.cpp @@ -1,10 +1,12 @@ +#include "argument_test_fixture.hpp" #include "doctest.h" -#include "positional_argument_test_fixture.hpp" +#include "utility.hpp" using namespace ap_testing; +using namespace ap::nargs; using ap::parsing_failure; -using ap::argument::positional; +using ap::positional_argument; using ap::detail::argument_name; using ap::detail::parameter_descriptor; @@ -22,23 +24,22 @@ const argument_name arg_name(primary_name_opt, secondary_name_opt); const argument_name arg_name_primary(primary_name_opt, std::nullopt); using sut_value_type = int; -using sut_type = positional; +using sut_type = positional_argument; const std::string empty_str = ""; const std::string invalid_value_str = "invalid value"; constexpr sut_value_type default_value = 0; -constexpr sut_value_type value_1 = 1; -constexpr sut_value_type value_2 = 2; +constexpr sut_value_type valid_value = 1; const std::vector choices{1, 2, 3}; constexpr sut_value_type invalid_choice = 4; +const range non_default_range = range{1ull, choices.size()}; + } // namespace -TEST_CASE_FIXTURE( - positional_argument_test_fixture, "name() should return the proper argument_name instance" -) { +TEST_CASE_FIXTURE(argument_test_fixture, "name() should return the proper argument_name instance") { SUBCASE("initialized with the primary name only") { const auto sut = sut_type(arg_name_primary); const auto name = get_name(sut); @@ -56,14 +57,12 @@ TEST_CASE_FIXTURE( } } -TEST_CASE_FIXTURE(positional_argument_test_fixture, "help() should return nullopt by default") { +TEST_CASE_FIXTURE(argument_test_fixture, "help() should return nullopt by default") { const auto sut = sut_type(arg_name_primary); CHECK_FALSE(get_help(sut)); } -TEST_CASE_FIXTURE( - positional_argument_test_fixture, "help() should return a massage set for the argument" -) { +TEST_CASE_FIXTURE(argument_test_fixture, "help() should return a massage set for the argument") { auto sut = sut_type(arg_name_primary); sut.help(help_msg); @@ -74,8 +73,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - positional_argument_test_fixture, - "desc(verbose=false) should return an argument_descriptor with no params" + argument_test_fixture, "desc(verbose=false) should return an argument_descriptor with no params" ) { constexpr bool verbose = false; @@ -96,7 +94,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - positional_argument_test_fixture, + argument_test_fixture, "desc(verbose=true) should return an argument_descriptor with non-default params" ) { constexpr bool verbose = true; @@ -118,6 +116,7 @@ TEST_CASE_FIXTURE( // other parameters sut.bypass_required(); + sut.nargs(non_default_range); sut.choices(choices); sut.default_value(default_value); @@ -135,6 +134,10 @@ TEST_CASE_FIXTURE( REQUIRE_NE(required_it, desc.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()); + CHECK_EQ(nargs_it->value, ap::detail::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()); CHECK_EQ(choices_it->value, ap::detail::join(choices, ", ")); @@ -146,50 +149,51 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - positional_argument_test_fixture, + argument_test_fixture, "is_hidden() should return false by default or the value passed in the attribute setter" ) { auto sut = sut_type(arg_name_primary); - REQUIRE_FALSE(is_hidden(sut)); + REQUIRE_FALSE(sut.is_hidden()); sut.hidden(); - CHECK(is_hidden(sut)); + CHECK(sut.is_hidden()); } -TEST_CASE_FIXTURE(positional_argument_test_fixture, "is_required() should return true by default") { +TEST_CASE_FIXTURE(argument_test_fixture, "is_required() should return true by default") { auto sut = sut_type(arg_name_primary); - CHECK(is_required(sut)); + CHECK(sut.is_required()); } TEST_CASE_FIXTURE( - positional_argument_test_fixture, + argument_test_fixture, "is_required() should return the value set using the `required` param setter" ) { auto sut = sut_type(arg_name_primary); sut.required(false); - CHECK_FALSE(is_required(sut)); + CHECK_FALSE(sut.is_required()); sut.required(); - CHECK(is_required(sut)); + CHECK(sut.is_required()); } TEST_CASE_FIXTURE( - positional_argument_test_fixture, + argument_test_fixture, "bypass_required() should return the value set using the `bypass_required` param setter" ) { auto sut = sut_type(arg_name_primary); sut.bypass_required(true); - CHECK(is_bypass_required_enabled(sut)); + CHECK(sut.is_bypass_required_enabled()); sut.bypass_required(false); - CHECK_FALSE(is_bypass_required_enabled(sut)); + CHECK_FALSE(sut.is_bypass_required_enabled()); } TEST_CASE_FIXTURE( - positional_argument_test_fixture, - "bypass_required_enabled() should return true only if the `required` flag is set to false and " + argument_test_fixture, + "is_bypass_required_enabled() should return true only if the `required` flag is set to false " + "and " "the `bypass_required` flags is set to true" ) { auto sut = sut_type(arg_name_primary); @@ -197,86 +201,82 @@ TEST_CASE_FIXTURE( // disabled set_required(sut, false); set_bypass_required(sut, false); - CHECK_FALSE(is_bypass_required_enabled(sut)); + CHECK_FALSE(sut.is_bypass_required_enabled()); set_required(sut, true); set_bypass_required(sut, false); - CHECK_FALSE(is_bypass_required_enabled(sut)); + CHECK_FALSE(sut.is_bypass_required_enabled()); set_required(sut, true); set_bypass_required(sut, true); - CHECK_FALSE(is_bypass_required_enabled(sut)); + CHECK_FALSE(sut.is_bypass_required_enabled()); // enabled set_required(sut, false); set_bypass_required(sut, true); - CHECK(is_bypass_required_enabled(sut)); + CHECK(sut.is_bypass_required_enabled()); } TEST_CASE_FIXTURE( - positional_argument_test_fixture, + argument_test_fixture, "required(true) should disable `bypass_required` option and bypass_required(true) should " "disable the `required` option" ) { auto sut = sut_type(arg_name_primary); - REQUIRE(is_required(sut)); - REQUIRE_FALSE(is_bypass_required_enabled(sut)); + REQUIRE(sut.is_required()); + REQUIRE_FALSE(sut.is_bypass_required_enabled()); sut.bypass_required(); - CHECK(is_bypass_required_enabled(sut)); - CHECK_FALSE(is_required(sut)); + CHECK(sut.is_bypass_required_enabled()); + CHECK_FALSE(sut.is_required()); sut.required(); - CHECK(is_required(sut)); - CHECK_FALSE(is_bypass_required_enabled(sut)); + CHECK(sut.is_required()); + CHECK_FALSE(sut.is_bypass_required_enabled()); } -TEST_CASE_FIXTURE(positional_argument_test_fixture, "is_used() should return false by default") { +TEST_CASE_FIXTURE(argument_test_fixture, "is_used() should return false by default") { const auto sut = sut_type(arg_name_primary); CHECK_FALSE(is_used(sut)); } TEST_CASE_FIXTURE( - positional_argument_test_fixture, "is_used() should return true when argument contains a value" + argument_test_fixture, "is_used() should return true when argument contains a value" ) { auto sut = sut_type(arg_name_primary); REQUIRE_FALSE(is_used(sut)); - set_value(sut, value_1); + set_value(sut, valid_value); CHECK(is_used(sut)); } -TEST_CASE_FIXTURE(positional_argument_test_fixture, "count() should return 0 by default") { +TEST_CASE_FIXTURE(argument_test_fixture, "count() should return 0 by default") { const auto sut = sut_type(arg_name_primary); CHECK_EQ(get_count(sut), 0ull); } -TEST_CASE_FIXTURE( - positional_argument_test_fixture, "count() should return 1 when argument contains a value" -) { +TEST_CASE_FIXTURE(argument_test_fixture, "count() should return 1 when argument contains a value") { auto sut = sut_type(arg_name_primary); - set_value(sut, value_1); + set_value(sut, valid_value); CHECK_EQ(get_count(sut), 1ull); } -TEST_CASE_FIXTURE(positional_argument_test_fixture, "has_value() should return false by default") { +TEST_CASE_FIXTURE(argument_test_fixture, "has_value() should return false by default") { const auto sut = sut_type(arg_name_primary); CHECK_FALSE(has_value(sut)); } -TEST_CASE_FIXTURE( - positional_argument_test_fixture, "has_value() should return true if the value is set" -) { +TEST_CASE_FIXTURE(argument_test_fixture, "has_value() should return true if the value is set") { auto sut = sut_type(arg_name_primary); - set_value(sut, value_1); + set_value(sut, valid_value); CHECK(has_value(sut)); } TEST_CASE_FIXTURE( - positional_argument_test_fixture, "has_value() should return true if the default value is set" + argument_test_fixture, "has_value() should return true if the default value is set" ) { auto sut = sut_type(arg_name_primary); sut.default_value(default_value); @@ -284,16 +284,14 @@ TEST_CASE_FIXTURE( CHECK(has_value(sut)); } -TEST_CASE_FIXTURE( - positional_argument_test_fixture, "has_parsed_values() should return false by default" -) { +TEST_CASE_FIXTURE(argument_test_fixture, "has_parsed_values() should return false by default") { const auto sut = sut_type(arg_name_primary); CHECK_FALSE(has_parsed_values(sut)); } TEST_CASE_FIXTURE( - positional_argument_test_fixture, - "has_parsed_values() should false if only the default value is set" + argument_test_fixture, + "has_parsed_values() should return false regardless of the default value parameter" ) { auto sut = sut_type(arg_name_primary); sut.default_value(default_value); @@ -301,33 +299,71 @@ TEST_CASE_FIXTURE( CHECK_FALSE(has_parsed_values(sut)); } +TEST_CASE_FIXTURE(argument_test_fixture, "has_parsed_values() should true if the value is set") { + auto sut = sut_type(arg_name_primary); + set_value(sut, valid_value); + + CHECK(has_parsed_values(sut)); +} + +TEST_CASE_FIXTURE(argument_test_fixture, "has_predefined_values() should return false by default") { + const auto sut = sut_type(arg_name_primary); + CHECK_FALSE(has_predefined_values(sut)); +} + TEST_CASE_FIXTURE( - positional_argument_test_fixture, "has_parsed_values() should true if the value is set" + argument_test_fixture, "has_predefined_values() should return true if the default value is set" ) { auto sut = sut_type(arg_name_primary); - set_value(sut, value_1); + sut.default_value(default_value); - CHECK(has_parsed_values(sut)); + CHECK(has_predefined_values(sut)); +} + +TEST_CASE_FIXTURE( + argument_test_fixture, "value() should throw if the argument's value has not been set" +) { + auto sut = sut_type(arg_name_primary); + + REQUIRE_FALSE(has_value(sut)); + CHECK_THROWS_AS(static_cast(get_value(sut)), std::logic_error); } TEST_CASE_FIXTURE( - positional_argument_test_fixture, - "set_value(any) should throw when a value has already been set" + argument_test_fixture, "value() should return the argument's value if it has been set" ) { auto sut = sut_type(arg_name_primary); + set_value(sut, valid_value); - REQUIRE_NOTHROW(set_value(sut, value_1)); REQUIRE(has_value(sut)); + CHECK_EQ(std::any_cast(get_value(sut)), valid_value); +} - CHECK_THROWS_WITH_AS( - set_value(sut, value_2), - parsing_failure::value_already_set(arg_name_primary).what(), - parsing_failure - ); +TEST_CASE_FIXTURE( + argument_test_fixture, + "value() should return the default argument's default value if it has been set and no values " + "were parsed" +) { + auto sut = sut_type(arg_name_primary); + sut.default_value(default_value); + + REQUIRE(has_value(sut)); + CHECK_EQ(std::any_cast(get_value(sut)), default_value); } TEST_CASE_FIXTURE( - positional_argument_test_fixture, + argument_test_fixture, "value() should return the argument's parsed value if it has been set" +) { + auto sut = sut_type(arg_name_primary); + sut.default_value(default_value); + set_value(sut, valid_value); + + REQUIRE(has_value(sut)); + CHECK_EQ(std::any_cast(get_value(sut)), valid_value); +} + +TEST_CASE_FIXTURE( + argument_test_fixture, "set_value(any) should throw when the given string cannot be converted to an instance of " "value_type" ) { @@ -353,7 +389,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - positional_argument_test_fixture, + argument_test_fixture, "set_value(any) should throw when the choices set does not contain the parsed value" ) { auto sut = sut_type(arg_name_primary); @@ -368,26 +404,28 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - positional_argument_test_fixture, - "set_value(any) should accept the given value only when no value has been set yet " - "and if the given value is present in the choices set" + argument_test_fixture, + "set_value(any) should throw when adding the given value would result in exceeding the maximum " + "number of values specified by nargs" ) { - auto sut = sut_type(arg_name_primary); - sut.choices(choices); - - for (const sut_value_type& value : choices) { - reset_value(sut); - REQUIRE_FALSE(has_value(sut)); + auto sut = sut_type(arg_name_primary).nargs(non_default_range); + for (const auto value : choices) REQUIRE_NOTHROW(set_value(sut, value)); - REQUIRE(has_value(sut)); - CHECK_EQ(std::any_cast(get_value(sut)), value); - } + + const auto stored_values = get_values(sut); + REQUIRE_EQ(stored_values.size(), choices.size()); + for (std::size_t i = 0; i < stored_values.size(); ++i) + REQUIRE_EQ(std::any_cast(stored_values[i]), choices[i]); + + CHECK_THROWS_WITH_AS( + set_value(sut, valid_value), + parsing_failure::invalid_nvalues(arg_name_primary, std::weak_ordering::greater).what(), + parsing_failure + ); } -TEST_CASE_FIXTURE( - positional_argument_test_fixture, "set_value(any) should perform the specified action" -) { +TEST_CASE_FIXTURE(argument_test_fixture, "set_value(any) should perform the specified action") { auto sut = sut_type(arg_name_primary); SUBCASE("observe action") { @@ -410,16 +448,16 @@ TEST_CASE_FIXTURE( const auto double_action = [](const sut_value_type& value) { return 2 * value; }; sut.action(double_action); - set_value(sut, value_1); + set_value(sut, valid_value); - CHECK_EQ(std::any_cast(get_value(sut)), double_action(value_1)); + CHECK_EQ(std::any_cast(get_value(sut)), double_action(valid_value)); } SUBCASE("modify action") { const auto double_action = [](sut_value_type& value) { value *= 2; }; sut.action(double_action); - auto test_value = value_1; + auto test_value = valid_value; set_value(sut, test_value); @@ -429,69 +467,39 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - positional_argument_test_fixture, - "value() should throw if the argument's value has not been set" + argument_test_fixture, "nvalues_ordering() should return less for default nargs (1)" ) { - auto sut = sut_type(arg_name_primary); - - REQUIRE_FALSE(has_value(sut)); - CHECK_THROWS_AS(static_cast(get_value(sut)), std::logic_error); + const auto sut = sut_type(arg_name_primary); + CHECK(std::is_lt(nvalues_ordering(sut))); } TEST_CASE_FIXTURE( - positional_argument_test_fixture, - "value() should return the default argument's value if it has been set and no values were " - "parsed" + argument_test_fixture, + "nvalues_ordering() should return equivalent if a default value has been set" ) { auto sut = sut_type(arg_name_primary); - sut.default_value(default_value); - - REQUIRE(has_value(sut)); - CHECK_EQ(std::any_cast(get_value(sut)), default_value); -} + sut.nargs(non_default_range); -TEST_CASE_FIXTURE( - positional_argument_test_fixture, - "value() should return the argument's value if it has been set" -) { - auto sut = sut_type(arg_name_primary); - set_value(sut, value_1); + sut.default_value(default_value); - REQUIRE(has_value(sut)); - CHECK_EQ(std::any_cast(get_value(sut)), value_1); + CHECK(std::is_eq(nvalues_ordering(sut))); } TEST_CASE_FIXTURE( - positional_argument_test_fixture, - "value() should return the argument's parsed value if it has been set (with a defined default " - "value)" + argument_test_fixture, + "nvalues_ordering() should return equivalent only when the number of values " + "is in the specified range" ) { auto sut = sut_type(arg_name_primary); - sut.default_value(default_value); - set_value(sut, value_1); + sut.nargs(non_default_range); - REQUIRE(has_value(sut)); - CHECK_EQ(std::any_cast(get_value(sut)), value_1); -} - -TEST_CASE_FIXTURE(positional_argument_test_fixture, "values() should throw logic_error") { - auto sut = sut_type(arg_name_primary); - CHECK_THROWS_AS(get_values(sut), std::logic_error); -} + REQUIRE(std::is_lt(nvalues_ordering(sut))); -TEST_CASE_FIXTURE( - positional_argument_test_fixture, "nvalues_ordering() should return less by default" -) { - const auto sut = sut_type(arg_name_primary); - CHECK(std::is_lt(nvalues_ordering(sut))); -} - -TEST_CASE_FIXTURE( - positional_argument_test_fixture, - "nvalues_ordering() should return equivalent if a value has been set" -) { - auto sut = sut_type(arg_name_primary); - set_value(sut, value_1); + for (const auto value : choices) { + REQUIRE_NOTHROW(set_value(sut, value)); + CHECK(std::is_eq(nvalues_ordering(sut))); + } - CHECK(std::is_eq(nvalues_ordering(sut))); + set_value_force(sut, invalid_choice); + CHECK(std::is_gt(nvalues_ordering(sut))); } From f25e12427732cd14d6d54601e3c3a484fde238ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Musia=C5=82?= <111433005+SpectraL519@users.noreply.github.com> Date: Thu, 18 Sep 2025 13:25:01 +0200 Subject: [PATCH 03/10] YT-CPPAP-58: Add predefined value collections support - Changed the predefined (default/implicit) value parameter types in the `argument` class to `std::vector` - Replaced the current `default_value` and `implicit_value` setter functions with new `default_values` and `implicit_values` functions which accept: - A variadic number of arguments - A `std::ranges::range` type with the value type convertible to the argument's value type - Added a `choices` setter function overload which takes a variadic number of arguments - Add a `default_arguments` function overload which takes a variadic number of arguments --- CMakeLists.txt | 2 +- Doxyfile | 2 +- MODULE.bazel | 2 +- cpp-ap-demo | 2 +- docs/tutorial.md | 59 ++--- include/ap/argument.hpp | 171 +++++++++++---- include/ap/argument_parser.hpp | 51 +++-- include/ap/detail/ranges_utility.hpp | 23 ++ .../test_argument_parser_add_argument.cpp | 2 +- .../test_argument_parser_parse_args.cpp | 206 +++++++++--------- tests/source/test_optional_argument.cpp | 50 ++--- tests/source/test_positional_argument.cpp | 16 +- 12 files changed, 362 insertions(+), 224 deletions(-) create mode 100644 include/ap/detail/ranges_utility.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d02740bb..6ba0d97f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ else() endif() project(cpp-ap - VERSION 3.0.0.1 + VERSION 3.0.0.2 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 e91c4729..ef9c9ce0 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.1 +PROJECT_NUMBER = 3.0.0.2 # 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 64894bff..8bf53d10 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,4 +1,4 @@ module( name = "cpp-ap", - version = "3.0.0.1", + version = "3.0.0.2", ) diff --git a/cpp-ap-demo b/cpp-ap-demo index 9cd984da..1c50d9e0 160000 --- a/cpp-ap-demo +++ b/cpp-ap-demo @@ -1 +1 @@ -Subproject commit 9cd984da2520f9115479afd39d52b7e6065c94b5 +Subproject commit 1c50d9e003ab9f14aafac5afd389d6580d898a21 diff --git a/docs/tutorial.md b/docs/tutorial.md index f4c98793..55aa79de 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -189,8 +189,8 @@ parser.add__argument("argument", "a"); > - [nargs](#5-nargs---sets-the-allowed-number-of-values-to-be-parsed-for-an-argument-this-can-be-set-as-a) > - [choices](#6-choices---a-list-of-valid-argument-values) > - [value actions](#7-value-actions---function-performed-after-parsing-an-arguments-value) -> - [default_value](#8-default_value---the-default-value-for-an-argument-which-will-be-used-if-no-values-for-this-argument-are-parsed) -> - [implicit_value](#2-implicit_value---a-value-which-will-be-set-for-an-argument-if-only-its-flag-is-parsed-from-the-command-line-but-no-values-follow) +> - [default_values](#8-default_values---a-list-of-values-which-will-be-used-if-no-values-for-an-argument-have-been-parsed) +> - [implicit_values](#2-implicit_values---a-list-of-values-which-will-be-set-for-an-argument-if-only-its-flag-but-no-values-are-parsed-from-the-command-line) You can also add boolean flags: @@ -198,8 +198,8 @@ You can also add boolean flags: parser.add_flag("enable_some_option", "eso").help("enables option: some option"); /* equivalent to: parser.add_optional_argument("enable_some_option", "eso") - .default_value(false) - .implicit_value(true) + .default_values(false) + .implicit_values(true) .nargs(0) .help("enables option: some option"); */ @@ -211,8 +211,8 @@ Boolean flags store `true` by default but you can specify whether the flag shoul parser.add_flag("disable_another_option", "dao").help("disables option: another option"); /* equivalent to: parser.add_optional_argument("disable_another_option", "dao") - .default_value(true) - .implicit_value(false) + .default_values(true) + .implicit_values(false) .nargs(0) .help("disables option: another option"); */ @@ -244,7 +244,7 @@ By default all arguments are visible, but this can be modified using the `hidden ```cpp parser.program_name("hidden-test") .program_description("A simple program") - .default_arguments({ap::default_argument::o_help}); + .default_arguments(ap::default_argument::o_help); parser.add_optional_argument("hidden") .hidden() @@ -416,16 +416,18 @@ os << data << std::endl; #### 6. `choices` - A list of valid argument values. -The `choices` parameter takes as an argument an instance of `std::initializer_list` or any `std::ranges::range` type such that its value type is convertible to the argument's `value_type`. - ```cpp -parser.add_optional_argument("method", "m").choices({'a', 'b', 'c'}); +parser.add_optional_argument("method", "m").choices('a', 'b', 'c'); +// equivalent to: parser.add_optional_argument("method", "m").choices({'a', 'b', 'c'}); // passing a value other than a, b or c for the `method` argument will result in an error ``` > [!IMPORTANT] > -> The `choices` function can be used only if the argument's `value_type` is equality comparable (defines the `==` operator). +> - The `choices` function can be used only if the argument's `value_type` is equality comparable (defines the `==` operator) +> - The `choices` function can be called with: +> - A variadic number of values [convertible to](https://en.cppreference.com/w/cpp/concepts/convertible_to.html) the argument's value type +> - An arbitrary [`std::ranges::range`](https://en.cppreference.com/w/cpp/ranges/range.html) type with a value type [convertible to](https://en.cppreference.com/w/cpp/concepts/convertible_to.html) the argument's value type
@@ -474,16 +476,16 @@ Actions are represented as functions, which take the argument's value as an argu
-#### 8. `default_value` - The default value for an argument which will be used if no values for this argument are parsed +#### 8. `default_values` - A list of values which will be used if no values for an argument have been parsed > [!WARNING] > -> For both positional and optional arguments, setting the `default_value` parameter disables the `required` option. +> For both positional and optional arguments, setting the `default_values` parameter disables the `required` option. ```cpp // example: positional arguments parser.add_positional_argument("input"); -parser.add_positional_argument("output").default_value("output.txt"); +parser.add_positional_argument("output").default_values("output.txt"); parser.parse_args(argc, argv); @@ -505,7 +507,7 @@ Command Result ```cpp // example: optional arguments parser.add_optional_argument("input", "i").required(); -parser.add_optional_argument("output", "o").default_value("output.txt"); +parser.add_optional_argument("output", "o").default_values("output.txt"); parser.parse_args(argc, argv); @@ -524,9 +526,11 @@ Command Result ./program -i input.txt -o myfile.txt Parsing success; Printing data to the `myfile.txt` file ``` -> [!TIP] +> [!NOTE] > -> The setter of the `default_value` parameter accepts any type that is convertible to the argument's value type. +> The `default_values` function can be called with: +> - A variadic number of values [convertible to](https://en.cppreference.com/w/cpp/concepts/convertible_to.html) the argument's value type +> - An arbitrary [`std::ranges::range`](https://en.cppreference.com/w/cpp/ranges/range.html) type with a value type [convertible to](https://en.cppreference.com/w/cpp/concepts/convertible_to.html) the argument's value type

@@ -555,11 +559,11 @@ Here the `print_debug_info` function will be called right after parsing the `--d
-#### 2. `implicit_value` - A value which will be set for an argument if only it's flag is parsed from the command-line but no values follow. +#### 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. ```cpp // example -parser.add_optional_argument("save", "s").implicit_value("output.txt"); +parser.add_optional_argument("save", "s").implicit_values("output.txt"); parser.parse_args(argc, argv); @@ -579,10 +583,15 @@ Command Result ./program --save myfile.txt The data will be saved to `myfile.txt` ``` +> [!NOTE] +> +> The `implicit_values` function can be called with: +> - A variadic number of values [convertible to](https://en.cppreference.com/w/cpp/concepts/convertible_to.html) the argument's value type +> - An arbitrary [`std::ranges::range`](https://en.cppreference.com/w/cpp/ranges/range.html) type with a value type [convertible to](https://en.cppreference.com/w/cpp/concepts/convertible_to.html) the argument's value type + > [!TIP] > -> - The `implicit_value` parameter is extremely useful when combined with default value (e.g. in case of boolean flags - see [Adding Arguments](#adding-arguments)). -> - The setter of the `implicit_value` parameter accepts any type that is convertible to the argument's value type. +> The `implicit_values` parameter is extremely useful when combined with default value (e.g. in case of boolean flags - see [Adding Arguments](#adding-arguments)).

@@ -673,9 +682,9 @@ parser.default_arguments(); > [!NOTE] > -> - The `default_arguments` function takes as parameter (``) either a `std::initializer_list` or a type satisfying the [`std::ranges::range`](https://en.cppreference.com/w/cpp/ranges/range.html) concept with the `ap::default_argument` value type. - -The available default arguments are: +> The `default_arguments` function can be called with: +> - A variadic number of `ap::default_argument` values +> - An arbitrary [`std::ranges::range`](https://en.cppreference.com/w/cpp/ranges/range.html) type with the `ap::default_argument` value type - `p_input`: @@ -789,7 +798,7 @@ int main(int argc, char* argv[]) { 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}); + parser.default_arguments(ap::default_argument::o_help); // parse command-line arguments parser.try_parse_args(argc, argv); diff --git a/include/ap/argument.hpp b/include/ap/argument.hpp index 924ccba7..82a36bb7 100644 --- a/include/ap/argument.hpp +++ b/include/ap/argument.hpp @@ -11,6 +11,7 @@ #include "detail/argument_base.hpp" #include "detail/argument_descriptor.hpp" #include "detail/concepts.hpp" +#include "detail/ranges_utility.hpp" #include "nargs/range.hpp" #include "types.hpp" @@ -46,7 +47,7 @@ enum class argument_type : bool { positional, optional }; * parser.add_positional_argument("input", "i") * .help("An input file path"); * parser.add_optional_argument("output", "o") - * .default_value("out.txt") + * .default_values("out.txt") * .help("An output file path"); * @endcode * @@ -246,7 +247,7 @@ class argument : public detail::argument_base { } /** - * @brief Set the choices for the argument. + * @brief Add the choices for the argument. * @tparam CR The choices range type. * @param choices The range of valid choices for the argument. * @return Reference to the argument instance. @@ -264,7 +265,7 @@ class argument : public detail::argument_base { } /** - * @brief Set the choices for the argument. + * @brief Add the choices for the argument. * @param choices The list of valid choices for the argument. * @return Reference to the argument instance. * @note The method is enabled only if `value_type` is not `none_type` and is equality comparable. @@ -276,30 +277,102 @@ class argument : public detail::argument_base { } /** - * @brief Set the default value for the argument. - * @param default_value The attribute value. + * @brief Add the choices for the argument. + * @tparam Args The types of the choices. + * @param choices The list of valid choices for the argument. + * @return Reference to the argument instance. + * @note The method is enabled only if `value_type` is not `none_type` and is equality comparable. + */ + argument& choices(const std::convertible_to auto&... choices) noexcept + requires(not detail::c_is_none and std::equality_comparable) + { + (this->_choices.emplace_back(choices), ...); + return *this; + } + + /** + * @brief Add default values for the argument. + * @param values The default values to add. + * @return Reference to the argument instance. + * @attention Setting the default values resets the `required` attribute to `false`. + * @note The method is enabled only if `value_type` is not `none_type`. + */ + template CR> + argument& default_values(const CR& values) noexcept + requires(not detail::c_is_none and std::equality_comparable) + { + for (const auto& value : values) + this->_default_values.emplace_back(std::make_any(value)); + this->_required = false; + return *this; + } + + /** + * @brief Add default values for the argument. + * @param values The default values to add. * @return Reference to the argument instance. - * @attention Setting the default value sets the `required` attribute to `false`. + * @attention Setting the default values resets the `required` attribute to `false`. * @note The method is enabled only if `value_type` is not `none_type`. */ - argument& default_value(const std::convertible_to auto& default_value) noexcept + argument& default_values(std::initializer_list values) noexcept + requires(not detail::c_is_none and std::equality_comparable) + { + return this->default_values<>(values); + } + + /** + * @brief Add default values for the argument. + * @param values The default values to add. + * @return Reference to the argument instance. + * @attention Setting the default values resets the `required` attribute to `false`. + * @note The method is enabled only if `value_type` is not `none_type`. + */ + argument& default_values(const std::convertible_to auto&... values) noexcept requires(not detail::c_is_none) { - this->_default_value = std::make_any(default_value); + (this->_default_values.emplace_back(std::make_any(values)), ...); this->_required = false; return *this; } /** - * @brief Set the implicit value for the optional argument. - * @param implicit_value The implicit value to set. + * @brief Add implicit values for the optional argument. + * @tparam CR The choices range type. + * @param values The range of implicit values to set. * @return Reference to the optional argument instance. * @note The method is enabled only for optional arguments and if `value_type` is not `none_type`. */ - argument& implicit_value(const std::convertible_to auto& implicit_value) noexcept + template CR> + argument& implicit_values(const CR& values) noexcept requires(not detail::c_is_none and type == argument_type::optional) { - this->_implicit_value = std::make_any(implicit_value); + for (const auto& value : values) + this->_implicit_values.emplace_back(std::make_any(value)); + return *this; + } + + /** + * @brief Add implicit values for the optional argument. + * @param values The initializer list of implicit values to set. + * @return Reference to the optional argument instance. + * @note The method is enabled only for optional arguments and if `value_type` is not `none_type`. + */ + argument& implicit_values(std::initializer_list values) noexcept + requires(not detail::c_is_none and type == argument_type::optional) + { + return this->implicit_values<>(values); + } + + /** + * @brief Add a implicit values for the optional argument. + * @param values The implicit values to set. + * @return Reference to the optional argument instance. + * @note The method is enabled only for optional arguments and if `value_type` is not `none_type`. + */ + argument& implicit_values(const std::convertible_to auto&... values) noexcept + requires(not detail::c_is_none and type == argument_type::optional) + { + (this->_implicit_values.emplace_back(std::make_any(values)), ...); return *this; } @@ -356,12 +429,16 @@ class argument : public detail::argument_base { if constexpr (detail::c_writable) { if (not this->_choices.empty()) desc.add_range_param("choices", this->_choices); - if (this->_default_value.has_value()) - desc.add_param("default value", std::any_cast(this->_default_value)); + if (not this->_default_values.empty()) + desc.add_range_param( + "default value(s)", + detail::any_range_cast_view(this->_default_values) + ); if constexpr (type == argument_type::optional) { - if (this->_implicit_value.has_value()) - desc.add_param( - "implicit value", std::any_cast(this->_implicit_value) + if (not this->_implicit_values.empty()) + desc.add_range_param( + "implicit value(s)", + detail::any_range_cast_view(this->_implicit_values) ); } } @@ -438,7 +515,7 @@ class argument : public detail::argument_base { * @throws std::logic_error if no values are available. */ [[nodiscard]] const std::any& value() const override { - if (not this->_values.empty()) + if (this->has_parsed_values()) return this->_values.front(); if constexpr (detail::c_is_none) @@ -446,14 +523,34 @@ class argument : public detail::argument_base { std::format("No values parsed for argument '{}'.", this->_name.str()) ); else - return this->_predefined_value(); + return this->_predefined_values().front(); } /// @return Reference to the vector of parsed values for the argument. [[nodiscard]] const std::vector& values() const override { + return this->_values_impl(); + } + + [[nodiscard]] const std::vector& _values_impl() const noexcept + requires(detail::c_is_none) + { return this->_values; } + [[nodiscard]] const std::vector& _values_impl() const noexcept + requires(not detail::c_is_none) + { + if (this->has_parsed_values()) + return this->_values; + + try { + return this->_predefined_values(); + } + catch (const std::logic_error&) { + return this->_values; // fallback: empty vector + } + } + /// @return `true` if the argument has a predefined value, `false` otherwise. [[nodiscard]] bool _has_predefined_values_impl() const noexcept requires(detail::c_is_none) @@ -471,39 +568,39 @@ class argument : public detail::argument_base { requires(not detail::c_is_none) { if constexpr (type == argument_type::positional) - return this->_default_value.has_value(); + return not this->_default_values.empty(); else - return this->_default_value.has_value() - or (this->is_used() and this->_implicit_value.has_value()); + return not this->_default_values.empty() + or (this->is_used() and not this->_implicit_values.empty()); } /** - * @return Reference to the predefined value of the argument. - * @throws std::logic_error if no predefined value is available. + * @return Reference to the argument's predefined value list. + * @throws std::logic_error if no predefined values are available. * @note The method is enabled only if `value_type` is not `none_type`. - * @note - For positional arguments, the default value is returned. - * @note - For optional arguments, if the argument has been used, the implicit value is returned, otherwise the default value is returned. + * @note - For positional arguments, the default value list is returned. + * @note - For optional arguments, if the argument has been used, the implicit value list is returned, otherwise the default value list is returned. */ - [[nodiscard]] const std::any& _predefined_value() const + [[nodiscard]] const std::vector& _predefined_values() const requires(not detail::c_is_none) { if constexpr (type == argument_type::optional) { if (this->is_used()) { - if (not this->_implicit_value.has_value()) + if (this->_implicit_values.empty()) throw(std::logic_error(std::format( - "No implicit value specified for argument '{}'.", this->_name.str() + "No implicit values specified for argument '{}'.", this->_name.str() ))); - return this->_implicit_value; + return this->_implicit_values; } } - if (not this->_default_value.has_value()) + if (this->_default_values.empty()) throw(std::logic_error( - std::format("No default value specified for argument '{}'.", this->_name.str()) + std::format("No default values specified for argument '{}'.", this->_name.str()) )); - return this->_default_value; + return this->_default_values; } /// @return `true` if the argument accepts further values, `false` otherwise. @@ -576,10 +673,10 @@ class argument : public detail::argument_base { const ap::detail::argument_name _name; ///< The argument's name. std::optional _help_msg; ///< The argument's help message. nargs::range _nargs_range; ///< The argument's nargs range attribute. - [[no_unique_address]] value_arg_specific_type - _default_value; ///< The argument's default value. - [[no_unique_address]] value_arg_specific_type> - _implicit_value; ///< The optional argument's implicit value. + [[no_unique_address]] value_arg_specific_type> + _default_values; ///< The argument's default value list. + [[no_unique_address]] value_arg_specific_type>> + _implicit_values; ///< The optional argument's implicit value list. [[no_unique_address]] value_arg_specific_type> _choices; ///< The argument's valid choices collection. [[no_unique_address]] optional_specific_type> diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index a34bc2f5..82e1c9d7 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -123,27 +123,39 @@ class argument_parser { } /** - * @brief Set default positional arguments. + * @brief Add default arguments to the argument parser. * @tparam AR Type of the positional argument discriminator range. - * @param arg_discriminator_range A range of default positional argument discriminators. + * @param arg_discriminators A range of default positional argument discriminators. * @return Reference to the argument parser. */ template AR> - argument_parser& default_arguments(const AR& arg_discriminator_range) noexcept { - for (const auto arg_discriminator : arg_discriminator_range) + argument_parser& default_arguments(const AR& arg_discriminators) noexcept { + for (const auto arg_discriminator : arg_discriminators) detail::add_default_argument(arg_discriminator, *this); return *this; } /** - * @brief Set default positional arguments. - * @param arg_discriminator_list A list of default positional argument discriminators. + * @brief Add default arguments to the argument parser. + * @param arg_discriminators A list of default positional argument discriminators. * @return Reference to the argument parser. */ argument_parser& default_arguments( - const std::initializer_list arg_discriminator_list + const std::initializer_list& arg_discriminators ) noexcept { - return this->default_arguments<>(arg_discriminator_list); + return this->default_arguments<>(arg_discriminators); + } + + /** + * @brief Add default arguments to the argument parser. + * @param arg_discriminators A list of default positional argument discriminators. + * @return Reference to the argument parser. + */ + argument_parser& default_arguments( + const std::same_as auto... arg_discriminators + ) noexcept { + (detail::add_default_argument(arg_discriminators, *this), ...); + return *this; } /** @@ -250,7 +262,8 @@ class argument_parser { /** * @brief Adds a boolean flag argument (an optional argument with `value_type = bool`) to the parser's configuration. - * @tparam StoreImplicitly A boolean value used as the `implicit_value` parameter of the argument. + * @tparam StoreImplicitly A boolean value used as the `implicit_values` parameter of the argument. + * @note The argument's `default_values` attribute will be set to `not StoreImplicitly`. * @param name The primary name of the flag. * @param 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. @@ -261,14 +274,15 @@ class argument_parser { const detail::argument_name_discriminator name_discr = n_primary ) { return this->add_optional_argument(name, name_discr) - .default_value(not StoreImplicitly) - .implicit_value(StoreImplicitly) + .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. - * @tparam StoreImplicitly A boolean value used as the `implicit_value` parameter of the argument. + * @tparam StoreImplicitly A boolean value used as the `implicit_values` parameter of the argument. + * @note The argument's `default_values` attribute will be set to `not StoreImplicitly`. * @param primary_name The primary name of the flag. * @param secondary_name The secondary name of the flag. * @return Reference to the added boolean flag argument. @@ -278,8 +292,8 @@ class argument_parser { const std::string_view primary_name, const std::string_view secondary_name ) { return this->add_optional_argument(primary_name, secondary_name) - .default_value(not StoreImplicitly) - .implicit_value(StoreImplicitly) + .default_values(not StoreImplicitly) + .implicit_values(StoreImplicitly) .nargs(0ull); } @@ -551,17 +565,10 @@ class argument_parser { const auto& arg = arg_opt->get(); try { - if (arg.has_predefined_values()) - // currently an argument may have only one predefined value - return std::vector{std::any_cast(arg.value())}; - std::vector values; // TODO: use std::ranges::to after transition to C++23 std::ranges::copy( - std::views::transform( - arg.values(), [](const std::any& value) { return std::any_cast(value); } - ), - std::back_inserter(values) + detail::any_range_cast_view(arg.values()), std::back_inserter(values) ); return values; } diff --git a/include/ap/detail/ranges_utility.hpp b/include/ap/detail/ranges_utility.hpp new file mode 100644 index 00000000..80ae4075 --- /dev/null +++ b/include/ap/detail/ranges_utility.hpp @@ -0,0 +1,23 @@ +// 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/detail/ranges_utility.hpp + +#pragma once + +#include "concepts.hpp" + +#include +#include + +namespace ap::detail { + +template +auto any_range_cast_view(const c_range_of auto& range) { + return range | std::views::transform([](const std::any& value) -> T { + return std::any_cast(value); + }); +} + +} // namespace ap::detail diff --git a/tests/source/test_argument_parser_add_argument.cpp b/tests/source/test_argument_parser_add_argument.cpp index 043b652a..8b9d363d 100644 --- a/tests/source/test_argument_parser_add_argument.cpp +++ b/tests/source/test_argument_parser_add_argument.cpp @@ -304,7 +304,7 @@ TEST_CASE_FIXTURE( "default_arguments should add the specified optional arguments" ) { sut.default_arguments( - {default_argument::o_help, default_argument::o_input, default_argument::o_output} + default_argument::o_help, default_argument::o_input, default_argument::o_output ); std::string help_flag; diff --git a/tests/source/test_argument_parser_parse_args.cpp b/tests/source/test_argument_parser_parse_args.cpp index 787ee3fa..8cdc422e 100644 --- a/tests/source/test_argument_parser_parse_args.cpp +++ b/tests/source/test_argument_parser_parse_args.cpp @@ -402,8 +402,8 @@ TEST_CASE_FIXTURE( sut.add_optional_argument( bypass_required_arg_name.primary.value(), bypass_required_arg_name.secondary.value() ) - .default_value(false) - .implicit_value(true) + .default_values(false) + .implicit_values(true) .bypass_required(); std::string arg_flag; @@ -746,7 +746,7 @@ TEST_CASE_FIXTURE( for (std::size_t i = 0ull; i < n_optional_args; ++i) { const auto arg_name = init_arg_name(i); sut.add_optional_argument(arg_name.primary.value(), arg_name.secondary.value()) - .default_value(init_arg_value(i)); + .default_values(init_arg_value(i)); } for (std::size_t i = 0ull; i < n_optional_args; ++i) { @@ -829,7 +829,7 @@ TEST_CASE_FIXTURE( for (std::size_t i = 0ull; i < n_optional_args; ++i) { const auto arg_name = init_arg_name(i); sut.add_optional_argument(arg_name.primary.value(), arg_name.secondary.value()) - .default_value(init_arg_value(i)); + .default_values(init_arg_value(i)); } for (std::size_t i = 0ull; i < n_optional_args; ++i) { @@ -855,11 +855,13 @@ TEST_CASE_FIXTURE( for (std::size_t i = 0ull; i < n_args_total; ++i) { const auto arg_name = init_arg_name(i); - const default_value_type default_value = 2 * static_cast(i); + const default_value_type fallback_value = 2 * static_cast(i); - CHECK_EQ(sut.value_or(arg_name.primary.value(), default_value), default_value); CHECK_EQ( - sut.value_or(arg_name.secondary.value(), default_value), default_value + sut.value_or(arg_name.primary.value(), fallback_value), fallback_value + ); + CHECK_EQ( + sut.value_or(arg_name.secondary.value(), fallback_value), fallback_value ); } } @@ -873,51 +875,45 @@ TEST_CASE_FIXTURE( for (std::size_t i = 0ull; i < n_args_total; ++i) { const auto arg_name = init_arg_name(i); - const auto default_value = init_arg_value(i); + const auto fallback_value = init_arg_value(i); - CHECK_EQ(sut.value_or(arg_name.primary.value(), default_value), default_value); - CHECK_EQ(sut.value_or(arg_name.secondary.value(), default_value), default_value); + CHECK_EQ(sut.value_or(arg_name.primary.value(), fallback_value), fallback_value); + CHECK_EQ(sut.value_or(arg_name.secondary.value(), fallback_value), fallback_value); } } -// values: optional arguments +// values: positional arguments TEST_CASE_FIXTURE( test_argument_parser_parse_args, - "values() [optional arguments] should return an empty vector if an argument has no values" + "values() [positional arguments] should return an empty vector if an argument has no values" ) { - sut.add_optional_argument(optional_primary_name, optional_secondary_name); + sut.add_positional_argument(positional_primary_name, positional_secondary_name); - CHECK(sut.values(optional_primary_name).empty()); - CHECK(sut.values(optional_secondary_name).empty()); + CHECK(sut.values(positional_primary_name).empty()); + CHECK(sut.values(positional_secondary_name).empty()); } TEST_CASE_FIXTURE( test_argument_parser_parse_args, - "values() [optional arguments] should throw when an argument has values but the given type is " - "invalid" + "values() [positional arguments] should throw when an argument has values but the given type " + "is invalid" ) { - sut.add_optional_argument(optional_primary_name, optional_secondary_name).nargs(at_least(1)); + sut.add_positional_argument(positional_primary_name, positional_secondary_name); // prepare argc & argv - const std::string optional_arg_flag = "--" + optional_primary_name; - const std::string optional_arg_value = optional_primary_name + "_value"; - std::vector argv_vec{ - "program", optional_arg_flag, optional_arg_value, optional_arg_value - }; - - const int argc = static_cast(argv_vec.size()); - auto argv = to_char_2d_array(argv_vec); + const int argc = get_argc(1ull, no_args); + auto argv = init_argv(1ull, no_args); // parse args sut.parse_args(argc, argv); CHECK_THROWS_AS( - discard_result(sut.values(optional_primary_name)), + discard_result(sut.values(positional_primary_name)), ap::type_error ); CHECK_THROWS_AS( - discard_result(sut.values(optional_secondary_name)), + discard_result(sut.values(positional_secondary_name)), ap::type_error ); @@ -926,66 +922,44 @@ TEST_CASE_FIXTURE( TEST_CASE_FIXTURE( test_argument_parser_parse_args, - "values() [optional arguments] should return a vector containing a predefined value of an " + "values() [positional arguments] should return a vector containing the predefined values of an " "argument if no values for an argument have been parsed" ) { - const std::string default_value = "default_value"; - const std::string implicit_value = "implicit_value"; + const std::vector default_values{"default_value_1", "default_value_2"}; - sut.add_optional_argument(optional_primary_name, optional_secondary_name) - .default_value(default_value) - .implicit_value(implicit_value); + sut.add_positional_argument(positional_primary_name, positional_secondary_name) + .default_values(default_values); // prepare argc & argv - std::vector argv_vec{"program"}; - std::string expected_value; - - SUBCASE("default_value") { - expected_value = default_value; - } - - SUBCASE("implicit_value") { - expected_value = implicit_value; - - const auto optional_arg_flag = "--" + optional_primary_name; - argv_vec.push_back(optional_arg_flag); - } - - CAPTURE(argv_vec); - CAPTURE(expected_value); - - const int argc = static_cast(argv_vec.size()); - auto argv = to_char_2d_array(argv_vec); + const int argc = get_argc(no_args, no_args); + auto argv = init_argv(no_args, no_args); // parse args sut.parse_args(argc, argv); - const auto& stored_values = sut.values(optional_primary_name); + const auto& stored_values = sut.values(positional_primary_name); - REQUIRE_EQ(stored_values.size(), 1); - CHECK_EQ(stored_values.front(), expected_value); + REQUIRE_EQ(stored_values.size(), default_values.size()); + CHECK_EQ(stored_values, default_values); free_argv(argc, argv); } TEST_CASE_FIXTURE( test_argument_parser_parse_args, - "values() [optional arguments] should return a correct vector of values when there is an " + "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_optional_argument(optional_primary_name, optional_secondary_name).nargs(at_least(1)); + sut.add_positional_argument(positional_primary_name, positional_secondary_name).nargs(any()); // prepare argc & argv - const std::string optional_arg_flag = "--" + optional_primary_name; - const std::string optional_arg_value = optional_primary_name + "_value"; - - std::vector optional_arg_values{ - optional_arg_value + "_1", optional_arg_value + "_2", optional_arg_value + "_3" - }; + const std::size_t n_positional_values = 3ull; + std::vector positional_arg_values; + for (std::size_t i = 0ull; i < n_positional_values; ++i) + positional_arg_values.emplace_back(std::format("positional_value_{}", i + 1ull)); - std::vector argv_vec{"program", optional_arg_flag}; - for (const auto& value : optional_arg_values) - argv_vec.push_back(value); + std::vector argv_vec{"program"}; + argv_vec.insert(argv_vec.end(), positional_arg_values.begin(), positional_arg_values.end()); const int argc = static_cast(argv_vec.size()); auto argv = to_char_2d_array(argv_vec); @@ -993,46 +967,52 @@ TEST_CASE_FIXTURE( // parse args sut.parse_args(argc, argv); - const auto& stored_values = sut.values(optional_primary_name); + const auto& stored_values = sut.values(positional_primary_name); - REQUIRE_EQ(stored_values.size(), optional_arg_values.size()); - CHECK_EQ(stored_values, optional_arg_values); + REQUIRE_EQ(stored_values.size(), positional_arg_values.size()); + CHECK_EQ(stored_values, positional_arg_values); free_argv(argc, argv); } -// values: positional arguments +// values: optional arguments TEST_CASE_FIXTURE( test_argument_parser_parse_args, - "values() [positional arguments] should return an empty vector if an argument has no values" + "values() [optional arguments] should return an empty vector if an argument has no values" ) { - sut.add_positional_argument(positional_primary_name, positional_secondary_name); + sut.add_optional_argument(optional_primary_name, optional_secondary_name); - CHECK(sut.values(positional_primary_name).empty()); - CHECK(sut.values(positional_secondary_name).empty()); + CHECK(sut.values(optional_primary_name).empty()); + CHECK(sut.values(optional_secondary_name).empty()); } TEST_CASE_FIXTURE( test_argument_parser_parse_args, - "values() [positional arguments] should throw when an argument has values but the given type " - "is invalid" + "values() [optional 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_optional_argument(optional_primary_name, optional_secondary_name).nargs(at_least(1)); // prepare argc & argv - const int argc = get_argc(1ull, no_args); - auto argv = init_argv(1ull, no_args); + const std::string optional_arg_flag = "--" + optional_primary_name; + const std::string optional_arg_value = optional_primary_name + "_value"; + std::vector argv_vec{ + "program", optional_arg_flag, optional_arg_value, optional_arg_value + }; + + const int argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); // parse args sut.parse_args(argc, argv); CHECK_THROWS_AS( - discard_result(sut.values(positional_primary_name)), + discard_result(sut.values(optional_primary_name)), ap::type_error ); CHECK_THROWS_AS( - discard_result(sut.values(positional_secondary_name)), + discard_result(sut.values(optional_secondary_name)), ap::type_error ); @@ -1041,44 +1021,66 @@ TEST_CASE_FIXTURE( TEST_CASE_FIXTURE( test_argument_parser_parse_args, - "values() [positional arguments] should return a vector containing a predefined value of an " + "values() [optional arguments] should return a vector containing the predefined values of an " "argument if no values for an argument have been parsed" ) { - const std::string default_value = "default_value"; + const std::vector default_values{"default_value_1", "default_value_2"}; + const std::vector implicit_values{"implicit_value_1", "implicit_value_2"}; - sut.add_positional_argument(positional_primary_name, positional_secondary_name) - .default_value(default_value); + sut.add_optional_argument(optional_primary_name, optional_secondary_name) + .default_values(default_values) + .implicit_values(implicit_values); // prepare argc & argv - const int argc = get_argc(no_args, no_args); - auto argv = init_argv(no_args, no_args); + std::vector argv_vec{"program"}; + std::vector expected_values; + + SUBCASE("default_values") { + expected_values = default_values; + } + + SUBCASE("implicit_values") { + expected_values = implicit_values; + + const auto optional_arg_flag = "--" + optional_primary_name; + argv_vec.push_back(optional_arg_flag); + } + + CAPTURE(argv_vec); + CAPTURE(expected_values); + + const int argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); // parse args sut.parse_args(argc, argv); - const auto& stored_values = sut.values(positional_primary_name); + const auto& stored_values = sut.values(optional_primary_name); - REQUIRE_EQ(stored_values.size(), 1); - CHECK_EQ(stored_values.front(), default_value); + REQUIRE_EQ(stored_values.size(), expected_values.size()); + CHECK_EQ(stored_values, expected_values); free_argv(argc, argv); } TEST_CASE_FIXTURE( test_argument_parser_parse_args, - "values() [positional arguments] should return a correct vector of values when there is an " + "values() [optional 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_optional_argument(optional_primary_name, optional_secondary_name).nargs(at_least(1)); // prepare argc & argv - const std::size_t n_positional_values = 3ull; - std::vector positional_arg_values; - for (std::size_t i = 0ull; i < n_positional_values; ++i) - positional_arg_values.emplace_back(std::format("positional_value_{}", i + 1ull)); + const std::string optional_arg_flag = "--" + optional_primary_name; + const std::string optional_arg_value = optional_primary_name + "_value"; - std::vector argv_vec{"program"}; - argv_vec.insert(argv_vec.end(), positional_arg_values.begin(), positional_arg_values.end()); + std::vector optional_arg_values{ + optional_arg_value + "_1", optional_arg_value + "_2", optional_arg_value + "_3" + }; + + std::vector argv_vec{"program", optional_arg_flag}; + for (const auto& value : optional_arg_values) + argv_vec.push_back(value); const int argc = static_cast(argv_vec.size()); auto argv = to_char_2d_array(argv_vec); @@ -1086,10 +1088,10 @@ 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(optional_primary_name); - REQUIRE_EQ(stored_values.size(), positional_arg_values.size()); - CHECK_EQ(stored_values, positional_arg_values); + REQUIRE_EQ(stored_values.size(), optional_arg_values.size()); + CHECK_EQ(stored_values, optional_arg_values); free_argv(argc, argv); } diff --git a/tests/source/test_optional_argument.cpp b/tests/source/test_optional_argument.cpp index a26f0221..a9bc72ed 100644 --- a/tests/source/test_optional_argument.cpp +++ b/tests/source/test_optional_argument.cpp @@ -132,8 +132,8 @@ TEST_CASE_FIXTURE( sut.bypass_required(); sut.nargs(non_default_range); sut.choices(choices); - sut.default_value(default_value); - sut.implicit_value(implicit_value); + sut.default_values(default_value); + sut.implicit_values(implicit_value); // check the descriptor parameters desc = get_desc(sut, verbose); @@ -152,12 +152,12 @@ TEST_CASE_FIXTURE( CHECK_EQ(choices_it->value, ap::detail::join(choices, ", ")); const auto default_value_it = - std::ranges::find(desc.params, "default value", ¶meter_descriptor::name); + std::ranges::find(desc.params, "default value(s)", ¶meter_descriptor::name); REQUIRE_NE(default_value_it, desc.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", ¶meter_descriptor::name); + std::ranges::find(desc.params, "implicit value(s)", ¶meter_descriptor::name); REQUIRE_NE(implicit_value_it, desc.params.end()); CHECK_EQ(implicit_value_it->value, std::to_string(implicit_value)); } @@ -314,7 +314,7 @@ TEST_CASE_FIXTURE( "default value" ) { auto sut = sut_type(arg_name_primary); - sut.default_value(default_value); + sut.default_values(default_value); REQUIRE(has_value(sut)); CHECK_EQ(std::any_cast(get_value(sut)), default_value); @@ -325,7 +325,7 @@ TEST_CASE_FIXTURE( "has_value() should return false if only the implicit value is set but the argument is not used" ) { auto sut = sut_type(arg_name_primary); - sut.implicit_value(implicit_value); + sut.implicit_values(implicit_value); CHECK_FALSE(has_value(sut)); } @@ -335,7 +335,7 @@ TEST_CASE_FIXTURE( "has_value() should return true if the implicit value is set and the agument is used" ) { auto sut = sut_type(arg_name_primary); - sut.implicit_value(implicit_value); + sut.implicit_values(implicit_value); mark_used(sut); @@ -351,23 +351,23 @@ TEST_CASE_FIXTURE(argument_test_fixture, "has_parsed_values() should return fals TEST_CASE_FIXTURE( argument_test_fixture, "has_parsed_values() should return false regardles of the " - "default_value and implicit_value parameters" + "default_values and implicit_values parameters" ) { auto sut = sut_type(arg_name_primary); - SUBCASE("default_value") { - sut.default_value(default_value); + SUBCASE("default_values") { + sut.default_values(default_value); CHECK_FALSE(has_parsed_values(sut)); } - SUBCASE("implicit_value") { - sut.implicit_value(implicit_value); + SUBCASE("implicit_values") { + sut.implicit_values(implicit_value); CHECK_FALSE(has_parsed_values(sut)); } - SUBCASE("default_value and implicit_value") { - sut.default_value(default_value); - sut.implicit_value(implicit_value); + SUBCASE("default_values and implicit_values") { + sut.default_values(default_value); + sut.implicit_values(implicit_value); CHECK_FALSE(has_parsed_values(sut)); } } @@ -387,7 +387,7 @@ TEST_CASE_FIXTURE( argument_test_fixture, "has_predefined_values() should return true if the default value is set" ) { auto sut = sut_type(arg_name_primary); - sut.default_value(default_value); + sut.default_values(default_value); CHECK(has_predefined_values(sut)); } @@ -398,7 +398,7 @@ TEST_CASE_FIXTURE( "not used" ) { auto sut = sut_type(arg_name_primary); - sut.implicit_value(implicit_value); + sut.implicit_values(implicit_value); CHECK_FALSE(has_predefined_values(sut)); } @@ -409,7 +409,7 @@ TEST_CASE_FIXTURE( "used" ) { auto sut = sut_type(arg_name_primary); - sut.implicit_value(implicit_value); + sut.implicit_values(implicit_value); mark_used(sut); @@ -421,8 +421,8 @@ TEST_CASE_FIXTURE( "has_predefined_values() should return true if both the default and implicit values are set" ) { auto sut = sut_type(arg_name_primary); - sut.default_value(default_value); - sut.implicit_value(implicit_value); + sut.default_values(default_value); + sut.implicit_values(implicit_value); CHECK(has_predefined_values(sut)); } @@ -441,7 +441,7 @@ TEST_CASE_FIXTURE( "value() should return the default value if one has been provided and argument is not used" ) { auto sut = sut_type(arg_name_primary); - sut.default_value(arbitrary_value); + sut.default_values(arbitrary_value); REQUIRE(has_value(sut)); CHECK_EQ(std::any_cast(get_value(sut)), arbitrary_value); @@ -452,7 +452,7 @@ TEST_CASE_FIXTURE( "value() should return the implicit value if one has been provided and argument is used" ) { auto sut = sut_type(arg_name_primary); - sut.implicit_value(implicit_value); + sut.implicit_values(implicit_value); mark_used(sut); @@ -464,8 +464,8 @@ TEST_CASE_FIXTURE( argument_test_fixture, "value() should return the argument's value if it has been set" ) { auto sut = sut_type(arg_name_primary); - sut.default_value(default_value); - sut.implicit_value(implicit_value); + sut.default_values(default_value); + sut.implicit_values(implicit_value); set_value(sut, arbitrary_value); REQUIRE(has_value(sut)); @@ -591,7 +591,7 @@ TEST_CASE_FIXTURE( auto sut = sut_type(arg_name_primary); sut.nargs(non_default_range); - sut.default_value(default_value); + sut.default_values(default_value); CHECK(std::is_eq(nvalues_ordering(sut))); } diff --git a/tests/source/test_positional_argument.cpp b/tests/source/test_positional_argument.cpp index 3aceaf01..89120d62 100644 --- a/tests/source/test_positional_argument.cpp +++ b/tests/source/test_positional_argument.cpp @@ -118,7 +118,7 @@ TEST_CASE_FIXTURE( sut.bypass_required(); sut.nargs(non_default_range); sut.choices(choices); - sut.default_value(default_value); + sut.default_values(default_value); // check the descriptor parameters desc = get_desc(sut, verbose); @@ -143,7 +143,7 @@ TEST_CASE_FIXTURE( CHECK_EQ(choices_it->value, ap::detail::join(choices, ", ")); const auto default_value_it = - std::ranges::find(desc.params, "default value", ¶meter_descriptor::name); + std::ranges::find(desc.params, "default value(s)", ¶meter_descriptor::name); REQUIRE_NE(default_value_it, desc.params.end()); CHECK_EQ(default_value_it->value, std::to_string(default_value)); } @@ -279,7 +279,7 @@ TEST_CASE_FIXTURE( argument_test_fixture, "has_value() should return true if the default value is set" ) { auto sut = sut_type(arg_name_primary); - sut.default_value(default_value); + sut.default_values(default_value); CHECK(has_value(sut)); } @@ -294,7 +294,7 @@ TEST_CASE_FIXTURE( "has_parsed_values() should return false regardless of the default value parameter" ) { auto sut = sut_type(arg_name_primary); - sut.default_value(default_value); + sut.default_values(default_value); CHECK_FALSE(has_parsed_values(sut)); } @@ -315,7 +315,7 @@ TEST_CASE_FIXTURE( argument_test_fixture, "has_predefined_values() should return true if the default value is set" ) { auto sut = sut_type(arg_name_primary); - sut.default_value(default_value); + sut.default_values(default_value); CHECK(has_predefined_values(sut)); } @@ -345,7 +345,7 @@ TEST_CASE_FIXTURE( "were parsed" ) { auto sut = sut_type(arg_name_primary); - sut.default_value(default_value); + sut.default_values(default_value); REQUIRE(has_value(sut)); CHECK_EQ(std::any_cast(get_value(sut)), default_value); @@ -355,7 +355,7 @@ TEST_CASE_FIXTURE( argument_test_fixture, "value() should return the argument's parsed value if it has been set" ) { auto sut = sut_type(arg_name_primary); - sut.default_value(default_value); + sut.default_values(default_value); set_value(sut, valid_value); REQUIRE(has_value(sut)); @@ -480,7 +480,7 @@ TEST_CASE_FIXTURE( auto sut = sut_type(arg_name_primary); sut.nargs(non_default_range); - sut.default_value(default_value); + sut.default_values(default_value); CHECK(std::is_eq(nvalues_ordering(sut))); } From e83739563e4d5c7873ef96d5c287e319e8c63ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Musia=C5=82?= <111433005+SpectraL519@users.noreply.github.com> Date: Thu, 18 Sep 2025 22:21:02 +0200 Subject: [PATCH 04/10] YT-CPPAP-62: Argument parsing improvement - Replaced `std::unique_ptr` with `std::shared_ptr` in the argument lists within `argument_parser` - Replaced optional argument reference type with `std::shared_ptr` - Previously: `std::optional>>` - Aligned argument token building to store original (unstripped) command-line values - Defined a *parsing state* structure within `argument_parser` to replace passing multiple parsing parameters by reference with a single compound type --- CMakeLists.txt | 2 +- Doxyfile | 2 +- MODULE.bazel | 2 +- include/ap/argument_parser.hpp | 277 +++++++++--------- include/ap/detail/argument_token.hpp | 13 +- include/ap/detail/concepts.hpp | 13 +- .../include/argument_parser_test_fixture.hpp | 49 ++-- .../test_argument_parser_add_argument.cpp | 10 +- .../test_argument_parser_parse_args.cpp | 8 +- tests/source/test_argument_token.cpp | 9 +- 10 files changed, 193 insertions(+), 192 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ba0d97f..11453918 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ else() endif() project(cpp-ap - VERSION 3.0.0.2 + VERSION 3.0.0.3 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 ef9c9ce0..45c3db63 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.2 +PROJECT_NUMBER = 3.0.0.3 # 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 8bf53d10..0cf5f8a1 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,4 +1,4 @@ module( name = "cpp-ap", - version = "3.0.0.2", + version = "3.0.0.3", ) diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index 82e1c9d7..ea48de55 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -126,6 +126,7 @@ class argument_parser { * @brief Add default arguments to the argument parser. * @tparam AR Type of the positional argument discriminator range. * @param arg_discriminators A range of default positional argument discriminators. + * @note `arg_discriminators` must be a `std::ranges::range` with the `ap::default_argument` value type. * @return Reference to the argument parser. */ template AR> @@ -173,7 +174,7 @@ 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_unique>(arg_name)); + this->_positional_args.emplace_back(std::make_shared>(arg_name)); return static_cast&>(*this->_positional_args.back()); } @@ -199,7 +200,7 @@ 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_unique>(arg_name)); + this->_positional_args.emplace_back(std::make_shared>(arg_name)); return static_cast&>(*this->_positional_args.back()); } @@ -229,7 +230,7 @@ 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_unique>(arg_name)); + this->_optional_args.push_back(std::make_shared>(arg_name)); return static_cast&>(*this->_optional_args.back()); } @@ -256,7 +257,7 @@ 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_unique>(arg_name)); + this->_optional_args.emplace_back(std::make_shared>(arg_name)); return static_cast&>(*this->_optional_args.back()); } @@ -315,9 +316,11 @@ 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`. * @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. */ @@ -325,11 +328,11 @@ class argument_parser { void parse_args(const AR& argv_rng) { this->_validate_argument_configuration(); - std::vector unknown_args; - this->_parse_args_impl(this->_tokenize(argv_rng), unknown_args); + parsing_state state{.curr_arg = nullptr, .curr_pos_arg_it = this->_positional_args.begin()}; + this->_parse_args_impl(this->_tokenize(argv_rng), state); - if (not unknown_args.empty()) - throw parsing_failure::argument_deduction_failure(unknown_args); + 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(); @@ -354,6 +357,7 @@ 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 @@ -362,6 +366,7 @@ 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`. * @attention This overload of the `try_parse_args` function assumes that the program name argument has already been discarded. */ template AR> @@ -398,6 +403,7 @@ 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: @@ -407,6 +413,7 @@ 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`. * @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. */ @@ -414,15 +421,19 @@ class argument_parser { std::vector parse_known_args(const AR& argv_rng) { this->_validate_argument_configuration(); - std::vector unknown_args; - this->_parse_args_impl(this->_tokenize(argv_rng), unknown_args, false); + parsing_state state{ + .curr_arg = nullptr, + .curr_pos_arg_it = this->_positional_args.begin(), + .fail_on_unknown = false + }; + this->_parse_args_impl(this->_tokenize(argv_rng), state); if (not this->_are_required_args_bypassed()) { this->_verify_required_args(); this->_verify_nvalues(); } - return unknown_args; + return std::move(state.unknown_args); } /** @@ -443,6 +454,7 @@ 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 @@ -451,6 +463,7 @@ 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`. * @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. */ @@ -488,8 +501,8 @@ class argument_parser { * @return True if the argument has a value, false otherwise. */ [[nodiscard]] bool has_value(std::string_view arg_name) const noexcept { - const auto arg_opt = this->_get_argument(arg_name); - return arg_opt ? arg_opt->get().has_value() : false; + const auto arg = this->_get_argument(arg_name); + return arg ? arg->has_value() : false; } /** @@ -497,8 +510,8 @@ class argument_parser { * @return The count of times the argument has been used. */ [[nodiscard]] std::size_t count(std::string_view arg_name) const noexcept { - const auto arg_opt = this->_get_argument(arg_name); - return arg_opt ? arg_opt->get().count() : 0ull; + const auto arg = this->_get_argument(arg_name); + return arg ? arg->count() : 0ull; } /** @@ -509,16 +522,16 @@ class argument_parser { */ template [[nodiscard]] T value(std::string_view arg_name) const { - const auto arg_opt = this->_get_argument(arg_name); - if (not arg_opt) + const auto arg = this->_get_argument(arg_name); + if (not arg) throw lookup_failure::argument_not_found(arg_name); - const auto& arg_value = arg_opt->get().value(); + const auto& arg_value = arg->value(); try { return std::any_cast(arg_value); } catch (const std::bad_any_cast&) { - throw type_error::invalid_value_type(arg_opt->get().name()); + throw type_error::invalid_value_type(arg->name()); } } @@ -532,12 +545,12 @@ class argument_parser { */ template U> [[nodiscard]] T value_or(std::string_view arg_name, U&& fallback_value) const { - const auto arg_opt = this->_get_argument(arg_name); - if (not arg_opt) + const auto arg = this->_get_argument(arg_name); + if (not arg) throw lookup_failure::argument_not_found(arg_name); try { - const auto& arg_value = arg_opt->get().value(); + const auto& arg_value = arg->value(); return std::any_cast(arg_value); } catch (const std::logic_error&) { @@ -546,7 +559,7 @@ class argument_parser { return T{std::forward(fallback_value)}; } catch (const std::bad_any_cast&) { - throw type_error::invalid_value_type(arg_opt->get().name()); + throw type_error::invalid_value_type(arg->name()); } } @@ -558,22 +571,20 @@ class argument_parser { */ template [[nodiscard]] std::vector values(std::string_view arg_name) const { - const auto arg_opt = this->_get_argument(arg_name); - if (not arg_opt) + const auto arg = this->_get_argument(arg_name); + if (not arg) throw lookup_failure::argument_not_found(arg_name); - const auto& arg = arg_opt->get(); - try { std::vector values; // TODO: use std::ranges::to after transition to C++23 std::ranges::copy( - detail::any_range_cast_view(arg.values()), std::back_inserter(values) + detail::any_range_cast_view(arg->values()), std::back_inserter(values) ); return values; } catch (const std::bad_any_cast&) { - throw type_error::invalid_value_type(arg.name()); + throw type_error::invalid_value_type(arg->name()); } } @@ -627,16 +638,25 @@ class argument_parser { #endif private: - using arg_ptr_t = std::unique_ptr; + 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 arg_ptr_opt_t = detail::uptr_opt_t; - using arg_opt_t = std::optional>; using const_arg_opt_t = std::optional>; using arg_token_list_t = std::vector; using arg_token_list_iter_t = typename arg_token_list_t::const_iterator; + /// @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 + 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 fail_on_unknown = + true; ///< A flag indicating whether to end parsing with an error on unknown arguments. + }; + /** * @brief Verifies the pattern of an argument name and if it's invalid, an error is thrown * @throws ap::invalid_configuration @@ -742,19 +762,20 @@ 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`. * @return A list of preprocessed command-line argument tokens. */ - template AR> + template AR> [[nodiscard]] arg_token_list_t _tokenize(const AR& arg_range) { - const auto n_args = std::ranges::size(arg_range); - if (n_args == 0ull) - return arg_token_list_t{}; - arg_token_list_t toks; - toks.reserve(n_args); + + if constexpr (std::ranges::sized_range) + toks.reserve(std::ranges::size(arg_range)); + std::ranges::for_each( arg_range, std::bind_front(&argument_parser::_tokenize_arg, this, std::ref(toks)) ); + return toks; } @@ -764,7 +785,9 @@ class argument_parser { * @param arg_value The command-line argument's value to be processed. */ void _tokenize_arg(arg_token_list_t& toks, const std::string_view arg_value) { - auto tok = this->_build_token(arg_value); + detail::argument_token tok{ + .type = this->_deduce_token_type(arg_value), .value = std::string(arg_value) + }; if (not tok.is_flag_token() or this->_validate_flag_token(tok)) { toks.emplace_back(std::move(tok)); @@ -779,10 +802,40 @@ class argument_parser { } #ifdef AP_UNKNOWN_FLAGS_AS_VALUES - toks.emplace_back(detail::argument_token::t_value, std::string(arg_value)); -#else - toks.emplace_back(std::move(tok)); + tok.type = detail::argument_token::t_value; #endif + toks.emplace_back(std::move(tok)); + } + + [[nodiscard]] detail::argument_token::token_type _deduce_token_type( + const std::string_view arg_value + ) const noexcept { + if (detail::contains_whitespaces(arg_value)) + return detail::argument_token::t_value; + + if (arg_value.starts_with(this->_flag_prefix)) + return detail::argument_token::t_flag_primary; + + if (arg_value.starts_with(this->_flag_prefix_char)) + return detail::argument_token::t_flag_secondary; + + return detail::argument_token::t_value; + } + + /** + * @brief Removes the flag prefix from a flag token's value. + * @param tok The argument token to be processed. + * @return The token's value without the flag prefix. + */ + [[nodiscard]] std::string _strip_flag_prefix(const detail::argument_token& tok) const noexcept { + switch (tok.type) { + case detail::argument_token::t_flag_primary: + return tok.value.substr(this->_primary_flag_prefix_length); + case detail::argument_token::t_flag_secondary: + return tok.value.substr(this->_secondary_flag_prefix_length); + default: + return tok.value; + } } /** @@ -821,7 +874,7 @@ class argument_parser { if (opt_arg_it == this->_optional_args.end()) return false; - tok.arg.emplace(*opt_arg_it); + tok.arg = *opt_arg_it; return true; } @@ -835,14 +888,17 @@ class argument_parser { const detail::argument_token& tok ) noexcept { std::vector compound_toks; - compound_toks.reserve(tok.value.size()); + const auto actual_tok_value = this->_strip_flag_prefix(tok); + + compound_toks.reserve(actual_tok_value.size()); if (tok.type != detail::argument_token::t_flag_secondary) return compound_toks; - for (const char c : tok.value) { + for (const char c : actual_tok_value) { detail::argument_token ctok{ - detail::argument_token::t_flag_secondary, std::string(1ull, c) + detail::argument_token::t_flag_secondary, + std::format("{}{}", this->_flag_prefix_char, c) }; if (not this->_validate_flag_token(ctok)) { compound_toks.clear(); @@ -854,120 +910,71 @@ class argument_parser { return compound_toks; } - /** - * @brief Get the unstripped token value (including the flag prefix). - * - * Given an argument token, this function reconstructs and returns the original argument string, - * including any flag prefix that may have been stripped during tokenization. - * - * @param tok An argument token, the value of which will be processed. - * @return The reconstructed argument value: - * - If the token type is `t_flag_primary`, returns the value prefixed with "--". - * - If the token type is `t_flag_secondary`, returns the value prefixed with "-". - * - For all other token types, returns the token's value as is (without any prefix). - */ - [[nodiscard]] std::string _unstripped_token_value(const detail::argument_token& tok - ) const noexcept { - switch (tok.type) { - case detail::argument_token::t_flag_primary: - return std::format("{}{}", this->_flag_prefix, tok.value); - case detail::argument_token::t_flag_secondary: - return std::format("{}{}", this->_flag_prefix_char, tok.value); - default: - return tok.value; - } - } - /** * @brief Implementation of parsing command-line arguments. * @param arg_tokens The list of command-line argument tokens. - * @param handle_unknown A flag specifying whether unknown arguments should be handled or collected. + * @param state The current parsing state. * @throws ap::parsing_failure */ - void _parse_args_impl( - const arg_token_list_t& arg_tokens, - std::vector& unknown_args, - const bool handle_unknown = true - ) { - // set the current argument indicators - arg_ptr_opt_t curr_arg_opt = std::nullopt; - arg_ptr_list_iter_t curr_positional_arg_it = this->_positional_args.begin(); - - if (curr_positional_arg_it != this->_positional_args.end()) - curr_arg_opt.emplace(*curr_positional_arg_it); + void _parse_args_impl(const arg_token_list_t& arg_tokens, parsing_state& state) { + if (state.curr_pos_arg_it != this->_positional_args.end()) + state.curr_arg = *state.curr_pos_arg_it; // process argument tokens std::ranges::for_each( - arg_tokens, - std::bind_front( - &argument_parser::_parse_token, - this, - std::ref(curr_arg_opt), - std::ref(curr_positional_arg_it), - std::ref(unknown_args), - handle_unknown - ) + arg_tokens, std::bind_front(&argument_parser::_parse_token, this, std::ref(state)) ); } /** * @brief Parse a single command-line argument token. - * @param curr_arg_opt The currently processed argument. - * @param curr_positional_arg_it An iterator pointing to the current positional argument. - * @param unknown_args The unknown arguments collection. - * @param handle_unknown A flag specifying whether unknown arguments should be handled or collected. - * @param tok The argument token to be processed. + * @param curr_arg The currently processed argument. + * @param state The current parsing state. * @throws ap::parsing_failure */ - void _parse_token( - arg_ptr_opt_t& curr_arg_opt, - arg_ptr_list_iter_t& curr_positional_arg_it, - std::vector& unknown_args, - const bool handle_unknown, - const detail::argument_token& tok - ) { + void _parse_token(parsing_state& state, const detail::argument_token& tok) { switch (tok.type) { case detail::argument_token::t_flag_primary: [[fallthrough]]; case detail::argument_token::t_flag_secondary: { if (not tok.is_valid_flag_token()) { - if (handle_unknown) { - throw parsing_failure::unrecognized_argument(this->_unstripped_token_value(tok) - ); + if (state.fail_on_unknown) { + throw parsing_failure::unrecognized_argument(tok.value); } else { - curr_arg_opt.reset(); - unknown_args.emplace_back(this->_unstripped_token_value(tok)); + state.curr_arg.reset(); + state.unknown_args.emplace_back(tok.value); break; } } - if (tok.arg->get()->mark_used()) - curr_arg_opt = tok.arg; + if (tok.arg->mark_used()) + state.curr_arg = tok.arg; else - curr_arg_opt.reset(); + state.curr_arg.reset(); break; } case detail::argument_token::t_value: { - if (not curr_arg_opt) { - if (curr_positional_arg_it == this->_positional_args.end()) { - unknown_args.emplace_back(tok.value); + if (not state.curr_arg) { + if (state.curr_pos_arg_it == this->_positional_args.end()) { + state.unknown_args.emplace_back(tok.value); break; } - curr_arg_opt.emplace(*curr_positional_arg_it); + state.curr_arg = *state.curr_pos_arg_it; } - if (auto& curr_arg = *curr_arg_opt->get(); not curr_arg.set_value(tok.value)) { - if (curr_arg.is_positional() - and curr_positional_arg_it != this->_positional_args.end() - and ++curr_positional_arg_it != this->_positional_args.end()) { - curr_arg_opt.emplace(*curr_positional_arg_it); + if (not state.curr_arg->set_value(tok.value)) { + // advance to the next positional argument if possible + if (state.curr_arg->is_positional() + and state.curr_pos_arg_it != this->_positional_args.end() + and ++state.curr_pos_arg_it != this->_positional_args.end()) { + state.curr_arg = *state.curr_pos_arg_it; break; } - curr_arg_opt.reset(); + state.curr_arg.reset(); } break; @@ -1027,20 +1034,20 @@ class argument_parser { * @param arg_name The name of the argument. * @return The argument with the specified name, if found; otherwise, std::nullopt. */ - arg_opt_t _get_argument(std::string_view arg_name) const noexcept { + arg_ptr_t _get_argument(std::string_view arg_name) const noexcept { const auto predicate = this->_name_match_predicate(arg_name); if (auto pos_arg_it = std::ranges::find_if(this->_positional_args, predicate); pos_arg_it != this->_positional_args.end()) { - return std::ref(**pos_arg_it); + return *pos_arg_it; } if (auto opt_arg_it = std::ranges::find_if(this->_optional_args, predicate); opt_arg_it != this->_optional_args.end()) { - return std::ref(**opt_arg_it); + return *opt_arg_it; } - return std::nullopt; + return nullptr; } /** @@ -1051,18 +1058,18 @@ class argument_parser { */ [[nodiscard]] arg_ptr_list_iter_t _find_opt_arg(const detail::argument_token& flag_tok ) noexcept { - switch (flag_tok.type) { - case detail::argument_token::t_flag_primary: - return std::ranges::find(this->_optional_args, flag_tok.value, [](const auto& arg_ptr) { - return arg_ptr->name().primary; - }); - case detail::argument_token::t_flag_secondary: - return std::ranges::find(this->_optional_args, flag_tok.value, [](const auto& arg_ptr) { - return arg_ptr->name().secondary; - }); - default: + if (not flag_tok.is_flag_token()) return this->_optional_args.end(); - } + + const auto actual_tok_value = this->_strip_flag_prefix(flag_tok); + const auto match_type = + flag_tok.type == detail::argument_token::t_flag_primary + ? detail::argument_name::m_primary + : detail::argument_name::m_secondary; + + return std::ranges::find_if( + this->_optional_args, this->_name_match_predicate(actual_tok_value, match_type) + ); } /** diff --git a/include/ap/detail/argument_token.hpp b/include/ap/detail/argument_token.hpp index e1fbf346..a532eb1f 100644 --- a/include/ap/detail/argument_token.hpp +++ b/include/ap/detail/argument_token.hpp @@ -7,24 +7,21 @@ #pragma once #include "argument_base.hpp" -#include "typing_utility.hpp" #include -#include -#include #include namespace ap::detail { /// @brief Structure representing a single command-line argument token. struct argument_token { - using arg_ptr_opt_t = uptr_opt_t; + using arg_ptr_t = std::shared_ptr; ///< Argument pointer type alias. /// @brief The token type discriminator. enum class token_type : std::uint8_t { + t_value, ///< Represents a value argument. t_flag_primary, ///< Represents the primary (--) flag argument. - t_flag_secondary, ///< Represents the secondary (-) flag argument. - t_value ///< Represents a value argument. + t_flag_secondary ///< Represents the secondary (-) flag argument. }; using enum token_type; @@ -55,12 +52,12 @@ struct argument_token { * @return true if `type` is either `t_flag_primary` or `t_flag_secondary`, false otherwise. */ [[nodiscard]] bool is_valid_flag_token() const noexcept { - return this->is_flag_token() and this->arg.has_value(); + return this->is_flag_token() and this->arg != nullptr; } token_type type; ///< The token's type discrimiator value. std::string value; ///< The actual token's value. - arg_ptr_opt_t arg = std::nullopt; ///< The corresponding argument. + arg_ptr_t arg = nullptr; ///< The corresponding argument }; } // namespace ap::detail diff --git a/include/ap/detail/concepts.hpp b/include/ap/detail/concepts.hpp index 484b2d8a..1230340a 100644 --- a/include/ap/detail/concepts.hpp +++ b/include/ap/detail/concepts.hpp @@ -116,7 +116,7 @@ template concept c_valid_type = is_valid_type_v; /** - * @brief Validates that R is a range of type T (ignoring the cvref attributes). + * @brief Validates that R is a range of type T (ignoring the cvref qualifiers). * @tparam R The range type to check. * @tparam V The expected range value type. * @tparam TV The validation rule (`same` or `convertible`). @@ -126,15 +126,4 @@ concept c_range_of = std::ranges::range and c_valid_type>, V, TV>; -/** - * @brief Validates that R is a sized range of type T (ignoring the cvref attributes). - * @tparam R The range type to check. - * @tparam V The expected range value type. - * @tparam TV The validation rule (`same` or `convertible`). - */ -template -concept c_sized_range_of = - std::ranges::sized_range - and c_valid_type>, V, TV>; - } // namespace ap::detail diff --git a/tests/include/argument_parser_test_fixture.hpp b/tests/include/argument_parser_test_fixture.hpp index 5a613785..615f0aa2 100644 --- a/tests/include/argument_parser_test_fixture.hpp +++ b/tests/include/argument_parser_test_fixture.hpp @@ -14,15 +14,17 @@ using ap::detail::c_argument_value_type; namespace ap_testing { struct argument_parser_test_fixture { - argument_parser_test_fixture() = default; - virtual ~argument_parser_test_fixture() = default; - + using arg_ptr_t = ap::argument_parser::arg_ptr_t; using arg_token_list_t = ap::argument_parser::arg_token_list_t; - using arg_opt_t = ap::argument_parser::arg_opt_t; + + using parsing_state = ap::argument_parser::parsing_state; using argument_value_type = std::string; using invalid_argument_value_type = int; + argument_parser_test_fixture() = default; + virtual ~argument_parser_test_fixture() = default; + // test utility functions [[nodiscard]] std::string init_arg_flag_primary(std::size_t i) const { return std::format("--test-arg-{}", i); @@ -32,6 +34,10 @@ struct argument_parser_test_fixture { return std::format("-ta-{}", i); } + [[nodiscard]] std::string strip_flag_prefix(const argument_token& tok) const { + return this->sut._strip_flag_prefix(tok); + } + [[nodiscard]] argument_value_type init_arg_value(std::size_t i) const { return std::format("test-value-{}", i); } @@ -109,9 +115,9 @@ 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( - 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), init_arg_name_secondary(i) + )); } template < @@ -123,7 +129,7 @@ struct argument_parser_test_fixture { F&& setup_arg = [](optional_argument&) {} ) { for (std::size_t i = 0ull; i < n; ++i) - setup_arg(sut.add_optional_argument( + setup_arg(this->sut.add_optional_argument( init_arg_name_primary(begin_idx + i), init_arg_name_secondary(begin_idx + i) )); } @@ -145,12 +151,11 @@ struct argument_parser_test_fixture { for (std::size_t i = 0ull; i < n_optional_args; ++i) { const auto arg_idx = n_positional_args + i; - const auto arg_name = init_arg_name_primary(arg_idx); - argument_token flag_tok{argument_token::t_flag_primary, arg_name}; - const auto opt_arg_it = sut._find_opt_arg(flag_tok); - if (opt_arg_it != sut._optional_args.end()) - flag_tok.arg.emplace(*opt_arg_it); + argument_token flag_tok{argument_token::t_flag_primary, init_arg_flag_primary(arg_idx)}; + const auto opt_arg_it = this->sut._find_opt_arg(flag_tok); + if (opt_arg_it != this->sut._optional_args.end()) + flag_tok.arg = *opt_arg_it; arg_tokens.push_back(std::move(flag_tok)); arg_tokens.push_back(argument_token{argument_token::t_value, init_arg_value(arg_idx)}); @@ -161,32 +166,34 @@ struct argument_parser_test_fixture { // argument_parser private member accessors [[nodiscard]] const std::optional& get_program_name() const { - return sut._program_name; + return this->sut._program_name; } [[nodiscard]] const std::optional& get_program_description() const { - return sut._program_description; + return this->sut._program_description; } [[nodiscard]] const std::optional& get_program_version() const { - return sut._program_version; + return this->sut._program_version; } // private function callers [[nodiscard]] arg_token_list_t tokenize(int argc, char* argv[]) { - return sut._tokenize(std::span(argv + 1, static_cast(argc - 1))); + return this->sut._tokenize(std::span(argv + 1, static_cast(argc - 1))); } void parse_args_impl(const arg_token_list_t& arg_tokens) { - sut._parse_args_impl(arg_tokens, this->unknown_args); + 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); } - [[nodiscard]] arg_opt_t get_argument(std::string_view arg_name) const { - return sut._get_argument(arg_name); + [[nodiscard]] arg_ptr_t get_argument(std::string_view arg_name) const { + return this->sut._get_argument(arg_name); } ap::argument_parser sut; - std::vector unknown_args; + parsing_state state; }; } // namespace ap_testing diff --git a/tests/source/test_argument_parser_add_argument.cpp b/tests/source/test_argument_parser_add_argument.cpp index 8b9d363d..bc17bb4c 100644 --- a/tests/source/test_argument_parser_add_argument.cpp +++ b/tests/source/test_argument_parser_add_argument.cpp @@ -292,11 +292,11 @@ TEST_CASE_FIXTURE( const auto input_arg = get_argument("input"); REQUIRE(input_arg); - CHECK(is_positional(input_arg.value())); + CHECK(is_positional(*input_arg)); const auto output_arg = get_argument("output"); REQUIRE(output_arg); - CHECK(is_positional(output_arg.value())); + CHECK(is_positional(*output_arg)); } TEST_CASE_FIXTURE( @@ -329,14 +329,14 @@ TEST_CASE_FIXTURE( const auto help_arg = get_argument(help_flag); REQUIRE(help_arg); - CHECK(is_optional(help_arg.value())); + CHECK(is_optional(*help_arg)); const auto input_arg = get_argument(input_flag); REQUIRE(input_arg); - CHECK(is_optional(input_arg.value())); + CHECK(is_optional(*input_arg)); const auto output_arg = get_argument(output_flag); REQUIRE(output_arg); - CHECK(is_optional(output_arg.value())); + CHECK(is_optional(*output_arg)); } diff --git a/tests/source/test_argument_parser_parse_args.cpp b/tests/source/test_argument_parser_parse_args.cpp index 8cdc422e..4fa71624 100644 --- a/tests/source/test_argument_parser_parse_args.cpp +++ b/tests/source/test_argument_parser_parse_args.cpp @@ -72,7 +72,8 @@ TEST_CASE_FIXTURE( std::size_t opt_arg_idx = n_positional_args; for (std::size_t i = n_positional_args; i < arg_tokens.size(); i += 2ull) { REQUIRE_EQ(arg_tokens.at(i).type, argument_token::t_flag_primary); - CHECK(init_arg_name(opt_arg_idx).match(arg_tokens.at(i).value)); + const auto stripped_flag = strip_flag_prefix(arg_tokens.at(i)); + CHECK(init_arg_name(opt_arg_idx).match(stripped_flag)); REQUIRE_EQ(arg_tokens.at(i + 1ull).type, argument_token::t_value); CHECK_EQ(arg_tokens.at(i + 1ull).value, init_arg_value(opt_arg_idx)); @@ -97,7 +98,8 @@ TEST_CASE_FIXTURE( CHECK_NOTHROW(parse_args_impl(arg_tokens)); CHECK_NE( - std::ranges::find(unknown_args, init_arg_value(first_opt_arg_idx)), unknown_args.end() + std::ranges::find(state.unknown_args, init_arg_value(first_opt_arg_idx)), + state.unknown_args.end() ); } @@ -110,7 +112,7 @@ TEST_CASE_FIXTURE( const auto arg_tokens = init_arg_tokens(n_positional_args, n_optional_args); CHECK_NOTHROW(parse_args_impl(arg_tokens)); - CHECK(unknown_args.empty()); + CHECK(state.unknown_args.empty()); } // _get_argument diff --git a/tests/source/test_argument_token.cpp b/tests/source/test_argument_token.cpp index b05a9c49..36717e7d 100644 --- a/tests/source/test_argument_token.cpp +++ b/tests/source/test_argument_token.cpp @@ -54,10 +54,9 @@ TEST_CASE("is_valid_flag_token should return true if the token is a flag token a CHECK_FALSE(sut_type{t_flag_primary, ""}.is_valid_flag_token()); CHECK_FALSE(sut_type{t_flag_secondary, ""}.is_valid_flag_token()); - std::unique_ptr arg_ptr = - std::make_unique>(argument_name{""}); - const typename sut_type::arg_ptr_opt_t arg_ptr_opt{std::ref(arg_ptr)}; + std::shared_ptr arg_ptr = + std::make_shared>(argument_name{""}); - CHECK(sut_type{t_flag_primary, "", arg_ptr_opt}.is_valid_flag_token()); - CHECK(sut_type{t_flag_secondary, "", arg_ptr_opt}.is_valid_flag_token()); + CHECK(sut_type{t_flag_primary, "", arg_ptr}.is_valid_flag_token()); + CHECK(sut_type{t_flag_secondary, "", arg_ptr}.is_valid_flag_token()); } From 06d715ce4eea4810fddcee43100591ac04213cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Musia=C5=82?= <111433005+SpectraL519@users.noreply.github.com> Date: Fri, 19 Sep 2025 13:17:12 +0200 Subject: [PATCH 05/10] YT-CPPAP-57: Separate generic utility from the detail namespace - Moved the common utility elements to the `util` namespace - Moved the action-related utility elements to the `action::util` namespace - Added a `Utility` group in the documentation output - Added some missing doc comments --- CMakeLists.txt | 2 +- Doxyfile | 4 +- MODULE.bazel | 2 +- README.md | 1 + docs/groups.dox | 9 ++ docs/tutorial.md | 49 ++++++----- include/ap/action/predefined.hpp | 24 +++--- include/ap/action/types.hpp | 8 +- include/ap/action/util/concepts.hpp | 44 ++++++++++ .../{detail/utility.hpp => util/helpers.hpp} | 40 +++------ include/ap/argument.hpp | 85 +++++++++---------- include/ap/argument_parser.hpp | 47 +++++----- include/ap/detail/argument_descriptor.hpp | 18 ++-- include/ap/exceptions.hpp | 8 +- include/ap/{detail => util}/concepts.hpp | 19 ++++- .../ranges_utility.hpp => util/ranges.hpp} | 17 +++- .../str_utility.hpp => util/string.hpp} | 10 ++- .../typing_utility.hpp => util/typing.hpp} | 20 +++-- tests/CMakeLists.txt | 2 +- .../include/argument_parser_test_fixture.hpp | 4 +- tests/include/argument_test_fixture.hpp | 6 +- tests/include/utility.hpp | 4 +- tests/source/test_optional_argument.cpp | 6 +- tests/source/test_positional_argument.cpp | 4 +- ...tr_utility.cpp => test_string_utility.cpp} | 4 +- 25 files changed, 256 insertions(+), 181 deletions(-) create mode 100644 docs/groups.dox create mode 100644 include/ap/action/util/concepts.hpp rename include/ap/action/{detail/utility.hpp => util/helpers.hpp} (60%) rename include/ap/{detail => util}/concepts.hpp (92%) rename include/ap/{detail/ranges_utility.hpp => util/ranges.hpp} (54%) rename include/ap/{detail/str_utility.hpp => util/string.hpp} (90%) rename include/ap/{detail/typing_utility.hpp => util/typing.hpp} (72%) rename tests/source/{test_str_utility.cpp => test_string_utility.cpp} (95%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 11453918..b39ed06b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ else() endif() project(cpp-ap - VERSION 3.0.0.3 + VERSION 3.0.0.4 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 45c3db63..0c61af0b 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.3 +PROJECT_NUMBER = 3.0.0.4 # 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 @@ -989,7 +989,7 @@ INPUT_FILE_ENCODING = # be provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, # *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. -FILE_PATTERNS = *.c *.cpp *.h *.hpp *.md *.py +FILE_PATTERNS = *.c *.cpp *.h *.hpp *.md *.py *.dox # The RECURSIVE tag can be used to specify whether or not subdirectories should # be searched for input files as well. diff --git a/MODULE.bazel b/MODULE.bazel index 0cf5f8a1..789c11a5 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,4 +1,4 @@ module( name = "cpp-ap", - version = "3.0.0.3", + version = "3.0.0.4", ) diff --git a/README.md b/README.md index 09f34eac..6cec0ddd 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ Command-line argument parser for C++20 - [Parsing Known Arguments](/docs/tutorial.md#parsing-known-arguments) - [Retrieving Argument Values](/docs/tutorial.md#retrieving-argument-values) - [Examples](/docs/tutorial.md#examples) + - [Common Utility](/docs/tutorial.md#common-utility) - [Dev notes](/docs/dev_notes.md#dev-notes) - [Building and testing](/docs/dev_notes.md#building-and-testing) - [Formatting](/docs/dev_notes.md#formatting) diff --git a/docs/groups.dox b/docs/groups.dox new file mode 100644 index 00000000..129940d1 --- /dev/null +++ b/docs/groups.dox @@ -0,0 +1,9 @@ +/** + * @file groups.dox + * @brief Defines custom groups for documentation. + */ + +/** + * @defgroup util Utility + * @brief Helper functions, types, concepts, etc. + */ diff --git a/docs/tutorial.md b/docs/tutorial.md index 55aa79de..4a498082 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -17,6 +17,7 @@ - [Parsing Known Arguments](#parsing-known-arguments) - [Retrieving Argument Values](#retrieving-argument-values) - [Examples](#examples) +- [Common Utility](#common-utility)
@@ -618,7 +619,7 @@ Command Result Throws if the provided file path does not exist. ```cpp - detail::callable_type check_file_exists() noexcept; + ap::action::util::callable_type check_file_exists() noexcept; ``` - `gt` | observe (value type: [arithmetic](https://en.cppreference.com/w/cpp/types/is_arithmetic)) @@ -626,8 +627,8 @@ Command Result Validates that the value is strictly greater than `lower_bound`. ```cpp - template - detail::callable_type gt(const T lower_bound) noexcept; + template + ap::action::util::callable_type gt(const T lower_bound) noexcept; ``` - `geq` | observe (value type: [arithmetic](https://en.cppreference.com/w/cpp/types/is_arithmetic)) @@ -635,8 +636,8 @@ Command Result Validates that the value is greater than or equal to `lower_bound`. ```cpp - template - detail::callable_type geq(const T lower_bound) noexcept; + template + ap::action::util::callable_type geq(const T lower_bound) noexcept; ``` - `lt` | observe (value type: [arithmetic](https://en.cppreference.com/w/cpp/types/is_arithmetic)) @@ -644,8 +645,8 @@ Command Result Validates that the value is strictly less than `upper_bound`. ```cpp - template - detail::callable_type lt(const T upper_bound) noexcept + template + ap::action::util::callable_type lt(const T upper_bound) noexcept ``` - `leq` | observe (value type: [arithmetic](https://en.cppreference.com/w/cpp/types/is_arithmetic)) @@ -653,8 +654,8 @@ Command Result Validates that the value is less than or equal to `upper_bound`. ```cpp - template - detail::callable_type leq(const T upper_bound) noexcept + template + ap::action::util::callable_type leq(const T upper_bound) noexcept ``` - `within` | observe (value type: [arithmetic](https://en.cppreference.com/w/cpp/types/is_arithmetic)) @@ -662,8 +663,8 @@ Command Result Checks if the value is within the given interval. Bound inclusivity is customizable using template parameters. ```cpp - template - detail::callable_type within( + template + ap::action::util::callable_type within( const T lower_bound, const T upper_bound ) noexcept ``` @@ -805,7 +806,7 @@ int main(int argc, char* argv[]) { // use the program's arguments std::cout << "positional: " << parser.value("positional") << std::endl - << "optional: " << join(parser.values("optional")) << std::endl + << "optional: " << ap::util::join(parser.values("optional")) << std::endl << "flag: " << std::boolalpha << parser.value("flag") << std::endl; return 0; @@ -837,8 +838,8 @@ Here, the argument is parsed only if either `--optional` (primary flag) or `-o` > > parser.try_parse_args(argc, argv); > -> std::cout << "first: " << join(parser.values("first")) << std::endl -> << "second: " << join(parser.values("second")) << std::endl; +> std::cout << "first: " << ap::util::join(parser.values("first")) << std::endl +> << "second: " << ap::util::join(parser.values("second")) << std::endl; > > /* Example execution: > > ./program --first value1 value2 --second value3 value4 @@ -988,7 +989,7 @@ parser.try_parse_args(argc, argv); std::cout << "Verbosity level: " << parser.count("verbose") << "\nOption used: " << std::boolalpha << parser.value("use-option") - << "\nNumbers: " << join(parser.values("numbers"), ", ") // join is an imaginary function :) + << "\nNumbers: " << ap::util::join(parser.values("numbers"), ", ") << std::endl; /* @@ -1020,7 +1021,7 @@ parser.add_optional_argument("recognized", "r") parser.parse_args(argc, argv); -std::cout << "recognized = " << join(parser.values("recognized")) << std::endl; +std::cout << "recognized = " << ap::util::join(parser.values("recognized")) << std::endl; /* Example executions: > ./program --recognized value1 value2 @@ -1052,8 +1053,8 @@ parser.add_optional_argument("recognized", "r") const auto unknown_args = parser.parse_known_args(argc, argv); -std::cout << "recognized = " << join(parser.values("recognized")) << std::endl - << "unkown = " << join(unknown_args) << std::endl; +std::cout << "recognized = " << ap::util::join(parser.values("recognized")) << std::endl + << "unkown = " << ap::util::join(unknown_args) << std::endl; /* Example execution: > ./program value0 --recognized value1 value2 value3 --unrecognized value @@ -1077,8 +1078,8 @@ Now all the values, that caused an exception for the `parse_args` example, are c > const auto unknown_args = parser.parse_known_args(argc, argv); > > std::cout << "positional = " << parser.value("positional") << std::endl -> << "recognized = " << join(parser.values("recognized")) << std::endl -> << "unkown = " << join(unknown_args) << std::endl; +> << "recognized = " << ap::util::join(parser.values("recognized")) << std::endl +> << "unkown = " << ap::util::join(unknown_args) << std::endl; > > /* Example execution: > > ./program --recognized value1 value2 value3 --unrecognized value4 value5 --recognized value6 @@ -1144,3 +1145,11 @@ You can retrieve the argument's value(s) with: ## Examples The library usage examples / demo projects can be found in the [cpp-ap-demo](https://github.com/SpectraL519/cpp-ap-demo) repository. + +
+
+
+ +## Common Utility + +The CPP-AP library provides some additional utility, the descriptions of which can be found on the [Utility topic page](https://spectral519.github.io/cpp-ap/latest/group__util.html). diff --git a/include/ap/action/predefined.hpp b/include/ap/action/predefined.hpp index c6bfee1d..8603274b 100644 --- a/include/ap/action/predefined.hpp +++ b/include/ap/action/predefined.hpp @@ -7,7 +7,7 @@ #pragma once #include "ap/exceptions.hpp" -#include "detail/utility.hpp" +#include "util/helpers.hpp" #include #include @@ -38,7 +38,7 @@ inline typename ap::action_type::on_flag::type print_config( } /// @brief Returns an *observe* action which checks whether lower_bound file with the given name exists. -inline detail::callable_type check_file_exists() noexcept { +inline util::callable_type check_file_exists() noexcept { return [](const std::string& file_path) { if (not std::filesystem::exists(file_path)) throw std::filesystem::filesystem_error( @@ -54,8 +54,8 @@ inline detail::callable_type check_file_e * @tparam T The *arithmetic* value type. * @param lower_bound The exclusive lower bound to validate against. */ -template -detail::callable_type gt(const T lower_bound) noexcept { +template +util::callable_type gt(const T lower_bound) noexcept { return [lower_bound](const T& value) { if (not (value > lower_bound)) throw std::out_of_range( @@ -69,8 +69,8 @@ detail::callable_type gt(const T lower_bound) noexc * @tparam T The *arithmetic* value type. * @param lower_bound The inclusive lower bound to validate against. */ -template -detail::callable_type geq(const T lower_bound) noexcept { +template +util::callable_type geq(const T lower_bound) noexcept { return [lower_bound](const T& value) { if (! (value >= lower_bound)) throw std::out_of_range( @@ -84,8 +84,8 @@ detail::callable_type geq(const T lower_bound) noex * @tparam T The *arithmetic* value type. * @param lower_bound The exclusive upper bound to validate against. */ -template -detail::callable_type lt(const T upper_bound) noexcept { +template +util::callable_type lt(const T upper_bound) noexcept { return [upper_bound](const T& value) { if (! (value < upper_bound)) throw std::out_of_range( @@ -99,8 +99,8 @@ detail::callable_type lt(const T upper_bound) noexc * @tparam T The *arithmetic* value type. * @param lower_bound The inclusive upper bound to validate against. */ -template -detail::callable_type leq(const T upper_bound) noexcept { +template +util::callable_type leq(const T upper_bound) noexcept { return [upper_bound](const T& value) { if (! (value <= upper_bound)) throw std::out_of_range( @@ -120,8 +120,8 @@ detail::callable_type leq(const T upper_bound) noex * @param lower_bound The lower bound of the interval. * @param upper_bound The upper bound of the interval. */ -template -detail::callable_type within( +template +util::callable_type within( const T lower_bound, const T upper_bound ) noexcept { return [lower_bound, upper_bound](const T& value) { diff --git a/include/ap/action/types.hpp b/include/ap/action/types.hpp index b1b601c1..05ea9820 100644 --- a/include/ap/action/types.hpp +++ b/include/ap/action/types.hpp @@ -9,7 +9,7 @@ #pragma once -#include "ap/detail/concepts.hpp" +#include "ap/util/concepts.hpp" #include @@ -22,7 +22,7 @@ namespace ap::action_type { * performs some logic on it without modifying it. */ struct observe { - template + template using type = std::function; }; @@ -33,7 +33,7 @@ struct observe { * returns a new value with which the argument will be initialized. */ struct transform { - template + template using type = std::function; }; @@ -44,7 +44,7 @@ struct transform { * already initialized argument. */ struct modify { - template + template using type = std::function; }; diff --git a/include/ap/action/util/concepts.hpp b/include/ap/action/util/concepts.hpp new file mode 100644 index 00000000..d6547ffc --- /dev/null +++ b/include/ap/action/util/concepts.hpp @@ -0,0 +1,44 @@ +// 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/action/util/concepts.hpp + * @brief Defines action-related concepts. + */ + +#pragma once + +#include "ap/action/types.hpp" + +#include +#include + +namespace ap::action::util { + +/** + * @brief The concept is satisfied when `AS` is a valid *value* action action specifier. + * @tparam AS The action specifier type. + * @ingroup util + */ +template +concept c_value_action_specifier = + ap::util::c_one_of; + +/** + * @brief The concept is satisfied when `AS` is a valid *on-flag* action action specifier. + * @tparam AS The action specifier type. + * @ingroup util + */ +template +concept c_flag_action_specifier = ap::util::c_one_of; + +/** + * @brief The concept is satisfied when `AS` is a valid action action specifier. + * @tparam AS The action specifier type. + * @ingroup util + */ +template +concept c_action_specifier = c_value_action_specifier or std::same_as; + +} // namespace ap::action::util diff --git a/include/ap/action/detail/utility.hpp b/include/ap/action/util/helpers.hpp similarity index 60% rename from include/ap/action/detail/utility.hpp rename to include/ap/action/util/helpers.hpp index ebec8951..092fba05 100644 --- a/include/ap/action/detail/utility.hpp +++ b/include/ap/action/util/helpers.hpp @@ -3,47 +3,26 @@ // Licensed under the MIT License. See the LICENSE file in the project root for full license information. /** - * @file ap/action/detail/utility.hpp - * @brief Defines general action-related utility. + * @file ap/action/util/helpers.hpp + * @brief Defines general action-related helper utility. */ #pragma once -#include "ap/action/types.hpp" +#include "concepts.hpp" #include #include -namespace ap::action::detail { - -/** - * @brief The concept is satisfied when `AS` is a valid *value* action action specifier. - * @tparam AS The action specifier type. - */ -template -concept c_value_action_specifier = - ap::detail::c_one_of; - -/** - * @brief The concept is satisfied when `AS` is a valid *on-flag* action action specifier. - * @tparam AS The action specifier type. - */ -template -concept c_flag_action_specifier = ap::detail::c_one_of; - -/** - * @brief The concept is satisfied when `AS` is a valid action action specifier. - * @tparam AS The action specifier type. - */ -template -concept c_action_specifier = c_value_action_specifier or std::same_as; +namespace ap::action::util { /// @brief Template argument action callable type alias. -template +/// @ingroup util +template using callable_type = typename AS::template type; /// @brief Template argument action callabla variant type alias. -template +template using value_action_variant_type = std::variant< callable_type, callable_type, @@ -52,8 +31,9 @@ using value_action_variant_type = std::variant< /** * @brief A visitor structure used to apply *value* actions. * @tparam T The argument's value type + * @ingroup util */ -template +template struct apply_visitor { using value_type = T; @@ -84,4 +64,4 @@ struct apply_visitor { value_type& value; ///< A reference to the argument's value for which the action will be applied. }; -} // namespace ap::action::detail +} // namespace ap::action::util diff --git a/include/ap/argument.hpp b/include/ap/argument.hpp index 82a36bb7..b9b94ee8 100644 --- a/include/ap/argument.hpp +++ b/include/ap/argument.hpp @@ -6,14 +6,14 @@ #pragma once -#include "action/detail/utility.hpp" #include "action/predefined.hpp" +#include "action/util/helpers.hpp" #include "detail/argument_base.hpp" #include "detail/argument_descriptor.hpp" -#include "detail/concepts.hpp" -#include "detail/ranges_utility.hpp" #include "nargs/range.hpp" #include "types.hpp" +#include "util/concepts.hpp" +#include "util/ranges.hpp" #ifdef AP_TESTING @@ -54,7 +54,7 @@ enum class argument_type : bool { positional, optional }; * @tparam ArgT The argument type, either @ref ap::argument_type::positional or @ref ap::argument_type::optional. * @tparam T The value type accepted by the argument (defaults to std::string). */ -template +template class argument : public detail::argument_base { public: using value_type = T; ///< The argument's value type alias. @@ -178,7 +178,7 @@ class argument : public detail::argument_base { * @note The method is enabled only if `value_type` is not `none_type`. */ argument& nargs(const nargs::range& range) noexcept - requires(not detail::c_is_none) + requires(not util::c_is_none) { this->_nargs_range = range; return *this; @@ -191,7 +191,7 @@ class argument : public detail::argument_base { * @note The method is enabled only if `value_type` is not `none_type`. */ argument& nargs(const count_type n) noexcept - requires(not detail::c_is_none) + requires(not util::c_is_none) { this->_nargs_range = nargs::range(n); return *this; @@ -205,7 +205,7 @@ class argument : public detail::argument_base { * @note The method is enabled only if `value_type` is not `none_type`. */ argument& nargs(const count_type lower, const count_type upper) noexcept - requires(not detail::c_is_none) + requires(not util::c_is_none) { this->_nargs_range = nargs::range(lower, upper); return *this; @@ -221,11 +221,11 @@ class argument : public detail::argument_base { * @note - `value_type` is not `none_type`. * @note - `AS` is a valid value action specifier: `action_type::observe`, `action_type::transform`, `action_type::modify`. */ - template + template argument& action(F&& action) noexcept - requires(not detail::c_is_none) + requires(not util::c_is_none) { - using callable_type = action::detail::callable_type; + using callable_type = action::util::callable_type; this->_value_actions.emplace_back(std::forward(action)); return *this; } @@ -238,7 +238,7 @@ class argument : public detail::argument_base { * @return Reference to the argument instance. * @note The method is enabled only for optional arguments and if `AS` is `action_type::on_flag`. */ - template + template argument& action(F&& action) noexcept requires(type == argument_type::optional) { @@ -255,9 +255,9 @@ class argument : public detail::argument_base { * @note - `value_type` must not be `none_type` and must be equality comparable * @note - `CR` must be a range such that its value type is convertible to the argument's `value_type` */ - template CR> + template CR> argument& choices(const CR& choices) noexcept - requires(not detail::c_is_none and std::equality_comparable) + requires(not util::c_is_none and std::equality_comparable) { for (const auto& choice : choices) this->_choices.emplace_back(choice); @@ -271,7 +271,7 @@ class argument : public detail::argument_base { * @note The method is enabled only if `value_type` is not `none_type` and is equality comparable. */ argument& choices(std::initializer_list choices) noexcept - requires(not detail::c_is_none and std::equality_comparable) + requires(not util::c_is_none and std::equality_comparable) { return this->choices<>(choices); } @@ -284,7 +284,7 @@ class argument : public detail::argument_base { * @note The method is enabled only if `value_type` is not `none_type` and is equality comparable. */ argument& choices(const std::convertible_to auto&... choices) noexcept - requires(not detail::c_is_none and std::equality_comparable) + requires(not util::c_is_none and std::equality_comparable) { (this->_choices.emplace_back(choices), ...); return *this; @@ -297,9 +297,9 @@ class argument : public detail::argument_base { * @attention Setting the default values resets the `required` attribute to `false`. * @note The method is enabled only if `value_type` is not `none_type`. */ - template CR> + template CR> argument& default_values(const CR& values) noexcept - requires(not detail::c_is_none and std::equality_comparable) + requires(not util::c_is_none and std::equality_comparable) { for (const auto& value : values) this->_default_values.emplace_back(std::make_any(value)); @@ -315,7 +315,7 @@ class argument : public detail::argument_base { * @note The method is enabled only if `value_type` is not `none_type`. */ argument& default_values(std::initializer_list values) noexcept - requires(not detail::c_is_none and std::equality_comparable) + requires(not util::c_is_none and std::equality_comparable) { return this->default_values<>(values); } @@ -328,7 +328,7 @@ class argument : public detail::argument_base { * @note The method is enabled only if `value_type` is not `none_type`. */ argument& default_values(const std::convertible_to auto&... values) noexcept - requires(not detail::c_is_none) + requires(not util::c_is_none) { (this->_default_values.emplace_back(std::make_any(values)), ...); this->_required = false; @@ -342,9 +342,9 @@ class argument : public detail::argument_base { * @return Reference to the optional argument instance. * @note The method is enabled only for optional arguments and if `value_type` is not `none_type`. */ - template CR> + template CR> argument& implicit_values(const CR& values) noexcept - requires(not detail::c_is_none and type == argument_type::optional) + requires(not util::c_is_none and type == argument_type::optional) { for (const auto& value : values) this->_implicit_values.emplace_back(std::make_any(value)); @@ -358,7 +358,7 @@ class argument : public detail::argument_base { * @note The method is enabled only for optional arguments and if `value_type` is not `none_type`. */ argument& implicit_values(std::initializer_list values) noexcept - requires(not detail::c_is_none and type == argument_type::optional) + requires(not util::c_is_none and type == argument_type::optional) { return this->implicit_values<>(values); } @@ -370,7 +370,7 @@ class argument : public detail::argument_base { * @note The method is enabled only for optional arguments and if `value_type` is not `none_type`. */ argument& implicit_values(const std::convertible_to auto&... values) noexcept - requires(not detail::c_is_none and type == argument_type::optional) + requires(not util::c_is_none and type == argument_type::optional) { (this->_implicit_values.emplace_back(std::make_any(values)), ...); return *this; @@ -382,7 +382,7 @@ class argument : public detail::argument_base { private: /// @brief The argument's value action type alias. - using value_action_type = action::detail::value_action_variant_type; + using value_action_type = action::util::value_action_variant_type; /// @brief The argument's flag action type alias. using flag_action_type = typename action_type::on_flag::type; @@ -391,7 +391,7 @@ class argument : public detail::argument_base { /// @tparam _T The actual type used if the argument's `value_type` is not `none_type`. template using value_arg_specific_type = std::conditional_t< - detail::c_is_none, + util::c_is_none, none_type, _T>; ///< Type alias for value-argument-specific types. @@ -426,19 +426,18 @@ class argument : public detail::argument_base { desc.add_param("bypass required", "true"); if (this->_nargs_range != _default_nargs_range) desc.add_param("nargs", this->_nargs_range); - if constexpr (detail::c_writable) { + if constexpr (util::c_writable) { if (not this->_choices.empty()) desc.add_range_param("choices", this->_choices); if (not this->_default_values.empty()) desc.add_range_param( - "default value(s)", - detail::any_range_cast_view(this->_default_values) + "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( "implicit value(s)", - detail::any_range_cast_view(this->_implicit_values) + util::any_range_cast_view(this->_implicit_values) ); } } @@ -518,7 +517,7 @@ class argument : public detail::argument_base { if (this->has_parsed_values()) return this->_values.front(); - if constexpr (detail::c_is_none) + if constexpr (util::c_is_none) throw std::logic_error( std::format("No values parsed for argument '{}'.", this->_name.str()) ); @@ -532,13 +531,13 @@ class argument : public detail::argument_base { } [[nodiscard]] const std::vector& _values_impl() const noexcept - requires(detail::c_is_none) + requires(util::c_is_none) { return this->_values; } [[nodiscard]] const std::vector& _values_impl() const noexcept - requires(not detail::c_is_none) + requires(not util::c_is_none) { if (this->has_parsed_values()) return this->_values; @@ -553,7 +552,7 @@ class argument : public detail::argument_base { /// @return `true` if the argument has a predefined value, `false` otherwise. [[nodiscard]] bool _has_predefined_values_impl() const noexcept - requires(detail::c_is_none) + requires(util::c_is_none) { return false; } @@ -565,7 +564,7 @@ class argument : public detail::argument_base { * @note - For optional arguments, a predefined value exists if either a default value is set or if the argument has been used and an implicit value is set. */ [[nodiscard]] bool _has_predefined_values_impl() const noexcept - requires(not detail::c_is_none) + requires(not util::c_is_none) { if constexpr (type == argument_type::positional) return not this->_default_values.empty(); @@ -582,7 +581,7 @@ class argument : public detail::argument_base { * @note - For optional arguments, if the argument has been used, the implicit value list is returned, otherwise the default value list is returned. */ [[nodiscard]] const std::vector& _predefined_values() const - requires(not detail::c_is_none) + requires(not util::c_is_none) { if constexpr (type == argument_type::optional) { if (this->is_used()) { @@ -611,7 +610,7 @@ class argument : public detail::argument_base { /// @return `true` if the given value is a valid choice for the argument, `false` otherwise. /// @todo Use std::ranges::contains after the switch to C++23 [[nodiscard]] bool _is_valid_choice(const value_type& value) const noexcept - requires(not detail::c_is_none) + requires(not util::c_is_none) { return this->_choices.empty() or std::ranges::find(this->_choices, value) != this->_choices.end(); @@ -624,7 +623,7 @@ class argument : public detail::argument_base { * @attention Always throws! (`set_value` should never be called for a none-type argument). */ bool _set_value_impl(const std::string& str_value) - requires(detail::c_is_none) + requires(util::c_is_none) { throw parsing_failure(std::format( "Cannot set values for a none-type argument '{}' (value: '{}')", @@ -644,13 +643,13 @@ class argument : public detail::argument_base { * @note The method is enabled only if `value_type` is not `none_type`. */ bool _set_value_impl(const std::string& str_value) - requires(not detail::c_is_none) + requires(not util::c_is_none) { if (not this->_accepts_further_values()) throw parsing_failure::invalid_nvalues(this->_name, std::weak_ordering::greater); value_type value; - if constexpr (detail::c_trivially_readable) { + if constexpr (util::c_trivially_readable) { value = value_type(str_value); } else { @@ -661,7 +660,7 @@ class argument : public detail::argument_base { if (not this->_is_valid_choice(value)) throw parsing_failure::invalid_choice(this->_name, str_value); - const auto apply_visitor = action::detail::apply_visitor{value}; + const auto apply_visitor = action::util::apply_visitor{value}; for (const auto& action : this->_value_actions) std::visit(apply_visitor, action); @@ -698,7 +697,7 @@ class argument : public detail::argument_base { static constexpr nargs::range _default_nargs_range = (type == argument_type::positional) ? nargs::range(1ull) : nargs::any(); static constexpr nargs::range _default_nargs_range_actual = - detail::c_is_none ? nargs::range(0ull) : _default_nargs_range; + util::c_is_none ? nargs::range(0ull) : _default_nargs_range; }; /** @@ -706,7 +705,7 @@ class argument : public detail::argument_base { * @tparam T The value type accepted by the argument (defaults to std::string). * @see ap::argument */ -template +template using positional_argument = argument; /** @@ -714,7 +713,7 @@ using positional_argument = argument; * @tparam T The value type accepted by the argument (defaults to std::string). * @see ap::argument */ -template +template using optional_argument = argument; } // namespace ap diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index ea48de55..bc051550 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -11,7 +11,6 @@ #include "argument.hpp" #include "detail/argument_token.hpp" -#include "detail/concepts.hpp" #include "types.hpp" #include @@ -70,7 +69,7 @@ class argument_parser { * @return Reference to the argument parser. */ argument_parser& program_name(std::string_view name) { - if (detail::contains_whitespaces(name)) + if (util::contains_whitespaces(name)) throw invalid_configuration("The program name cannot contain whitespace characters!"); this->_program_name.emplace(name); @@ -93,7 +92,7 @@ class argument_parser { * @return Reference to the argument parser. */ argument_parser& program_version(std::string_view version) { - if (detail::contains_whitespaces(version)) + if (util::contains_whitespaces(version)) throw invalid_configuration("The program version cannot contain whitespace characters!" ); @@ -129,7 +128,7 @@ class argument_parser { * @note `arg_discriminators` must be a `std::ranges::range` with the `ap::default_argument` value type. * @return Reference to the argument parser. */ - template AR> + template AR> argument_parser& default_arguments(const AR& arg_discriminators) noexcept { for (const auto arg_discriminator : arg_discriminators) detail::add_default_argument(arg_discriminator, *this); @@ -166,7 +165,7 @@ class argument_parser { * @return Reference to the added positional argument. * @throws ap::invalid_configuration */ - template + template positional_argument& add_positional_argument(const std::string_view primary_name) { this->_verify_arg_name_pattern(primary_name); @@ -186,7 +185,7 @@ class argument_parser { * @return Reference to the added positional argument. * @throws ap::invalid_configuration */ - template + template positional_argument& add_positional_argument( const std::string_view primary_name, const std::string_view secondary_name ) { @@ -212,7 +211,7 @@ class argument_parser { * @return Reference to the added optional argument. * @throws ap::invalid_configuration */ - template + template optional_argument& add_optional_argument( const std::string_view name, const detail::argument_name_discriminator name_discr = n_primary @@ -242,7 +241,7 @@ class argument_parser { * @return Reference to the added optional argument. * @throws ap::invalid_configuration */ - template + template optional_argument& add_optional_argument( const std::string_view primary_name, const std::string_view secondary_name ) { @@ -324,7 +323,7 @@ class argument_parser { * @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(); @@ -369,7 +368,7 @@ class argument_parser { * @note `argv_rng` must be a `std::ranges::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); @@ -417,7 +416,7 @@ class argument_parser { * @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(); @@ -467,7 +466,7 @@ class argument_parser { * @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); @@ -520,7 +519,7 @@ class argument_parser { * @return The value of the argument. * @throws ap::lookup_failure, ap::type_error */ - template + template [[nodiscard]] T value(std::string_view arg_name) const { const auto arg = this->_get_argument(arg_name); if (not arg) @@ -543,7 +542,7 @@ class argument_parser { * @return The value of the argument. * @throws ap::lookup_failure, ap::type_error */ - template U> + template U> [[nodiscard]] T value_or(std::string_view arg_name, U&& fallback_value) const { const auto arg = this->_get_argument(arg_name); if (not arg) @@ -569,7 +568,7 @@ class argument_parser { * @return The values of the argument as a vector. * @throws ap::lookup_failure, ap::type_error */ - template + template [[nodiscard]] std::vector values(std::string_view arg_name) const { const auto arg = this->_get_argument(arg_name); if (not arg) @@ -579,7 +578,7 @@ class argument_parser { std::vector values; // TODO: use std::ranges::to after transition to C++23 std::ranges::copy( - detail::any_range_cast_view(arg->values()), std::back_inserter(values) + util::any_range_cast_view(arg->values()), std::back_inserter(values) ); return values; } @@ -641,7 +640,6 @@ class argument_parser { 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 arg_ptr_opt_t = detail::uptr_opt_t; using const_arg_opt_t = std::optional>; using arg_token_list_t = std::vector; @@ -667,7 +665,7 @@ class argument_parser { arg_name, "An argument name cannot be empty." ); - if (detail::contains_whitespaces(arg_name)) + if (util::contains_whitespaces(arg_name)) throw invalid_configuration::invalid_argument_name( arg_name, "An argument name cannot contain whitespaces." ); @@ -765,7 +763,7 @@ class argument_parser { * @note `arg_range` must be a `std::ranges::range` with a value type convertible to `std::string`. * @return A list of preprocessed command-line argument tokens. */ - template AR> + template AR> [[nodiscard]] arg_token_list_t _tokenize(const AR& arg_range) { arg_token_list_t toks; @@ -810,7 +808,7 @@ class argument_parser { [[nodiscard]] detail::argument_token::token_type _deduce_token_type( const std::string_view arg_value ) const noexcept { - if (detail::contains_whitespaces(arg_value)) + if (util::contains_whitespaces(arg_value)) return detail::argument_token::t_value; if (arg_value.starts_with(this->_flag_prefix)) @@ -827,12 +825,13 @@ class argument_parser { * @param tok The argument token to be processed. * @return The token's value without the flag prefix. */ - [[nodiscard]] std::string _strip_flag_prefix(const detail::argument_token& tok) const noexcept { + [[nodiscard]] std::string_view _strip_flag_prefix(const detail::argument_token& tok + ) const noexcept { switch (tok.type) { case detail::argument_token::t_flag_primary: - return tok.value.substr(this->_primary_flag_prefix_length); + return std::string_view(tok.value).substr(this->_primary_flag_prefix_length); case detail::argument_token::t_flag_secondary: - return tok.value.substr(this->_secondary_flag_prefix_length); + return std::string_view(tok.value).substr(this->_secondary_flag_prefix_length); default: return tok.value; } @@ -845,7 +844,7 @@ class argument_parser { */ [[nodiscard]] detail::argument_token _build_token(const std::string_view arg_value ) const noexcept { - if (detail::contains_whitespaces(arg_value)) + if (util::contains_whitespaces(arg_value)) return {.type = detail::argument_token::t_value, .value = std::string(arg_value)}; if (arg_value.starts_with(this->_flag_prefix)) diff --git a/include/ap/detail/argument_descriptor.hpp b/include/ap/detail/argument_descriptor.hpp index f8097def..026f878d 100644 --- a/include/ap/detail/argument_descriptor.hpp +++ b/include/ap/detail/argument_descriptor.hpp @@ -9,9 +9,9 @@ #pragma once -#include "ap/detail/argument_name.hpp" -#include "concepts.hpp" -#include "str_utility.hpp" +#include "ap/util/concepts.hpp" +#include "ap/util/string.hpp" +#include "argument_name.hpp" #include #include @@ -49,11 +49,11 @@ class argument_descriptor { /** * @brief Adds a parameter descriptor with the given value. - * @tparam T The type of the parameter; must satisfy the @ref ap::detail::c_writable concept. + * @tparam T The type of the parameter; must satisfy the @ref ap::util::c_writable concept. * @param param_name The parameter's name. * @param value The parameter's value. */ - template + template void add_param(const std::string& param_name, const T& value) { std::ostringstream oss; oss << std::boolalpha << value; @@ -62,19 +62,19 @@ class argument_descriptor { /** * @brief Adds a range parameter descriptor with the given value. - * @tparam R The type of the parameter range. The value type of R must satisfy the @ref ap::detail::c_writable concept. + * @tparam R The type of the parameter range. The value type of R must satisfy the @ref ap::util::c_writable concept. * @param param_name The parameter's name. * @param range The parameter value range. * @param delimiter The delimiter used to separate the range values. */ template - requires(c_writable>) + requires(util::c_writable>) void add_range_param( const std::string& param_name, const R& range, const std::string_view delimiter = default_delimiter ) { - this->params.emplace_back(param_name, join(range, delimiter)); + this->params.emplace_back(param_name, util::join(range, delimiter)); } /** @@ -151,7 +151,7 @@ class argument_descriptor { oss << this->get_basic(indent_width); if (not this->params.empty()) { oss << " (" - << join(this->params | std::views::transform( + << util::join(this->params | std::views::transform( [](const auto& param) { return std::format("{}: {}", param.name, param.value); } )) << ")"; diff --git a/include/ap/exceptions.hpp b/include/ap/exceptions.hpp index e1b72482..7eba079c 100644 --- a/include/ap/exceptions.hpp +++ b/include/ap/exceptions.hpp @@ -7,8 +7,8 @@ #pragma once #include "ap/detail/argument_name.hpp" -#include "ap/detail/str_utility.hpp" -#include "ap/detail/typing_utility.hpp" +#include "ap/util/string.hpp" +#include "ap/util/typing.hpp" #include @@ -93,7 +93,7 @@ struct parsing_failure : public argument_parser_exception { static parsing_failure argument_deduction_failure(const std::vector& values ) noexcept { return parsing_failure( - std::format("Failed to deduce the argument for values [{}]", detail::join(values)) + std::format("Failed to deduce the argument for values [{}]", util::join(values)) ); } @@ -135,7 +135,7 @@ struct type_error : public argument_parser_exception { return type_error(std::format( "Invalid value type specified for argument [{}] = {}.", arg_name.str(), - detail::get_demangled_type_name() + util::get_demangled_type_name() )); } }; diff --git a/include/ap/detail/concepts.hpp b/include/ap/util/concepts.hpp similarity index 92% rename from include/ap/detail/concepts.hpp rename to include/ap/util/concepts.hpp index 1230340a..753f4019 100644 --- a/include/ap/detail/concepts.hpp +++ b/include/ap/util/concepts.hpp @@ -3,7 +3,7 @@ // Licensed under the MIT License. See the LICENSE file in the project root for full license information. /** - * @file ap/detail/concepts.hpp + * @file ap/util/concepts.hpp * @brief Provides the general concept definitions. */ @@ -14,11 +14,12 @@ #include #include -namespace ap::detail { +namespace ap::util { /** * @brief The concept is satisfied when `T` is @ref ap::none_type. * @tparam T Type to check. + * @ingroup util */ template concept c_is_none = std::same_as; @@ -26,6 +27,7 @@ concept c_is_none = std::same_as; /** * @brief The concept is satisfied when `T` overloads the std::istream operator `>>`. * @tparam T Type to check. + * @ingroup util */ template concept c_readable = requires(T value, std::istream& input_stream) { input_stream >> value; }; @@ -33,6 +35,7 @@ concept c_readable = requires(T value, std::istream& input_stream) { input_strea /** * @brief The concept is satisfied when `T` can be constructed from `const std::string&`. * @tparam T Type to check. + * @ingroup util */ template concept c_trivially_readable = std::constructible_from; @@ -40,6 +43,7 @@ concept c_trivially_readable = std::constructible_from; /** * @brief The concept is satisfied when `T` overloads the std::ostream operator `<<`. * @tparam T Type to check. + * @ingroup util */ template concept c_writable = requires(T value, std::ostream& output_stream) { output_stream << value; }; @@ -47,6 +51,7 @@ concept c_writable = requires(T value, std::ostream& output_stream) { output_str /** * @brief The concept is satisfied when `T` is an arithmetic type. * @tparam T Type to check. + * @ingroup util */ template concept c_arithmetic = std::is_arithmetic_v; @@ -54,6 +59,7 @@ concept c_arithmetic = std::is_arithmetic_v; /** * @brief The concept is used to verify the validity of the arguments' value types. * @tparam T Type to check. + * @ingroup util */ template concept c_argument_value_type = @@ -64,12 +70,14 @@ concept c_argument_value_type = * @brief Validates that `T` is the same as one of the types defined by `Types`. * @tparam T Type to check. * @tparam Types The valid types to compare against. + * @ingroup util */ template concept c_one_of = std::disjunction_v...>; /** * @brief Specifies the type validation rule. + * @ingroup util */ enum class type_validator : bool { same, ///< Exact type match. @@ -85,6 +93,7 @@ enum class type_validator : bool { * @tparam T The type to check. * @tparam U The type to check agains. * @tparam TV The validation rule. + * @ingroup util */ template inline constexpr bool is_valid_type_v = false; @@ -93,6 +102,7 @@ inline constexpr bool is_valid_type_v = false; * @brief Checks if `T` and `U` are the same type. * @tparam T The type to check. * @tparam U The type to check agains. + * @ingroup util */ template inline constexpr bool is_valid_type_v = std::same_as; @@ -101,6 +111,7 @@ inline constexpr bool is_valid_type_v = std::same_as * @brief Checks if `T` is convertible to `U`. * @tparam T The type to check. * @tparam U The type to check agains. + * @ingroup util */ template inline constexpr bool is_valid_type_v = @@ -111,6 +122,7 @@ inline constexpr bool is_valid_type_v = * @tparam T The type to check. * @tparam U The type to check agains. * @tparam TV The validation rule (`same` or `convertible`). + * @ingroup util */ template concept c_valid_type = is_valid_type_v; @@ -120,10 +132,11 @@ concept c_valid_type = is_valid_type_v; * @tparam R The range type to check. * @tparam V The expected range value type. * @tparam TV The validation rule (`same` or `convertible`). + * @ingroup util */ template concept c_range_of = std::ranges::range and c_valid_type>, V, TV>; -} // namespace ap::detail +} // namespace ap::util diff --git a/include/ap/detail/ranges_utility.hpp b/include/ap/util/ranges.hpp similarity index 54% rename from include/ap/detail/ranges_utility.hpp rename to include/ap/util/ranges.hpp index 80ae4075..8f82f9be 100644 --- a/include/ap/detail/ranges_utility.hpp +++ b/include/ap/util/ranges.hpp @@ -2,7 +2,10 @@ // 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/detail/ranges_utility.hpp +/** + * @file ap/util/ranges.hpp + * @brief Provides common ranges utility functions. + */ #pragma once @@ -11,8 +14,16 @@ #include #include -namespace ap::detail { +namespace ap::util { +/** + * @brief Casts a range of `std::any` to a range of type `T`. + * @tparam T The target type. + * @param range The input range of `std::any`. + * @return A view representing the casted range of type `T`. + * @throws std::bad_any_cast if any element in the input range cannot be cast to type `T`. + * @ingroup util + */ template auto any_range_cast_view(const c_range_of auto& range) { return range | std::views::transform([](const std::any& value) -> T { @@ -20,4 +31,4 @@ auto any_range_cast_view(const c_range_of auto& range) { }); } -} // namespace ap::detail +} // namespace ap::util diff --git a/include/ap/detail/str_utility.hpp b/include/ap/util/string.hpp similarity index 90% rename from include/ap/detail/str_utility.hpp rename to include/ap/util/string.hpp index f730bbc0..8c640e00 100644 --- a/include/ap/detail/str_utility.hpp +++ b/include/ap/util/string.hpp @@ -3,7 +3,7 @@ // Licensed under the MIT License. See the LICENSE file in the project root for full license information. /** - * @file ap/detail/str_utility.hpp + * @file ap/util/string.hpp * @brief Provides common string utility functions. */ @@ -15,12 +15,13 @@ #include #include -namespace ap::detail { +namespace ap::util { /** * @brief Converts a value to `std::string`. - * @tparam T The value type (must satisfy the @ref ap::detail::c_writable concept). + * @tparam T The value type (must satisfy the @ref ap::util::c_writable concept). * @param value The value to convert. + * @ingroup util */ template [[nodiscard]] std::string as_string(const T& value) noexcept { @@ -41,6 +42,7 @@ template * @param delimiter The separator string to insert between elements. * @return A single string with all elements joined by the delimiter. * \todo Replace with std::views::join after transition to C++23. + * @ingroup util */ template requires(c_writable>) @@ -59,4 +61,4 @@ requires(c_writable>) return oss.str(); } -} // namespace ap::detail +} // namespace ap::util diff --git a/include/ap/detail/typing_utility.hpp b/include/ap/util/typing.hpp similarity index 72% rename from include/ap/detail/typing_utility.hpp rename to include/ap/util/typing.hpp index e985a875..d3f2e0d4 100644 --- a/include/ap/detail/typing_utility.hpp +++ b/include/ap/util/typing.hpp @@ -2,7 +2,10 @@ // 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/detail/typing_utility.hpp +/** + * @file ap/util/typing.hpp + * @brief Provides common typing utility functions. + */ #pragma once @@ -12,11 +15,16 @@ #include #include -namespace ap::detail { - -template -using uptr_opt_t = std::optional>>; +namespace ap::util { +/** + * @brief Retrieves the demangled name of a type `T`. + * @tparam T The type to retrieve the name for. + * @return A string view representing the demangled name of type `T`. + * @note - The function uses compiler-specific macros to extract the type name. + * @note - Supported compilers: GCC, Clang, MSVC. + * @ingroup util + */ template constexpr std::string_view get_demangled_type_name() { #if defined(__clang__) || defined(__GNUC__) @@ -40,4 +48,4 @@ constexpr std::string_view get_demangled_type_name() { #endif } -} // namespace ap::detail +} // namespace ap::util diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3c465b4f..6aae4a83 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -76,4 +76,4 @@ add_doctest("source/test_nargs_range.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_str_utility.cpp") +add_doctest("source/test_string_utility.cpp") diff --git a/tests/include/argument_parser_test_fixture.hpp b/tests/include/argument_parser_test_fixture.hpp index 615f0aa2..d56b6c7b 100644 --- a/tests/include/argument_parser_test_fixture.hpp +++ b/tests/include/argument_parser_test_fixture.hpp @@ -9,7 +9,7 @@ using ap::optional_argument; using ap::positional_argument; using ap::detail::argument_name; using ap::detail::argument_token; -using ap::detail::c_argument_value_type; +using ap::util::c_argument_value_type; namespace ap_testing { @@ -34,7 +34,7 @@ struct argument_parser_test_fixture { return std::format("-ta-{}", i); } - [[nodiscard]] std::string strip_flag_prefix(const argument_token& tok) const { + [[nodiscard]] std::string_view strip_flag_prefix(const argument_token& tok) const { return this->sut._strip_flag_prefix(tok); } diff --git a/tests/include/argument_test_fixture.hpp b/tests/include/argument_test_fixture.hpp index f10f0e41..749640dc 100644 --- a/tests/include/argument_test_fixture.hpp +++ b/tests/include/argument_test_fixture.hpp @@ -1,14 +1,14 @@ #pragma once #include -#include +#include using ap::argument; using ap::argument_type; using ap::detail::argument_descriptor; using ap::detail::argument_name; -using ap::detail::as_string; -using ap::detail::c_argument_value_type; +using ap::util::as_string; +using ap::util::c_argument_value_type; namespace ap_testing { diff --git a/tests/include/utility.hpp b/tests/include/utility.hpp index 8553f895..5e2fd0e7 100644 --- a/tests/include/utility.hpp +++ b/tests/include/utility.hpp @@ -9,12 +9,12 @@ void discard_result(T&&) { // do nothing } -template +template bool is_positional(const ap::detail::argument_base& arg) { return dynamic_cast*>(&arg); } -template +template bool is_optional(const ap::detail::argument_base& arg) { return dynamic_cast*>(&arg); } diff --git a/tests/source/test_optional_argument.cpp b/tests/source/test_optional_argument.cpp index a9bc72ed..5048f030 100644 --- a/tests/source/test_optional_argument.cpp +++ b/tests/source/test_optional_argument.cpp @@ -1,7 +1,7 @@ #include "argument_test_fixture.hpp" #include "doctest.h" -#include +#include using namespace ap_testing; using namespace ap::nargs; @@ -145,11 +145,11 @@ TEST_CASE_FIXTURE( const auto nargs_it = std::ranges::find(desc.params, "nargs", ¶meter_descriptor::name); REQUIRE_NE(nargs_it, desc.params.end()); - CHECK_EQ(nargs_it->value, ap::detail::as_string(non_default_range)); + 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()); - CHECK_EQ(choices_it->value, ap::detail::join(choices, ", ")); + 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); diff --git a/tests/source/test_positional_argument.cpp b/tests/source/test_positional_argument.cpp index 89120d62..e1e7e5c2 100644 --- a/tests/source/test_positional_argument.cpp +++ b/tests/source/test_positional_argument.cpp @@ -136,11 +136,11 @@ TEST_CASE_FIXTURE( const auto nargs_it = std::ranges::find(desc.params, "nargs", ¶meter_descriptor::name); REQUIRE_NE(nargs_it, desc.params.end()); - CHECK_EQ(nargs_it->value, ap::detail::as_string(non_default_range)); + 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()); - CHECK_EQ(choices_it->value, ap::detail::join(choices, ", ")); + 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); diff --git a/tests/source/test_str_utility.cpp b/tests/source/test_string_utility.cpp similarity index 95% rename from tests/source/test_str_utility.cpp rename to tests/source/test_string_utility.cpp index 7b1014ab..cc5c686c 100644 --- a/tests/source/test_str_utility.cpp +++ b/tests/source/test_string_utility.cpp @@ -1,12 +1,12 @@ #include "doctest.h" -#include +#include #include #include #include -using namespace ap::detail; +using namespace ap::util; namespace { From 1e2d705f3a432fc8f6af868b5f209d3c3df71dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Musia=C5=82?= <111433005+SpectraL519@users.noreply.github.com> Date: Sun, 21 Sep 2025 11:20:57 +0200 Subject: [PATCH 06/10] YT-CPPAP-60: Runtime unknown flag handling method parameter - Removed the `AP_UNKNOWN_FLAGS_AS_VALUES` macro support - Introduced the `unknown_policy` discriminator type with the following values: - fail (default): throw an exception when an unknown argument is encountered - warn: issue a warning when an unknown argument is encountered - ignore: ignore unknown arguments - as_values: treat unknown arguments as positional values - Added an `unknown_arguments_policy` method to the `argument_parser` class - Aligned the tokenization and parsing logic to properly handle the new unknown argument flags handling policies - Renamed the `print_config` action and method of `argument_parser` to `print_help` - Changed the value type of the predefined `help` argument from `bool` to `none_type` --- CMakeLists.txt | 2 +- Doxyfile | 2 +- MODULE.bazel | 2 +- docs/tutorial.md | 113 ++++++-- include/ap/action/predefined.hpp | 8 +- include/ap/argument.hpp | 2 +- include/ap/argument_parser.hpp | 261 +++++++++++++----- include/ap/exceptions.hpp | 4 +- tests/CMakeLists.txt | 3 - .../include/argument_parser_test_fixture.hpp | 2 +- .../test_argument_parser_add_argument.cpp | 2 +- .../test_argument_parser_parse_args.cpp | 132 ++++++++- ...ser_parse_args_unknown_flags_as_values.cpp | 55 ---- 13 files changed, 417 insertions(+), 171 deletions(-) delete mode 100644 tests/source/test_argument_parser_parse_args_unknown_flags_as_values.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b39ed06b..b786012e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ else() endif() project(cpp-ap - VERSION 3.0.0.4 + VERSION 3.0.0.5 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 0c61af0b..3c2db87b 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.4 +PROJECT_NUMBER = 3.0.0.5 # 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 789c11a5..135ad513 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,4 +1,4 @@ module( name = "cpp-ap", - version = "3.0.0.4", + version = "3.0.0.5", ) diff --git a/docs/tutorial.md b/docs/tutorial.md index 4a498082..428c7d15 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -112,6 +112,7 @@ 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; @@ -602,12 +603,12 @@ Command Result ### Actions -- `print_config` | on-flag +- `print_help` | on-flag - Prints the configuration of the parser to the output stream and optionally exits with the given code. + Prints the parser's help message to the output stream and optionally exits with the given code. ```cpp - typename ap::action_type::on_flag::type print_config( + typename ap::action_type::on_flag::type print_help( const ap::argument_parser& parser, const std::optional exit_code = std::nullopt, std::ostream& os = std::cout @@ -707,8 +708,8 @@ parser.default_arguments(); ```cpp // equivalent to: - parser.add_flag("help", "h") - .action(action::print_config(arg_parser, EXIT_SUCCESS)) + parser.add_optional_argument("help", "h") + .action(ap::action::print_help(parser, EXIT_SUCCESS)) .help("Display the help message"); ``` @@ -937,33 +938,95 @@ optional: opt-value
-#### 4. Unrecognized argument flag handling +#### 4. Unknown Argument Flag Handling -By default the `argument_parser` class treats *all\** command-line arguments beggining with a `--` or `-` prefix as optional argument flags and if the flag's value does not match any of the specified arguments, then such flag is considered *unknown* and an exception will be thrown. +A command-line argument beginning with a flag prefix (`--` or `-`) that doesn't match any of the specified optional arguments or a compound of optional arguments (only for short flags) is considered **unknown** or **unrecognized**. -> [*all\**] If a command-line argument begins with a flag prefix, but contains whitespaces (e.g. `"--flag value"`), then it is treated as a value and not a flag. +By default an argument parser will throw an exception if an unkown argument flag is encountered. -This behavior can be altered so that the unknown argument flags will be treated as values, not flags. For example: +This behavior can be modified using the `unknown_arguments_policy` method of the `argument_parser` class, which sets the policy for handling unknown argument flags. -```cpp -parser.add_optional_argument("option", "o"); -parser.try_parse_args(argc, argv); -std::cout << "option: " << parser.value("option"); -/* -./program --option --unknown-flag -option: --unknown-flag -``` +**Example:** -To do this add the following in your `CMakeLists.txt` file: -```cmake -target_compile_definitions(cpp-ap PRIVATE AP_UNKNOWN_FLAGS_AS_VALUES) -``` -or simply add: ```cpp -#define AP_UNKNOWN_FLAGS_AS_VALUES +#include + +int main(int argc, char* argv[]) { + ap::argument_parser parser; + + parser.program_name("test") + .program_description("A simple test program") + .default_arguments(ap::default_argument::o_help) + // set the unknown argument flags handling policy + .unknown_arguments_policy(ap::unknown_policy::); + + parser.add_optional_argument("known", "k") + .help("A known optional argument"); + + parser.try_parse_args(argc, argv); + + std::cout << "known = " << ap::util::join(parser.values("known")) << std::endl; + + return 0; +} ``` -before the `#include ` statement. + +The available policies are: +- `ap::unknown_policy::fail` (default) - throws an exception if an unknown argument flag is encountered: + + ```bash + > ./test --known --unknown + [ap::error] Unknown argument [--unknown]. + Program: test + + A simple test program + + Optional arguments: + + --help, -h : Display the help message + --known, -k : A known optional argument + ``` + +- `ap::unknown_policy::warn` - prints a warning message to the standard error stream and continues parsing the remaining arguments: + + ```bash + > ./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 + known = + ``` + +- `ap::unknown_policy::as_values` - treats unknown argument flags as values: + + ```shell + > ./test --known --unknown + known = --unknown + ``` + +> [!IMPORTANT] +> +> - The unkown argument flags handling polciy only affects the parser's behaviour when calling the `parse_args` or `try_parse_args` methods. +> - When parsing known args with `parse_known_args` or `try_parse_known_args` all unknown arguments (flags and values) are collected and returned as the parsing result, ignoring the specified policy for handling unknown arguments. +> +> Consider a similar example as above with only the argument parsing function changed: +> ```cpp +> const auto unknown_args = parser.try_parse_known_args(argc, argv); +> std::cout << "known = " << ap::util::join(parser.values("known")) << std::endl +> << "unknown = " << ap::util::join(unknown_args) << std::endl; +> ``` +> This would produce the following output regardless of the specified unknown arguments policy. +> ```shell +> > ./test --known --unknown +> known = +> unknown = --unknown +> ```

@@ -1094,8 +1157,6 @@ Now all the values, that caused an exception for the `parse_args` example, are c > ``` > > Here `value` is treated either as the `positional` argument's value or as an unknown argument (depending on the input arguments) even though the `recognized` optional argument still accepts values and only after the `--recognized` argument flag is encountered the parser continues collecting values for this argument. -> -> **NOTE:** If the `AP_UNKNOWN_FLAGS_AS_VALUES` is set, the unrecognized argument flags will be treated as values during parsing and therefore they **may** not be collected as unknown arguments, depending on the argument's configuration and the command-line argument list. > [!TIP] > diff --git a/include/ap/action/predefined.hpp b/include/ap/action/predefined.hpp index 8603274b..006808ca 100644 --- a/include/ap/action/predefined.hpp +++ b/include/ap/action/predefined.hpp @@ -20,12 +20,12 @@ std::ostream& operator<<(std::ostream& os, const argument_parser&) noexcept; namespace action { /** - * @brief Returns an *on-flag* action which prints the argument parser's configuration. - * @param parser The argument parser the configuration of which will be printed. + * @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 exit_code The exit code with which `std::exit` will be called (if not `std::nullopt`). - * @param os The output stream to which the configuration will be printed. + * @param os The output stream to which the help message will be printed. */ -inline typename ap::action_type::on_flag::type print_config( +inline typename ap::action_type::on_flag::type print_help( const argument_parser& parser, const std::optional exit_code = std::nullopt, std::ostream& os = std::cout diff --git a/include/ap/argument.hpp b/include/ap/argument.hpp index b9b94ee8..81a74533 100644 --- a/include/ap/argument.hpp +++ b/include/ap/argument.hpp @@ -42,7 +42,7 @@ enum class argument_type : bool { positional, optional }; * @attention Some member functions are conditionally enabled/disabled depending on the argument type and value type. * * Example usage: - * @code + * @code{.cpp} * ap::argument_parser parser; * parser.add_positional_argument("input", "i") * .help("An input file path"); diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index bc051550..08e5591f 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -31,16 +31,95 @@ namespace ap { class argument_parser; -enum class default_argument { +/// @brief The enumeration of default arguments provided by the library. +enum class default_argument : std::uint8_t { + /** + * @brief A positional argument representing a single input file path. + * Equivalent to: + * @code{.cpp} + * parser.add_positional_argument("input") + * .action(ap::action::check_file_exists()) + * .help("Input file path"); + * @endcode + */ p_input, + + /** + * @brief A positional argument representing a single output file path. + * Equivalent to: + * @code{.cpp} + * parser.add_positional_argument("output") + * .help("Output file path"); + * @endcode + */ p_output, + + /** + * @brief An optional argument representing the program's help flag. + * Equivalent to: + * @code{.cpp} + * parser.add_optional_argument("help") + * .action(ap::action::print_help(parser, EXIT_SUCCESS)) + * .help("Display the help message"); + * @endcode + */ o_help, + + /** + * @brief A positional argument representing multiple input file paths. + * Equivalent to: + * @code{.cpp} + * parser.add_positional_argument("input", "i") + * .nargs(1ull) + * .action(ap::action::check_file_exists()) + * .help("Input file path"); + * @endcode + */ o_input, + + /** + * @brief A positional argument representing multiple output file paths. + * Equivalent to: + * @code{.cpp} + * parser.add_positional_argument("output", "o") + * .nargs(1ull) + * .help("Output file path"); + * @endcode + */ o_output, + + /** + * @brief A positional argument representing multiple input file paths. + * Equivalent to: + * @code{.cpp} + * parser.add_positional_argument("input", "i") + * .nargs(ap::nargs::at_least(1ull)) + * .action(ap::action::check_file_exists()) + * .help("Input file path"); + * @endcode + */ o_multi_input, + + /** + * @brief A positional argument representing multiple output file paths. + * Equivalent to: + * @code{.cpp} + * parser.add_positional_argument("output", "o") + * .nargs(ap::nargs::at_least(1ull)) + * .help("Output file path"); + * @endcode + */ o_multi_output }; +/// @brief The enumeration of policies for handling unknown arguments. +enum class unknown_policy : std::uint8_t { + fail, ///< Throw an exception when an unknown argument is encountered. + warn, ///< Issue a warning when an unknown argument is encountered. + ignore, ///< Ignore unknown arguments. + as_values ///< Treat unknown arguments as positional values. +}; + namespace detail { void add_default_argument(const default_argument, argument_parser&) noexcept; @@ -49,7 +128,39 @@ void add_default_argument(const default_argument, argument_parser&) noexcept; /** * @brief The main argument parser class. - * This class is responsible for the configuration and parsing of command-line arguments. + * + * This class provides methods to define positional and optional arguments, set parser options, + * and parse the command-line input. + * + * Example usage: + * @code{.cpp} + * #include + * + * 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 }) + * .program_description("A simple file copy utility.") + * .default_arguments( + * ap::default_argument::o_help, + * ap::default_argument::o_input, + * ap::default_argument::o_output + * ) + * .verbose() + * .unknown_arguments_policy(ap::unknown_policy::ignore) + * .try_parse_args(argc, argv); + * + * // Access parsed argument values + * const std::string input_file = parser.value("input"); + * const std::string output_file = parser.value("output"); + * + * // Application logic here + * std::cout << "Copying from " << input_file << " to " << output_file << std::endl; + * + * return 0; + * } + * @endcode */ class argument_parser { public: @@ -121,6 +232,17 @@ class argument_parser { return *this; } + /** + * @brief Set the unknown argument flags handling policy. + * @param policy The unknown arguments policy value. + * @return Reference to the argument parser. + * @note The default unknown arguments policy value is `ap::unknown_policy::fail`. + */ + argument_parser& unknown_arguments_policy(const unknown_policy policy) noexcept { + this->_unknown_policy = policy; + return *this; + } + /** * @brief Add default arguments to the argument parser. * @tparam AR Type of the positional argument discriminator range. @@ -328,7 +450,7 @@ class argument_parser { 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); + this->_parse_args_impl(this->_tokenize(argv_rng, state), state); if (not state.unknown_args.empty()) throw parsing_failure::argument_deduction_failure(state.unknown_args); @@ -374,7 +496,7 @@ class argument_parser { this->parse_args(argv_rng); } catch (const ap::argument_parser_exception& err) { - std::cerr << "[ERROR] : " << err.what() << std::endl << *this << std::endl; + std::cerr << "[ap::error] " << err.what() << std::endl << *this << std::endl; std::exit(EXIT_FAILURE); } } @@ -423,9 +545,9 @@ class argument_parser { parsing_state state{ .curr_arg = nullptr, .curr_pos_arg_it = this->_positional_args.begin(), - .fail_on_unknown = false + .parse_known_only = true }; - this->_parse_args_impl(this->_tokenize(argv_rng), state); + this->_parse_args_impl(this->_tokenize(argv_rng, state), state); if (not this->_are_required_args_bypassed()) { this->_verify_required_args(); @@ -472,29 +594,11 @@ class argument_parser { return this->parse_known_args(argv_rng); } catch (const ap::argument_parser_exception& err) { - std::cerr << "[ERROR] : " << err.what() << std::endl << *this << std::endl; + std::cerr << "[ap::error] " << err.what() << std::endl << *this << std::endl; std::exit(EXIT_FAILURE); } } - // clang-format off - - /** - * @brief Handles the `help` argument logic. - * - * Checks the value of the `help` boolean flag argument and if the value `is` true, - * prints the parser to `std::cout` anb exists with `EXIT_SUCCESS` status. - */ - [[deprecated("The default help argument now uses the `print_config` on-flag action")]] - void handle_help_action() const noexcept { - if (this->value("help")) { - std::cout << *this << std::endl; - std::exit(EXIT_SUCCESS); - } - } - - // clang-format on - /** * @param arg_name The name of the argument. * @return True if the argument has a value, false otherwise. @@ -588,11 +692,11 @@ class argument_parser { } /** - * @brief Prints the argument parser's details to an output stream. + * @brief Prints the argument parser's help message to an output stream. * @param verbose The verbosity mode value. * @param os Output stream. */ - void print_config(const bool verbose, std::ostream& os = std::cout) const noexcept { + 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) @@ -619,7 +723,7 @@ class argument_parser { /** * @brief Prints the argument parser's details to an output stream. * - * An `os << parser` operation is equivalent to a `parser.print_config(_verbose, os)` call, + * 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. @@ -627,7 +731,7 @@ class argument_parser { * @return The modified output stream. */ friend std::ostream& operator<<(std::ostream& os, const argument_parser& parser) noexcept { - parser.print_config(parser._verbose, os); + parser.print_help(parser._verbose, os); return os; } @@ -651,8 +755,8 @@ class argument_parser { arg_ptr_list_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 fail_on_unknown = - true; ///< A flag indicating whether to end parsing with an error on unknown arguments. + const bool parse_known_only = + false; ///< A flag indicating whether only known arguments should be parsed. }; /** @@ -764,14 +868,15 @@ class argument_parser { * @return A list of preprocessed command-line argument tokens. */ template AR> - [[nodiscard]] arg_token_list_t _tokenize(const AR& arg_range) { + [[nodiscard]] arg_token_list_t _tokenize(const AR& arg_range, const parsing_state& state) { arg_token_list_t toks; if constexpr (std::ranges::sized_range) toks.reserve(std::ranges::size(arg_range)); std::ranges::for_each( - arg_range, std::bind_front(&argument_parser::_tokenize_arg, this, std::ref(toks)) + arg_range, + std::bind_front(&argument_parser::_tokenize_arg, this, std::ref(state), std::ref(toks)) ); return toks; @@ -782,7 +887,9 @@ class argument_parser { * @param toks The argument token list to which the processed token(s) will be appended. * @param arg_value The command-line argument's value to be processed. */ - void _tokenize_arg(arg_token_list_t& toks, const std::string_view arg_value) { + void _tokenize_arg( + const parsing_state& state, arg_token_list_t& toks, const std::string_view arg_value + ) { detail::argument_token tok{ .type = this->_deduce_token_type(arg_value), .value = std::string(arg_value) }; @@ -799,10 +906,26 @@ class argument_parser { return; } -#ifdef AP_UNKNOWN_FLAGS_AS_VALUES - tok.type = detail::argument_token::t_value; -#endif - toks.emplace_back(std::move(tok)); + // unknown flag + if (state.parse_known_only) { + toks.emplace_back(std::move(tok)); + return; + } + + switch (this->_unknown_policy) { + case unknown_policy::fail: + throw parsing_failure::unknown_argument(tok.value); + case unknown_policy::warn: + std::cerr << "[ap::warning] Unknown argument '" << tok.value << "' will be ignored." + << std::endl; + [[fallthrough]]; + case unknown_policy::ignore: + return; + case unknown_policy::as_values: + tok.type = detail::argument_token::t_value; + toks.emplace_back(std::move(tok)); + break; + } } [[nodiscard]] detail::argument_token::token_type _deduce_token_type( @@ -820,23 +943,6 @@ class argument_parser { return detail::argument_token::t_value; } - /** - * @brief Removes the flag prefix from a flag token's value. - * @param tok The argument token to be processed. - * @return The token's value without the flag prefix. - */ - [[nodiscard]] std::string_view _strip_flag_prefix(const detail::argument_token& tok - ) const noexcept { - switch (tok.type) { - case detail::argument_token::t_flag_primary: - return std::string_view(tok.value).substr(this->_primary_flag_prefix_length); - case detail::argument_token::t_flag_secondary: - return std::string_view(tok.value).substr(this->_secondary_flag_prefix_length); - default: - return tok.value; - } - } - /** * @brief Builds an argument token from the given value. * @param arg_value The command-line argument's value to be processed. @@ -909,6 +1015,23 @@ class argument_parser { return compound_toks; } + /** + * @brief Removes the flag prefix from a flag token's value. + * @param tok The argument token to be processed. + * @return The token's value without the flag prefix. + */ + [[nodiscard]] std::string_view _strip_flag_prefix(const detail::argument_token& tok + ) const noexcept { + switch (tok.type) { + case detail::argument_token::t_flag_primary: + return std::string_view(tok.value).substr(this->_primary_flag_prefix_length); + case detail::argument_token::t_flag_secondary: + return std::string_view(tok.value).substr(this->_secondary_flag_prefix_length); + default: + return tok.value; + } + } + /** * @brief Implementation of parsing command-line arguments. * @param arg_tokens The list of command-line argument tokens. @@ -937,14 +1060,15 @@ class argument_parser { [[fallthrough]]; case detail::argument_token::t_flag_secondary: { if (not tok.is_valid_flag_token()) { - if (state.fail_on_unknown) { - throw parsing_failure::unrecognized_argument(tok.value); - } - else { + if (state.parse_known_only) { state.curr_arg.reset(); state.unknown_args.emplace_back(tok.value); break; } + else { + // should never happen as unknown flags are filtered out during tokenization + throw parsing_failure::unknown_argument(tok.value); + } } if (tok.arg->mark_used()) @@ -1106,15 +1230,16 @@ class argument_parser { 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; - static constexpr uint8_t _primary_flag_prefix_length = 2u; - static constexpr uint8_t _secondary_flag_prefix_length = 1u; + static constexpr std::uint8_t _primary_flag_prefix_length = 2u; + static constexpr std::uint8_t _secondary_flag_prefix_length = 1u; static constexpr char _flag_prefix_char = '-'; static constexpr std::string_view _flag_prefix = "--"; - static constexpr uint8_t _indent_width = 2; + static constexpr std::uint8_t _indent_width = 2; }; namespace detail { @@ -1139,29 +1264,24 @@ inline void add_default_argument( break; case default_argument::o_help: - arg_parser.add_flag("help", "h") - .action(action::print_config(arg_parser, EXIT_SUCCESS)) + arg_parser.add_optional_argument("help", "h") + .action(action::print_help(arg_parser, EXIT_SUCCESS)) .help("Display the help message"); break; case default_argument::o_input: arg_parser.add_optional_argument("input", "i") - .required() .nargs(1ull) .action(action::check_file_exists()) .help("Input file path"); break; case default_argument::o_output: - arg_parser.add_optional_argument("output", "o") - .required() - .nargs(1ull) - .help("Output file path"); + arg_parser.add_optional_argument("output", "o").nargs(1ull).help("Output file path"); break; case default_argument::o_multi_input: arg_parser.add_optional_argument("input", "i") - .required() .nargs(ap::nargs::at_least(1ull)) .action(action::check_file_exists()) .help("Input files paths"); @@ -1169,7 +1289,6 @@ inline void add_default_argument( case default_argument::o_multi_output: arg_parser.add_optional_argument("output", "o") - .required() .nargs(ap::nargs::at_least(1ull)) .help("Output files paths"); break; diff --git a/include/ap/exceptions.hpp b/include/ap/exceptions.hpp index 7eba079c..bcf0c516 100644 --- a/include/ap/exceptions.hpp +++ b/include/ap/exceptions.hpp @@ -57,8 +57,8 @@ struct invalid_configuration : public argument_parser_exception { struct parsing_failure : public argument_parser_exception { explicit parsing_failure(const std::string& message) : argument_parser_exception(message) {} - static parsing_failure unrecognized_argument(const std::string_view arg_name) noexcept { - return parsing_failure(std::format("Unrecognized argument [{}].", arg_name)); + static parsing_failure unknown_argument(const std::string_view arg_name) noexcept { + return parsing_failure(std::format("Unknown argument [{}].", arg_name)); } static parsing_failure value_already_set(const detail::argument_name& arg_name) noexcept { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6aae4a83..4c9067d3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -69,9 +69,6 @@ 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_argument_parser_parse_args_unknown_flags_as_values.cpp" - COMPILE_DEFINITIONS "AP_UNKNOWN_FLAGS_AS_VALUES" -) add_doctest("source/test_nargs_range.cpp") add_doctest("source/test_none_type_argument.cpp") add_doctest("source/test_optional_argument.cpp") diff --git a/tests/include/argument_parser_test_fixture.hpp b/tests/include/argument_parser_test_fixture.hpp index d56b6c7b..7657cce5 100644 --- a/tests/include/argument_parser_test_fixture.hpp +++ b/tests/include/argument_parser_test_fixture.hpp @@ -179,7 +179,7 @@ struct argument_parser_test_fixture { // private function callers [[nodiscard]] arg_token_list_t tokenize(int argc, char* argv[]) { - return this->sut._tokenize(std::span(argv + 1, static_cast(argc - 1))); + return this->sut._tokenize(std::span(argv + 1, static_cast(argc - 1)), state); } void parse_args_impl(const arg_token_list_t& arg_tokens) { diff --git a/tests/source/test_argument_parser_add_argument.cpp b/tests/source/test_argument_parser_add_argument.cpp index bc17bb4c..23901bf4 100644 --- a/tests/source/test_argument_parser_add_argument.cpp +++ b/tests/source/test_argument_parser_add_argument.cpp @@ -329,7 +329,7 @@ TEST_CASE_FIXTURE( const auto help_arg = get_argument(help_flag); REQUIRE(help_arg); - CHECK(is_optional(*help_arg)); + CHECK(is_optional(*help_arg)); const auto input_arg = get_argument(input_flag); REQUIRE(input_arg); diff --git a/tests/source/test_argument_parser_parse_args.cpp b/tests/source/test_argument_parser_parse_args.cpp index 4fa71624..9a66bf5c 100644 --- a/tests/source/test_argument_parser_parse_args.cpp +++ b/tests/source/test_argument_parser_parse_args.cpp @@ -6,6 +6,7 @@ using namespace ap_testing; using namespace ap::nargs; using ap::invalid_configuration; using ap::parsing_failure; +using ap::unknown_policy; struct test_argument_parser_parse_args : public argument_parser_test_fixture { const std::string_view test_program_name = "test program name"; @@ -277,11 +278,11 @@ TEST_CASE_FIXTURE( auto argc = get_argc(no_args, n_opt_clargs); auto argv = init_argv(no_args, n_opt_clargs); - const auto unknown_arg_name = init_arg_flag_primary(opt_arg_idx); + const auto unknown_arg_flag = init_arg_flag_primary(opt_arg_idx); CHECK_THROWS_WITH_AS( sut.parse_args(argc, argv), - parsing_failure::unrecognized_argument(unknown_arg_name).what(), + parsing_failure::unknown_argument(unknown_arg_flag).what(), parsing_failure ); @@ -322,7 +323,7 @@ TEST_CASE_FIXTURE( CHECK_THROWS_WITH_AS( sut.parse_args(argc, argv), - parsing_failure::unrecognized_argument(invalid_flag).what(), + parsing_failure::unknown_argument(invalid_flag).what(), parsing_failure ); @@ -1118,7 +1119,7 @@ TEST_CASE_FIXTURE( // parse args CHECK_THROWS_WITH_AS( sut.parse_args(argc, argv), - parsing_failure::unrecognized_argument(invalid_flag).what(), + parsing_failure::unknown_argument(invalid_flag).what(), parsing_failure ); @@ -1174,3 +1175,126 @@ TEST_CASE_FIXTURE( // cleanup free_argv(argc, argv); } + +// unknown_arguments_policy + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should throw when an unrecognized argument flag is used with the default unknown " + "arguments handling policy (fail)" +) { + add_arguments(no_args, no_args); + + constexpr std::size_t n_opt_clargs = 1ull; + constexpr std::size_t opt_arg_idx = 0ull; + + auto argc = get_argc(no_args, n_opt_clargs); + auto argv = init_argv(no_args, n_opt_clargs); + + const auto unknown_arg_name = init_arg_flag_primary(opt_arg_idx); + + CHECK_THROWS_WITH_AS( + sut.parse_args(argc, argv), + parsing_failure::unknown_argument(unknown_arg_name).what(), + parsing_failure + ); + + free_argv(argc, argv); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should throw when an unrecognized argument flag is used with the default unknown " + "arguments handling policy (fail)" +) { + const auto unknown_arg_flag = "--unknown"; + const std::vector argv_vec{"program", unknown_arg_flag}; + + const auto argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + CHECK_THROWS_WITH_AS( + sut.parse_args(argc, argv), + parsing_failure::unknown_argument(unknown_arg_flag).what(), + parsing_failure + ); + + free_argv(argc, argv); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should print a warning to std::cerr when an unrecognized argument flag is used " + "with the warn unknown arguments handling policy" +) { + sut.unknown_arguments_policy(unknown_policy::warn); + + const auto unknown_arg_flag = "--unknown"; + const std::vector argv_vec{"program", unknown_arg_flag}; + + const auto argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + // redirect std::cerr + std::stringstream tmp_buffer; + auto* cerr_buffer = std::cerr.rdbuf(tmp_buffer.rdbuf()); + + REQUIRE_NOTHROW(sut.parse_args(argc, argv)); + + CHECK_EQ( + tmp_buffer.str(), + std::format("[ap::warning] Unknown argument '{}' will be ignored.\n", unknown_arg_flag) + ); + + free_argv(argc, argv); + + // reset std::cerr + std::cerr.rdbuf(cerr_buffer); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should do nothing when an unrecognized argument flag is used with the ignore " + "unknown arguments handling policy" +) { + sut.unknown_arguments_policy(unknown_policy::ignore); + + const auto unknown_arg_flag = "--unknown"; + const std::vector argv_vec{"program", unknown_arg_flag}; + + const auto argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + // redirect std::cerr + std::stringstream tmp_buffer; + auto* cerr_buffer = std::cerr.rdbuf(tmp_buffer.rdbuf()); + + REQUIRE_NOTHROW(sut.parse_args(argc, argv)); + + CHECK_EQ(tmp_buffer.str(), ""); + + free_argv(argc, argv); + + // reset std::cerr + std::cerr.rdbuf(cerr_buffer); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should treat an unrecognized argument flag as a value with the as_values unknown " + "arguments handling policy" +) { + sut.unknown_arguments_policy(unknown_policy::as_values); + sut.add_positional_argument("known"); + + const auto unknown_arg_flag = "--unknown"; + const std::vector argv_vec{"program", unknown_arg_flag}; + + const auto argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + CHECK_NOTHROW(sut.parse_args(argc, argv)); + CHECK_EQ(sut.value("known"), unknown_arg_flag); + + free_argv(argc, argv); +} diff --git a/tests/source/test_argument_parser_parse_args_unknown_flags_as_values.cpp b/tests/source/test_argument_parser_parse_args_unknown_flags_as_values.cpp deleted file mode 100644 index 851848b4..00000000 --- a/tests/source/test_argument_parser_parse_args_unknown_flags_as_values.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include "argument_parser_test_fixture.hpp" -#include "doctest.h" -#include "utility.hpp" - -using namespace ap_testing; -using ap::parsing_failure; - -struct test_argument_parser_parse_args_unknown_flags_as_values -: public argument_parser_test_fixture { - const std::string test_program_name = "test program name"; - const std::string unknown_arg_flag = "--unknown-arg"; - - const std::size_t no_args = 0ull; -}; - -TEST_CASE_FIXTURE( - test_argument_parser_parse_args_unknown_flags_as_values, - "parse_args should treat an unknown argument flag as a positional value if it's not preceeded " - "by any valid argument flags" -) { - const std::vector args{test_program_name, unknown_arg_flag}; - - const auto argc = static_cast(args.size()); - auto argv = to_char_2d_array(args); - - CHECK_THROWS_WITH_AS( - sut.parse_args(argc, argv), - parsing_failure::argument_deduction_failure({unknown_arg_flag}).what(), - parsing_failure - ); - - free_argv(argc, argv); -} - -TEST_CASE_FIXTURE( - test_argument_parser_parse_args_unknown_flags_as_values, - "parse_args should treat an unknown argument flag as an optional argument's value if it's " - "proceeded by an optional argument's flag" -) { - const std::string opt_arg_name = "known-opt-arg"; - sut.add_optional_argument(opt_arg_name); - - const std::vector args{ - test_program_name, std::format("--{}", opt_arg_name), unknown_arg_flag - }; - - const auto argc = static_cast(args.size()); - auto argv = to_char_2d_array(args); - - CHECK_NOTHROW(sut.parse_args(argc, argv)); - - CHECK_EQ(sut.value(opt_arg_name), unknown_arg_flag); - - free_argv(argc, argv); -} From 511e6f88fd929314125245a2c3443e1c5e3b4b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Musia=C5=82?= <111433005+SpectraL519@users.noreply.github.com> Date: Sun, 21 Sep 2025 16:31:02 +0200 Subject: [PATCH 07/10] YT-CPPAP-37: Greedy arguments - Extended the `argument` class with a `bool _greedy` member (`false` by default) - Added a `greedy(bool)` setter method to the `argument` class. - Aligned the parsing logic to assign all values to a greedy argument (after it's encountered) until its upper `nargs` range is reached --- CMakeLists.txt | 2 +- Doxyfile | 2 +- MODULE.bazel | 2 +- README.md | 4 +- docs/tutorial.md | 76 +++++- include/ap/argument.hpp | 33 ++- include/ap/argument_parser.hpp | 252 +++++++++--------- include/ap/detail/argument_base.hpp | 3 + include/ap/detail/argument_token.hpp | 30 ++- include/ap/exceptions.hpp | 4 +- .../include/argument_parser_test_fixture.hpp | 2 +- .../test_argument_parser_parse_args.cpp | 55 ++++ tests/source/test_argument_token.cpp | 8 +- 13 files changed, 311 insertions(+), 162 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b786012e..0f4de373 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ else() endif() project(cpp-ap - VERSION 3.0.0.5 + VERSION 3.0.0.6 DESCRIPTION "Command-line argument parser for C++20" HOMEPAGE_URL "https://github.com/SpectraL519/cpp-ap" LANGUAGES CXX diff --git a/Doxyfile b/Doxyfile index 3c2db87b..057383cc 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = CPP-AP # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 3.0.0.5 +PROJECT_NUMBER = 3.0.0.6 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/MODULE.bazel b/MODULE.bazel index 135ad513..eb14f2e5 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,4 +1,4 @@ module( name = "cpp-ap", - version = "3.0.0.5", + version = "3.0.0.6", ) diff --git a/README.md b/README.md index 6cec0ddd..2187d65e 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,8 @@ Command-line argument parser for C++20 - [The Parser Class](/docs/tutorial.md#the-parser-class) - [Adding Arguments](/docs/tutorial.md#adding-arguments) - [Argument Parameters](/docs/tutorial.md#argument-parameters) - - [Common Parameters](/docs/tutorial.md##common-parameters) - - [Parameters Specific for Optional Arguments](/docs/tutorial.md##parameters-specific-for-optional-arguments) + - [Common Parameters](/docs/tutorial.md#common-parameters) + - [Parameters Specific for Optional Arguments](/docs/tutorial.md#parameters-specific-for-optional-arguments) - [Default Arguments](/docs/tutorial.md#default-arguments) - [Parsing Arguments](/docs/tutorial.md#parsing-arguments) - [Basic Argument Parsing Rules](/docs/tutorial.md#basic-argument-parsing-rules) diff --git a/docs/tutorial.md b/docs/tutorial.md index 428c7d15..d1bf0e4c 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -8,7 +8,18 @@ - [Adding Arguments](#adding-arguments) - [Argument Parameters](#argument-parameters) - [Common Parameters](#common-parameters) + - [help](#1-help---the-arguments-description-which-will-be-printed-when-printing-the-parser-class-instance) + - [hidden](#2-hidden---if-this-option-is-set-for-an-argument-then-it-will-not-be-included-in-the-program-description) + - [required](#3-required---if-this-option-is-set-for-an-argument-and-its-value-is-not-passed-in-the-command-line-an-exception-will-be-thrown) + - [bypass required](#4-bypass_required---if-this-option-is-set-for-an-argument-the-required-option-for-other-arguments-will-be-discarded-if-the-bypassing-argument-is-used-in-the-command-line) + - [nargs](#5-nargs---sets-the-allowed-number-of-values-to-be-parsed-for-an-argument) + - [greedy](#6-greedy---if-this-option-is-set-the-argument-will-consume-all-command-line-values-until-its-upper-nargs-bound-is-reached) + - [choices](#7-choices---a-list-of-valid-argument-values) + - [value actions](#8-value-actions---functions-that-are-called-after-parsing-an-arguments-value) + - [default values](#9-default_values---a-list-of-values-which-will-be-used-if-no-values-for-an-argument-have-been-parsed) - [Parameters Specific for Optional Arguments](#parameters-specific-for-optional-arguments) + - [on-flag actions](#1-on-flag-actions---functions-that-are-called-immediately-after-parsing-an-arguments-flag) + - [implicit values](#2-implicit_values---a-list-of-values-which-will-be-set-for-an-argument-if-only-its-flag-but-no-values-are-parsed-from-the-command-line) - [Predefined Parameter Values](#predefined-parameter-values) - [Default Arguments](#default-arguments) - [Parsing Arguments](#parsing-arguments) @@ -189,9 +200,10 @@ parser.add__argument("argument", "a"); > > - If the argument's value type is `ap::none_type`, the argument will not accept any values and therefore no value-related parameters can be set for such argument. This includes: > - [nargs](#5-nargs---sets-the-allowed-number-of-values-to-be-parsed-for-an-argument-this-can-be-set-as-a) -> - [choices](#6-choices---a-list-of-valid-argument-values) -> - [value actions](#7-value-actions---function-performed-after-parsing-an-arguments-value) -> - [default_values](#8-default_values---a-list-of-values-which-will-be-used-if-no-values-for-an-argument-have-been-parsed) +> - [greedy](#6-greedy---if-this-option-is-set-the-argument-will-consume-all-command-line-values-until-its-upper-nargs-bound-is-reached) +> - [choices](#7-choices---a-list-of-valid-argument-values) +> - [value actions](#8-value-actions---functions-that-are-called-after-parsing-an-arguments-value) +> - [default_values](#9-default_values---a-list-of-values-which-will-be-used-if-no-values-for-an-argument-have-been-parsed) > - [implicit_values](#2-implicit_values---a-list-of-values-which-will-be-set-for-an-argument-if-only-its-flag-but-no-values-are-parsed-from-the-command-line) You can also add boolean flags: @@ -343,8 +355,8 @@ Command Result > [!NOTE] > -> - Both positional and optional arguments have the `bypass_required` option disabled. -> - The default value of the value parameter of the `bypass_required(bool)` function is `true` for both positional and optional arguments. +> - Both all arguments have the `bypass_required` option disabled. +> - The default value of the value parameter of the `argument::bypass_required(bool)` method is `true` for all arguments. > [!WARNING] > @@ -377,7 +389,9 @@ os << data << std::endl;
-#### 5. `nargs` - Sets the allowed number of values to be parsed for an argument. This can be set as a: +#### 5. `nargs` - Sets the allowed number of values to be parsed for an argument. + +The `nargs` parameter can be set as: - Specific number: @@ -416,7 +430,49 @@ os << data << std::endl;
-#### 6. `choices` - A list of valid argument values. +#### 6. `greedy` - If this option is set, the argument will consume ALL command-line values until it's upper nargs bound is reached. + +> [!NOTE] +> +> - By default the `greedy` option is disabled for all arguments. +> - The default value of the parameter of the `argument::greedy(bool)` method is true for all arguments. + +> [!TIP] +> +> - Enabling the `greedy` option for an argument only makes sense for arguments with string-like value types. +> - If no explicit `nargs` bound is set for a greedy argument, once it starts being parsed, it will consume all remaining command-line arguments. + +Consider a simple example: + +```cpp +ap::argument_parser parser; +parser.program_name("run-script") + .default_arguments(ap::default_argument::o_help); + +parser.add_positional_argument("script") + .help("The name of the script to run"); +parser.add_optional_argument("args") + .greedy() + .help("Set the execution option"); + +parser.try_parse_args(argc, argv); + +// Application logic here +std::cout << "Executing: " << parser.value("script") << " " << ap::util::join(parser.values("args")) << std::endl; +``` + +Here the program execution should look something like this: + +```txt +> ./run-script remove-comments --args module.py -v --type py +Executing: remove-comments module.py -v --type py +``` + +Notice that even though the `-v` and `--type` command-line arguments have flag prefixes and are not defined in the program, they are not treated as unknown arguments (and therefore no exception is thrown) because the `--args` argument is marked as `greedy` and it consumes these command-line arguments as its values. + +
+ +#### 7. `choices` - A list of valid argument values. ```cpp parser.add_optional_argument("method", "m").choices('a', 'b', 'c'); @@ -433,7 +489,7 @@ parser.add_optional_argument("method", "m").choices('a', 'b', 'c');
-#### 7. Value actions - Function performed after parsing an argument's value. +#### 8. value actions - Functions that are called after parsing an argument's value. Actions are represented as functions, which take the argument's value as an argument. The available action types are: - `observe` actions | `void(const value_type&)` - applied to the parsed value. No value is returned - this action type is used to perform some logic on the parsed value without modifying it. @@ -478,7 +534,7 @@ Actions are represented as functions, which take the argument's value as an argu
-#### 8. `default_values` - A list of values which will be used if no values for an argument have been parsed +#### 9. `default_values` - A list of values which will be used if no values for an argument have been parsed > [!WARNING] > @@ -541,7 +597,7 @@ Command Result Apart from the common parameters listed above, for optional arguments you can also specify the following parameters: -#### 1. On-flag actions - For optional arguments, apart from value actions, you can specify on-flag actions which are executed immediately after parsing an argument's flag. +#### 1. on-flag actions - Functions that are called immediately after parsing an argument's flag. ```cpp void print_debug_info() noexcept { diff --git a/include/ap/argument.hpp b/include/ap/argument.hpp index 81a74533..56ac3310 100644 --- a/include/ap/argument.hpp +++ b/include/ap/argument.hpp @@ -123,6 +123,11 @@ class argument : public detail::argument_base { return not this->_required and this->_bypass_required; } + /// @return `true` if the argument is greedy, `false` otherwise. + [[nodiscard]] bool is_greedy() const noexcept override { + return this->_greedy; + } + // attribute setters /** @@ -171,6 +176,19 @@ class argument : public detail::argument_base { return *this; } + /** + * @brief Set the `greedy` attribute of the argument. + * @param g The attribute value. + * @return Reference to the argument instance. + * @note The method is enabled only if `value_type` is not `none_type`. + */ + argument& greedy(const bool g = true) noexcept + requires(not util::c_is_none) + { + this->_greedy = g; + return *this; + } + /** * @brief Set the nargs range for the argument. * @param range The attribute value. @@ -193,8 +211,7 @@ class argument : public detail::argument_base { argument& nargs(const count_type n) noexcept requires(not util::c_is_none) { - this->_nargs_range = nargs::range(n); - return *this; + return this->nargs(nargs::range(n)); } /** @@ -207,8 +224,7 @@ class argument : public detail::argument_base { argument& nargs(const count_type lower, const count_type upper) noexcept requires(not util::c_is_none) { - this->_nargs_range = nargs::range(lower, upper); - return *this; + return this->nargs(nargs::range(lower, upper)); } /** @@ -671,7 +687,7 @@ class argument : public detail::argument_base { // attributes const ap::detail::argument_name _name; ///< The argument's name. std::optional _help_msg; ///< The argument's help message. - nargs::range _nargs_range; ///< The argument's nargs range attribute. + nargs::range _nargs_range; ///< The argument's nargs range attribute value. [[no_unique_address]] value_arg_specific_type> _default_values; ///< The argument's default value list. [[no_unique_address]] value_arg_specific_type>> @@ -683,9 +699,10 @@ class argument : public detail::argument_base { [[no_unique_address]] value_arg_specific_type> _value_actions; ///< The argument's value actions collection. - bool _required : 1; ///< The argument's `required` attribute. - bool _bypass_required : 1 = false; ///< The argument's `bypass_required` attribute. - bool _hidden : 1 = false; ///< The argument's `hidden` attribute. + bool _required : 1; ///< The argument's `required` attribute value. + bool _bypass_required : 1 = false; ///< The argument's `bypass_required` attribute value. + bool _greedy : 1 = false; ///< The argument's `greedy` attribute value. + bool _hidden : 1 = false; ///< The argument's `hidden` attribute value. // parsing result [[no_unique_address]] optional_specific_type diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index 08e5591f..8e5cccd0 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -899,15 +899,9 @@ class argument_parser { return; } - // invalid flag - check for compound secondary flag - const auto compound_toks = this->_try_split_compound_flag(tok); - if (not compound_toks.empty()) { // not a valid compound flag - toks.insert(toks.end(), compound_toks.begin(), compound_toks.end()); - return; - } - - // unknown flag - if (state.parse_known_only) { + // not a value token -> flag token + // flag token could not be validated -> unknown flag + if (state.parse_known_only) { // do nothing (will be handled during parsing) toks.emplace_back(std::move(tok)); return; } @@ -928,6 +922,15 @@ class argument_parser { } } + /** + * @brief Returns the most appropriate *initial* token type based on a command-line argument's value. + * + * The token's *initial* type is deduced using the following rules: + * - `t_value`: an argument contains whitespace characters or cannot be a flag token + * - `t_flag_primary`: an argument begins with a primary flag prefix (`--`) + * - `t_flag_secondary`: an argument begins with a secondary flag prefix (`-`) + * - `t_flag_compound`: INITIALLY a token can NEVER have a compound flag type (may only be set when a flag token is validated) + */ [[nodiscard]] detail::argument_token::token_type _deduce_token_type( const std::string_view arg_value ) const noexcept { @@ -943,76 +946,75 @@ class argument_parser { return detail::argument_token::t_value; } - /** - * @brief Builds an argument token from the given value. - * @param arg_value The command-line argument's value to be processed. - * @return An argument token with removed flag prefix (if present) and an adequate token type. - */ - [[nodiscard]] detail::argument_token _build_token(const std::string_view arg_value - ) const noexcept { - if (util::contains_whitespaces(arg_value)) - return {.type = detail::argument_token::t_value, .value = std::string(arg_value)}; - - if (arg_value.starts_with(this->_flag_prefix)) - return { - .type = detail::argument_token::t_flag_primary, - .value = std::string(arg_value.substr(this->_primary_flag_prefix_length)) - }; - - if (arg_value.starts_with(this->_flag_prefix_char)) - return { - .type = detail::argument_token::t_flag_secondary, - .value = std::string(arg_value.substr(this->_secondary_flag_prefix_length)) - }; - - return {.type = detail::argument_token::t_value, .value = std::string(arg_value)}; - } - /** * @brief Check if a flag token is valid based on its value. - * @attention Sets the `arg` member of the token if an argument with the given name (token's value) is present. + * @attention Sets the `args` member of the token if an argument with the given name (token's value) is present. * @param tok The argument token to validate. - * @return true if the given token represents a valid argument flag. + * @return `true` if the given token represents a valid argument flag. */ [[nodiscard]] bool _validate_flag_token(detail::argument_token& tok) noexcept { const auto opt_arg_it = this->_find_opt_arg(tok); if (opt_arg_it == this->_optional_args.end()) - return false; + return this->_validate_compound_flag_token(tok); - tok.arg = *opt_arg_it; + tok.args.emplace_back(*opt_arg_it); return true; } /** - * @brief Tries to split a secondary flag token into separate flag token (one for each character of the token's value). - * @param tok The token to be processed. - * @return A vector of new argument tokens. - * @note If ANY of the characters in the token's value does not match an argument, an empty vector will be returned. + * @brief Check if a flag token is a valid compound argument flag based on its value. + * @attention If the token indeed represents valid compound flag, the token's type is changed to `t_flag_compuund` + * @attention and its `args` list is filled with all the arguments the token represents. + * @param tok The argument token to validate. + * @return `true` if the given token represents a valid compound argument flag. */ - [[nodiscard]] std::vector _try_split_compound_flag( - const detail::argument_token& tok - ) noexcept { - std::vector compound_toks; - const auto actual_tok_value = this->_strip_flag_prefix(tok); - - compound_toks.reserve(actual_tok_value.size()); - + bool _validate_compound_flag_token(detail::argument_token& tok) noexcept { if (tok.type != detail::argument_token::t_flag_secondary) - return compound_toks; + return false; + + const auto actual_tok_value = this->_strip_flag_prefix(tok); + tok.args.reserve(actual_tok_value.size()); for (const char c : actual_tok_value) { - detail::argument_token ctok{ - detail::argument_token::t_flag_secondary, - std::format("{}{}", this->_flag_prefix_char, c) - }; - if (not this->_validate_flag_token(ctok)) { - compound_toks.clear(); - return compound_toks; + const auto opt_arg_it = std::ranges::find_if( + this->_optional_args, + this->_name_match_predicate( + std::string_view(&c, 1ull), detail::argument_name::m_secondary + ) + ); + + if (opt_arg_it == this->_optional_args.end()) { + tok.args.clear(); + return false; } - compound_toks.emplace_back(std::move(ctok)); + + tok.args.emplace_back(*opt_arg_it); } - return compound_toks; + tok.type = detail::argument_token::t_flag_compound; + return true; + } + + /** + * @brief Find an optional argument based on a flag token. + * @param flag_tok An argument_token instance, the value of which will be used to find the argument. + * @return An iterator to the argument's position. + * @note If the `flag_tok.type` is not a valid flag token, then the end iterator will be returned. + */ + [[nodiscard]] arg_ptr_list_iter_t _find_opt_arg(const detail::argument_token& flag_tok + ) noexcept { + if (not flag_tok.is_flag_token()) + return this->_optional_args.end(); + + const auto actual_tok_value = this->_strip_flag_prefix(flag_tok); + const auto match_type = + flag_tok.type == detail::argument_token::t_flag_primary + ? detail::argument_name::m_primary + : detail::argument_name::m_secondary; + + return std::ranges::find_if( + this->_optional_args, this->_name_match_predicate(actual_tok_value, match_type) + ); } /** @@ -1039,9 +1041,6 @@ class argument_parser { * @throws ap::parsing_failure */ void _parse_args_impl(const arg_token_list_t& arg_tokens, parsing_state& state) { - if (state.curr_pos_arg_it != this->_positional_args.end()) - state.curr_arg = *state.curr_pos_arg_it; - // process argument tokens std::ranges::for_each( arg_tokens, std::bind_front(&argument_parser::_parse_token, this, std::ref(state)) @@ -1050,59 +1049,84 @@ class argument_parser { /** * @brief Parse a single command-line argument token. - * @param curr_arg The currently processed argument. * @param state The current parsing state. + * @param tok The token to be parsed. * @throws ap::parsing_failure */ void _parse_token(parsing_state& state, const detail::argument_token& tok) { - switch (tok.type) { - case detail::argument_token::t_flag_primary: - [[fallthrough]]; - case detail::argument_token::t_flag_secondary: { - if (not tok.is_valid_flag_token()) { - if (state.parse_known_only) { - state.curr_arg.reset(); - state.unknown_args.emplace_back(tok.value); - break; - } - else { - // should never happen as unknown flags are filtered out during tokenization - throw parsing_failure::unknown_argument(tok.value); - } + if (state.curr_arg and state.curr_arg->is_greedy()) { + this->_set_argument_value(state, tok.value); + return; + } + + if (tok.is_flag_token()) + this->_parse_flag_token(state, tok); + else + this->_parse_value_token(state, tok); + } + + /** + * @brief Parse a single command-line argument *flag* token. + * @param state The current parsing state. + * @param tok The token to be parsed. + * @throws ap::parsing_failure + */ + void _parse_flag_token(parsing_state& state, const detail::argument_token& tok) { + if (not tok.is_valid_flag_token()) { + if (state.parse_known_only) { + state.curr_arg.reset(); + state.unknown_args.emplace_back(tok.value); + return; } + else { + // should never happen as unknown flags are filtered out during tokenization + throw parsing_failure::unknown_argument(tok.value); + } + } - if (tok.arg->mark_used()) - state.curr_arg = tok.arg; + for (const auto& arg : tok.args) { + if (arg->mark_used()) + state.curr_arg = arg; else state.curr_arg.reset(); - - break; } - case detail::argument_token::t_value: { - if (not state.curr_arg) { - if (state.curr_pos_arg_it == this->_positional_args.end()) { - state.unknown_args.emplace_back(tok.value); - break; - } - - state.curr_arg = *state.curr_pos_arg_it; - } - - if (not state.curr_arg->set_value(tok.value)) { - // advance to the next positional argument if possible - if (state.curr_arg->is_positional() - and state.curr_pos_arg_it != this->_positional_args.end() - and ++state.curr_pos_arg_it != this->_positional_args.end()) { - state.curr_arg = *state.curr_pos_arg_it; - break; - } + } - state.curr_arg.reset(); + /** + * @brief Parse a single command-line argument *value* token. + * @param state The current parsing state. + * @param tok The token to be parsed. + * @throws ap::parsing_failure + */ + void _parse_value_token(parsing_state& state, const detail::argument_token& tok) { + if (not state.curr_arg) { + if (state.curr_pos_arg_it == this->_positional_args.end()) { + state.unknown_args.emplace_back(tok.value); + return; } - break; - } + state.curr_arg = *state.curr_pos_arg_it; } + + this->_set_argument_value(state, tok.value); + } + + /** + * @brief Set the value for the currently processed argument. + * @attention This function assumes that the current argument is set (i.e. `state.curr_arg != nullptr`). + * @param state The current parsing state. + * @param value The value to be set for the current argument. + */ + void _set_argument_value(parsing_state& state, const std::string_view value) noexcept { + if (state.curr_arg->set_value(std::string(value))) + return; // argument still accepts values + + // advance to the next positional argument if possible + if (state.curr_arg->is_positional() + and state.curr_pos_arg_it != this->_positional_args.end()) + ++state.curr_pos_arg_it; + + state.curr_arg.reset(); } /** @@ -1173,28 +1197,6 @@ class argument_parser { return nullptr; } - /** - * @brief Find an optional argument based on a flag token. - * @param flag_tok An argument_token instance, the value of which will be used to find the argument. - * @return An iterator to the argument's position. - * @note If the `flag_tok.type` is not a valid flag token, then the end iterator will be returned. - */ - [[nodiscard]] arg_ptr_list_iter_t _find_opt_arg(const detail::argument_token& flag_tok - ) noexcept { - if (not flag_tok.is_flag_token()) - return this->_optional_args.end(); - - const auto actual_tok_value = this->_strip_flag_prefix(flag_tok); - const auto match_type = - flag_tok.type == detail::argument_token::t_flag_primary - ? detail::argument_name::m_primary - : detail::argument_name::m_secondary; - - return std::ranges::find_if( - this->_optional_args, this->_name_match_predicate(actual_tok_value, match_type) - ); - } - /** * @brief Print the given argument list to an output stream. * @param os The output stream to print to. diff --git a/include/ap/detail/argument_base.hpp b/include/ap/detail/argument_base.hpp index 0355b5dd..7a76809a 100644 --- a/include/ap/detail/argument_base.hpp +++ b/include/ap/detail/argument_base.hpp @@ -50,6 +50,9 @@ class argument_base { /// @return `true` if the argument is allowed to bypass the required check, `false` otherwise. virtual bool is_bypass_required_enabled() const noexcept = 0; + /// @return `true` if the argument is greedy, `false` otherwise. + virtual bool is_greedy() const noexcept = 0; + protected: /// @param verbose The verbosity mode value. If `true` all non-default parameters will be included in the output. /// @return An argument descriptor object for the argument. diff --git a/include/ap/detail/argument_token.hpp b/include/ap/detail/argument_token.hpp index a532eb1f..90ae3406 100644 --- a/include/ap/detail/argument_token.hpp +++ b/include/ap/detail/argument_token.hpp @@ -10,18 +10,21 @@ #include #include +#include namespace ap::detail { /// @brief Structure representing a single command-line argument token. struct argument_token { using arg_ptr_t = std::shared_ptr; ///< Argument pointer type alias. + using arg_ptr_list_t = std::vector; /// @brief The token type discriminator. enum class token_type : std::uint8_t { t_value, ///< Represents a value argument. - t_flag_primary, ///< Represents the primary (--) flag argument. - t_flag_secondary ///< Represents the secondary (-) flag argument. + t_flag_primary, ///< Represents a primary (--) flag argument. + t_flag_secondary, ///< Represents a secondary (-) flag argument. + t_flag_compound ///< Represents a compound flag argument (secondary flag matching multiple arguments). }; using enum token_type; @@ -36,28 +39,41 @@ struct argument_token { /** * @brief Checks whether the `type` member is a flag token type. - * @return true if `type` is either `t_flag_primary` or `t_flag_secondary`, false otherwise. + * @return true if the token has a *flag* type + * @note The token's type is considered a *flag* type if it has one of the following values: + * @note - t_flag_primary + * @note - t_flag_secondary + * @note - t_flag_compound */ [[nodiscard]] bool is_flag_token() const noexcept { - return this->type == t_flag_primary or this->type == t_flag_secondary; + switch (this->type) { + case t_flag_primary: + [[fallthrough]]; + case t_flag_secondary: + [[fallthrough]]; + case t_flag_compound: + return true; + default: + return false; + } } /** * @brief Checks whether the token represents a valid flag. * * A token is considered a valid flag token if: - * 1. The token's type if a valid flag token type (`t_flag_primary` or `t_flag_secondary`) + * 1. The token's type if a valid flag token type (see @ref is_flag_token) * 2. The token's `arg` member is set. * * @return true if `type` is either `t_flag_primary` or `t_flag_secondary`, false otherwise. */ [[nodiscard]] bool is_valid_flag_token() const noexcept { - return this->is_flag_token() and this->arg != nullptr; + return this->is_flag_token() and not this->args.empty(); } token_type type; ///< The token's type discrimiator value. std::string value; ///< The actual token's value. - arg_ptr_t arg = nullptr; ///< The corresponding argument + arg_ptr_list_t args = {}; ///< The corresponding argument }; } // namespace ap::detail diff --git a/include/ap/exceptions.hpp b/include/ap/exceptions.hpp index bcf0c516..b9ceb461 100644 --- a/include/ap/exceptions.hpp +++ b/include/ap/exceptions.hpp @@ -102,12 +102,12 @@ struct parsing_failure : public argument_parser_exception { ) noexcept { if (std::is_lt(ordering)) return parsing_failure( - std::format("Not enough values provided for optional argument [{}]", arg_name.str()) + std::format("Not enough values provided for argument [{}]", arg_name.str()) ); if (std::is_gt(ordering)) return parsing_failure( - std::format("Too many values provided for optional argument [{}]", arg_name.str()) + std::format("Too many values provided for argument [{}]", arg_name.str()) ); return parsing_failure( diff --git a/tests/include/argument_parser_test_fixture.hpp b/tests/include/argument_parser_test_fixture.hpp index 7657cce5..a4a0d4f5 100644 --- a/tests/include/argument_parser_test_fixture.hpp +++ b/tests/include/argument_parser_test_fixture.hpp @@ -155,7 +155,7 @@ struct argument_parser_test_fixture { argument_token flag_tok{argument_token::t_flag_primary, init_arg_flag_primary(arg_idx)}; const auto opt_arg_it = this->sut._find_opt_arg(flag_tok); if (opt_arg_it != this->sut._optional_args.end()) - flag_tok.arg = *opt_arg_it; + flag_tok.args.emplace_back(*opt_arg_it); arg_tokens.push_back(std::move(flag_tok)); arg_tokens.push_back(argument_token{argument_token::t_value, init_arg_value(arg_idx)}); diff --git a/tests/source/test_argument_parser_parse_args.cpp b/tests/source/test_argument_parser_parse_args.cpp index 9a66bf5c..b8e5b419 100644 --- a/tests/source/test_argument_parser_parse_args.cpp +++ b/tests/source/test_argument_parser_parse_args.cpp @@ -1176,6 +1176,61 @@ TEST_CASE_FIXTURE( free_argv(argc, argv); } +// greedy arguments + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "greedy arguments should consume all command-line values until their upper nargs bound is " + "reached regardless of the tokens' types" +) { + const std::string greedy_arg_name = "greedy"; + const std::string actual_value = "value"; + + const std::size_t n_greedily_consumed_values = 3ull; + const std::size_t greedy_nargs_bound = 1ull + n_greedily_consumed_values; + const std::size_t opt_begin_idx = 0ull; + + std::vector argv_vec{"program"}; + + SUBCASE("greedy positional argument") { + sut.add_positional_argument(greedy_arg_name).greedy().nargs(greedy_nargs_bound); + argv_vec.emplace_back(actual_value); + } + SUBCASE("greedy optional arg") { + sut.add_optional_argument(greedy_arg_name).greedy().nargs(greedy_nargs_bound); + argv_vec.emplace_back(std::format("--{}", greedy_arg_name)); + argv_vec.emplace_back(actual_value); + } + + CAPTURE(sut); + CAPTURE(argv_vec); + + add_optional_args(n_greedily_consumed_values, opt_begin_idx); + + // add the greedily consumed flags + std::vector expected_greedy_arg_values{actual_value}; + for (std::size_t i = opt_begin_idx; i < n_greedily_consumed_values; ++i) { + expected_greedy_arg_values.emplace_back(init_arg_flag_primary(i)); + argv_vec.emplace_back(init_arg_flag_primary(i)); + } + + // add the normally parsed flags + const std::size_t expected_opt_count = 1ull; + for (std::size_t i = opt_begin_idx; i < n_greedily_consumed_values; ++i) + argv_vec.emplace_back(init_arg_flag_primary(i)); + + const int argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + REQUIRE_NOTHROW(sut.parse_args(argc, argv)); + + CHECK_EQ(sut.values(greedy_arg_name), expected_greedy_arg_values); + for (std::size_t i = opt_begin_idx; i < n_greedily_consumed_values; ++i) + CHECK_EQ(sut.count(init_arg_name_primary(i)), expected_opt_count); + + free_argv(argc, argv); +} + // unknown_arguments_policy TEST_CASE_FIXTURE( diff --git a/tests/source/test_argument_token.cpp b/tests/source/test_argument_token.cpp index 36717e7d..f88c6adc 100644 --- a/tests/source/test_argument_token.cpp +++ b/tests/source/test_argument_token.cpp @@ -47,8 +47,8 @@ TEST_CASE("is_flag_token should return true if the token's type is either primar CHECK_FALSE(sut_type{t_value, ""}.is_flag_token()); } -TEST_CASE("is_valid_flag_token should return true if the token is a flag token and it's arg member " - "is set") { +TEST_CASE("is_valid_flag_token should return true if the token is a flag token and it's args list " + "is not empty") { CHECK_FALSE(sut_type{t_value, ""}.is_valid_flag_token()); CHECK_FALSE(sut_type{t_flag_primary, ""}.is_valid_flag_token()); @@ -57,6 +57,6 @@ TEST_CASE("is_valid_flag_token should return true if the token is a flag token a std::shared_ptr arg_ptr = std::make_shared>(argument_name{""}); - CHECK(sut_type{t_flag_primary, "", arg_ptr}.is_valid_flag_token()); - CHECK(sut_type{t_flag_secondary, "", arg_ptr}.is_valid_flag_token()); + CHECK(sut_type{t_flag_primary, "", {arg_ptr}}.is_valid_flag_token()); + CHECK(sut_type{t_flag_secondary, "", {arg_ptr}}.is_valid_flag_token()); } From daaaf059b0b9829a3dc92a243dc07b0b3f3fe0fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Musia=C5=82?= <111433005+SpectraL519@users.noreply.github.com> Date: Tue, 23 Sep 2025 23:16:24 +0200 Subject: [PATCH 08/10] YT-CPPAP-45: Add argument groups support - Added an `argument_group` class with the following attribute setters: - `required` - at least one argument from the group must be used - `mutually_exclusive` - at most one argument from the group can be used - Added an `add_group` method to `argument_parser` - Added new overloads for the `add_optional_argument` and `add_flag` functions with an additional `group` parameter - Aligned the final parsing validation logic to properly verify the requirements of defined argument groups - Removed the `program_name` attribute setter from `argument_parser` and added the `name` parameter to the class constructor to enforce the program name's presence in a parser instance --- CMakeLists.txt | 2 +- Doxyfile | 2 +- MODULE.bazel | 2 +- README.md | 1 + cpp-ap-demo | 2 +- docs/tutorial.md | 177 +++++++++-- include/ap/argument_group.hpp | 108 +++++++ include/ap/argument_parser.hpp | 299 +++++++++++++----- include/ap/detail/argument_base.hpp | 4 +- include/ap/detail/argument_token.hpp | 4 +- .../include/argument_parser_test_fixture.hpp | 16 +- .../test_argument_parser_add_argument.cpp | 46 +++ tests/source/test_argument_parser_info.cpp | 29 +- .../test_argument_parser_parse_args.cpp | 90 +++++- 14 files changed, 636 insertions(+), 146 deletions(-) create mode 100644 include/ap/argument_group.hpp 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); +} From 8d06847b74bf375feaabba5463faf5521b36b1bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Musia=C5=82?= <111433005+SpectraL519@users.noreply.github.com> Date: Thu, 25 Sep 2025 19:22:01 +0200 Subject: [PATCH 09/10] YT-CPPAP-61: Add subparsers support - Extended the `argument_parser` class with members and functions allowing users to manage subparsers - Aligned the argument parsing functionality: - A parser tries to match the first argument from its input argument range to a subparser - If a match is found, the corresponding subparser is invoked with the remaining arguments - If no match is found, the current parser finalizes parsing arguments - Aligned the argument parsing functionality to allow forward ranges only - Added the `name` and `program_name` attribute accessor functions in `argument_parser` - Restricted the creation of positional arguments to using only one argument name - Added the possibility to bind positional arguments to argument groups - Added an `is_used(arg_name)` method for checking the usage status of an argument - Added an `o_version` default argument which prints the parser's version info - Renamed the `argument_descriptor` class to `help_builder` - Fixed an error where the requirements of all arguments within a mutually exclusive group were verified, which caused unexpected exceptions - Added the `logging_mode` and `ap_git` demo projects --- CMakeLists.txt | 2 +- Doxyfile | 2 +- MODULE.bazel | 2 +- README.md | 9 + cpp-ap-demo | 2 +- docs/tutorial.md | 262 ++++++++-- include/ap/action/predefined.hpp | 4 +- include/ap/argument.hpp | 26 +- include/ap/argument_parser.hpp | 487 ++++++++++++------ include/ap/detail/argument_base.hpp | 6 +- ...gument_descriptor.hpp => help_builder.hpp} | 10 +- include/ap/util/concepts.hpp | 23 + tests/CMakeLists.txt | 14 +- .../include/argument_parser_test_fixture.hpp | 29 +- tests/include/argument_test_fixture.hpp | 6 +- ... => test_argument_parser_add_elements.cpp} | 84 +-- tests/source/test_argument_parser_info.cpp | 21 +- .../test_argument_parser_parse_args.cpp | 163 ++++-- ...t_descriptor.cpp => test_help_builder.cpp} | 6 +- tests/source/test_optional_argument.cpp | 68 +-- tests/source/test_positional_argument.cpp | 62 +-- 21 files changed, 880 insertions(+), 408 deletions(-) rename include/ap/detail/{argument_descriptor.hpp => help_builder.hpp} (95%) rename tests/source/{test_argument_parser_add_argument.cpp => test_argument_parser_add_elements.cpp} (85%) rename tests/source/{test_argument_descriptor.cpp => test_help_builder.cpp} (96%) 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)); } From 0b149fb127d39c72c2cff63f0803669d37bd90d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Musia=C5=82?= <111433005+SpectraL519@users.noreply.github.com> Date: Fri, 26 Sep 2025 22:52:40 +0200 Subject: [PATCH 10/10] CPP-AP: version 3.0.0 - Renamed the `bypass_required` argument attribute to `suppress_arg_checks` - Introduced the `suppress_group_checks` argument attribute - Aligned the tutorial page and documentation comments - Made code cleanup changes --- CMakeLists.txt | 2 +- Doxyfile | 2 +- MODULE.bazel | 2 +- README.md | 8 +- cpp-ap-demo | 2 +- docs/tutorial.md | 68 ++++-- include/ap/argument.hpp | 92 +++++--- include/ap/argument_parser.hpp | 164 ++++++++------- include/ap/detail/argument_base.hpp | 7 +- include/ap/exceptions.hpp | 50 ----- include/ap/nargs/range.hpp | 3 + include/ap/types.hpp | 2 +- .../include/argument_parser_test_fixture.hpp | 5 + tests/include/argument_test_fixture.hpp | 21 +- .../test_argument_parser_add_elements.cpp | 6 +- tests/source/test_argument_parser_info.cpp | 6 + .../test_argument_parser_parse_args.cpp | 49 ++++- tests/source/test_optional_argument.cpp | 114 +++++----- tests/source/test_positional_argument.cpp | 198 +++++++++--------- 19 files changed, 452 insertions(+), 349 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 19bcd720..c349b3fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ else() endif() project(cpp-ap - VERSION 3.0.0.8 + VERSION 3.0.0 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 fb9fd682..c2cbab5f 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.8 +PROJECT_NUMBER = 3.0.0 # 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 597d9bb6..91d3db86 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,4 +1,4 @@ module( name = "cpp-ap", - version = "3.0.0.8", + version = "3.0.0", ) diff --git a/README.md b/README.md index 0527f171..60bddaea 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Command-line argument parser for C++20 > [!NOTE] > -> [v1.0](https://github.com/SpectraL519/cpp-ap/commit/9a9e5360766b732f322ae2efe3cf5ec5f9268eef) of the library has been developed for the *Team Programming* course at the *Wrocław University of Science and Technology*. +> [v1.0](https://github.com/SpectraL519/cpp-ap/releases/tag/v1.0) of the library has been developed for the *Team Programming* course at the *Wrocław University of Science and Technology*. > > Faculty: *W04N - Faculty of Information and Communication Technology* > @@ -62,6 +62,11 @@ Command-line argument parser for C++20 - [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) + - [Creating New Groups](/docs/tutorial.md#creating-new-groups) + - [Adding Arguments to Groups](/docs/tutorial.md#adding-arguments-to-groups) + - [Group Attributes](/docs/tutorial.md#group-attributes) + - [Complete Example](/docs/tutorial.md#complete-example) + - [Suppressing Argument Group Checks](/docs/tutorial.md#suppressing-argument-group-checks) - [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) @@ -72,6 +77,7 @@ Command-line argument parser for C++20 - [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) + - [Suppressing Argument Group Checks](/docs/tutorial.md#suppressing-argument-group-checks) - [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 bb13a211..cf3f2ad8 160000 --- a/cpp-ap-demo +++ b/cpp-ap-demo @@ -1 +1 @@ -Subproject commit bb13a2111f075e388d48e0cc4bba1bf62dfaad45 +Subproject commit cf3f2ad8e9c06af9adcde99288eea517bfb6ecae diff --git a/docs/tutorial.md b/docs/tutorial.md index 87567155..c45e46f8 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -12,18 +12,18 @@ - [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) - - [hidden](#2-hidden---if-this-option-is-set-for-an-argument-then-it-will-not-be-included-in-the-program-description) - - [required](#3-required---if-this-option-is-set-for-an-argument-and-its-value-is-not-passed-in-the-command-line-an-exception-will-be-thrown) - - [bypass required](#4-bypass_required---if-this-option-is-set-for-an-argument-the-required-option-for-other-arguments-will-be-discarded-if-the-bypassing-argument-is-used-in-the-command-line) - - [nargs](#5-nargs---sets-the-allowed-number-of-values-to-be-parsed-for-an-argument) - - [greedy](#6-greedy---if-this-option-is-set-the-argument-will-consume-all-command-line-values-until-its-upper-nargs-bound-is-reached) - - [choices](#7-choices---a-list-of-valid-argument-values) - - [value actions](#8-value-actions---functions-that-are-called-after-parsing-an-arguments-value) - - [default values](#9-default_values---a-list-of-values-which-will-be-used-if-no-values-for-an-argument-have-been-parsed) + - [help](#1-help---the-arguments-description-which-will-be-printed-when-printing-the-parser-class-instance) - the text shown in the help message to describe an argument + - [hidden](#2-hidden---if-this-option-is-set-for-an-argument-then-it-will-not-be-included-in-the-program-description) - hides the argument from the generated program description and help output + - [required](#3-required---if-this-option-is-set-for-an-argument-and-its-value-is-not-passed-in-the-command-line-an-exception-will-be-thrown) - marks the argument as mandatory; not using it will cause an error + - [suppress arg checks](#4-suppress_arg_checks---using-a-suppressing-argument-results-in-suppressing-requirement-checks-for-other-arguments) - if a suppressing argument is used, other requirement validation will be skipped for other arguments + - [nargs](#5-nargs---sets-the-allowed-number-of-values-to-be-parsed-for-an-argument) - defines how many values an argument can or must accept + - [greedy](#6-greedy---if-this-option-is-set-the-argument-will-consume-all-command-line-values-until-its-upper-nargs-bound-is-reached) - makes the argument consume all following values until its limit is reached + - [choices](#7-choices---a-list-of-valid-argument-values) - restricts the valid inputs to a predefined set of values + - [value actions](#8-value-actions---functions-that-are-called-after-parsing-an-arguments-value) - allows you to run custom code after the argument’s value is parsed + - [default values](#9-default_values---a-list-of-values-which-will-be-used-if-no-values-for-an-argument-have-been-parsed) - specifies fallback values to use if none are provided - [Parameters Specific for Optional Arguments](#parameters-specific-for-optional-arguments) - - [on-flag actions](#1-on-flag-actions---functions-that-are-called-immediately-after-parsing-an-arguments-flag) - - [implicit values](#2-implicit_values---a-list-of-values-which-will-be-set-for-an-argument-if-only-its-flag-but-no-values-are-parsed-from-the-command-line) + - [on-flag actions](#1-on-flag-actions---functions-that-are-called-immediately-after-parsing-an-arguments-flag) - executes custom code immediately when the argument’s flag is present + - [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) - automatically assigns a value if an argument flag is used without an explicit value - [Predefined Parameter Values](#predefined-parameter-values) - [Default Arguments](#default-arguments) - [Argument Groups](#argument-groups) @@ -31,6 +31,7 @@ - [Adding Arguments to Groups](#adding-arguments-to-groups) - [Group Attributes](#group-attributes) - [Complete Example](#complete-example) + - [Suppressing Argument Group Checks](#suppressing-argument-group-checks) - [Parsing Arguments](#parsing-arguments) - [Basic Argument Parsing Rules](#basic-argument-parsing-rules) - [Compound Arguments](#compound-arguments) @@ -222,7 +223,7 @@ parser.add_optional_argument("n", ap::n_secondary); > > - 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) +> - [nargs](#5-nargs---sets-the-allowed-number-of-values-to-be-parsed-for-an-argument) > - [greedy](#6-greedy---if-this-option-is-set-the-argument-will-consume-all-command-line-values-until-its-upper-nargs-bound-is-reached) > - [choices](#7-choices---a-list-of-valid-argument-values) > - [value actions](#8-value-actions---functions-that-are-called-after-parsing-an-arguments-value) @@ -318,9 +319,7 @@ Optional arguments: > [!WARNING] > > - If a positional argument is defined as non-required, then no required positional argument can be defined after (only other non-required positional arguments and optional arguments will be allowed). -> - For both positional and optional arguments: -> - enabling the `required` option disables the `bypass_required` option -> - disabling the `required` option has no effect on the `bypass_required` option. +> - If an argument is suppressing (see [suppress arg checks](#4-suppress_arg_checks---using-a-suppressing-argument-results-in-suppressing-requirement-checks-for-other-arguments) and [Suppressing Argument Group Checks](#suppressing-argument-group-checks)), then it cannot be required (an exception will be thrown). ```cpp // example: positional arguments @@ -377,24 +376,27 @@ Command Result
-#### 4. `bypass_required` - If this option is set for an argument, the `required` option for other arguments will be discarded if the bypassing argument is used in the command-line. +#### 4. `suppress_arg_checks` - Using a suppressing argument results in suppressing requirement checks for other arguments. + +If an argument is defined with the `suppress_arg_checks` option enabled and such argument is explicitly used in the command-line, then requirement validation will be suppressed/skipped for other arguments. This includes validating whether: +- a required argument has been parsed +- the number of values parsed for an argument matches the specified [nargs](#5-nargs---sets-the-allowed-number-of-values-to-be-parsed-for-an-argument) range. > [!NOTE] > -> - Both all arguments have the `bypass_required` option disabled. -> - The default value of the value parameter of the `argument::bypass_required(bool)` method is `true` for all arguments. +> - All arguments have the `suppress_arg_checks` option disabled by default. +> - The default value of the value parameter of the `argument::suppress_arg_checks(bool)` method is `true` for all arguments. > [!WARNING] > -> For both positional and optional arguments: -> - enabling the `bypass_required` option disables the `required` option -> - disabling the `bypass_required` option has no effect on the `required` option. +> - Enabling the `suppress_arg_checks` option has no effect on [argument group](#argument-groups) requirements validation. +> - Enabling argument checks suppressing is not possible for required arguments (an exception will be thrown). ```cpp // example: optional arguments parser.add_positional_argument("input"); parser.add_optional_argument("output", "o").required(); -parser.add_optional_argument("version", "v").bypass_required(); +parser.add_optional_argument("version", "v").suppress_arg_checks(); parser.parse_args(argc, argv); @@ -522,7 +524,7 @@ Actions are represented as functions, which take the argument's value as an argu ```cpp void is_valid_user_tag(const std::string& tag) { if (tag.empty() or tag.front() != '@') - throw std::runtime_error(std::format("Invalid user tag: `{}` — must start with '@'", tag)); + throw std::runtime_error(std::format("Invalid user tag: `{}` - must start with '@'", tag)); } parser.add_optional_argument("user", "u") @@ -934,6 +936,26 @@ Output Options: (required, mutually exclusive) --print, -p : Print output to the console ``` +### Suppressing Argument Group Checks + +Similarly to [suppressing argument checks](#4-suppress_arg_checks---using-a-suppressing-argument-results-in-suppressing-requirement-checks-for-other-arguments), an argument can suppress the requirement checks of argument groups: + +```c++ +argument.suppress_group_checks(); +``` + +If such argument is used the requirement checks associated with the [group attributes](#group-attributes) will not be validated. + +> [!NOTE] +> +> - All arguments have the `suppress_group_checks` option disabled by default. +> - The default value of the value parameter of the `argument::suppress_group_checks(bool)` method is `true` for all arguments. + +> [!WARNING] +> +> - Enabling the `suppress_group_checks` option has no effect on argument requirements validation. +> - Enabling argument group checks suppressing is not possible for required arguments (an exception will be thrown). +


diff --git a/include/ap/argument.hpp b/include/ap/argument.hpp index 9fcdc472..3ddf8462 100644 --- a/include/ap/argument.hpp +++ b/include/ap/argument.hpp @@ -117,10 +117,14 @@ class argument : public detail::argument_base { return this->_required; } - /// @return `true` if required argument bypassing is enabled for the argument, `false` otherwise. - /// @note Required argument bypassing is enabled only if the argument is not required. - [[nodiscard]] bool is_bypass_required_enabled() const noexcept override { - return not this->_required and this->_bypass_required; + /// @return `true` if argument checks suppressing is enabled for the argument, `false` otherwise. + [[nodiscard]] bool suppresses_arg_checks() const noexcept override { + return this->_suppress_arg_checks; + } + + /// @return `true` if argument group checks suppressing is enabled for the argument, `false` otherwise. + [[nodiscard]] bool suppresses_group_checks() const noexcept override { + return this->_suppress_group_checks; } /// @return `true` if the argument is greedy, `false` otherwise. @@ -152,40 +156,62 @@ class argument : public detail::argument_base { /** * @brief Set the `required` attribute of the argument - * @param r The attribute value. + * @param value The attribute value (default: `true`). * @return Reference to the argument instance. - * @attention Setting the `required` attribute to `true` disables the `bypass_required` attribute. + * @throws ap::invalid_configuration if the argument is configured to suppress argument/group checks. */ - argument& required(const bool r = true) noexcept { - this->_required = r; - if (this->_required) - this->_bypass_required = false; + argument& required(const bool value = true) { + if (value and (this->_suppress_arg_checks or this->_suppress_group_checks)) + throw invalid_configuration( + std::format("A suppressing argument [{}] cannot be required!", this->_name.str()) + ); + + this->_required = value; return *this; } /** - * @brief Enable/disable bypassing the `required` attribute for the argument. - * @param br The attribute value. + * @brief Enable/disable suppressing argument checks for other arguments. + * @param value The attribute value (default: `true`). * @return Reference to the argument instance. - * @attention Setting the `bypass_required` option to `true` disables the `required` attribute. + * @throws ap::invalid_configuration if the argument is configured to be required. */ - argument& bypass_required(const bool br = true) noexcept { - this->_bypass_required = br; - if (this->_bypass_required) - this->_required = false; + argument& suppress_arg_checks(const bool value = true) { + if (value and this->_required) + throw invalid_configuration(std::format( + "A required argument [{}] cannot suppress argument checks!", this->_name.str() + )); + + this->_suppress_arg_checks = value; + return *this; + } + + /** + * @brief Enable/disable suppressing argument group checks. + * @param value The attribute value (default: `true`). + * @return Reference to the argument instance. + * @throws ap::invalid_configuration if the argument is configured to be required. + */ + argument& suppress_group_checks(const bool value = true) { + if (value and this->_required) + throw invalid_configuration(std::format( + "A required argument [{}] cannot suppress argument group checks!", this->_name.str() + )); + + this->_suppress_group_checks = value; return *this; } /** * @brief Set the `greedy` attribute of the argument. - * @param g The attribute value. + * @param value The attribute value (default: `true`). * @return Reference to the argument instance. * @note The method is enabled only if `value_type` is not `none_type`. */ - argument& greedy(const bool g = true) noexcept + argument& greedy(const bool value = true) noexcept requires(not util::c_is_none) { - this->_greedy = g; + this->_greedy = value; return *this; } @@ -438,8 +464,10 @@ class argument : public detail::argument_base { bld.params.reserve(6ull); if (this->_required != _default_required) bld.add_param("required", std::format("{}", this->_required)); - if (this->is_bypass_required_enabled()) - bld.add_param("bypass required", "true"); + if (this->_suppress_arg_checks) + bld.add_param("suppress arg checks", "true"); + if (this->_suppress_group_checks) + bld.add_param("suppress group checks", "true"); if (this->_nargs_range != _default_nargs_range) bld.add_param("nargs", this->_nargs_range); if constexpr (util::c_writable) { @@ -546,12 +574,19 @@ class argument : public detail::argument_base { return this->_values_impl(); } + /// @return Reference to the vector of parsed values for the argument. + /// @note For none-type arguments, the method always returns an empty vector. [[nodiscard]] const std::vector& _values_impl() const noexcept requires(util::c_is_none) { return this->_values; } + /** + * @return Reference to the vector of parsed values for the argument. + * @note If no parsed values are available, the method attempts to return the predefined values (default/implicit). + * @note The method is enabled only if `value_type` is not `none_type`. + */ [[nodiscard]] const std::vector& _values_impl() const noexcept requires(not util::c_is_none) { @@ -670,11 +705,15 @@ class argument : public detail::argument_base { } else { if (not (std::istringstream(str_value) >> value)) - throw parsing_failure::invalid_value(this->_name, str_value); + throw parsing_failure(std::format( + "Cannot parse value `{}` for argument [{}].", str_value, this->_name.str() + )); } if (not this->_is_valid_choice(value)) - throw parsing_failure::invalid_choice(this->_name, str_value); + throw parsing_failure(std::format( + "Value `{}` is not a valid choice for argument [{}].", str_value, this->_name.str() + )); const auto apply_visitor = action::util::apply_visitor{value}; for (const auto& action : this->_value_actions) @@ -700,7 +739,10 @@ class argument : public detail::argument_base { _value_actions; ///< The argument's value actions collection. bool _required : 1; ///< The argument's `required` attribute value. - bool _bypass_required : 1 = false; ///< The argument's `bypass_required` attribute value. + bool _suppress_arg_checks : 1 = + false; ///< The argument's `suppress_arg_checks` attribute value. + bool _suppress_group_checks : 1 = + false; ///< The argument's `suppress_group_checks` attribute value. bool _greedy : 1 = false; ///< The argument's `greedy` attribute value. bool _hidden : 1 = false; ///< The argument's `hidden` attribute value. diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index 099f2524..61950c9d 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -558,7 +558,9 @@ class argument_parser { 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); + throw parsing_failure(std::format( + "Failed to deduce the argument for values [{}]", util::join(state.unknown_args) + )); } /** @@ -939,7 +941,7 @@ class argument_parser { ), _gr_positional_args(add_group("Positional Arguments")), _gr_optional_args(add_group("Optional Arguments")) { - if (name.empty()) // TODO: add test case + if (name.empty()) throw invalid_configuration("The program name cannot be empty!"); if (util::contains_whitespaces(name)) @@ -1066,10 +1068,8 @@ class argument_parser { // 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)) - ); + for (const auto& tok : this->_tokenize(args_begin, args_end, state)) + this->_parse_token(tok, state); this->_verify_final_state(); this->_finalized = true; } @@ -1090,10 +1090,12 @@ 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() - ); + throw invalid_configuration(std::format( + "Required positional argument [{}] cannot be defined after a non-required " + "positional argument [{}].", + arg->name().str(), + non_required_arg->name().str() + )); } } @@ -1103,6 +1105,7 @@ class argument_parser { * @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. * @return A list of preprocessed command-line argument tokens. */ template AIt> @@ -1111,23 +1114,20 @@ class argument_parser { ) { arg_token_vec_t toks; toks.reserve(static_cast(std::ranges::distance(args_begin, args_end))); - - std::ranges::for_each( - args_begin, - args_end, - std::bind_front(&argument_parser::_tokenize_arg, this, std::ref(state), std::ref(toks)) - ); - + std::ranges::for_each(args_begin, args_end, [&](const auto& arg_value) { + this->_tokenize_arg(arg_value, toks, state); + }); return toks; } /** * @brief Appends an argument token(s) created from `arg_value` to the `toks` vector. - * @param toks The argument token list to which the processed token(s) will be appended. * @param arg_value The command-line argument's value to be processed. + * @param toks The argument token list to which the processed token(s) will be appended. + * @param state The current parsing state. */ void _tokenize_arg( - const parsing_state& state, arg_token_vec_t& toks, const std::string_view arg_value + const std::string_view arg_value, arg_token_vec_t& toks, const parsing_state& state ) { detail::argument_token tok{ .type = this->_deduce_token_type(arg_value), .value = std::string(arg_value) @@ -1187,7 +1187,7 @@ class argument_parser { /** * @brief Check if a flag token is valid based on its value. - * @attention Sets the `args` member of the token if an argument with the given name (token's value) is present. + * @attention Extends the `args` member of the token if an argument with the given name (token's value) is present. * @param tok The argument token to validate. * @return `true` if the given token represents a valid argument flag. */ @@ -1275,29 +1275,29 @@ class argument_parser { /** * @brief Parse a single command-line argument token. - * @param state The current parsing state. * @param tok The token to be parsed. + * @param state The current parsing state. * @throws ap::parsing_failure */ - void _parse_token(parsing_state& state, const detail::argument_token& tok) { + void _parse_token(const detail::argument_token& tok, parsing_state& state) { if (state.curr_arg and state.curr_arg->is_greedy()) { - this->_set_argument_value(state, tok.value); + this->_set_argument_value(tok.value, state); return; } if (tok.is_flag_token()) - this->_parse_flag_token(state, tok); + this->_parse_flag_token(tok, state); else - this->_parse_value_token(state, tok); + this->_parse_value_token(tok, state); } /** * @brief Parse a single command-line argument *flag* token. - * @param state The current parsing state. * @param tok The token to be parsed. + * @param state The current parsing state. * @throws ap::parsing_failure */ - void _parse_flag_token(parsing_state& state, const detail::argument_token& tok) { + void _parse_flag_token(const detail::argument_token& tok, parsing_state& state) { if (not tok.is_valid_flag_token()) { if (state.parse_known_only) { state.curr_arg.reset(); @@ -1320,11 +1320,11 @@ class argument_parser { /** * @brief Parse a single command-line argument *value* token. - * @param state The current parsing state. * @param tok The token to be parsed. + * @param state The current parsing state. * @throws ap::parsing_failure */ - void _parse_value_token(parsing_state& state, const detail::argument_token& tok) { + void _parse_value_token(const detail::argument_token& tok, parsing_state& state) { if (not state.curr_arg) { if (state.curr_pos_arg_it == this->_positional_args.end()) { state.unknown_args.emplace_back(tok.value); @@ -1334,16 +1334,16 @@ class argument_parser { state.curr_arg = *state.curr_pos_arg_it; } - this->_set_argument_value(state, tok.value); + this->_set_argument_value(tok.value, state); } /** * @brief Set the value for the currently processed argument. * @attention This function assumes that the current argument is set (i.e. `state.curr_arg != nullptr`). - * @param state The current parsing state. * @param value The value to be set for the current argument. + * @param state The current parsing state. */ - void _set_argument_value(parsing_state& state, const std::string_view value) noexcept { + void _set_argument_value(const std::string_view value, parsing_state& state) noexcept { if (state.curr_arg->set_value(std::string(value))) return; // argument still accepts values @@ -1360,86 +1360,98 @@ class argument_parser { * @throws ap::parsing_failure if the state of the parsed arguments is invalid. */ void _verify_final_state() const { - const bool are_required_args_bypassed = this->_are_required_args_bypassed(); + const auto [supress_group_checks, suppress_arg_checks] = this->_are_checks_suppressed(); for (const auto& group : this->_argument_groups) - this->_verify_group_requirements(*group, are_required_args_bypassed); + this->_verify_group_requirements(*group, supress_group_checks, suppress_arg_checks); } /** - * @brief Check whether required argument bypassing is enabled - * @return true if at least one argument with enabled required argument bypassing is used, false otherwise. + * @brief Check whether required argument group checks or argument checks suppressing is enabled. + * @return A pair of boolean flags indicating whether suppressing is enabled. + * @note The first flag of the returned pair indicates whetehr argument group check suppressing is enabled, + * @note while the second flag indicated whether argument check suppressing is enabled. */ - [[nodiscard]] bool _are_required_args_bypassed() const noexcept { + [[nodiscard]] std::pair _are_checks_suppressed() const noexcept { + bool suppress_group_checks = false; + bool suppress_arg_checks = false; + + auto check_arg = [&](const arg_ptr_t& arg) { + if (arg->is_used()) { + if (arg->suppresses_group_checks()) + suppress_group_checks = true; + if (arg->suppresses_arg_checks()) + suppress_arg_checks = true; + } + }; + // TODO: use std::views::join after the transition to C++23 - return std::ranges::any_of( - this->_positional_args, - [](const arg_ptr_t& arg) { - return arg->is_used() and arg->is_bypass_required_enabled(); - } - ) - or std::ranges::any_of(this->_optional_args, [](const arg_ptr_t& arg) { - return arg->is_used() and arg->is_bypass_required_enabled(); - }); + std::ranges::for_each(this->_positional_args, check_arg); + std::ranges::for_each(this->_optional_args, check_arg); + return {suppress_group_checks, suppress_arg_checks}; } /** * @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. + * @param suppress_arg_checks A flag indicating whether argument checks are suppressed. * @throws ap::parsing_failure if the requirements are not satistied. */ void _verify_group_requirements( - const argument_group& group, const bool are_required_args_bypassed + const argument_group& group, + const bool suppress_group_checks, + const bool suppress_arg_checks ) 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 (not suppress_group_checks) { + 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 - )); + 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(); - }); + 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 (used_arg_it != group._arguments.end()) { + // only the one used argument has to be validated + this->_verify_argument_requirements(*used_arg_it, suppress_arg_checks); + 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._required and n_used_args == 0ull) + throw parsing_failure(std::format( + "At least one argument from the required group '{}' must 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); + this->_verify_argument_requirements(arg, suppress_arg_checks); } /** * @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. + * @param suppress_arg_checks A flag indicating whether argument checks are suppressed. * @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) + void _verify_argument_requirements(const arg_ptr_t& arg, const bool suppress_arg_checks) const { + if (suppress_arg_checks) return; if (arg->is_required() and not arg->has_value()) - throw parsing_failure::required_argument_not_parsed(arg->name()); - + throw parsing_failure( + std::format("No values parsed for a required argument [{}]", arg->name().str()) + ); if (const auto nv_ord = arg->nvalues_ordering(); not std::is_eq(nv_ord)) throw parsing_failure::invalid_nvalues(arg->name(), nv_ord); } diff --git a/include/ap/detail/argument_base.hpp b/include/ap/detail/argument_base.hpp index c01a5fcf..e3f1de6e 100644 --- a/include/ap/detail/argument_base.hpp +++ b/include/ap/detail/argument_base.hpp @@ -45,8 +45,11 @@ class argument_base { /// @return `true` if the argument is required, `false` otherwise. virtual bool is_required() const noexcept = 0; - /// @return `true` if the argument is allowed to bypass the required check, `false` otherwise. - virtual bool is_bypass_required_enabled() const noexcept = 0; + /// @return `true` if argument checks suppressing is enabled for the argument, `false` otherwise. + virtual bool suppresses_arg_checks() const noexcept = 0; + + /// @return `true` if argument group checks suppressing is enabled for the argument, `false` otherwise. + virtual bool suppresses_group_checks() const noexcept = 0; /// @return `true` if the argument is greedy, `false` otherwise. virtual bool is_greedy() const noexcept = 0; diff --git a/include/ap/exceptions.hpp b/include/ap/exceptions.hpp index b9ceb461..9670b285 100644 --- a/include/ap/exceptions.hpp +++ b/include/ap/exceptions.hpp @@ -37,20 +37,6 @@ struct invalid_configuration : public argument_parser_exception { ) noexcept { return invalid_configuration(std::format("Given name [{}] already used.", arg_name.str())); } - - struct positional { - static invalid_configuration required_after_non_required( - const detail::argument_name& required_arg_name, - const detail::argument_name& non_required_arg_name - ) noexcept { - return invalid_configuration(std::format( - "Required positional argument [{}] cannot be defined after a non-required " - "positional argument [{}].", - required_arg_name.str(), - non_required_arg_name.str() - )); - } - }; }; /// @brief Exception type used for errors encountered during the argument parsing operation. @@ -61,42 +47,6 @@ struct parsing_failure : public argument_parser_exception { return parsing_failure(std::format("Unknown argument [{}].", arg_name)); } - static parsing_failure value_already_set(const detail::argument_name& arg_name) noexcept { - return parsing_failure( - std::format("Value for argument [{}] has already been set.", arg_name.str()) - ); - } - - static parsing_failure invalid_value( - const detail::argument_name& arg_name, const std::string& value - ) noexcept { - return parsing_failure( - std::format("Cannot parse value `{}` for argument [{}].", value, arg_name.str()) - ); - } - - static parsing_failure invalid_choice( - const detail::argument_name& arg_name, const std::string& value - ) noexcept { - return parsing_failure(std::format( - "Value `{}` is not a valid choice for argument [{}].", value, arg_name.str() - )); - } - - static parsing_failure required_argument_not_parsed(const detail::argument_name& arg_name - ) noexcept { - return parsing_failure( - std::format("No values parsed for a required argument [{}]", arg_name.str()) - ); - } - - static parsing_failure argument_deduction_failure(const std::vector& values - ) noexcept { - return parsing_failure( - std::format("Failed to deduce the argument for values [{}]", util::join(values)) - ); - } - static parsing_failure invalid_nvalues( const detail::argument_name& arg_name, const std::weak_ordering ordering ) noexcept { diff --git a/include/ap/nargs/range.hpp b/include/ap/nargs/range.hpp index 7e66655e..7d115bbc 100644 --- a/include/ap/nargs/range.hpp +++ b/include/ap/nargs/range.hpp @@ -17,7 +17,10 @@ namespace ap::nargs { using count_type = std::size_t; +/// @brief The minimum bound for the `nargs::range` class. constexpr count_type min_bound = std::numeric_limits::min(); + +/// @brief The maximum bound for the `nargs::range` class. constexpr count_type max_bound = std::numeric_limits::max(); /// @brief Argument's number of values managing class. diff --git a/include/ap/types.hpp b/include/ap/types.hpp index f640302c..0ee9ce4b 100644 --- a/include/ap/types.hpp +++ b/include/ap/types.hpp @@ -21,7 +21,7 @@ struct none_type {}; /// @brief A helper structure used to represent a program's version. struct version { - std::uint32_t major = 1u; ///< The major version number. + std::uint32_t major = 0u; ///< The major version number. std::uint32_t minor = 0u; ///< The minor version number. std::uint32_t patch = 0u; ///< The patch number. diff --git a/tests/include/argument_parser_test_fixture.hpp b/tests/include/argument_parser_test_fixture.hpp index f2e7924a..564a938f 100644 --- a/tests/include/argument_parser_test_fixture.hpp +++ b/tests/include/argument_parser_test_fixture.hpp @@ -191,6 +191,11 @@ struct argument_parser_test_fixture { return this->sut._get_argument(arg_name); } + // exception message builders + std::string required_argument_not_parsed_msg(const argument_name& arg_name) const { + return std::format("No values parsed for a required argument [{}]", arg_name.str()); + } + ap::argument_parser sut{program_name}; parsing_state state{sut}; diff --git a/tests/include/argument_test_fixture.hpp b/tests/include/argument_test_fixture.hpp index 8580b376..df15671c 100644 --- a/tests/include/argument_test_fixture.hpp +++ b/tests/include/argument_test_fixture.hpp @@ -31,16 +31,6 @@ struct argument_test_fixture { return arg.count(); } - template - bool set_required(argument& arg, const bool r) const { - return arg._required = r; - } - - template - bool set_bypass_required(argument& arg, const bool br) const { - return arg._bypass_required = br; - } - template bool set_value(argument& arg, const T& value) const { return set_value(arg, as_string(value)); @@ -126,6 +116,17 @@ struct argument_test_fixture { [[nodiscard]] bool is_bypass_required_enabled(const argument& arg) const { return arg.is_bypass_required_enabled(); } + + // exception message builders + std::string invalid_value_msg(const argument_name& arg_name, const std::string& value) const { + return std::format("Cannot parse value `{}` for argument [{}].", value, arg_name.str()); + } + + std::string invalid_choice_msg(const argument_name& arg_name, const std::string& value) const { + return std::format( + "Value `{}` is not a valid choice for argument [{}].", value, arg_name.str() + ); + } }; } // namespace ap_testing diff --git a/tests/source/test_argument_parser_add_elements.cpp b/tests/source/test_argument_parser_add_elements.cpp index ab14f042..9da936fc 100644 --- a/tests/source/test_argument_parser_add_elements.cpp +++ b/tests/source/test_argument_parser_add_elements.cpp @@ -326,7 +326,7 @@ TEST_CASE_FIXTURE( TEST_CASE_FIXTURE( test_argument_parser_add_elements, - "add_optional_argument and add_flag should throw if a group does not belong to the parser" + "argument adding functions should throw if a group does not belong to the parser" ) { argument_parser different_parser("different-program"); @@ -340,6 +340,10 @@ TEST_CASE_FIXTURE( 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, 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(), diff --git a/tests/source/test_argument_parser_info.cpp b/tests/source/test_argument_parser_info.cpp index b8f55c65..fe4d0c7a 100644 --- a/tests/source/test_argument_parser_info.cpp +++ b/tests/source/test_argument_parser_info.cpp @@ -12,6 +12,12 @@ struct test_argument_parser_info : public argument_parser_test_fixture { const std::string test_str_version = "alpha"; }; +TEST_CASE("argument_parser() should throw if the name is empty") { + CHECK_THROWS_WITH_AS( + argument_parser(""), "The program name cannot be empty!", invalid_configuration + ); +} + TEST_CASE("argument_parser() should throw if the name contains whitespaces") { CHECK_THROWS_WITH_AS( argument_parser("invalid name"), diff --git a/tests/source/test_argument_parser_parse_args.cpp b/tests/source/test_argument_parser_parse_args.cpp index 19dbe5b3..5a540101 100644 --- a/tests/source/test_argument_parser_parse_args.cpp +++ b/tests/source/test_argument_parser_parse_args.cpp @@ -155,8 +155,7 @@ TEST_CASE_FIXTURE( CHECK_THROWS_WITH_AS( sut.parse_args(argc, argv), - parsing_failure::required_argument_not_parsed({init_arg_name_primary(last_pos_arg_idx)}) - .what(), + required_argument_not_parsed_msg({init_arg_name_primary(last_pos_arg_idx)}).c_str(), parsing_failure ); @@ -179,10 +178,13 @@ 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} + std::format( + "Required positional argument [{}] cannot be defined after a non-required positional " + "argument [{}].", + required_arg_name, + non_required_arg_name ) - .what(), + .c_str(), invalid_configuration ); @@ -206,7 +208,8 @@ TEST_CASE_FIXTURE( CHECK_THROWS_WITH_AS( sut.parse_args(argc, argv), - parsing_failure::argument_deduction_failure(unknown_args).what(), + std::format("Failed to deduce the argument for values [{}]", ap::util::join(unknown_args)) + .c_str(), parsing_failure ); @@ -230,7 +233,7 @@ TEST_CASE_FIXTURE( CHECK_THROWS_WITH_AS( sut.parse_args(argc, argv), - parsing_failure::required_argument_not_parsed(required_arg_name).what(), + required_argument_not_parsed_msg(required_arg_name).c_str(), parsing_failure ); @@ -366,12 +369,13 @@ TEST_CASE_FIXTURE( TEST_CASE_FIXTURE( test_argument_parser_parse_args, - "parse_args should not throw if there is a positional argument which has the bypass_required " + "parse_args should not throw if there is a positional argument which has the " + "suppress_arg_checks " "option enabled and is used" ) { const std::size_t n_positional_args = 1ull; const auto bypass_required_arg_name = init_arg_name(n_positional_args - 1ull).primary.value(); - sut.add_positional_argument(bypass_required_arg_name).bypass_required(); + sut.add_positional_argument(bypass_required_arg_name).required(false).suppress_arg_checks(); const std::string bypass_required_arg_value = "bypass_required_arg_value"; for (std::size_t i = 0ull; i < n_optional_args; ++i) @@ -389,7 +393,8 @@ TEST_CASE_FIXTURE( TEST_CASE_FIXTURE( test_argument_parser_parse_args, - "parse_args should not throw if there is an optional argument which has the bypass_required " + "parse_args should not throw if there is an optional argument which has the " + "suppress_arg_checks " "option enabled and is used" ) { add_arguments(n_positional_args, n_optional_args); @@ -400,7 +405,7 @@ TEST_CASE_FIXTURE( ) .default_values(false) .implicit_values(true) - .bypass_required(); + .suppress_arg_checks(); std::string arg_flag; @@ -1459,6 +1464,28 @@ TEST_CASE_FIXTURE( free_argv(argc, argv); } +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should not throw when the group requirements are not satisfied but a group check " + "suppressing argument is used" +) { + const std::string suppressing_arg_name = "suppress"; + sut.add_flag(suppressing_arg_name).suppress_group_checks(); + + 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)); + + std::vector argv_vec{std::format("--{}", suppressing_arg_name)}; + + REQUIRE_NOTHROW(sut.parse_args(argv_vec)); + CHECK(sut.value(suppressing_arg_name)); + for (std::size_t i = 0ull; i < n_optional_args; ++i) + CHECK_FALSE(sut.is_used(init_arg_name_primary(i))); +} + // subparsers TEST_CASE_FIXTURE( diff --git a/tests/source/test_optional_argument.cpp b/tests/source/test_optional_argument.cpp index 91880983..b1e456b1 100644 --- a/tests/source/test_optional_argument.cpp +++ b/tests/source/test_optional_argument.cpp @@ -129,7 +129,9 @@ TEST_CASE_FIXTURE( CHECK_EQ(required_it->value, "true"); // other parameters - sut.bypass_required(); + sut.required(false); // required for argument check suppressing + sut.suppress_arg_checks(); + sut.suppress_group_checks(); sut.nargs(non_default_range); sut.choices(choices); sut.default_values(default_value); @@ -138,10 +140,15 @@ TEST_CASE_FIXTURE( // check the descriptor parameters bld = get_help_builder(sut, verbose); - const auto bypass_required_it = - 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 suppress_arg_checks_it = + std::ranges::find(bld.params, "suppress arg checks", ¶meter_descriptor::name); + REQUIRE_NE(suppress_arg_checks_it, bld.params.end()); + CHECK_EQ(suppress_arg_checks_it->value, "true"); + + const auto suppress_group_checks_it = + std::ranges::find(bld.params, "suppress group checks", ¶meter_descriptor::name); + REQUIRE_NE(suppress_group_checks_it, bld.params.end()); + CHECK_EQ(suppress_group_checks_it->value, "true"); const auto nargs_it = std::ranges::find(bld.params, "nargs", ¶meter_descriptor::name); REQUIRE_NE(nargs_it, bld.params.end()); @@ -192,66 +199,79 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - argument_test_fixture, - "bypass_required() should return the value set using the `bypass_required` param setter" + argument_test_fixture, "required(true) should throw if an argument is supressing" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); + sut.required(false); - sut.bypass_required(true); - CHECK(sut.is_bypass_required_enabled()); + SUBCASE("suppressing argument checks") { + sut.suppress_arg_checks(); + } + SUBCASE("suppressing argument group checks") { + sut.suppress_group_checks(); + } + SUBCASE("suppressing all checks") { + sut.suppress_arg_checks(); + sut.suppress_group_checks(); + } + + CAPTURE(sut); - sut.bypass_required(false); - CHECK_FALSE(sut.is_bypass_required_enabled()); + CHECK_THROWS_WITH_AS( + sut.required(true), + std::format("A suppressing argument [{}] cannot be required!", arg_name.str()).c_str(), + ap::invalid_configuration + ); } TEST_CASE_FIXTURE( argument_test_fixture, - "is_bypass_required_enabled() should return true only if the `required` flag is set to false " - "and " - "the `bypass_required` flags is set to true" + "suppress_arg_checks() should return the value set using the `suppress_arg_checks` param " + "setter if the argument is not required" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); - // disabled - set_required(sut, false); - set_bypass_required(sut, false); - CHECK_FALSE(sut.is_bypass_required_enabled()); + sut.required(true); + CHECK_THROWS_WITH_AS( + sut.suppress_arg_checks(true), + std::format("A required argument [{}] cannot suppress argument checks!", arg_name.str()) + .c_str(), + ap::invalid_configuration + ); - set_required(sut, true); - set_bypass_required(sut, false); - CHECK_FALSE(sut.is_bypass_required_enabled()); + sut.required(false); - set_required(sut, true); - set_bypass_required(sut, true); - CHECK_FALSE(sut.is_bypass_required_enabled()); + sut.suppress_arg_checks(true); + CHECK(sut.suppresses_arg_checks()); - // enabled - set_required(sut, false); - set_bypass_required(sut, true); - CHECK(sut.is_bypass_required_enabled()); + sut.suppress_arg_checks(false); + CHECK_FALSE(sut.suppresses_arg_checks()); } TEST_CASE_FIXTURE( argument_test_fixture, - "required(true) should disable `bypass_required` option and bypass_required(true) should " - "disable the `required` option" + "suppresses_group_checks() should return the value set using the `suppress_group_checks` param " + "setter if the argument is not required" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); - REQUIRE_FALSE(sut.is_required()); - REQUIRE_FALSE(sut.is_bypass_required_enabled()); + sut.required(true); + CHECK_THROWS_WITH_AS( + sut.suppress_group_checks(true), + std::format( + "A required argument [{}] cannot suppress argument group checks!", arg_name.str() + ) + .c_str(), + ap::invalid_configuration + ); - sut.bypass_required(); - CHECK(sut.is_bypass_required_enabled()); - CHECK_FALSE(sut.is_required()); + sut.required(false); - sut.required(); - CHECK(sut.is_required()); - CHECK_FALSE(sut.is_bypass_required_enabled()); + sut.suppress_group_checks(true); + CHECK(sut.suppresses_group_checks()); - sut.bypass_required(); - CHECK(sut.is_bypass_required_enabled()); - CHECK_FALSE(sut.is_required()); + sut.suppress_group_checks(false); + CHECK_FALSE(sut.suppresses_group_checks()); } TEST_CASE_FIXTURE(argument_test_fixture, "is_used() should return false by default") { @@ -482,7 +502,7 @@ TEST_CASE_FIXTURE( SUBCASE("given string is empty") { REQUIRE_THROWS_WITH_AS( set_value(sut, empty_str), - parsing_failure::invalid_value(arg_name_primary, empty_str).what(), + invalid_value_msg(arg_name_primary, empty_str).c_str(), parsing_failure ); CHECK_FALSE(has_value(sut)); @@ -490,7 +510,7 @@ TEST_CASE_FIXTURE( SUBCASE("given string is non-convertible to value_type") { REQUIRE_THROWS_WITH_AS( set_value(sut, invalid_value_str), - parsing_failure::invalid_value(arg_name_primary, invalid_value_str).what(), + invalid_value_msg(arg_name_primary, invalid_value_str).c_str(), parsing_failure ); CHECK_FALSE(has_value(sut)); @@ -506,7 +526,7 @@ TEST_CASE_FIXTURE( REQUIRE_THROWS_WITH_AS( set_value(sut, invalid_choice), - parsing_failure::invalid_choice(arg_name_primary, as_string(invalid_choice)).what(), + invalid_choice_msg(arg_name_primary, as_string(invalid_choice)).c_str(), parsing_failure ); CHECK_FALSE(has_value(sut)); diff --git a/tests/source/test_positional_argument.cpp b/tests/source/test_positional_argument.cpp index 1a403cbc..759892c6 100644 --- a/tests/source/test_positional_argument.cpp +++ b/tests/source/test_positional_argument.cpp @@ -14,14 +14,8 @@ namespace { constexpr std::string_view help_msg = "test help msg"; -constexpr std::string_view primary_name = "test"; -const auto primary_name_opt = std::make_optional(primary_name); - -constexpr std::string_view secondary_name = "t"; -const auto secondary_name_opt = std::make_optional(secondary_name); - -const argument_name arg_name(primary_name_opt, secondary_name_opt); -const argument_name arg_name_primary(primary_name_opt, std::nullopt); +constexpr std::string_view name_value = "test"; +const argument_name arg_name(std::make_optional(name_value), std::nullopt); using sut_value_type = int; using sut_type = positional_argument; @@ -40,30 +34,17 @@ const range non_default_range = range{1ull, choices.size()}; } // namespace TEST_CASE_FIXTURE(argument_test_fixture, "name() should return the proper argument_name instance") { - SUBCASE("initialized with the primary name only") { - const auto sut = sut_type(arg_name_primary); - const auto name = get_name(sut); - - CHECK(name.match(primary_name)); - CHECK_FALSE(name.match(secondary_name)); - } - - SUBCASE("initialized with the primary and secondary names") { - const auto sut = sut_type(arg_name); - const auto name = get_name(sut); - - CHECK(name.match(primary_name)); - CHECK(name.match(secondary_name)); - } + const auto sut = sut_type(arg_name); + CHECK_EQ(get_name(sut), arg_name); } TEST_CASE_FIXTURE(argument_test_fixture, "help() should return nullopt by default") { - const auto sut = sut_type(arg_name_primary); + const auto sut = sut_type(arg_name); CHECK_FALSE(get_help(sut)); } TEST_CASE_FIXTURE(argument_test_fixture, "help() should return a massage set for the argument") { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); sut.help(help_msg); const auto stored_help_msg = get_help(sut); @@ -116,7 +97,9 @@ TEST_CASE_FIXTURE( CHECK(bld.params.empty()); // other parameters - sut.bypass_required(); + sut.required(false); + sut.suppress_arg_checks(); + sut.suppress_group_checks(); sut.nargs(non_default_range); sut.choices(choices); sut.default_values(default_value); @@ -124,16 +107,20 @@ TEST_CASE_FIXTURE( // check the descriptor parameters bld = get_help_builder(sut, verbose); - const auto bypass_required_it = - 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(bld.params, "required", ¶meter_descriptor::name); REQUIRE_NE(required_it, bld.params.end()); CHECK_EQ(required_it->value, "false"); + const auto suppress_arg_checks_it = + std::ranges::find(bld.params, "suppress arg checks", ¶meter_descriptor::name); + REQUIRE_NE(suppress_arg_checks_it, bld.params.end()); + CHECK_EQ(suppress_arg_checks_it->value, "true"); + + const auto suppress_group_checks_it = + std::ranges::find(bld.params, "suppress group checks", ¶meter_descriptor::name); + REQUIRE_NE(suppress_group_checks_it, bld.params.end()); + CHECK_EQ(suppress_group_checks_it->value, "true"); + 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)); @@ -152,7 +139,7 @@ TEST_CASE_FIXTURE( argument_test_fixture, "is_hidden() should return false by default or the value passed in the attribute setter" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); REQUIRE_FALSE(sut.is_hidden()); sut.hidden(); @@ -160,7 +147,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE(argument_test_fixture, "is_required() should return true by default") { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); CHECK(sut.is_required()); } @@ -168,7 +155,7 @@ TEST_CASE_FIXTURE( argument_test_fixture, "is_required() should return the value set using the `required` param setter" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); sut.required(false); CHECK_FALSE(sut.is_required()); @@ -178,73 +165,88 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - argument_test_fixture, - "bypass_required() should return the value set using the `bypass_required` param setter" + argument_test_fixture, "required(true) should throw if an argument is supressing" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); + sut.required(false); - sut.bypass_required(true); - CHECK(sut.is_bypass_required_enabled()); + SUBCASE("suppressing argument checks") { + sut.suppress_arg_checks(); + } + SUBCASE("suppressing argument group checks") { + sut.suppress_group_checks(); + } + SUBCASE("suppressing all checks") { + sut.suppress_arg_checks(); + sut.suppress_group_checks(); + } + + CAPTURE(sut); - sut.bypass_required(false); - CHECK_FALSE(sut.is_bypass_required_enabled()); + CHECK_THROWS_WITH_AS( + sut.required(true), + std::format("A suppressing argument [{}] cannot be required!", arg_name.str()).c_str(), + ap::invalid_configuration + ); } TEST_CASE_FIXTURE( argument_test_fixture, - "is_bypass_required_enabled() should return true only if the `required` flag is set to false " - "and " - "the `bypass_required` flags is set to true" + "suppresses_arg_checks() should return the value set using the `suppress_arg_checks` param " + "setter if the argument is not required" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); - // disabled - set_required(sut, false); - set_bypass_required(sut, false); - CHECK_FALSE(sut.is_bypass_required_enabled()); + CHECK_THROWS_WITH_AS( + sut.suppress_arg_checks(true), + std::format("A required argument [{}] cannot suppress argument checks!", arg_name.str()) + .c_str(), + ap::invalid_configuration + ); - set_required(sut, true); - set_bypass_required(sut, false); - CHECK_FALSE(sut.is_bypass_required_enabled()); + sut.required(false); - set_required(sut, true); - set_bypass_required(sut, true); - CHECK_FALSE(sut.is_bypass_required_enabled()); + sut.suppress_arg_checks(true); + CHECK(sut.suppresses_arg_checks()); - // enabled - set_required(sut, false); - set_bypass_required(sut, true); - CHECK(sut.is_bypass_required_enabled()); + sut.suppress_arg_checks(false); + CHECK_FALSE(sut.suppresses_arg_checks()); } TEST_CASE_FIXTURE( argument_test_fixture, - "required(true) should disable `bypass_required` option and bypass_required(true) should " - "disable the `required` option" + "suppresses_group_checks() should return the value set using the `suppress_group_checks` param " + "setter if the argument is not required" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); - REQUIRE(sut.is_required()); - REQUIRE_FALSE(sut.is_bypass_required_enabled()); + CHECK_THROWS_WITH_AS( + sut.suppress_group_checks(true), + std::format( + "A required argument [{}] cannot suppress argument group checks!", arg_name.str() + ) + .c_str(), + ap::invalid_configuration + ); - sut.bypass_required(); - CHECK(sut.is_bypass_required_enabled()); - CHECK_FALSE(sut.is_required()); + sut.required(false); - sut.required(); - CHECK(sut.is_required()); - CHECK_FALSE(sut.is_bypass_required_enabled()); + sut.suppress_group_checks(true); + CHECK(sut.suppresses_group_checks()); + + sut.suppress_group_checks(false); + CHECK_FALSE(sut.suppresses_group_checks()); } TEST_CASE_FIXTURE(argument_test_fixture, "is_used() should return false by default") { - const auto sut = sut_type(arg_name_primary); + const auto sut = sut_type(arg_name); CHECK_FALSE(is_used(sut)); } TEST_CASE_FIXTURE( argument_test_fixture, "is_used() should return true when argument contains a value" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); REQUIRE_FALSE(is_used(sut)); set_value(sut, valid_value); @@ -252,24 +254,24 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE(argument_test_fixture, "count() should return 0 by default") { - const auto sut = sut_type(arg_name_primary); + const auto sut = sut_type(arg_name); CHECK_EQ(get_count(sut), 0ull); } TEST_CASE_FIXTURE(argument_test_fixture, "count() should return 1 when argument contains a value") { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); set_value(sut, valid_value); CHECK_EQ(get_count(sut), 1ull); } TEST_CASE_FIXTURE(argument_test_fixture, "has_value() should return false by default") { - const auto sut = sut_type(arg_name_primary); + const auto sut = sut_type(arg_name); CHECK_FALSE(has_value(sut)); } TEST_CASE_FIXTURE(argument_test_fixture, "has_value() should return true if the value is set") { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); set_value(sut, valid_value); CHECK(has_value(sut)); @@ -278,14 +280,14 @@ TEST_CASE_FIXTURE(argument_test_fixture, "has_value() should return true if the TEST_CASE_FIXTURE( argument_test_fixture, "has_value() should return true if the default value is set" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); sut.default_values(default_value); CHECK(has_value(sut)); } TEST_CASE_FIXTURE(argument_test_fixture, "has_parsed_values() should return false by default") { - const auto sut = sut_type(arg_name_primary); + const auto sut = sut_type(arg_name); CHECK_FALSE(has_parsed_values(sut)); } @@ -293,28 +295,28 @@ TEST_CASE_FIXTURE( argument_test_fixture, "has_parsed_values() should return false regardless of the default value parameter" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); sut.default_values(default_value); CHECK_FALSE(has_parsed_values(sut)); } TEST_CASE_FIXTURE(argument_test_fixture, "has_parsed_values() should true if the value is set") { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); set_value(sut, valid_value); CHECK(has_parsed_values(sut)); } TEST_CASE_FIXTURE(argument_test_fixture, "has_predefined_values() should return false by default") { - const auto sut = sut_type(arg_name_primary); + const auto sut = sut_type(arg_name); CHECK_FALSE(has_predefined_values(sut)); } TEST_CASE_FIXTURE( argument_test_fixture, "has_predefined_values() should return true if the default value is set" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); sut.default_values(default_value); CHECK(has_predefined_values(sut)); @@ -323,7 +325,7 @@ TEST_CASE_FIXTURE( TEST_CASE_FIXTURE( argument_test_fixture, "value() should throw if the argument's value has not been set" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); REQUIRE_FALSE(has_value(sut)); CHECK_THROWS_AS(static_cast(get_value(sut)), std::logic_error); @@ -332,7 +334,7 @@ TEST_CASE_FIXTURE( TEST_CASE_FIXTURE( argument_test_fixture, "value() should return the argument's value if it has been set" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); set_value(sut, valid_value); REQUIRE(has_value(sut)); @@ -344,7 +346,7 @@ TEST_CASE_FIXTURE( "value() should return the default argument's default value if it has been set and no values " "were parsed" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); sut.default_values(default_value); REQUIRE(has_value(sut)); @@ -354,7 +356,7 @@ TEST_CASE_FIXTURE( TEST_CASE_FIXTURE( argument_test_fixture, "value() should return the argument's parsed value if it has been set" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); sut.default_values(default_value); set_value(sut, valid_value); @@ -367,12 +369,12 @@ TEST_CASE_FIXTURE( "set_value(any) should throw when the given string cannot be converted to an instance of " "value_type" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); SUBCASE("given string is empty") { REQUIRE_THROWS_WITH_AS( set_value(sut, empty_str), - parsing_failure::invalid_value(arg_name_primary, empty_str).what(), + invalid_value_msg(arg_name, empty_str).c_str(), parsing_failure ); CHECK_FALSE(has_value(sut)); @@ -381,7 +383,7 @@ TEST_CASE_FIXTURE( SUBCASE("given string is non-convertible to value_type") { REQUIRE_THROWS_WITH_AS( set_value(sut, invalid_value_str), - parsing_failure::invalid_value(arg_name_primary, invalid_value_str).what(), + invalid_value_msg(arg_name, invalid_value_str).c_str(), parsing_failure ); CHECK_FALSE(has_value(sut)); @@ -392,12 +394,12 @@ TEST_CASE_FIXTURE( argument_test_fixture, "set_value(any) should throw when the choices set does not contain the parsed value" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); sut.choices(choices); REQUIRE_THROWS_WITH_AS( set_value(sut, invalid_choice), - parsing_failure::invalid_choice(arg_name_primary, std::to_string(invalid_choice)).what(), + invalid_choice_msg(arg_name, std::to_string(invalid_choice)).c_str(), parsing_failure ); CHECK_FALSE(has_value(sut)); @@ -408,7 +410,7 @@ TEST_CASE_FIXTURE( "set_value(any) should throw when adding the given value would result in exceeding the maximum " "number of values specified by nargs" ) { - auto sut = sut_type(arg_name_primary).nargs(non_default_range); + auto sut = sut_type(arg_name).nargs(non_default_range); for (const auto value : choices) REQUIRE_NOTHROW(set_value(sut, value)); @@ -420,13 +422,13 @@ TEST_CASE_FIXTURE( CHECK_THROWS_WITH_AS( set_value(sut, valid_value), - parsing_failure::invalid_nvalues(arg_name_primary, std::weak_ordering::greater).what(), + parsing_failure::invalid_nvalues(arg_name, std::weak_ordering::greater).what(), parsing_failure ); } TEST_CASE_FIXTURE(argument_test_fixture, "set_value(any) should perform the specified action") { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); SUBCASE("observe action") { const auto is_power_of_two = [](const sut_value_type n) { @@ -469,7 +471,7 @@ TEST_CASE_FIXTURE(argument_test_fixture, "set_value(any) should perform the spec TEST_CASE_FIXTURE( argument_test_fixture, "nvalues_ordering() should return less for default nargs (1)" ) { - const auto sut = sut_type(arg_name_primary); + const auto sut = sut_type(arg_name); CHECK(std::is_lt(nvalues_ordering(sut))); } @@ -477,7 +479,7 @@ TEST_CASE_FIXTURE( argument_test_fixture, "nvalues_ordering() should return equivalent if a default value has been set" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); sut.nargs(non_default_range); sut.default_values(default_value); @@ -490,7 +492,7 @@ TEST_CASE_FIXTURE( "nvalues_ordering() should return equivalent only when the number of values " "is in the specified range" ) { - auto sut = sut_type(arg_name_primary); + auto sut = sut_type(arg_name); sut.nargs(non_default_range); REQUIRE(std::is_lt(nvalues_ordering(sut)));