diff --git a/CMakeLists.txt b/CMakeLists.txt index 2055e2d1..c349b3fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ else() endif() project(cpp-ap - VERSION 2.7.0 + 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 585f767e..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 = 2.7.0 +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 @@ -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 c7bf023c..91d3db86 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,4 +1,4 @@ module( name = "cpp-ap", - version = "2.7.0", + version = "3.0.0", ) diff --git a/README.md b/README.md index e793034b..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* > @@ -53,14 +53,33 @@ 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) - [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) - - [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) + - [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) + - [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) - [Building and testing](/docs/dev_notes.md#building-and-testing) - [Formatting](/docs/dev_notes.md#formatting) diff --git a/cpp-ap-demo b/cpp-ap-demo index 76d1d7a4..cf3f2ad8 160000 --- a/cpp-ap-demo +++ b/cpp-ap-demo @@ -1 +1 @@ -Subproject commit 76d1d7a49ca14ed7595739c118c07b6d98160f83 +Subproject commit cf3f2ad8e9c06af9adcde99288eea517bfb6ecae 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 850d8975..c45e46f8 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -6,15 +6,44 @@ - [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) - 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) - 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) + - [Creating New Groups](#creating-new-groups) + - [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) - - [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) +- [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)
@@ -104,20 +133,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. - ```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 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`). +> - 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: @@ -140,72 +175,89 @@ 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 **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. + +### Syntax -To add an argument to the parameter's configurations use the following 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") ``` +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: > -> The library supports any argument value types which meet 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`. +> - 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) +> - [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) + +### 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"); /* 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"); */ ``` -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"); /* 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"); */ -``` -> [!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); -> ``` +```

@@ -224,14 +276,16 @@ 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: ```cpp -parser.program_name("hidden-test") - .program_description("A simple program") - .default_optional_arguments({ap::argument::default_optional::help}); +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") .hidden() @@ -245,7 +299,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: @@ -253,9 +307,11 @@ 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. -> [!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. @@ -263,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 @@ -320,24 +374,29 @@ 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 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. +> - 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); @@ -356,74 +415,108 @@ 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 +
+ +#### 5. `nargs` - Sets the allowed number of values to be parsed for an argument. -> [!WARNING] +The `nargs` parameter can be set as: + +- Specific number: + + ```cpp + parser.add_optional_argument("input", "i").nargs(1); + ``` + +- Fully bound range: + + ```cpp + parser.add_optional_argument("input", "i").nargs(1, 3); + ``` + +- Partially bound range: + + ```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 + ``` + +- Unbound range: + + ```cpp + parser.add_optional_argument("input", "i").nargs(ap::nargs::any()); + ``` + +> [!IMPORTANT] > -> For both positional and optional arguments, setting the `default_value` parameter disables the `required` option. +> The default `nargs` parameter value is: +> +> - `ap::nargs::range(1ull)` for positional arguments +> +> - `ap::nargs::any()` for optional arguments -```cpp -// example: positional arguments -parser.add_positional_argument("input"); -parser.add_positional_argument("output").default_value("output.txt"); +
-parser.parse_args(argc, argv); +#### 6. `greedy` - If this option is set, the argument will consume ALL command-line values until it's upper nargs bound is reached. -// `input` is guaranteed to have a value if parsing was successfull -const auto data = read_data(parser.value("input")); +> [!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. -// `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; +> [!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. -/* -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 -``` +Consider a simple example: ```cpp -// example: optional arguments -parser.add_optional_argument("input", "i").required(); -parser.add_optional_argument("output", "o").default_value("output.txt"); +ap::argument_parser parser("run-script"); +parser.default_arguments(ap::default_argument::o_help); -parser.parse_args(argc, argv); +parser.add_positional_argument("script") + .help("The name of the script to run"); +parser.add_optional_argument("args") + .greedy() + .help("Set the execution option"); -// `input` is guaranteed to have a value if parsing was successfull -const auto data = read_data(parser.value("input")); +parser.try_parse_args(argc, argv); -// `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; +// Application logic here +std::cout << "Executing: " << parser.value("script") << " " << ap::util::join(parser.values("args")) << std::endl; +``` -/* -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 +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 ``` -> [!TIP] -> -> The setter of the `default_value` parameter accepts any type that is convertible to the argument's value type. +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. -#### 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`. +#### 7. `choices` - A list of valid argument values. ```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 -#### 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. @@ -431,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") @@ -466,50 +559,96 @@ 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 +#### 9. `default_values` - A list of values which will be used if no values for an argument have been 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_values` 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_values("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_values("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 +``` + +> [!NOTE] > -> The default `nargs` parameter value is `ap::nargs::any()`. +> 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 + +
+
+ +### 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 - Functions that are called 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. +#### 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); @@ -529,28 +668,15 @@ Command Result ./program --save myfile.txt The data will be saved to `myfile.txt` ``` -> [!TIP] +> [!NOTE] > -> - 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. +> 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 -```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. +> [!TIP] +> +> The `implicit_values` parameter is extremely useful when combined with default value (e.g. in case of boolean flags - see [Adding Arguments](#adding-arguments)).

@@ -560,12 +686,12 @@ Here the `print_debug_info` function will be called right after parsing the `--d ### 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 @@ -577,7 +703,7 @@ Here the `print_debug_info` function will be called right after parsing the `--d 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)) @@ -585,8 +711,8 @@ Here the `print_debug_info` function will be called right after parsing the `--d 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)) @@ -594,8 +720,8 @@ Here the `print_debug_info` function will be called right after parsing the `--d 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)) @@ -603,8 +729,8 @@ Here the `print_debug_info` function will be called right after parsing the `--d 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)) @@ -612,8 +738,8 @@ Here the `print_debug_info` function will be called right after parsing the `--d 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)) @@ -621,8 +747,8 @@ Here the `print_debug_info` function will be called right after parsing the `--d 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 ``` @@ -636,20 +762,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 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 -- `default_positional::input`: +- `p_input`: ```cpp // equivalent to: @@ -658,23 +780,23 @@ 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: - 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"); ``` -- `default_optional::input` and `default_optional::multi_input`: +- `o_input` and `o_multi_input`: ```cpp // input - equivalent to: @@ -692,7 +814,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: @@ -712,52 +834,149 @@ The available default arguments are:

-## Parsing Arguments +## Argument Groups -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`. +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**. -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. +By default, every parser comes with two predefined groups: -> [!IMPORTANT] -> -> The `parse_args(argc, argv)` method ignores the first argument (the program name) and is equivalent to calling: +- **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] > -> ```cpp -> parse_args(std::span(argv + 1, argc - 1)); -> ``` +> If a group has no visible arguments, it will not be included in the parser's help message output at all. -> [!WARNING] +### 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 +``` + +> [!IMPORANT] > -> 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. +> 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**. > -> > [*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. +> 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: + +```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 +``` + +### 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] > -> This behavior can be altered so that the unknown argument flags will be treated as values, not flags. +> - 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] > -> Example: -> ```cpp -> parser.add_optional_argument("option", "o"); -> parser.try_parse_args(argc, argv); -> std::cout << "option: " << parser.value("option"); +> - 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). + +
+
+
+ +## 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 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. + +> [!IMPORTANT] > -> /* -> ./program --option --unknown-flag -> option: --unknown-flag -> ``` +> The `parse_args(argc, argv)` method ignores the first argument (the program name) and is equivalent to calling: > -> 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 +> parse_args(std::span(argv + 1, argc - 1)); > ``` -> 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` 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: > @@ -766,147 +985,257 @@ 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); > } > ``` +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"); + ap::argument_parser parser("some-program"); - // 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 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); - parser.default_optional_arguments({ap::argument::default_optional::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"); // 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: " << ap::util::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: " << 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 +> 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); + +std::cout << "positional1: " << parser.value("positional1") << std::endl + << "positional2: " << parser.value("positional2") << std::endl + << "optional: " << parser.value("optional") << std::endl; + +/* 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 +``` - ```shell - ./power 2 1 2 3 - [ERROR] : Failed to deduce the argument for values [1, 2, 3] - Program: power calculator +> [!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). - Calculates the value of an expression: base ^ exponent +
- Positional arguments: +#### 4. Unknown Argument Flag Handling - base : the exponentation base value +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**. - Optional arguments: +By default an argument parser will throw an exception if an unkown argument flag is encountered. - --exponent, -e : the exponent value - --help, -h : Display the help message - ``` +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. -> [!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 +#include + +int main(int argc, char* argv[]) { + ap::argument_parser parser("unknown-policy-test"); + + 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::); + + 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; +} +``` + +The available policies are: +- `ap::unknown_policy::fail` (default) - throws an exception if an unknown argument flag is encountered: + + ```txt + > ./unknown-policy-test --known --unknown + [ap::error] Unknown argument [--unknown]. + Program: unknown-policy-test + + A simple test program for unknwon argument handling policies + + 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: + + ```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: + + ```txt + ./unknown-policy-test --known --unknown + known = + ``` + +- `ap::unknown_policy::as_values` - treats unknown argument flags as values: + + ```txt + > ./unknown-policy-test --known --unknown + known = --unknown + ``` + +> [!IMPORTANT] > -> **Example:** +> - 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 -> parser.add_optional_argument("exponent", "e").nargs(ap::nargs::up_to(3)) +> 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 -> ./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 +> > ./test --known --unknown +> known = +> unknown = --unknown > ``` +

