From ed135db4a83af73437b333bd0353b6f7f1e019f1 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sat, 20 Sep 2025 23:09:14 +0200 Subject: [PATCH 1/4] wip: initial impl --- CMakeLists.txt | 2 +- Doxyfile | 2 +- MODULE.bazel | 2 +- include/ap/argument.hpp | 2 +- include/ap/argument_parser.hpp | 148 ++++++++++++++++++++++++--------- 5 files changed, 112 insertions(+), 44 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b39ed06b..b786012e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ else() endif() project(cpp-ap - VERSION 3.0.0.4 + VERSION 3.0.0.5 DESCRIPTION "Command-line argument parser for C++20" HOMEPAGE_URL "https://github.com/SpectraL519/cpp-ap" LANGUAGES CXX diff --git a/Doxyfile b/Doxyfile index 0c61af0b..3c2db87b 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = CPP-AP # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 3.0.0.4 +PROJECT_NUMBER = 3.0.0.5 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/MODULE.bazel b/MODULE.bazel index 789c11a5..135ad513 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,4 +1,4 @@ module( name = "cpp-ap", - version = "3.0.0.4", + version = "3.0.0.5", ) diff --git a/include/ap/argument.hpp b/include/ap/argument.hpp index b9b94ee8..81a74533 100644 --- a/include/ap/argument.hpp +++ b/include/ap/argument.hpp @@ -42,7 +42,7 @@ enum class argument_type : bool { positional, optional }; * @attention Some member functions are conditionally enabled/disabled depending on the argument type and value type. * * Example usage: - * @code + * @code{.cpp} * ap::argument_parser parser; * parser.add_positional_argument("input", "i") * .help("An input file path"); diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index bc051550..97c1885b 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -31,7 +31,8 @@ namespace ap { class argument_parser; -enum class default_argument { +// TODO: add doc comment +enum class default_argument : std::uint8_t { p_input, p_output, o_help, @@ -41,6 +42,9 @@ enum class default_argument { o_multi_output }; +// TODO: add doc comment +enum class unknown_policy : std::uint8_t { fail, warn, ignore, as_values }; + namespace detail { void add_default_argument(const default_argument, argument_parser&) noexcept; @@ -49,7 +53,39 @@ void add_default_argument(const default_argument, argument_parser&) noexcept; /** * @brief The main argument parser class. - * This class is responsible for the configuration and parsing of command-line arguments. + * + * This class provides methods to define positional and optional arguments, set parser options, + * and parse the command-line input. + * + * Example usage: + * @code{.cpp} + * #include + * + * int main(int argc, char* argv[]) { + * // Create the argument parser instance + * ap::argument_parser parser; + * parser.program_name("fcopy") + * .program_version({ .major = 1, .minor = 0, .patch = 0 }) + * .program_description("A simple file copy utility.") + * .default_arguments( + * ap::default_argument::o_help, + * ap::default_argument::o_input, + * ap::default_argument::o_output + * ) + * .verbose() + * .unknown_arguments_policy(ap::unknown_policy::ignore) + * .try_parse_args(argc, argv); + * + * // Access parsed argument values + * const std::string input_file = parser.value("input"); + * const std::string output_file = parser.value("output"); + * + * // Application logic here + * std::cout << "Copying from " << input_file << " to " << output_file << std::endl; + * + * return 0; + * } + * @endcode */ class argument_parser { public: @@ -121,6 +157,17 @@ class argument_parser { return *this; } + /** + * @brief Set the unknown argument flags handling policy. + * @param policy The unknown arguments policy value. + * @return Reference to the argument parser. + * @note The default unknown arguments policy value is `ap::unknown_policy::fail`. + */ + argument_parser& unknown_arguments_policy(const unknown_policy policy) noexcept { + this->_unknown_policy = policy; + return *this; + } + /** * @brief Add default arguments to the argument parser. * @tparam AR Type of the positional argument discriminator range. @@ -328,7 +375,7 @@ class argument_parser { this->_validate_argument_configuration(); parsing_state state{.curr_arg = nullptr, .curr_pos_arg_it = this->_positional_args.begin()}; - this->_parse_args_impl(this->_tokenize(argv_rng), state); + this->_parse_args_impl(this->_tokenize(argv_rng, state), state); if (not state.unknown_args.empty()) throw parsing_failure::argument_deduction_failure(state.unknown_args); @@ -374,7 +421,7 @@ class argument_parser { this->parse_args(argv_rng); } catch (const ap::argument_parser_exception& err) { - std::cerr << "[ERROR] : " << err.what() << std::endl << *this << std::endl; + std::cerr << "[ap::error] " << err.what() << std::endl << *this << std::endl; std::exit(EXIT_FAILURE); } } @@ -423,9 +470,9 @@ class argument_parser { parsing_state state{ .curr_arg = nullptr, .curr_pos_arg_it = this->_positional_args.begin(), - .fail_on_unknown = false + .parse_known_only = true }; - this->_parse_args_impl(this->_tokenize(argv_rng), state); + this->_parse_args_impl(this->_tokenize(argv_rng, state), state); if (not this->_are_required_args_bypassed()) { this->_verify_required_args(); @@ -472,7 +519,7 @@ class argument_parser { return this->parse_known_args(argv_rng); } catch (const ap::argument_parser_exception& err) { - std::cerr << "[ERROR] : " << err.what() << std::endl << *this << std::endl; + std::cerr << "[ap::error] " << err.what() << std::endl << *this << std::endl; std::exit(EXIT_FAILURE); } } @@ -651,8 +698,8 @@ class argument_parser { arg_ptr_list_iter_t curr_pos_arg_it; ///< An iterator pointing to the next positional argument to be processed. std::vector unknown_args = {}; ///< A vector of unknown argument values. - const bool fail_on_unknown = - true; ///< A flag indicating whether to end parsing with an error on unknown arguments. + const bool parse_known_only = + false; ///< A flag indicating whether only known arguments should be parsed. }; /** @@ -764,14 +811,15 @@ class argument_parser { * @return A list of preprocessed command-line argument tokens. */ template AR> - [[nodiscard]] arg_token_list_t _tokenize(const AR& arg_range) { + [[nodiscard]] arg_token_list_t _tokenize(const AR& arg_range, const parsing_state& state) { arg_token_list_t toks; if constexpr (std::ranges::sized_range) toks.reserve(std::ranges::size(arg_range)); std::ranges::for_each( - arg_range, std::bind_front(&argument_parser::_tokenize_arg, this, std::ref(toks)) + arg_range, + std::bind_front(&argument_parser::_tokenize_arg, this, std::ref(state), std::ref(toks)) ); return toks; @@ -782,7 +830,9 @@ class argument_parser { * @param toks The argument token list to which the processed token(s) will be appended. * @param arg_value The command-line argument's value to be processed. */ - void _tokenize_arg(arg_token_list_t& toks, const std::string_view arg_value) { + void _tokenize_arg( + const parsing_state& state, arg_token_list_t& toks, const std::string_view arg_value + ) { detail::argument_token tok{ .type = this->_deduce_token_type(arg_value), .value = std::string(arg_value) }; @@ -799,10 +849,26 @@ class argument_parser { return; } -#ifdef AP_UNKNOWN_FLAGS_AS_VALUES - tok.type = detail::argument_token::t_value; -#endif - toks.emplace_back(std::move(tok)); + // unknown flag + if (state.parse_known_only) { + toks.emplace_back(std::move(tok)); + return; + } + + switch (this->_unknown_policy) { + case unknown_policy::fail: + throw parsing_failure::unrecognized_argument(tok.value); + case unknown_policy::warn: + std::cerr << "[ap::warning] Unrecognized argument '" << tok.value + << "' will be ignored." << std::endl; + [[fallthrough]]; + case unknown_policy::ignore: + return; + case unknown_policy::as_values: + tok.type = detail::argument_token::t_value; + toks.emplace_back(std::move(tok)); + break; + } } [[nodiscard]] detail::argument_token::token_type _deduce_token_type( @@ -820,23 +886,6 @@ class argument_parser { return detail::argument_token::t_value; } - /** - * @brief Removes the flag prefix from a flag token's value. - * @param tok The argument token to be processed. - * @return The token's value without the flag prefix. - */ - [[nodiscard]] std::string_view _strip_flag_prefix(const detail::argument_token& tok - ) const noexcept { - switch (tok.type) { - case detail::argument_token::t_flag_primary: - return std::string_view(tok.value).substr(this->_primary_flag_prefix_length); - case detail::argument_token::t_flag_secondary: - return std::string_view(tok.value).substr(this->_secondary_flag_prefix_length); - default: - return tok.value; - } - } - /** * @brief Builds an argument token from the given value. * @param arg_value The command-line argument's value to be processed. @@ -909,6 +958,23 @@ class argument_parser { return compound_toks; } + /** + * @brief Removes the flag prefix from a flag token's value. + * @param tok The argument token to be processed. + * @return The token's value without the flag prefix. + */ + [[nodiscard]] std::string_view _strip_flag_prefix(const detail::argument_token& tok + ) const noexcept { + switch (tok.type) { + case detail::argument_token::t_flag_primary: + return std::string_view(tok.value).substr(this->_primary_flag_prefix_length); + case detail::argument_token::t_flag_secondary: + return std::string_view(tok.value).substr(this->_secondary_flag_prefix_length); + default: + return tok.value; + } + } + /** * @brief Implementation of parsing command-line arguments. * @param arg_tokens The list of command-line argument tokens. @@ -937,14 +1003,15 @@ class argument_parser { [[fallthrough]]; case detail::argument_token::t_flag_secondary: { if (not tok.is_valid_flag_token()) { - if (state.fail_on_unknown) { - throw parsing_failure::unrecognized_argument(tok.value); - } - else { + if (state.parse_known_only) { state.curr_arg.reset(); state.unknown_args.emplace_back(tok.value); break; } + else { + // should never happen as unknown flags are filtered out during tokenization + throw parsing_failure::unrecognized_argument(tok.value); + } } if (tok.arg->mark_used()) @@ -1106,15 +1173,16 @@ class argument_parser { std::optional _program_version; std::optional _program_description; bool _verbose = false; + unknown_policy _unknown_policy = unknown_policy::fail; arg_ptr_list_t _positional_args; arg_ptr_list_t _optional_args; - static constexpr uint8_t _primary_flag_prefix_length = 2u; - static constexpr uint8_t _secondary_flag_prefix_length = 1u; + static constexpr std::uint8_t _primary_flag_prefix_length = 2u; + static constexpr std::uint8_t _secondary_flag_prefix_length = 1u; static constexpr char _flag_prefix_char = '-'; static constexpr std::string_view _flag_prefix = "--"; - static constexpr uint8_t _indent_width = 2; + static constexpr std::uint8_t _indent_width = 2; }; namespace detail { From 1c51e9cc81358c57708e3e36aca212b493dac63c Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 21 Sep 2025 00:03:05 +0200 Subject: [PATCH 2/4] updated docs; added missing doc comments --- docs/tutorial.md | 98 ++++++++++++---- include/ap/action/predefined.hpp | 8 +- include/ap/argument_parser.hpp | 111 ++++++++++++++---- include/ap/exceptions.hpp | 4 +- .../test_argument_parser_parse_args.cpp | 6 +- 5 files changed, 173 insertions(+), 54 deletions(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index 4a498082..8a0f5c45 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -112,6 +112,7 @@ The parameters you can specify for a parser's instance are: - The program's name, version and description - used in the parser's configuration output (`std::cout << parser`). - Verbosity mode - `false` by default; if set to `true` the parser's configuration output will include more detailed info about arguments' parameters in addition to their names and help messages. - [Arguments](#adding-arguments) - specify the values/options accepted by the program. +- [The unknown argument flags handling policy](#4-unknown-argument-flag-handling). ```cpp ap::argument_parser parser; @@ -602,12 +603,12 @@ Command Result ### Actions -- `print_config` | on-flag +- `print_help` | on-flag - Prints the configuration of the parser to the output stream and optionally exits with the given code. + Prints the parser's help message to the output stream and optionally exits with the given code. ```cpp - typename ap::action_type::on_flag::type print_config( + typename ap::action_type::on_flag::type print_help( const ap::argument_parser& parser, const std::optional exit_code = std::nullopt, std::ostream& os = std::cout @@ -707,8 +708,8 @@ parser.default_arguments(); ```cpp // equivalent to: - parser.add_flag("help", "h") - .action(action::print_config(arg_parser, EXIT_SUCCESS)) + parser.add_optional_argument("help", "h") + .action(ap::action::print_help(parser, EXIT_SUCCESS)) .help("Display the help message"); ``` @@ -937,33 +938,82 @@ optional: opt-value
-#### 4. Unrecognized argument flag handling +#### 4. Unknown Argument Flag Handling -By default the `argument_parser` class treats *all\** command-line arguments beggining with a `--` or `-` prefix as optional argument flags and if the flag's value does not match any of the specified arguments, then such flag is considered *unknown* and an exception will be thrown. +A command-line argument beginning with a flag prefix (`--` or `-`) that doesn't match any of the specified optional arguments or a compound of optional arguments (only for short flags) is considered **unknown** or **unrecognized**. -> [*all\**] If a command-line argument begins with a flag prefix, but contains whitespaces (e.g. `"--flag value"`), then it is treated as a value and not a flag. +By default an argument parser will throw an exception if an unkown argument flag is encountered. -This behavior can be altered so that the unknown argument flags will be treated as values, not flags. For example: +This behavior can be modified using the `unknown_arguments_policy` method of the `argument_parser` class, which sets the policy for handling unknown argument flags. -```cpp -parser.add_optional_argument("option", "o"); -parser.try_parse_args(argc, argv); -std::cout << "option: " << parser.value("option"); -/* -./program --option --unknown-flag -option: --unknown-flag -``` +**Example:** -To do this add the following in your `CMakeLists.txt` file: -```cmake -target_compile_definitions(cpp-ap PRIVATE AP_UNKNOWN_FLAGS_AS_VALUES) -``` -or simply add: ```cpp -#define AP_UNKNOWN_FLAGS_AS_VALUES +#include + +int main(int argc, char* argv[]) { + ap::argument_parser parser; + + parser.program_name("test") + .program_description("A simple test program") + .default_arguments(ap::default_argument::o_help) + // set the unknown argument flags handling policy + .unknown_arguments_policy(ap::unknown_policy::); + + parser.add_optional_argument("known", "k") + .help("A known optional argument"); + + parser.try_parse_args(argc, argv); + + std::cout << "known = " << ap::util::join(parser.values("known")) << std::endl; + + return 0; +} ``` -before the `#include ` statement. + +The available policies are: +- `ap::unknown_policy::fail` (default) - throws an exception if an unknown argument flag is encountered: + + ```bash + > ./test --known --unknown + [ap::error] Unknown argument [--unknown]. + Program: test + + A simple test program + + Optional arguments: + + --help, -h : Display the help message + --known, -k : A known optional argument + ``` + +- `ap::unknown_policy::warn` - prints a warning message to the standard error stream and continues parsing the remaining arguments: + + ```bash + > ./test --known --unknown + [ap::warning] Unknown argument '--unknown' will be ignored. + known = + ``` + +- `ap::unknown_policy::ignore` - ignores unknown argument flags and continues parsing the remaining arguments: + + ```shell + ./test --known --unknown + known = + ``` + +- `ap::unknown_policy::as_values` - treats unknown argument flags as values: + + ```shell + > ./test --known --unknown + known = --unknown + ``` + +> [!IMPORTANT] +> +> - The unkown argument flags handling polciy only affects the parser's behaviour when calling the `parse_args` or `try_parse_args` methods. +> - When parsing known args with `parse_known_args` or `try_parse_known_args` all unknown arguments (flags and values) are collected and returned as the parsing result, ignoring the specified policy for handling unknown arguments.

diff --git a/include/ap/action/predefined.hpp b/include/ap/action/predefined.hpp index 8603274b..006808ca 100644 --- a/include/ap/action/predefined.hpp +++ b/include/ap/action/predefined.hpp @@ -20,12 +20,12 @@ std::ostream& operator<<(std::ostream& os, const argument_parser&) noexcept; namespace action { /** - * @brief Returns an *on-flag* action which prints the argument parser's configuration. - * @param parser The argument parser the configuration of which will be printed. + * @brief Returns an *on-flag* action which prints the argument parser's help message. + * @param parser The argument parser the help message of which will be printed. * @param exit_code The exit code with which `std::exit` will be called (if not `std::nullopt`). - * @param os The output stream to which the configuration will be printed. + * @param os The output stream to which the help message will be printed. */ -inline typename ap::action_type::on_flag::type print_config( +inline typename ap::action_type::on_flag::type print_help( const argument_parser& parser, const std::optional exit_code = std::nullopt, std::ostream& os = std::cout diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index 97c1885b..e43e518a 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -31,19 +31,94 @@ namespace ap { class argument_parser; -// TODO: add doc comment +/// @brief The enumeration of default arguments provided by the library. enum class default_argument : std::uint8_t { + /** + * @brief A positional argument representing a single input file path. + * Equivalent to: + * @code{.cpp} + * parser.add_positional_argument("input") + * .action(ap::action::check_file_exists()) + * .help("Input file path"); + * @endcode + */ p_input, + + /** + * @brief A positional argument representing a single output file path. + * Equivalent to: + * @code{.cpp} + * parser.add_positional_argument("output") + * .help("Output file path"); + * @endcode + */ p_output, + + /** + * @brief An optional argument representing the program's help flag. + * Equivalent to: + * @code{.cpp} + * parser.add_optional_argument("help") + * .action(ap::action::print_help(parser, EXIT_SUCCESS)) + * .help("Display the help message"); + * @endcode + */ o_help, + + /** + * @brief A positional argument representing multiple input file paths. + * Equivalent to: + * @code{.cpp} + * parser.add_positional_argument("input", "i") + * .nargs(1ull) + * .action(ap::action::check_file_exists()) + * .help("Input file path"); + * @endcode + */ o_input, + + /** + * @brief A positional argument representing multiple output file paths. + * Equivalent to: + * @code{.cpp} + * parser.add_positional_argument("output", "o") + * .nargs(1ull) + * .help("Output file path"); + * @endcode + */ o_output, + + /** + * @brief A positional argument representing multiple input file paths. + * Equivalent to: + * @code{.cpp} + * parser.add_positional_argument("input", "i") + * .nargs(ap::nargs::at_least(1ull)) + * .action(ap::action::check_file_exists()) + * .help("Input file path"); + * @endcode + */ o_multi_input, + + /** + * @brief A positional argument representing multiple output file paths. + * Equivalent to: + * @code{.cpp} + * parser.add_positional_argument("output", "o") + * .nargs(ap::nargs::at_least(1ull)) + * .help("Output file path"); + * @endcode + */ o_multi_output }; -// TODO: add doc comment -enum class unknown_policy : std::uint8_t { fail, warn, ignore, as_values }; +/// @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 values. +}; namespace detail { @@ -532,7 +607,7 @@ class argument_parser { * 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")]] + [[deprecated("The default help argument now uses the `print_help` on-flag action")]] void handle_help_action() const noexcept { if (this->value("help")) { std::cout << *this << std::endl; @@ -635,11 +710,11 @@ class argument_parser { } /** - * @brief Prints the argument parser's details to an output stream. + * @brief Prints the argument parser's help message to an output stream. * @param verbose The verbosity mode value. * @param os Output stream. */ - void print_config(const bool verbose, std::ostream& os = std::cout) const noexcept { + void print_help(const bool verbose, std::ostream& os = std::cout) const noexcept { if (this->_program_name) { os << "Program: " << this->_program_name.value(); if (this->_program_version) @@ -666,7 +741,7 @@ class argument_parser { /** * @brief Prints the argument parser's details to an output stream. * - * An `os << parser` operation is equivalent to a `parser.print_config(_verbose, os)` call, + * An `os << parser` operation is equivalent to a `parser.print_help(_verbose, os)` call, * where `_verbose` is the inner verbosity mode, which can be set with the @ref verbose function. * * @param os Output stream. @@ -674,7 +749,7 @@ class argument_parser { * @return The modified output stream. */ friend std::ostream& operator<<(std::ostream& os, const argument_parser& parser) noexcept { - parser.print_config(parser._verbose, os); + parser.print_help(parser._verbose, os); return os; } @@ -857,10 +932,10 @@ class argument_parser { switch (this->_unknown_policy) { case unknown_policy::fail: - throw parsing_failure::unrecognized_argument(tok.value); + throw parsing_failure::unknwon_argument(tok.value); case unknown_policy::warn: - std::cerr << "[ap::warning] Unrecognized argument '" << tok.value - << "' will be ignored." << std::endl; + std::cerr << "[ap::warning] Unknown argument '" << tok.value << "' will be ignored." + << std::endl; [[fallthrough]]; case unknown_policy::ignore: return; @@ -1010,7 +1085,7 @@ class argument_parser { } else { // should never happen as unknown flags are filtered out during tokenization - throw parsing_failure::unrecognized_argument(tok.value); + throw parsing_failure::unknwon_argument(tok.value); } } @@ -1207,29 +1282,24 @@ inline void add_default_argument( break; case default_argument::o_help: - arg_parser.add_flag("help", "h") - .action(action::print_config(arg_parser, EXIT_SUCCESS)) + arg_parser.add_optional_argument("help", "h") + .action(action::print_help(arg_parser, EXIT_SUCCESS)) .help("Display the help message"); break; case default_argument::o_input: arg_parser.add_optional_argument("input", "i") - .required() .nargs(1ull) .action(action::check_file_exists()) .help("Input file path"); break; case default_argument::o_output: - arg_parser.add_optional_argument("output", "o") - .required() - .nargs(1ull) - .help("Output file path"); + arg_parser.add_optional_argument("output", "o").nargs(1ull).help("Output file path"); break; case default_argument::o_multi_input: arg_parser.add_optional_argument("input", "i") - .required() .nargs(ap::nargs::at_least(1ull)) .action(action::check_file_exists()) .help("Input files paths"); @@ -1237,7 +1307,6 @@ inline void add_default_argument( case default_argument::o_multi_output: arg_parser.add_optional_argument("output", "o") - .required() .nargs(ap::nargs::at_least(1ull)) .help("Output files paths"); break; diff --git a/include/ap/exceptions.hpp b/include/ap/exceptions.hpp index 7eba079c..89a721c7 100644 --- a/include/ap/exceptions.hpp +++ b/include/ap/exceptions.hpp @@ -57,8 +57,8 @@ struct invalid_configuration : public argument_parser_exception { struct parsing_failure : public argument_parser_exception { explicit parsing_failure(const std::string& message) : argument_parser_exception(message) {} - static parsing_failure unrecognized_argument(const std::string_view arg_name) noexcept { - return parsing_failure(std::format("Unrecognized argument [{}].", arg_name)); + static parsing_failure unknwon_argument(const std::string_view arg_name) noexcept { + return parsing_failure(std::format("Unknown argument [{}].", arg_name)); } static parsing_failure value_already_set(const detail::argument_name& arg_name) noexcept { diff --git a/tests/source/test_argument_parser_parse_args.cpp b/tests/source/test_argument_parser_parse_args.cpp index 4fa71624..2390a3ef 100644 --- a/tests/source/test_argument_parser_parse_args.cpp +++ b/tests/source/test_argument_parser_parse_args.cpp @@ -281,7 +281,7 @@ TEST_CASE_FIXTURE( CHECK_THROWS_WITH_AS( sut.parse_args(argc, argv), - parsing_failure::unrecognized_argument(unknown_arg_name).what(), + parsing_failure::unknwon_argument(unknown_arg_name).what(), parsing_failure ); @@ -322,7 +322,7 @@ TEST_CASE_FIXTURE( CHECK_THROWS_WITH_AS( sut.parse_args(argc, argv), - parsing_failure::unrecognized_argument(invalid_flag).what(), + parsing_failure::unknwon_argument(invalid_flag).what(), parsing_failure ); @@ -1118,7 +1118,7 @@ TEST_CASE_FIXTURE( // parse args CHECK_THROWS_WITH_AS( sut.parse_args(argc, argv), - parsing_failure::unrecognized_argument(invalid_flag).what(), + parsing_failure::unknwon_argument(invalid_flag).what(), parsing_failure ); From 4b73ca1ceca607d1609de16aaa6c4814523203f3 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 21 Sep 2025 10:39:09 +0200 Subject: [PATCH 3/4] aligned tests --- docs/tutorial.md | 13 ++ include/ap/argument_parser.hpp | 4 +- include/ap/exceptions.hpp | 2 +- tests/CMakeLists.txt | 6 +- .../include/argument_parser_test_fixture.hpp | 2 +- .../test_argument_parser_add_argument.cpp | 2 +- .../test_argument_parser_parse_args.cpp | 132 +++++++++++++++++- 7 files changed, 149 insertions(+), 12 deletions(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index 8a0f5c45..2c882b7f 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1014,6 +1014,19 @@ The available policies are: > > - The unkown argument flags handling polciy only affects the parser's behaviour when calling the `parse_args` or `try_parse_args` methods. > - When parsing known args with `parse_known_args` or `try_parse_known_args` all unknown arguments (flags and values) are collected and returned as the parsing result, ignoring the specified policy for handling unknown arguments. +> +> Consider a similar example as above with only the argument parsing function changed: +> ```cpp +> const auto unknown_args = parser.try_parse_known_args(argc, argv); +> std::cout << "known = " << ap::util::join(parser.values("known")) << std::endl +> << "unknown = " << ap::util::join(unknown_args) << std::endl; +> ``` +> This would produce the following output regardless of the specified unknown arguments policy. +> ```shell +> > ./test --known --unknown +> known = +> unknown = --unknown +> ```

diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index e43e518a..d7fe8101 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -932,7 +932,7 @@ class argument_parser { switch (this->_unknown_policy) { case unknown_policy::fail: - throw parsing_failure::unknwon_argument(tok.value); + throw parsing_failure::unknown_argument(tok.value); case unknown_policy::warn: std::cerr << "[ap::warning] Unknown argument '" << tok.value << "' will be ignored." << std::endl; @@ -1085,7 +1085,7 @@ class argument_parser { } else { // should never happen as unknown flags are filtered out during tokenization - throw parsing_failure::unknwon_argument(tok.value); + throw parsing_failure::unknown_argument(tok.value); } } diff --git a/include/ap/exceptions.hpp b/include/ap/exceptions.hpp index 89a721c7..bcf0c516 100644 --- a/include/ap/exceptions.hpp +++ b/include/ap/exceptions.hpp @@ -57,7 +57,7 @@ struct invalid_configuration : public argument_parser_exception { struct parsing_failure : public argument_parser_exception { explicit parsing_failure(const std::string& message) : argument_parser_exception(message) {} - static parsing_failure unknwon_argument(const std::string_view arg_name) noexcept { + static parsing_failure unknown_argument(const std::string_view arg_name) noexcept { return parsing_failure(std::format("Unknown argument [{}].", arg_name)); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6aae4a83..73a78928 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -69,9 +69,9 @@ add_doctest("source/test_argument_token.cpp") add_doctest("source/test_argument_parser_add_argument.cpp") add_doctest("source/test_argument_parser_info.cpp") add_doctest("source/test_argument_parser_parse_args.cpp") -add_doctest("source/test_argument_parser_parse_args_unknown_flags_as_values.cpp" - COMPILE_DEFINITIONS "AP_UNKNOWN_FLAGS_AS_VALUES" -) +# add_doctest("source/test_argument_parser_parse_args_unknown_flags_as_values.cpp" +# COMPILE_DEFINITIONS "AP_UNKNOWN_FLAGS_AS_VALUES" +# ) add_doctest("source/test_nargs_range.cpp") add_doctest("source/test_none_type_argument.cpp") add_doctest("source/test_optional_argument.cpp") diff --git a/tests/include/argument_parser_test_fixture.hpp b/tests/include/argument_parser_test_fixture.hpp index d56b6c7b..7657cce5 100644 --- a/tests/include/argument_parser_test_fixture.hpp +++ b/tests/include/argument_parser_test_fixture.hpp @@ -179,7 +179,7 @@ struct argument_parser_test_fixture { // private function callers [[nodiscard]] arg_token_list_t tokenize(int argc, char* argv[]) { - return this->sut._tokenize(std::span(argv + 1, static_cast(argc - 1))); + return this->sut._tokenize(std::span(argv + 1, static_cast(argc - 1)), state); } void parse_args_impl(const arg_token_list_t& arg_tokens) { diff --git a/tests/source/test_argument_parser_add_argument.cpp b/tests/source/test_argument_parser_add_argument.cpp index bc17bb4c..23901bf4 100644 --- a/tests/source/test_argument_parser_add_argument.cpp +++ b/tests/source/test_argument_parser_add_argument.cpp @@ -329,7 +329,7 @@ TEST_CASE_FIXTURE( const auto help_arg = get_argument(help_flag); REQUIRE(help_arg); - CHECK(is_optional(*help_arg)); + CHECK(is_optional(*help_arg)); const auto input_arg = get_argument(input_flag); REQUIRE(input_arg); diff --git a/tests/source/test_argument_parser_parse_args.cpp b/tests/source/test_argument_parser_parse_args.cpp index 2390a3ef..9a66bf5c 100644 --- a/tests/source/test_argument_parser_parse_args.cpp +++ b/tests/source/test_argument_parser_parse_args.cpp @@ -6,6 +6,7 @@ using namespace ap_testing; using namespace ap::nargs; using ap::invalid_configuration; using ap::parsing_failure; +using ap::unknown_policy; struct test_argument_parser_parse_args : public argument_parser_test_fixture { const std::string_view test_program_name = "test program name"; @@ -277,11 +278,11 @@ TEST_CASE_FIXTURE( auto argc = get_argc(no_args, n_opt_clargs); auto argv = init_argv(no_args, n_opt_clargs); - const auto unknown_arg_name = init_arg_flag_primary(opt_arg_idx); + const auto unknown_arg_flag = init_arg_flag_primary(opt_arg_idx); CHECK_THROWS_WITH_AS( sut.parse_args(argc, argv), - parsing_failure::unknwon_argument(unknown_arg_name).what(), + parsing_failure::unknown_argument(unknown_arg_flag).what(), parsing_failure ); @@ -322,7 +323,7 @@ TEST_CASE_FIXTURE( CHECK_THROWS_WITH_AS( sut.parse_args(argc, argv), - parsing_failure::unknwon_argument(invalid_flag).what(), + parsing_failure::unknown_argument(invalid_flag).what(), parsing_failure ); @@ -1118,7 +1119,7 @@ TEST_CASE_FIXTURE( // parse args CHECK_THROWS_WITH_AS( sut.parse_args(argc, argv), - parsing_failure::unknwon_argument(invalid_flag).what(), + parsing_failure::unknown_argument(invalid_flag).what(), parsing_failure ); @@ -1174,3 +1175,126 @@ TEST_CASE_FIXTURE( // cleanup free_argv(argc, argv); } + +// unknown_arguments_policy + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should throw when an unrecognized argument flag is used with the default unknown " + "arguments handling policy (fail)" +) { + add_arguments(no_args, no_args); + + constexpr std::size_t n_opt_clargs = 1ull; + constexpr std::size_t opt_arg_idx = 0ull; + + auto argc = get_argc(no_args, n_opt_clargs); + auto argv = init_argv(no_args, n_opt_clargs); + + const auto unknown_arg_name = init_arg_flag_primary(opt_arg_idx); + + CHECK_THROWS_WITH_AS( + sut.parse_args(argc, argv), + parsing_failure::unknown_argument(unknown_arg_name).what(), + parsing_failure + ); + + free_argv(argc, argv); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should throw when an unrecognized argument flag is used with the default unknown " + "arguments handling policy (fail)" +) { + const auto unknown_arg_flag = "--unknown"; + const std::vector argv_vec{"program", unknown_arg_flag}; + + const auto argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + CHECK_THROWS_WITH_AS( + sut.parse_args(argc, argv), + parsing_failure::unknown_argument(unknown_arg_flag).what(), + parsing_failure + ); + + free_argv(argc, argv); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should print a warning to std::cerr when an unrecognized argument flag is used " + "with the warn unknown arguments handling policy" +) { + sut.unknown_arguments_policy(unknown_policy::warn); + + const auto unknown_arg_flag = "--unknown"; + const std::vector argv_vec{"program", unknown_arg_flag}; + + const auto argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + // redirect std::cerr + std::stringstream tmp_buffer; + auto* cerr_buffer = std::cerr.rdbuf(tmp_buffer.rdbuf()); + + REQUIRE_NOTHROW(sut.parse_args(argc, argv)); + + CHECK_EQ( + tmp_buffer.str(), + std::format("[ap::warning] Unknown argument '{}' will be ignored.\n", unknown_arg_flag) + ); + + free_argv(argc, argv); + + // reset std::cerr + std::cerr.rdbuf(cerr_buffer); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should do nothing when an unrecognized argument flag is used with the ignore " + "unknown arguments handling policy" +) { + sut.unknown_arguments_policy(unknown_policy::ignore); + + const auto unknown_arg_flag = "--unknown"; + const std::vector argv_vec{"program", unknown_arg_flag}; + + const auto argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + // redirect std::cerr + std::stringstream tmp_buffer; + auto* cerr_buffer = std::cerr.rdbuf(tmp_buffer.rdbuf()); + + REQUIRE_NOTHROW(sut.parse_args(argc, argv)); + + CHECK_EQ(tmp_buffer.str(), ""); + + free_argv(argc, argv); + + // reset std::cerr + std::cerr.rdbuf(cerr_buffer); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should treat an unrecognized argument flag as a value with the as_values unknown " + "arguments handling policy" +) { + sut.unknown_arguments_policy(unknown_policy::as_values); + sut.add_positional_argument("known"); + + const auto unknown_arg_flag = "--unknown"; + const std::vector argv_vec{"program", unknown_arg_flag}; + + const auto argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + CHECK_NOTHROW(sut.parse_args(argc, argv)); + CHECK_EQ(sut.value("known"), unknown_arg_flag); + + free_argv(argc, argv); +} From f27d50112c84b6b9e3f84b21d509ea04fbad018f Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 21 Sep 2025 11:17:52 +0200 Subject: [PATCH 4/4] resolved comments --- docs/tutorial.md | 2 - include/ap/argument_parser.hpp | 4 +- tests/CMakeLists.txt | 3 - ...ser_parse_args_unknown_flags_as_values.cpp | 55 ------------------- 4 files changed, 2 insertions(+), 62 deletions(-) delete mode 100644 tests/source/test_argument_parser_parse_args_unknown_flags_as_values.cpp diff --git a/docs/tutorial.md b/docs/tutorial.md index 2c882b7f..428c7d15 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1157,8 +1157,6 @@ Now all the values, that caused an exception for the `parse_args` example, are c > ``` > > Here `value` is treated either as the `positional` argument's value or as an unknown argument (depending on the input arguments) even though the `recognized` optional argument still accepts values and only after the `--recognized` argument flag is encountered the parser continues collecting values for this argument. -> -> **NOTE:** If the `AP_UNKNOWN_FLAGS_AS_VALUES` is set, the unrecognized argument flags will be treated as values during parsing and therefore they **may** not be collected as unknown arguments, depending on the argument's configuration and the command-line argument list. > [!TIP] > diff --git a/include/ap/argument_parser.hpp b/include/ap/argument_parser.hpp index d7fe8101..9b8455de 100644 --- a/include/ap/argument_parser.hpp +++ b/include/ap/argument_parser.hpp @@ -117,7 +117,7 @@ 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 values. + as_values ///< Treat unknown arguments as positional values. }; namespace detail { @@ -609,7 +609,7 @@ class argument_parser { */ [[deprecated("The default help argument now uses the `print_help` on-flag action")]] void handle_help_action() const noexcept { - if (this->value("help")) { + if (this->count("help")) { std::cout << *this << std::endl; std::exit(EXIT_SUCCESS); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 73a78928..4c9067d3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -69,9 +69,6 @@ add_doctest("source/test_argument_token.cpp") add_doctest("source/test_argument_parser_add_argument.cpp") add_doctest("source/test_argument_parser_info.cpp") add_doctest("source/test_argument_parser_parse_args.cpp") -# add_doctest("source/test_argument_parser_parse_args_unknown_flags_as_values.cpp" -# COMPILE_DEFINITIONS "AP_UNKNOWN_FLAGS_AS_VALUES" -# ) add_doctest("source/test_nargs_range.cpp") add_doctest("source/test_none_type_argument.cpp") add_doctest("source/test_optional_argument.cpp") diff --git a/tests/source/test_argument_parser_parse_args_unknown_flags_as_values.cpp b/tests/source/test_argument_parser_parse_args_unknown_flags_as_values.cpp deleted file mode 100644 index 851848b4..00000000 --- a/tests/source/test_argument_parser_parse_args_unknown_flags_as_values.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include "argument_parser_test_fixture.hpp" -#include "doctest.h" -#include "utility.hpp" - -using namespace ap_testing; -using ap::parsing_failure; - -struct test_argument_parser_parse_args_unknown_flags_as_values -: public argument_parser_test_fixture { - const std::string test_program_name = "test program name"; - const std::string unknown_arg_flag = "--unknown-arg"; - - const std::size_t no_args = 0ull; -}; - -TEST_CASE_FIXTURE( - test_argument_parser_parse_args_unknown_flags_as_values, - "parse_args should treat an unknown argument flag as a positional value if it's not preceeded " - "by any valid argument flags" -) { - const std::vector args{test_program_name, unknown_arg_flag}; - - const auto argc = static_cast(args.size()); - auto argv = to_char_2d_array(args); - - CHECK_THROWS_WITH_AS( - sut.parse_args(argc, argv), - parsing_failure::argument_deduction_failure({unknown_arg_flag}).what(), - parsing_failure - ); - - free_argv(argc, argv); -} - -TEST_CASE_FIXTURE( - test_argument_parser_parse_args_unknown_flags_as_values, - "parse_args should treat an unknown argument flag as an optional argument's value if it's " - "proceeded by an optional argument's flag" -) { - const std::string opt_arg_name = "known-opt-arg"; - sut.add_optional_argument(opt_arg_name); - - const std::vector args{ - test_program_name, std::format("--{}", opt_arg_name), unknown_arg_flag - }; - - const auto argc = static_cast(args.size()); - auto argv = to_char_2d_array(args); - - CHECK_NOTHROW(sut.parse_args(argc, argv)); - - CHECK_EQ(sut.value(opt_arg_name), unknown_arg_flag); - - free_argv(argc, argv); -}