diff --git a/CMakeLists.txt b/CMakeLists.txt index 19bcd720..c349b3fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ else() endif() project(cpp-ap - VERSION 3.0.0.8 + VERSION 3.0.0 DESCRIPTION "Command-line argument parser for C++20" HOMEPAGE_URL "https://github.com/SpectraL519/cpp-ap" LANGUAGES CXX diff --git a/Doxyfile b/Doxyfile index fb9fd682..c2cbab5f 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = CPP-AP # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 3.0.0.8 +PROJECT_NUMBER = 3.0.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/MODULE.bazel b/MODULE.bazel index 597d9bb6..91d3db86 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,4 +1,4 @@ module( name = "cpp-ap", - version = "3.0.0.8", + version = "3.0.0", ) diff --git a/README.md b/README.md index 0527f171..60bddaea 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Command-line argument parser for C++20 > [!NOTE] > -> [v1.0](https://github.com/SpectraL519/cpp-ap/commit/9a9e5360766b732f322ae2efe3cf5ec5f9268eef) of the library has been developed for the *Team Programming* course at the *Wrocław University of Science and Technology*. +> [v1.0](https://github.com/SpectraL519/cpp-ap/releases/tag/v1.0) of the library has been developed for the *Team Programming* course at the *Wrocław University of Science and Technology*. > > Faculty: *W04N - Faculty of Information and Communication Technology* > @@ -62,6 +62,11 @@ Command-line argument parser for C++20 - [Parameters Specific for Optional Arguments](/docs/tutorial.md#parameters-specific-for-optional-arguments) - [Default Arguments](/docs/tutorial.md#default-arguments) - [Argument Groups](/docs/tutorial.md#argument-groups) + - [Creating New Groups](/docs/tutorial.md#creating-new-groups) + - [Adding Arguments to Groups](/docs/tutorial.md#adding-arguments-to-groups) + - [Group Attributes](/docs/tutorial.md#group-attributes) + - [Complete Example](/docs/tutorial.md#complete-example) + - [Suppressing Argument Group Checks](/docs/tutorial.md#suppressing-argument-group-checks) - [Parsing Arguments](/docs/tutorial.md#parsing-arguments) - [Basic Argument Parsing Rules](/docs/tutorial.md#basic-argument-parsing-rules) - [Compound Arguments](/docs/tutorial.md#compound-arguments) @@ -72,6 +77,7 @@ Command-line argument parser for C++20 - [Using Multiple Subparsers](/docs/tutorial.md#using-multiple-subparsers) - [Parsing Arguments with Subparsers](/docs/tutorial.md#parsing-arguments-with-subparsers) - [Tracking Parser State](/docs/tutorial.md#tracking-parser-state) + - [Suppressing Argument Group Checks](/docs/tutorial.md#suppressing-argument-group-checks) - [Examples](/docs/tutorial.md#examples) - [Common Utility](/docs/tutorial.md#common-utility) - [Dev notes](/docs/dev_notes.md#dev-notes) diff --git a/cpp-ap-demo b/cpp-ap-demo index bb13a211..cf3f2ad8 160000 --- a/cpp-ap-demo +++ b/cpp-ap-demo @@ -1 +1 @@ -Subproject commit bb13a2111f075e388d48e0cc4bba1bf62dfaad45 +Subproject commit cf3f2ad8e9c06af9adcde99288eea517bfb6ecae diff --git a/docs/tutorial.md b/docs/tutorial.md index 87567155..63142626 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -12,18 +12,18 @@ - [Boolean Flags](#boolean-flags) - [Argument Parameters](#argument-parameters) - [Common Parameters](#common-parameters) - - [help](#1-help---the-arguments-description-which-will-be-printed-when-printing-the-parser-class-instance) - - [hidden](#2-hidden---if-this-option-is-set-for-an-argument-then-it-will-not-be-included-in-the-program-description) - - [required](#3-required---if-this-option-is-set-for-an-argument-and-its-value-is-not-passed-in-the-command-line-an-exception-will-be-thrown) - - [bypass required](#4-bypass_required---if-this-option-is-set-for-an-argument-the-required-option-for-other-arguments-will-be-discarded-if-the-bypassing-argument-is-used-in-the-command-line) - - [nargs](#5-nargs---sets-the-allowed-number-of-values-to-be-parsed-for-an-argument) - - [greedy](#6-greedy---if-this-option-is-set-the-argument-will-consume-all-command-line-values-until-its-upper-nargs-bound-is-reached) - - [choices](#7-choices---a-list-of-valid-argument-values) - - [value actions](#8-value-actions---functions-that-are-called-after-parsing-an-arguments-value) - - [default values](#9-default_values---a-list-of-values-which-will-be-used-if-no-values-for-an-argument-have-been-parsed) + - [help](#1-help---the-arguments-description-which-will-be-printed-when-printing-the-parser-class-instance) - the text shown in the help message to describe an argument + - [hidden](#2-hidden---if-this-option-is-set-for-an-argument-then-it-will-not-be-included-in-the-program-description) - hides the argument from the generated program description and help output + - [required](#3-required---if-this-option-is-set-for-an-argument-and-its-value-is-not-passed-in-the-command-line-an-exception-will-be-thrown) - marks the argument as mandatory; not using it will cause an error + - [suppress arg checks](#4-suppress_arg_checks---using-a-suppressing-argument-results-in-suppressing-requirement-checks-for-other-arguments) - if a suppressing argument is used, other requirement validation will be skipped for other arguments + - [nargs](#5-nargs---sets-the-allowed-number-of-values-to-be-parsed-for-an-argument) - defines how many values an argument can or must accept + - [greedy](#6-greedy---if-this-option-is-set-the-argument-will-consume-all-command-line-values-until-its-upper-nargs-bound-is-reached) - makes the argument consume all following values until its limit is reached + - [choices](#7-choices---a-list-of-valid-argument-values) - restricts the valid inputs to a predefined set of values + - [value actions](#8-value-actions---functions-that-are-called-after-parsing-an-arguments-value) - allows you to run custom code after the argument’s value is parsed + - [default values](#9-default_values---a-list-of-values-which-will-be-used-if-no-values-for-an-argument-have-been-parsed) - specifies fallback values to use if none are provided - [Parameters Specific for Optional Arguments](#parameters-specific-for-optional-arguments) - - [on-flag actions](#1-on-flag-actions---functions-that-are-called-immediately-after-parsing-an-arguments-flag) - - [implicit values](#2-implicit_values---a-list-of-values-which-will-be-set-for-an-argument-if-only-its-flag-but-no-values-are-parsed-from-the-command-line) + - [on-flag actions](#1-on-flag-actions---functions-that-are-called-immediately-after-parsing-an-arguments-flag) - executes custom code immediately when the argument’s flag is present + - [implicit values](#2-implicit_values---a-list-of-values-which-will-be-set-for-an-argument-if-only-its-flag-but-no-values-are-parsed-from-the-command-line) - automatically assigns a value if an argument flag is used without an explicit value - [Predefined Parameter Values](#predefined-parameter-values) - [Default Arguments](#default-arguments) - [Argument Groups](#argument-groups) @@ -31,6 +31,7 @@ - [Adding Arguments to Groups](#adding-arguments-to-groups) - [Group Attributes](#group-attributes) - [Complete Example](#complete-example) + - [Suppressing Argument Group Checks](#suppressing-argument-group-checks) - [Parsing Arguments](#parsing-arguments) - [Basic Argument Parsing Rules](#basic-argument-parsing-rules) - [Compound Arguments](#compound-arguments) @@ -318,9 +319,7 @@ Optional arguments: > [!WARNING] > > - If a positional argument is defined as non-required, then no required positional argument can be defined after (only other non-required positional arguments and optional arguments will be allowed). -> - For both positional and optional arguments: -> - enabling the `required` option disables the `bypass_required` option -> - disabling the `required` option has no effect on the `bypass_required` option. +> - If an argument is suppressing (see [suppress arg checks](#4-suppress_arg_checks---using-a-suppressing-argument-results-in-suppressing-requirement-checks-for-other-arguments) and [Suppressing Argument Group Checks](#suppressing-argument-group-checks)), then it cannot be required (an exception will be thrown). ```cpp // example: positional arguments @@ -377,24 +376,27 @@ Command Result
-#### 4. `bypass_required` - If this option is set for an argument, the `required` option for other arguments will be discarded if the bypassing argument is used in the command-line. +#### 4. `suppress_arg_checks` - Using a suppressing argument results in suppressing requirement checks for other arguments. + +If an argument is defined with the `suppress_arg_checks` option enabled and such argument is explicitly used in the command-line, then requirement validation will be suppressed/skipped for other arguments. This includes validating whether: +- a required argument has been parsed +- the number of values parsed for an argument matches the specified [nargs](#5-nargs---sets-the-allowed-number-of-values-to-be-parsed-for-an-argument) range. > [!NOTE] > -> - Both all arguments have the `bypass_required` option disabled. -> - The default value of the value parameter of the `argument::bypass_required(bool)` method is `true` for all arguments. +> - All arguments have the `suppress_arg_checks` option disabled by default. +> - The default value of the value parameter of the `argument::suppress_arg_checks(bool)` method is `true` for all arguments. > [!WARNING] > -> For both positional and optional arguments: -> - enabling the `bypass_required` option disables the `required` option -> - disabling the `bypass_required` option has no effect on the `required` option. +> - Enabling the `suppress_arg_checks` option has no effect on [argument group](#argument-groups) requirements validation. +> - Enabling argument checks suppressing is not possible for required arguments (an exception will be thrown). ```cpp // example: optional arguments parser.add_positional_argument("input"); parser.add_optional_argument("output", "o").required(); -parser.add_optional_argument("version", "v").bypass_required(); +parser.add_optional_argument("version", "v").suppress_arg_checks(); parser.parse_args(argc, argv); @@ -522,7 +524,7 @@ Actions are represented as functions, which take the argument's value as an argu ```cpp void is_valid_user_tag(const std::string& tag) { if (tag.empty() or tag.front() != '@') - throw std::runtime_error(std::format("Invalid user tag: `{}` — must start with '@'", tag)); + throw std::runtime_error(std::format("Invalid user tag: `{}` - must start with '@'", tag)); } parser.add_optional_argument("user", "u") @@ -934,6 +936,26 @@ Output Options: (required, mutually exclusive) --print, -p : Print output to the console ``` +### Suppressing Argument Group Checks + +Similarly to [suppressing argument checks](#4-suppress_arg_checks---using-a-suppressing-argument-results-in-suppressing-requirement-checks-for-other-arguments), an argument can suppress the requirement checks of argument groups: + +```c++ +argument.suppress_group_checks(); +``` + +If such argument is used the requirement checks associated with the [group attributes](#group-attributes) will not be validated. + +> [!NOTE] +> +> - All arguments have the `suppress_group_checks` option disabled by default. +> - The default value of the value parameter of the `argument::suppress_group_checks(bool)` method is `true` for all arguments. + +> [!WARNING] +> +> - Enabling the `suppress_group_checks` option has no effect on argument requirements validation. +> - Enabling argument group checks suppressing is not possible for required arguments (an exception will be thrown). +


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