### Compound Arguments @@ -930,7 +1259,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; /* @@ -960,9 +1289,9 @@ 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; +std::cout << "recognized = " << ap::util::join(parser.values("recognized")) << std::endl; /* Example executions: > ./program --recognized value1 value2 @@ -994,11 +1323,11 @@ 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 +> ./program value0 --recognized value1 value2 value3 --unrecognized value recognized = value1, value2 unkown = value0, value3, --unrecognized, value ``` @@ -1007,27 +1336,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 -> << "unkown = " << join(unknown_args) << std::endl; +> std::cout << "positional = " << parser.value("positional") << std::endl +> << "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 --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 > -> 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. +> > ./program value0 --recognized value1 value2 value3 --unrecognized value4 --recognized value5 +> positional = value0 +> recognized = value1, value2, value3, value5 +> unkown = --unrecognized, value4 +> ``` > -> **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. +> 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. > [!TIP] > @@ -1039,31 +1375,191 @@ 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. + + - 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. Returns the given argument's value or the specified fallback value if the argument has no values. - 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. + - 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`). -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: +3. Returns a vector of the given argument's 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 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 function `(1)`) +> - The specified `value_type` does not match the value type of the argument
+
+
+ +
+
+
+ +## 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: -Additionally for optional arguments, you can use: +```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 -(const) std::vector values = parser.values("argument_name"); +ap::argument_parser git("ap-git"); +auto& submodule = git.add_subparser("submodule"); +auto& submodule_init = submodule.add_subparser("init"); ``` -which returns a `vector` containing all values parsed for the given argument. +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) +```

@@ -1071,4 +1567,31 @@ which returns a `vector` containing all values parsed for the given argument. ## 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. | + +
+
+
+ +## 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_actions.hpp b/include/ap/action/predefined.hpp similarity index 80% rename from include/ap/action/predefined_actions.hpp rename to include/ap/action/predefined.hpp index 405b4071..ecedc037 100644 --- a/include/ap/action/predefined_actions.hpp +++ b/include/ap/action/predefined.hpp @@ -2,12 +2,12 @@ // 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 #include "ap/exceptions.hpp" -#include "detail/utility.hpp" +#include "util/helpers.hpp" #include #include @@ -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 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 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 @@ -33,12 +33,12 @@ inline typename ap::action_type::on_flag::type print_config( return [&parser, &os, exit_code]() { os << parser << std::endl; if (exit_code) - std::exit(exit_code.value()); + std::exit(*exit_code); }; } /// @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/specifiers.hpp b/include/ap/action/types.hpp similarity index 87% rename from include/ap/action/specifiers.hpp rename to include/ap/action/types.hpp index b7079ac2..05ea9820 100644 --- a/include/ap/action/specifiers.hpp +++ b/include/ap/action/types.hpp @@ -3,13 +3,13 @@ // 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. */ #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 66% rename from include/ap/action/detail/utility.hpp rename to include/ap/action/util/helpers.hpp index 03483363..092fba05 100644 --- a/include/ap/action/detail/utility.hpp +++ b/include/ap/action/util/helpers.hpp @@ -3,40 +3,26 @@ // Licensed under the MIT License. See the LICENSE file in the project root for full license information. /** - * @file 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/specifiers.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 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, @@ -45,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; @@ -77,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 new file mode 100644 index 00000000..3ddf8462 --- /dev/null +++ b/include/ap/argument.hpp @@ -0,0 +1,778 @@ +// 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/predefined.hpp" +#include "action/util/helpers.hpp" +#include "detail/argument_base.hpp" +#include "detail/help_builder.hpp" +#include "nargs/range.hpp" +#include "types.hpp" +#include "util/concepts.hpp" +#include "util/ranges.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{.cpp} + * ap::argument_parser parser; + * parser.add_positional_argument("input", "i") + * .help("An input file path"); + * parser.add_optional_argument("output", "o") + * .default_values("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 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. + [[nodiscard]] bool is_greedy() const noexcept override { + return this->_greedy; + } + + // 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 value The attribute value (default: `true`). + * @return Reference to the argument instance. + * @throws ap::invalid_configuration if the argument is configured to suppress argument/group checks. + */ + 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 suppressing argument checks for other arguments. + * @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_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 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 value = true) noexcept + requires(not util::c_is_none) + { + this->_greedy = value; + 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 util::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 util::c_is_none) + { + return this->nargs(nargs::range(n)); + } + + /** + * @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 util::c_is_none) + { + return this->nargs(nargs::range(lower, upper)); + } + + /** + * @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 util::c_is_none) + { + using callable_type = action::util::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 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. + * @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 util::c_is_none and std::equality_comparable) + { + for (const auto& choice : choices) + this->_choices.emplace_back(choice); + return *this; + } + + /** + * @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. + */ + argument& choices(std::initializer_list choices) noexcept + requires(not util::c_is_none and std::equality_comparable) + { + return this->choices<>(choices); + } + + /** + * @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 util::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 util::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 values resets the `required` attribute to `false`. + * @note The method is enabled only if `value_type` is not `none_type`. + */ + argument& default_values(std::initializer_list values) noexcept + requires(not util::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 util::c_is_none) + { + (this->_default_values.emplace_back(std::make_any(values)), ...); + this->_required = false; + return *this; + } + + /** + * @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`. + */ + template CR> + argument& implicit_values(const CR& values) noexcept + 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)); + 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 util::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 util::c_is_none and type == argument_type::optional) + { + (this->_implicit_values.emplace_back(std::make_any(values)), ...); + 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::util::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< + util::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 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::help_builder help_builder(const bool verbose) const noexcept override { + detail::help_builder bld(this->_name.str(), this->_help_msg); + + if (not verbose) + return bld; + + bld.params.reserve(6ull); + if (this->_required != _default_required) + bld.add_param("required", std::format("{}", this->_required)); + 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) { + if (not this->_choices.empty()) + bld.add_range_param("choices", this->_choices); + if (not this->_default_values.empty()) + 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()) + bld.add_range_param( + "implicit value(s)", + util::any_range_cast_view(this->_implicit_values) + ); + } + } + + return bld; + } + + /// @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 (this->has_parsed_values()) + return this->_values.front(); + + if constexpr (util::c_is_none) + throw std::logic_error( + std::format("No values parsed for argument '{}'.", this->_name.str()) + ); + else + 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(); + } + + /// @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) + { + 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(util::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 util::c_is_none) + { + if constexpr (type == argument_type::positional) + return not this->_default_values.empty(); + else + return not this->_default_values.empty() + or (this->is_used() and not this->_implicit_values.empty()); + } + + /** + * @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 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::vector& _predefined_values() const + requires(not util::c_is_none) + { + if constexpr (type == argument_type::optional) { + if (this->is_used()) { + if (this->_implicit_values.empty()) + throw(std::logic_error(std::format( + "No implicit values specified for argument '{}'.", this->_name.str() + ))); + + return this->_implicit_values; + } + } + + if (this->_default_values.empty()) + throw(std::logic_error( + std::format("No default values specified for argument '{}'.", this->_name.str()) + )); + + return this->_default_values; + } + + /// @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 util::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(util::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 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 (util::c_trivially_readable) { + value = value_type(str_value); + } + else { + if (not (std::istringstream(str_value) >> 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(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) + 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 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> + _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 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. + + // 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 = + util::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 006de541..00000000 --- a/include/ap/argument/optional.hpp +++ /dev/null @@ -1,366 +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; - - /** - * @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 df82deb5..00000000 --- a/include/ap/argument/positional.hpp +++ /dev/null @@ -1,290 +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. - - /** - * @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_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 1297be76..61950c9d 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 "argument_group.hpp" #include "detail/argument_token.hpp" -#include "detail/concepts.hpp" -#include "version.hpp" +#include "types.hpp" #include #include @@ -34,36 +32,162 @@ namespace ap { class argument_parser; +/// @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 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: + * @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 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 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("fcopy"); + * parser.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: 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(const std::string_view name) : argument_parser(name, "") {} ~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) noexcept { - this->_program_name.emplace(name); - return *this; - } - /** * @brief Set the program version. * @param version The version of the program. @@ -79,7 +203,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 (util::contains_whitespaces(version)) + throw invalid_configuration("The program version cannot contain whitespace characters!" + ); + this->_program_version.emplace(version); return *this; } @@ -106,117 +234,138 @@ class argument_parser { } /** - * @brief Set default positional arguments. + * @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. - * @param arg_discriminator_range A range of default positional argument discriminators. + * @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> - argument_parser& default_positional_arguments(const AR& arg_discriminator_range) noexcept { - for (const auto arg_discriminator : arg_discriminator_range) + 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); 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_positional_arguments( - const std::initializer_list arg_discriminator_list + argument_parser& default_arguments( + const std::initializer_list& arg_discriminators ) noexcept { - return this->default_positional_arguments<>(arg_discriminator_list); + return this->default_arguments<>(arg_discriminators); } /** - * @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. + * @brief Add default arguments to the argument parser. + * @param arg_discriminators A list of default positional 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); + argument_parser& default_arguments( + const std::same_as auto... arg_discriminators + ) noexcept { + (detail::add_default_argument(arg_discriminators, *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. + * @brief Adds a positional argument to the parser's configuration. + * @tparam T Type of the argument value. + * @param name The name of the argument. + * @return Reference to the added positional argument. + * @throws ap::invalid_configuration */ - argument_parser& default_optional_arguments( - const std::initializer_list arg_discriminator_list - ) noexcept { - return this->default_optional_arguments<>(arg_discriminator_list); + template + 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 primary_name The 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) { - this->_verify_arg_name_pattern(primary_name); + template + positional_argument& add_positional_argument( + argument_group& group, const std::string_view name + ) { + this->_validate_group(group); + this->_verify_arg_name_pattern(name); - const detail::argument_name arg_name(std::make_optional(primary_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); - this->_positional_args.emplace_back(std::make_unique>(arg_name)); - return static_cast&>(*this->_positional_args.back()); + auto& new_arg_ptr = + this->_positional_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. + * @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. + * @return Reference to the added optional argument. + * @throws ap::invalid_configuration + */ + template + optional_argument& add_optional_argument( + 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 positional argument. + * @return Reference to the added optional argument. * @throws ap::invalid_configuration - * - * \todo Check forbidden characters (after adding the assignment character). */ - template - argument::positional& add_positional_argument( + template + optional_argument& add_optional_argument( const std::string_view primary_name, const std::string_view secondary_name ) { - this->_verify_arg_name_pattern(primary_name); - this->_verify_arg_name_pattern(secondary_name); - - const detail::argument_name arg_name{ - std::make_optional(primary_name), - std::make_optional(secondary_name) - }; - 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()); + return this->add_optional_argument( + this->_gr_optional_args, primary_name, secondary_name + ); } /** - * @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 name The name of the argument. * @param name_discr The discriminator value specifying whether the given name should be treated as primary or secondary. * @return Reference to the added optional argument. - * @throws ap::invalid_configuration - * - * \todo Check forbidden characters (after adding the assignment character). + * @throws std::logic_error, ap::invalid_configuration */ - template - argument::optional& add_optional_argument( + 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 = @@ -230,24 +379,28 @@ 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()); + 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. * @throws ap::invalid_configuration - * - * \todo Check forbidden characters (after adding the assignment character). */ - template - argument::optional& add_optional_argument( - const std::string_view primary_name, const std::string_view secondary_name + template + optional_argument& add_optional_argument( + argument_group& group, + const std::string_view primary_name, + const std::string_view secondary_name ) { + this->_validate_group(group); this->_verify_arg_name_pattern(primary_name); this->_verify_arg_name_pattern(secondary_name); @@ -259,49 +412,125 @@ 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()); + 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 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. */ template - argument::optional& add_flag( + optional_argument& add_flag( const std::string_view name, 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. */ 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) - .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 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 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. * - * * Equivalent to: + * Equivalent to: * ```cpp * parse_args(std::span(argv + 1, static_cast(argc - 1))) * ``` @@ -319,23 +548,19 @@ class argument_parser { * @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::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(); - - std::vector unknown_args; - this->_parse_args_impl(this->_tokenize(argv_rng), unknown_args); + parsing_state state(*this); + this->_parse_args_impl(std::ranges::begin(argv_rng), std::ranges::end(argv_rng), state); - if (not unknown_args.empty()) - throw parsing_failure::argument_deduction_failure(unknown_args); - - if (not this->_are_required_args_bypassed()) { - this->_verify_required_args(); - this->_verify_nvalues(); - } + if (not state.unknown_args.empty()) + throw parsing_failure(std::format( + "Failed to deduce the argument for values [{}]", util::join(state.unknown_args) + )); } /** @@ -363,15 +588,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::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 << "[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); } } @@ -408,22 +635,15 @@ 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::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(); - - std::vector unknown_args; - this->_parse_args_impl(this->_tokenize(argv_rng), unknown_args, false); - - if (not this->_are_required_args_bypassed()) { - this->_verify_required_args(); - this->_verify_nvalues(); - } - - return unknown_args; + 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); } /** @@ -452,78 +672,126 @@ 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::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 << "[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); } } - // clang-format off + /// @brief Returns the parser's name. + [[nodiscard]] std::string_view name() const noexcept { + return this->_name; + } /** - * @brief Handles the `help` argument logic. + * @brief Returns the parser's full program name. * - * 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); - } + * - 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; } - // clang-format on + /** + * @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 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. + * @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; } /** + * @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_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; } /** + * @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. * @throws ap::lookup_failure, ap::type_error */ - template + 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()); } } /** + * @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. @@ -531,14 +799,14 @@ 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_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&) { @@ -547,84 +815,81 @@ 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()); } } /** + * @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. * @throws ap::lookup_failure, ap::type_error */ - template + 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 { - if (not arg.has_parsed_values() and arg.has_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) + util::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()); } } /** - * @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. + * @param os The output stream. */ - void print_config(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'; - } + void print_help(const bool verbose, std::ostream& os = std::cout) const noexcept { + 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); - } + this->_print_subparsers(os); + for (const auto& group : this->_argument_groups) + this->_print_group(os, *group, verbose); + } - if (not this->_optional_args.empty()) { - os << "\nOptional arguments:\n"; - this->_print(os, this->_optional_args, 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; } /** * @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. + * @param os The output stream. * @param parser The argument parser to print. * @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; } @@ -634,15 +899,54 @@ class argument_parser { #endif private: - using arg_ptr_t = std::unique_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_ptr_t = std::shared_ptr; + using arg_ptr_vec_t = std::vector; + using arg_ptr_vec_iter_t = typename arg_ptr_vec_t::iterator; + + using arg_group_ptr_t = std::unique_ptr; + using arg_group_ptr_vec_t = std::vector; + + 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(); + } - using arg_token_list_t = std::vector; - using arg_token_list_iterator_t = typename arg_token_list_t::const_iterator; + 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. + }; + + 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()) + 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 @@ -654,7 +958,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." ); @@ -723,6 +1027,53 @@ 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 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(); + for (const auto& tok : this->_tokenize(args_begin, args_end, state)) + this->_parse_token(tok, state); + this->_verify_final_state(); + this->_finalized = true; + } + /** * @brief Validate whether the definition/configuration of the parser's arguments is correct. * @@ -731,298 +1082,378 @@ 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() - ); + 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() + )); } } /** * @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. + * @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. * @return A list of preprocessed command-line argument tokens. */ - 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); - std::ranges::for_each( - arg_range, std::bind_front(&argument_parser::_tokenize_arg, this, std::ref(toks)) - ); + template AIt> + [[nodiscard]] arg_token_vec_t _tokenize( + AIt args_begin, const AIt args_end, const parsing_state& state + ) { + arg_token_vec_t toks; + toks.reserve(static_cast(std::ranges::distance(args_begin, args_end))); + 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(arg_token_list_t& toks, const std::string_view arg_value) { - auto tok = this->_build_token(arg_value); + void _tokenize_arg( + 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) + }; if (not tok.is_flag_token() or this->_validate_flag_token(tok)) { toks.emplace_back(std::move(tok)); 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()); + // 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; } -#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)); -#endif + 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; + } } /** - * @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. + * @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 _build_token(const std::string_view arg_value + [[nodiscard]] detail::argument_token::token_type _deduce_token_type( + const std::string_view arg_value ) const noexcept { - if (detail::contains_whitespaces(arg_value)) - return {.type = detail::argument_token::t_value, .value = std::string(arg_value)}; + if (util::contains_whitespaces(arg_value)) + return detail::argument_token::t_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)) - }; + return detail::argument_token::t_flag_primary; 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 detail::argument_token::t_flag_secondary; - return {.type = detail::argument_token::t_value, .value = std::string(arg_value)}; + return detail::argument_token::t_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 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. + * @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.emplace(*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; - compound_toks.reserve(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; - - for (const char c : tok.value) { - detail::argument_token ctok{ - detail::argument_token::t_flag_secondary, std::string(1ull, c) - }; - if (not this->_validate_flag_token(ctok)) { - compound_toks.clear(); - 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) { + 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 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). + * @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]] std::string _unstripped_token_value(const detail::argument_token& 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(); + + 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 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::format("{}{}", this->_flag_prefix, tok.value); + return std::string_view(tok.value).substr(this->_primary_flag_prefix_length); case detail::argument_token::t_flag_secondary: - return std::format("{}{}", this->_flag_prefix_char, tok.value); + 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. + * @brief Parse a single command-line argument token. + * @param tok The token to be parsed. + * @param state The current parsing state. * @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); + 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(tok.value, state); + return; + } + + if (tok.is_flag_token()) + this->_parse_flag_token(tok, state); + else + this->_parse_value_token(tok, state); } /** - * @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. + * @brief Parse a single command-line argument *flag* token. + * @param tok The token to be parsed. + * @param state The current parsing state. + * @throws ap::parsing_failure */ - 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 (token_it->type != detail::argument_token::t_value) + 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(); + 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); + } + } - pos_arg->set_value(token_it->value); - ++token_it; + for (const auto& arg : tok.args) { + if (arg->mark_used()) + state.curr_arg = arg; + else + state.curr_arg.reset(); } } /** - * @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 *value* token. + * @param tok The token to be parsed. + * @param state The current parsing state. * @throws ap::parsing_failure */ - void _parse_optional_args( - arg_token_list_iterator_t& token_it, - const arg_token_list_iterator_t& tokens_end, - std::vector& unknown_args, - const bool handle_unknown = true - ) { - 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; - } - } + 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); + return; + } - if (token_it->arg->get()->mark_used()) - curr_opt_arg = token_it->arg; - else - curr_opt_arg.reset(); + state.curr_arg = *state.curr_pos_arg_it; + } - break; - } - case detail::argument_token::t_value: { - if (not curr_opt_arg) { - unknown_args.emplace_back(token_it->value); - break; - } + this->_set_argument_value(tok.value, state); + } - if (not curr_opt_arg->get()->set_value(token_it->value)) - curr_opt_arg.reset(); + /** + * @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 value The value to be set for the current argument. + * @param state The current parsing state. + */ + 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 - break; - } - } + // 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; - ++token_it; - } + state.curr_arg.reset(); } /** - * @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 Verifies the correctness of the parsed command-line arguments. + * @throws ap::parsing_failure if the state of the parsed arguments is invalid. */ - [[nodiscard]] bool _are_required_args_bypassed() const noexcept { - // 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->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(); - }); + void _verify_final_state() const { + const auto [supress_group_checks, suppress_arg_checks] = this->_are_checks_suppressed(); + for (const auto& group : this->_argument_groups) + this->_verify_group_requirements(*group, supress_group_checks, suppress_arg_checks); } /** - * @brief Check if all required positional and optional arguments are used. - * @throws ap::parsing_failure + * @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. */ - void _verify_required_args() const { + [[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 - 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()); + 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 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 suppress_group_checks, + const bool suppress_arg_checks + ) const { + if (group._arguments.empty()) + return; + + 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(); } + )); - 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()); + 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, 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 + )); + } + + // all arguments in the group have to be validated + for (const auto& arg : group._arguments) + this->_verify_argument_requirements(arg, suppress_arg_checks); } /** - * @brief Check if the number of argument values is within the specified range. - * @throws ap::parsing_failure + * @brief Verifies whether the requirements of the given argument are satisfied. + * @param arg The argument to verify. + * @param suppress_arg_checks A flag indicating whether argument checks are suppressed. + * @throws ap::parsing_failure if the requirements are not satistied. */ - 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); + void _verify_argument_requirements(const arg_ptr_t& arg, const bool suppress_arg_checks) const { + if (suppress_arg_checks) + return; - 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); + if (arg->is_required() and not arg->has_value()) + 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); } /** @@ -1030,88 +1461,118 @@ 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; } - /** - * @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 { - 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: - return this->_optional_args.end(); - } + 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. - * @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 << '\n' << group._name << ":"; + + std::vector group_attrs; + if (group._required) + group_attrs.emplace_back("required"); + if (group._mutually_exclusive) + group_attrs.emplace_back("mutually exclusive"); + if (not group_attrs.empty()) + os << " (" << util::join(group_attrs) << ')'; + + os << '\n'; if (verbose) { for (const auto& arg : visible_args) - os << '\n' << arg->desc(verbose).get(this->_indent_width) << '\n'; + os << '\n' << arg->help_builder(verbose).get(this->_indent_width) << '\n'; } else { - std::vector descriptors; - descriptors.reserve(args.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::optional _program_name; - std::optional _program_version; - std::optional _program_description; - bool _verbose = false; - - 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; + 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; 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 { @@ -1122,60 +1583,54 @@ 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: - arg_parser.add_flag("help", "h") - .action(action::print_config(arg_parser, EXIT_SUCCESS)) - .nargs(0ull) + case default_argument::o_help: + arg_parser.add_optional_argument("help", "h") + .action(action::print_help(arg_parser, EXIT_SUCCESS)) .help("Display the help message"); break; - case argument::default_optional::input: + 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") - .required() .nargs(1ull) .action(action::check_file_exists()) .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").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)) .action(action::check_file_exists()) .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)) .help("Output files paths"); break; diff --git a/include/ap/detail/argument_base.hpp b/include/ap/detail/argument_base.hpp index eb811111..e3f1de6e 100644 --- a/include/ap/detail/argument_base.hpp +++ b/include/ap/detail/argument_base.hpp @@ -3,14 +3,14 @@ // 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. */ #pragma once -#include "argument_descriptor.hpp" #include "argument_name.hpp" +#include "help_builder.hpp" #include #include @@ -27,50 +27,42 @@ class argument_base { public: virtual ~argument_base() = default; + /// @return `true` if the argument is a positional argument instance, `false` otherwise. + virtual bool is_positional() const noexcept = 0; + + /// @return `true` if the argument is an optional argument instance, `false` otherwise. + virtual bool is_optional() const noexcept = 0; + + /// @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 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; + 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 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 `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; - } - - /** - * @param verbose The verbosity mode value. - * @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. - */ + /// @param verbose The verbosity mode value. If `true` all non-default parameters will be included in the output. + /// @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. virtual bool mark_used() = 0; /// @return `true` if the argument has been used, `false` otherwise. @@ -79,11 +71,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. @@ -92,6 +82,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; @@ -100,26 +93,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_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..8328e4df 100644 --- a/include/ap/detail/argument_token.hpp +++ b/include/ap/detail/argument_token.hpp @@ -2,29 +2,29 @@ // 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 #include "argument_base.hpp" -#include "typing_utility.hpp" #include -#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. + using arg_ptr_vec_t = std::vector; /// @brief The token type discriminator. enum class token_type : std::uint8_t { - t_flag_primary, ///< Represents the primary (--) flag argument. - t_flag_secondary, ///< Represents the secondary (-) flag argument. - t_value ///< Represents a value argument. + t_value, ///< Represents a value 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; @@ -39,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.has_value(); + 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_opt_t arg = std::nullopt; ///< The corresponding argument. + arg_ptr_vec_t args = {}; ///< The corresponding argument }; } // namespace ap::detail diff --git a/include/ap/detail/argument_descriptor.hpp b/include/ap/detail/help_builder.hpp similarity index 90% rename from include/ap/detail/argument_descriptor.hpp rename to include/ap/detail/help_builder.hpp index 60928558..830a1982 100644 --- a/include/ap/detail/argument_descriptor.hpp +++ b/include/ap/detail/help_builder.hpp @@ -3,15 +3,15 @@ // Licensed under the MIT License. See the LICENSE file in the project root for full license information. /** - * @file 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 -#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 @@ -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) {} /** @@ -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 f0ad81ac..9670b285 100644 --- a/include/ap/exceptions.hpp +++ b/include/ap/exceptions.hpp @@ -2,13 +2,13 @@ // 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 #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 @@ -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,53 +47,17 @@ 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 [{}]", detail::join(values)) - ); - } - static parsing_failure invalid_nvalues( const detail::argument_name& arg_name, const std::weak_ordering ordering ) 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( @@ -117,7 +67,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) {} @@ -136,7 +85,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/nargs/range.hpp b/include/ap/nargs/range.hpp index 9e2b317c..7d115bbc 100644 --- a/include/ap/nargs/range.hpp +++ b/include/ap/nargs/range.hpp @@ -3,52 +3,69 @@ // 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; + +/// @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. 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 +73,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 +143,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 +152,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 65% rename from include/ap/version.hpp rename to include/ap/types.hpp index da4fea0f..0ee9ce4b 100644 --- a/include/ap/version.hpp +++ b/include/ap/types.hpp @@ -2,18 +2,28 @@ // 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; ///< The major version number. - std::uint32_t minor; ///< The minor version number. - std::uint32_t patch; ///< The patch 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. /// @brief Converts the structure into a string in the `v{major}.{minor}.{path}` format [[nodiscard]] std::string str() const noexcept { diff --git a/include/ap/detail/concepts.hpp b/include/ap/util/concepts.hpp similarity index 73% rename from include/ap/detail/concepts.hpp rename to include/ap/util/concepts.hpp index d7d234eb..3dc95bb1 100644 --- a/include/ap/detail/concepts.hpp +++ b/include/ap/util/concepts.hpp @@ -3,20 +3,31 @@ // Licensed under the MIT License. See the LICENSE file in the project root for full license information. /** - * @file concepts.hpp + * @file ap/util/concepts.hpp * @brief Provides the general concept definitions. */ #pragma once +#include "ap/types.hpp" + #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; /** * @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; }; @@ -24,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; @@ -31,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; }; @@ -38,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; @@ -45,20 +59,25 @@ 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 = 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`. * @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. @@ -74,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; @@ -82,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; @@ -90,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 = @@ -100,15 +122,17 @@ 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; /** - * @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`). + * @ingroup util */ template concept c_range_of = @@ -116,15 +140,26 @@ concept c_range_of = 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. + * @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_sized_range_of = - std::ranges::sized_range +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::detail +} // namespace ap::util diff --git a/include/ap/util/ranges.hpp b/include/ap/util/ranges.hpp new file mode 100644 index 00000000..8f82f9be --- /dev/null +++ b/include/ap/util/ranges.hpp @@ -0,0 +1,34 @@ +// 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/util/ranges.hpp + * @brief Provides common ranges utility functions. + */ + +#pragma once + +#include "concepts.hpp" + +#include +#include + +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 { + return std::any_cast(value); + }); +} + +} // 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 d3dab32e..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 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 ae1c3dbd..d3f2e0d4 100644 --- a/include/ap/detail/typing_utility.hpp +++ b/include/ap/util/typing.hpp @@ -2,6 +2,11 @@ // 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/util/typing.hpp + * @brief Provides common typing utility functions. + */ + #pragma once #include @@ -10,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__) @@ -38,4 +48,4 @@ constexpr std::string_view get_demangled_type_name() { #endif } -} // namespace ap::detail +} // namespace ap::util 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/CMakeLists.txt b/tests/CMakeLists.txt index 9a7ffc30..a9184b25 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -63,16 +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_nargs_range.cpp") +add_doctest("source/test_help_builder.cpp") add_doctest("source/test_argument_token.cpp") -add_doctest("source/test_argument_parser_add_argument.cpp") +add_doctest("source/test_none_type_argument.cpp") +add_doctest("source/test_positional_argument.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") -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_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..564a938f 100644 --- a/tests/include/argument_parser_test_fixture.hpp +++ b/tests/include/argument_parser_test_fixture.hpp @@ -5,24 +5,28 @@ #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; +using ap::util::c_argument_value_type; +using ap::util::c_forward_iterator_of; +using ap::util::type_validator; 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_vec_t = ap::argument_parser::arg_token_vec_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 +36,10 @@ struct argument_parser_test_fixture { return std::format("-ta-{}", i); } + [[nodiscard]] std::string_view 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); } @@ -58,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)); @@ -104,20 +115,24 @@ 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)) - ); + setup_arg(this->sut.add_positional_argument(init_arg_name_primary(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( + setup_arg(this->sut.add_optional_argument( init_arg_name_primary(begin_idx + i), init_arg_name_secondary(begin_idx + i) )); } @@ -128,10 +143,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) @@ -139,12 +154,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.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)}); @@ -154,33 +168,38 @@ struct argument_parser_test_fixture { } // argument_parser private member accessors - [[nodiscard]] const std::optional& get_program_name() const { - return 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))); + [[nodiscard]] arg_token_vec_t tokenize(int argc, char* argv[]) { + return this->sut._tokenize(argv + 1, argv + argc, state); } - void parse_args_impl(const arg_token_list_t& arg_tokens) { - sut._parse_args_impl(arg_tokens, this->unknown_args); + 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_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; + // 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}; + + static constexpr std::string_view program_name = "program"; }; } // namespace ap_testing diff --git a/tests/include/argument_test_fixture.hpp b/tests/include/argument_test_fixture.hpp new file mode 100644 index 00000000..df15671c --- /dev/null +++ b/tests/include/argument_test_fixture.hpp @@ -0,0 +1,132 @@ +#pragma once + +#include +#include + +using ap::argument; +using ap::argument_type; +using ap::detail::argument_name; +using ap::detail::help_builder; +using ap::util::as_string; +using ap::util::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_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]] help_builder get_help_builder(const argument& arg, const bool verbose) + const { + return arg.help_builder(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(); + } + + // 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/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..5e2fd0e7 100644 --- a/tests/include/utility.hpp +++ b/tests/include/utility.hpp @@ -1,7 +1,6 @@ #pragma once -#include -#include +#include namespace ap_testing { @@ -10,14 +9,14 @@ void discard_result(T&&) { // do nothing } -template +template bool is_positional(const ap::detail::argument_base& arg) { - return dynamic_cast*>(&arg); + return dynamic_cast*>(&arg); } -template +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_elements.cpp similarity index 69% rename from tests/source/test_argument_parser_add_argument.cpp rename to tests/source/test_argument_parser_add_elements.cpp index cb2a66d0..9da936fc 100644 --- a/tests/source/test_argument_parser_add_argument.cpp +++ b/tests/source/test_argument_parser_add_elements.cpp @@ -1,13 +1,15 @@ #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::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"; @@ -33,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" ) { @@ -76,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" ) { @@ -106,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; @@ -148,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(), @@ -162,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)); - } - - 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 - ); - } + // adding argument with a unique name + CHECK_NOTHROW(sut.add_positional_argument(primary_name_2)); - 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); @@ -224,10 +208,10 @@ 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 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 +219,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,13 +229,13 @@ 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)); } } 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); @@ -284,26 +268,26 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_argument_parser_add_argument, - "default_positional_arguments should add the specified arguments" + test_argument_parser_add_elements, + "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); - 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( - test_argument_parser_add_argument, - "default_optional_arguments should add the specified arguments" + test_argument_parser_add_elements, + "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; @@ -328,14 +312,83 @@ 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)); +} + +TEST_CASE_FIXTURE( + test_argument_parser_add_elements, + "argument adding functions 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, 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 + ); +} + +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 393e6042..fe4d0c7a 100644 --- a/tests/source/test_argument_parser_info.cpp +++ b/tests/source/test_argument_parser_info.cpp @@ -3,34 +3,63 @@ 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("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"), + "The program name cannot contain whitespace characters!", + invalid_configuration + ); +} + TEST_CASE_FIXTURE( - test_argument_parser_info, "parser's program name member should be nullopt by default" + test_argument_parser_info, "argument_parser() should set the name and program name members" ) { - const auto stored_program_name = get_program_name(); - CHECK_FALSE(stored_program_name); + CHECK_EQ(sut.name(), program_name); + CHECK_EQ(sut.program_name(), program_name); } -TEST_CASE_FIXTURE(test_argument_parser_info, "name() should set the program name member") { - sut.program_name(test_name); - - const auto stored_program_name = get_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"; - REQUIRE(stored_program_name); - CHECK_EQ(stored_program_name.value(), test_name); + 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( 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( + 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") { @@ -52,7 +81,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 2b8cd1a3..5a540101 100644 --- a/tests/source/test_argument_parser_parse_args.cpp +++ b/tests/source/test_argument_parser_parse_args.cpp @@ -3,29 +3,24 @@ #include "utility.hpp" using namespace ap_testing; -using namespace ap::argument; 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"; 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 = 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; const std::size_t first_opt_arg_idx = n_positional_args; 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"; @@ -73,7 +68,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)); @@ -93,12 +89,13 @@ 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(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() ); } @@ -108,10 +105,10 @@ 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(unknown_args.empty()); + CHECK_NOTHROW(parse_args_impl(argv_vec.begin(), argv_vec.end())); + CHECK(state.unknown_args.empty()); } // _get_argument @@ -130,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))); } } @@ -156,7 +155,7 @@ 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(), + required_argument_not_parsed_msg({init_arg_name_primary(last_pos_arg_idx)}).c_str(), parsing_failure ); @@ -168,26 +167,24 @@ 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); 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 ); @@ -211,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 ); @@ -235,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 ); @@ -265,7 +263,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); @@ -275,11 +274,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::unknown_argument(unknown_arg_name).what(), + parsing_failure::unknown_argument(unknown_arg_flag).what(), parsing_failure ); @@ -370,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) @@ -393,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); @@ -402,9 +403,9 @@ 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) - .bypass_required(); + .default_values(false) + .implicit_values(true) + .suppress_arg_checks(); std::string arg_flag; @@ -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( @@ -540,10 +601,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); @@ -573,7 +636,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()); @@ -599,7 +662,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 @@ -622,10 +685,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); } } @@ -666,7 +731,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); @@ -686,7 +759,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) { @@ -749,7 +822,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); @@ -769,7 +850,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) { @@ -793,13 +874,22 @@ 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 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 ); } } @@ -811,47 +901,121 @@ 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 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 +// 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_name); + CHECK(sut.values(positional_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_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_name)), ap::type_error + ); + + free_argv(argc, argv); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "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::vector default_values{"default_value_1", "default_value_2"}; + + sut.add_positional_argument(positional_name).default_values(default_values); + + // 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_name); + + 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() should throw when calling with a positional argument's name" + "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); + sut.add_positional_argument(positional_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); - 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); + const auto& stored_values = sut.values(positional_name); + + REQUIRE_EQ(stored_values.size(), positional_arg_values.size()); + CHECK_EQ(stored_values, positional_arg_values); + + free_argv(argc, argv); } +// values: optional arguments + 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)); @@ -882,33 +1046,33 @@ 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 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"}; + const std::vector implicit_values{"implicit_value_1", "implicit_value_2"}; sut.add_optional_argument(optional_primary_name, optional_secondary_name) - .default_value(default_value) - .implicit_value(implicit_value); + .default_values(default_values) + .implicit_values(implicit_values); // prepare argc & argv std::vector argv_vec{"program"}; - std::string expected_value; + std::vector expected_values; - SUBCASE("default_value") { - expected_value = default_value; + SUBCASE("default_values") { + expected_values = default_values; } - SUBCASE("implicit_value") { - expected_value = implicit_value; + 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_value); + CAPTURE(expected_values); const int argc = static_cast(argv_vec.size()); auto argv = to_char_2d_array(argv_vec); @@ -918,16 +1082,16 @@ TEST_CASE_FIXTURE( const auto& stored_values = sut.values(optional_primary_name); - REQUIRE_EQ(stored_values.size(), 1); - CHECK_EQ(stored_values.front(), expected_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() 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)); @@ -952,8 +1116,7 @@ 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); } @@ -1034,3 +1197,325 @@ TEST_CASE_FIXTURE( // cleanup 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( + 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); +} + +// 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); +} + +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( + 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_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 d890c1ac..00000000 --- a/tests/source/test_argument_parser_parse_args_unknown_flags_as_values.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include "argument_parser_test_fixture.hpp" -#include "doctest.h" -#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 -: 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); -} diff --git a/tests/source/test_argument_token.cpp b/tests/source/test_argument_token.cpp index 3a269b09..f88c6adc 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; @@ -47,16 +47,16 @@ 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()); 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()); } 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_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..b1e456b1 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 +#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,93 +78,99 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - optional_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( - optional_argument_test_fixture, - "desc(verbose=true) should return an argument_descriptor with non-default params" + argument_test_fixture, + "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 - 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_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); + bld = get_help_builder(sut, verbose); + + 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 bypass_required_it = - std::ranges::find(desc.params, "bypass required", ¶meter_descriptor::name); - REQUIRE_NE(bypass_required_it, desc.params.end()); - CHECK_EQ(bypass_required_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(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 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()); - CHECK_EQ(choices_it->value, ap::detail::join(choices, ", ")); + 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", ¶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", ¶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)); } 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,94 +180,107 @@ 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, - "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); + + 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(); + } - sut.bypass_required(true); - CHECK(is_bypass_required_enabled(sut)); + CAPTURE(sut); - sut.bypass_required(false); - CHECK_FALSE(is_bypass_required_enabled(sut)); + 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( - optional_argument_test_fixture, - "bypass_required_enabled() should return true only if the `required` flag is set to false and " - "the `bypass_required` flags is set to true" + argument_test_fixture, + "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(is_bypass_required_enabled(sut)); + 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(is_bypass_required_enabled(sut)); + sut.required(false); - set_required(sut, true); - set_bypass_required(sut, true); - CHECK_FALSE(is_bypass_required_enabled(sut)); + sut.suppress_arg_checks(true); + CHECK(sut.suppresses_arg_checks()); - // enabled - set_required(sut, false); - set_bypass_required(sut, true); - CHECK(is_bypass_required_enabled(sut)); + sut.suppress_arg_checks(false); + CHECK_FALSE(sut.suppresses_arg_checks()); } TEST_CASE_FIXTURE( - optional_argument_test_fixture, - "required(true) should disable `bypass_required` option and bypass_required(true) should " - "disable the `required` option" + argument_test_fixture, + "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(is_required(sut)); - REQUIRE_FALSE(is_bypass_required_enabled(sut)); + 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(is_bypass_required_enabled(sut)); - CHECK_FALSE(is_required(sut)); + sut.required(false); - sut.required(); - CHECK(is_required(sut)); - CHECK_FALSE(is_bypass_required_enabled(sut)); + sut.suppress_group_checks(true); + CHECK(sut.suppresses_group_checks()); - sut.bypass_required(); - CHECK(is_bypass_required_enabled(sut)); - CHECK_FALSE(is_required(sut)); + sut.suppress_group_checks(false); + CHECK_FALSE(sut.suppresses_group_checks()); } -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 +289,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 +308,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,45 +317,45 @@ 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" ) { 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); } 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); - sut.implicit_value(implicit_value); + sut.implicit_values(implicit_value); CHECK_FALSE(has_value(sut)); } 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); - sut.implicit_value(implicit_value); + sut.implicit_values(implicit_value); mark_used(sut); @@ -350,81 +363,116 @@ 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" + "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)); } } -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_values(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_values(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_values(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_values(default_value); + sut.implicit_values(implicit_value); + + CHECK(has_predefined_values(sut)); } TEST_CASE_FIXTURE( - optional_argument_test_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( + 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); - sut.default_value(arbitrary_value); + sut.default_values(arbitrary_value); REQUIRE(has_value(sut)); CHECK_EQ(std::any_cast(get_value(sut)), arbitrary_value); } 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); - sut.implicit_value(implicit_value); + sut.implicit_values(implicit_value); mark_used(sut); @@ -433,7 +481,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_values(default_value); + sut.implicit_values(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" ) { @@ -442,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)); @@ -450,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)); @@ -458,7 +518,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); @@ -466,40 +526,26 @@ 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)); } 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 +555,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,27 +598,26 @@ 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); sut.nargs(non_default_range); - sut.default_value(default_value); + sut.default_values(default_value); CHECK(std::is_eq(nvalues_ordering(sut))); } 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..759892c6 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; @@ -12,59 +14,37 @@ 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; +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; -} // namespace - -TEST_CASE_FIXTURE( - positional_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)); - } +const range non_default_range = range{1ull, choices.size()}; - SUBCASE("initialized with the primary and secondary names") { - const auto sut = sut_type(arg_name); - const auto name = get_name(sut); +} // namespace - CHECK(name.match(primary_name)); - CHECK(name.match(secondary_name)); - } +TEST_CASE_FIXTURE(argument_test_fixture, "name() should return the proper argument_name instance") { + const auto sut = sut_type(arg_name); + CHECK_EQ(get_name(sut), arg_name); } -TEST_CASE_FIXTURE(positional_argument_test_fixture, "help() should return nullopt by default") { - const auto sut = sut_type(arg_name_primary); +TEST_CASE_FIXTURE(argument_test_fixture, "help() should return nullopt by default") { + const auto sut = sut_type(arg_name); CHECK_FALSE(get_help(sut)); } -TEST_CASE_FIXTURE( - positional_argument_test_fixture, "help() should return a massage set for the argument" -) { - auto sut = sut_type(arg_name_primary); +TEST_CASE_FIXTURE(argument_test_fixture, "help() should return a massage set for the argument") { + auto sut = sut_type(arg_name); sut.help(help_msg); const auto stored_help_msg = get_help(sut); @@ -74,269 +54,327 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - positional_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( - positional_argument_test_fixture, - "desc(verbose=true) should return an argument_descriptor with non-default params" + argument_test_fixture, + "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(); + sut.required(false); + sut.suppress_arg_checks(); + sut.suppress_group_checks(); + 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); + 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()); - 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 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, ", ")); + 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)); + + 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", ¶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)); } 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)); + auto sut = sut_type(arg_name); + 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") { - auto sut = sut_type(arg_name_primary); - CHECK(is_required(sut)); +TEST_CASE_FIXTURE(argument_test_fixture, "is_required() should return true by default") { + auto sut = sut_type(arg_name); + 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); + auto sut = sut_type(arg_name); 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, - "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(is_bypass_required_enabled(sut)); + 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(); + } - sut.bypass_required(false); - CHECK_FALSE(is_bypass_required_enabled(sut)); + CAPTURE(sut); + + 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( - positional_argument_test_fixture, - "bypass_required_enabled() should return true only if the `required` flag is set to false and " - "the `bypass_required` flags is set to true" + argument_test_fixture, + "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(is_bypass_required_enabled(sut)); + 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(is_bypass_required_enabled(sut)); + sut.required(false); - set_required(sut, true); - set_bypass_required(sut, true); - CHECK_FALSE(is_bypass_required_enabled(sut)); + sut.suppress_arg_checks(true); + CHECK(sut.suppresses_arg_checks()); - // enabled - set_required(sut, false); - set_bypass_required(sut, true); - CHECK(is_bypass_required_enabled(sut)); + sut.suppress_arg_checks(false); + CHECK_FALSE(sut.suppresses_arg_checks()); } TEST_CASE_FIXTURE( - positional_argument_test_fixture, - "required(true) should disable `bypass_required` option and bypass_required(true) should " - "disable the `required` option" + argument_test_fixture, + "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); + + 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 + ); - REQUIRE(is_required(sut)); - REQUIRE_FALSE(is_bypass_required_enabled(sut)); + sut.required(false); - sut.bypass_required(); - CHECK(is_bypass_required_enabled(sut)); - CHECK_FALSE(is_required(sut)); + sut.suppress_group_checks(true); + CHECK(sut.suppresses_group_checks()); - sut.required(); - CHECK(is_required(sut)); - CHECK_FALSE(is_bypass_required_enabled(sut)); + sut.suppress_group_checks(false); + CHECK_FALSE(sut.suppresses_group_checks()); } -TEST_CASE_FIXTURE(positional_argument_test_fixture, "is_used() should return false by default") { - const auto sut = sut_type(arg_name_primary); +TEST_CASE_FIXTURE(argument_test_fixture, "is_used() should return false by default") { + const auto sut = sut_type(arg_name); 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); + auto sut = sut_type(arg_name); 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") { - const auto sut = sut_type(arg_name_primary); +TEST_CASE_FIXTURE(argument_test_fixture, "count() should return 0 by default") { + const auto sut = sut_type(arg_name); CHECK_EQ(get_count(sut), 0ull); } -TEST_CASE_FIXTURE( - positional_argument_test_fixture, "count() should return 1 when argument contains a value" -) { - auto sut = sut_type(arg_name_primary); - set_value(sut, value_1); +TEST_CASE_FIXTURE(argument_test_fixture, "count() should return 1 when argument contains a value") { + auto sut = sut_type(arg_name); + 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") { - const auto sut = sut_type(arg_name_primary); +TEST_CASE_FIXTURE(argument_test_fixture, "has_value() should return false by default") { + const auto sut = sut_type(arg_name); CHECK_FALSE(has_value(sut)); } -TEST_CASE_FIXTURE( - positional_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); +TEST_CASE_FIXTURE(argument_test_fixture, "has_value() should return true if the value is set") { + auto sut = sut_type(arg_name); + 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); + 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); + CHECK_FALSE(has_parsed_values(sut)); +} + TEST_CASE_FIXTURE( - positional_argument_test_fixture, "has_parsed_values() should return false by default" + argument_test_fixture, + "has_parsed_values() should return false regardless of the default value parameter" ) { - const 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); + 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); + CHECK_FALSE(has_predefined_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_predefined_values() should return true if the default value is set" ) { - auto sut = sut_type(arg_name_primary); - sut.default_value(default_value); + auto sut = sut_type(arg_name); + sut.default_values(default_value); - CHECK_FALSE(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); + + REQUIRE_FALSE(has_value(sut)); + CHECK_THROWS_AS(static_cast(get_value(sut)), std::logic_error); } TEST_CASE_FIXTURE( - positional_argument_test_fixture, "has_parsed_values() should true if the value is 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, value_1); + auto sut = sut_type(arg_name); + set_value(sut, valid_value); - CHECK(has_parsed_values(sut)); + REQUIRE(has_value(sut)); + CHECK_EQ(std::any_cast(get_value(sut)), valid_value); } 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 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_NOTHROW(set_value(sut, value_1)); REQUIRE(has_value(sut)); + CHECK_EQ(std::any_cast(get_value(sut)), default_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 argument's parsed value if it has been set" +) { + auto sut = sut_type(arg_name); + sut.default_values(default_value); + set_value(sut, valid_value); + + REQUIRE(has_value(sut)); + CHECK_EQ(std::any_cast(get_value(sut)), valid_value); } TEST_CASE_FIXTURE( - positional_argument_test_fixture, + argument_test_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)); @@ -345,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)); @@ -353,42 +391,44 @@ 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); + 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)); } 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).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, std::weak_ordering::greater).what(), + parsing_failure + ); } -TEST_CASE_FIXTURE( - positional_argument_test_fixture, "set_value(any) should perform the specified action" -) { - auto sut = sut_type(arg_name_primary); +TEST_CASE_FIXTURE(argument_test_fixture, "set_value(any) should perform the specified action") { + auto sut = sut_type(arg_name); SUBCASE("observe action") { const auto is_power_of_two = [](const sut_value_type n) { @@ -410,16 +450,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 +469,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); + 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); -} + auto sut = sut_type(arg_name); + 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_values(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); - - REQUIRE(has_value(sut)); - CHECK_EQ(std::any_cast(get_value(sut)), value_1); -} + auto sut = sut_type(arg_name); + sut.nargs(non_default_range); -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))); } 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 {