diff --git a/README.md b/README.md index eedde6d3..d2c2a55e 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ - [Bazel Build System](/docs/tutorial.md#bazel-build-system) - [Downloading the Library](/docs/tutorial.md#downloading-the-library) - [The Parser Class](/docs/tutorial.md#the-parser-class) + - [Dynamic Program Name](/docs/tutorial.md#dynamic-program-name) - [Adding Arguments](/docs/tutorial.md#adding-arguments) - [Syntax](/docs/tutorial.md#syntax) - [Names](/docs/tutorial.md#names) @@ -69,6 +70,8 @@ - [Argument Parameters](/docs/tutorial.md#argument-parameters) - [Common Parameters](/docs/tutorial.md#common-parameters) - [Parameters Specific for Optional Arguments](/docs/tutorial.md#parameters-specific-for-optional-arguments) + - [Predefined Parameter Values](/docs/tutorial.md#predefined-parameter-values) + - [Actions](/docs/tutorial.md#actions) - [Default Arguments](/docs/tutorial.md#default-arguments) - [Argument Groups](/docs/tutorial.md#argument-groups) - [Creating New Groups](/docs/tutorial.md#creating-new-groups) @@ -81,6 +84,9 @@ - [Compound Arguments](/docs/tutorial.md#compound-arguments) - [Parsing Known Arguments](/docs/tutorial.md#parsing-known-arguments) - [Retrieving Argument Values](/docs/tutorial.md#retrieving-argument-values) + - [Using the Parser](/docs/tutorial.md#using-the-parser) + - [Using Argument References](/docs/tutorial.md#using-argument-references) + - [Using Argument Groups](/docs/tutorial.md#using-argument-groups) - [Subparsers](/docs/tutorial.md#subparsers) - [Creating Subparsers](/docs/tutorial.md#creating-subparsers) - [Using Multiple Subparsers](/docs/tutorial.md#using-multiple-subparsers) diff --git a/docs/tutorial.md b/docs/tutorial.md index 3869f4d7..54893d77 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -5,6 +5,7 @@ - [Bazel Build System](#bazel-build-system) - [Downloading the Library](#downloading-the-library) - [The Parser Class](#the-parser-class) + - [Dynamic Program Name](#dynamic-program-name) - [Adding Arguments](#adding-arguments) - [Syntax](#syntax) - [Names](#names) @@ -155,22 +156,17 @@ parser.program_version("alpha") ``` > [!IMPORTANT] -> -> - When creating an argument parser instance, you must provide a program name to the constructor. -> -> The program name given to the parser cannot be empty and must not contain whitespace characters. -> -> - Additional parameters you can specify for a parser instance include: -> - The program's version and description - used in the parser's configuration output (`std::cout << parser`). -> - Verbosity mode - `false` by default; if set to `true` the parser's configuration output will include more detailed info about arguments' parameters in addition to their names and help messages. -> - [Arguments](#adding-arguments) - specify the values/options accepted by the program. -> - [Argument Groups](#argument-groups) - organize related optional arguments into sections and optionally enforce usage rules. -> - [The unknown argument flags handling policy](#4-unknown-argument-flag-handling). +> * When creating an argument parser instance, you must provide either a strict program name string or the [`argon::dynamic_name` tag](#dynamic-program-name) to the constructor. If a strict program name is provided, it cannot be empty and must not contain whitespace characters. +> * Additional parameters you can specify for a parser instance include: +> * The program's version and description - used in the parser's configuration output (`std::cout << parser`). +> * Verbosity mode - `false` by default; if set to `true` the parser's configuration output will include more detailed info about arguments' parameters in addition to their names and help messages. +> * [Arguments](#arguments) - specify the values/options accepted by the program. +> * [The unknown argument flags handling policy](#4-unknown-argument-flag-handling). +> * [Argument Groups](#argument-groups) - organize related optional arguments into sections and optionally enforce usage rules. +> * [Subparsers](#subparsers) - create hierarchical CLI subcommand structures. > [!TIP] -> > You can specify the program version using a string (like in the example above) or using the `argon::version` structure: -> > ```cpp > parser.program_version({0u, 0u, 0u}) > parser.program_version({ .major = 1u, .minor = 1u, .patch = 1u }); @@ -179,9 +175,25 @@ parser.program_version("alpha") > ``` > > **NOTE:** The `argon::version` struct -> - contains the three members - `major`, `minor`, `patch` - all of which are of type `std::uint32_t`, -> - defines a `std::string str() const` method which returns a `v{major}.{minor}.{path}` version string, -> - defines the `std::ostream& operator<<` for stream insertion. +> * contains the three members - `major`, `minor`, `patch` - all of which are of type `std::uint32_t`, +> * defines a `std::string str() const` method which returns a `v{major}.{minor}.{path}` version string, +> * defines the `std::ostream& operator<<` for stream insertion. + +### Dynamic Program Name + +If you prefer not to hardcode the program name, you can instruct the parser to dynamically deduce it from the command-line input (specifically `argv[0]`) by initializing it with the `argon::dynamic_name` tag: + +```cpp +argon::argument_parser parser(argon::dynamic_name); + +// The program name is automatically resolved from argv[0] when parsing begins +parser.try_parse_args(argc, argv); +``` + +When the parser processes `argc` and `argv`, it extracts the executable's base name, strips any preceding directory paths (e.g., `/usr/bin/my_app` or `C:\path\to\my_app.exe` cleanly becomes `my_app` / `my_app.exe`), and automatically propagates this resolved name down to all of its [subparsers](#subparsers). + +> [!WARNING] +> If you configure a parser with `argon::dynamic_name`, you **must** use the `argc, argv` parsing overloads (like `parse_args(argc, argv)`). Attempting to use the range-based parsing functions (e.g., passing a `std::span` or `std::vector`) before the parser has resolved its name will result in a `std::logic_error`.

diff --git a/include/argon/argument_parser.hpp b/include/argon/argument_parser.hpp index 27e773d4..29575d60 100644 --- a/include/argon/argument_parser.hpp +++ b/include/argon/argument_parser.hpp @@ -183,7 +183,12 @@ class argument_parser { argument_parser(argument_parser&&) = delete; argument_parser& operator=(argument_parser&&) = delete; - argument_parser(const std::string_view name) : argument_parser(name, "") {} + /// Constructs a parser with the given name + explicit argument_parser(const std::string_view name) : argument_parser(name, "", false) {} + + /// @brief Constructs a parser with the name dynamically deduced from argv[0] + /// @param tag The argon::dynamic_name tag. + explicit argument_parser(dynamic_name_t) : argument_parser("", "", true) {} ~argument_parser() = default; @@ -634,7 +639,7 @@ class argument_parser { )); return *this->_subparsers.emplace_back( - std::unique_ptr(new argument_parser(name, this->_program_name)) + std::unique_ptr(new argument_parser(name, this->_program_name, false)) ); } @@ -671,7 +676,7 @@ class argument_parser { /** * @brief Parses the command-line arguments. * - * Equivalent to: + * Roughly equivalent to: * ```cpp * parse_args(std::span(argv + 1, static_cast(argc - 1))) * ``` @@ -682,6 +687,9 @@ class argument_parser { * @attention The first argument (the program name) is ignored. */ void parse_args(int argc, char* argv[]) { + if (argc > 0) + this->_resolve_name(argv[0]); + this->parse_args(std::span(argv + 1, static_cast(argc - 1))); } @@ -695,6 +703,10 @@ class argument_parser { */ template ArgvRange> void parse_args(const ArgvRange& argv_rng) { + if (not this->_is_name_resolved) + throw std::logic_error("Dynamic program name must be resolved before calling parsing " + "the program's arguments"); + parsing_state state(*this); this->_parse_args_impl(std::ranges::begin(argv_rng), std::ranges::end(argv_rng), state); @@ -707,7 +719,7 @@ class argument_parser { /** * @brief Parses the command-line arguments and exits on error. * - * Equivalent to: + * Roughly equivalent to: * ```cpp * try_parse_args(std::span(argv + 1, static_cast(argc - 1))) * ``` @@ -717,6 +729,9 @@ class argument_parser { * @note The first argument (the program name) is ignored. */ void try_parse_args(int argc, char* argv[]) { + if (argc > 0) + this->_resolve_name(argv[0]); + this->try_parse_args(std::span(argv + 1, static_cast(argc - 1))); } @@ -747,7 +762,7 @@ class argument_parser { /** * @brief Parses the known command-line arguments. * - * Equivalent to: + * Roughly equivalent to: * ```cpp * try_parse_known_args(std::span(argv + 1, static_cast(argc - 1))) * ``` @@ -763,6 +778,9 @@ class argument_parser { * @attention The first argument (the program name) is ignored. */ std::vector parse_known_args(int argc, char* argv[]) { + if (argc > 0) + this->_resolve_name(argv[0]); + return this->parse_known_args(std::span(argv + 1, static_cast(argc - 1))); } @@ -782,6 +800,10 @@ class argument_parser { */ template ArgvRange> std::vector parse_known_args(const ArgvRange& argv_rng) { + if (not this->_is_name_resolved) + throw std::logic_error("Dynamic program name must be resolved before calling parsing " + "the program's arguments"); + parsing_state state(*this, true); this->_parse_args_impl(std::ranges::begin(argv_rng), std::ranges::end(argv_rng), state); return std::move(state.unknown_args); @@ -790,7 +812,7 @@ class argument_parser { /** * @brief Parses the known command-line arguments and exits on error. * - * Equivalent to: + * Roughly equivalent to: * ```cpp * try_parse_known_args(std::span(argv + 1, static_cast(argc - 1))) * ``` @@ -801,6 +823,9 @@ class argument_parser { * @attention The first argument (the program name) is ignored. */ std::vector try_parse_known_args(int argc, char* argv[]) { + if (argc > 0) + this->_resolve_name(argv[0]); + return this->try_parse_known_args(std::span(argv + 1, static_cast(argc - 1))); } @@ -1005,7 +1030,6 @@ class argument_parser { } #ifdef AP_TESTING - /// @brief Friend struct for testing purposes. friend struct ::argon_testing::argument_parser_test_fixture; #endif @@ -1023,35 +1047,38 @@ class argument_parser { using arg_token_vec_t = std::vector; using arg_token_vec_iter_t = typename arg_token_vec_t::const_iterator; - /// @brief A collection of values used during the parsing process. struct parsing_state { parsing_state(argument_parser& parser, const bool parse_known_only = false) : curr_arg(nullptr), curr_pos_arg_it(parser._positional_args.begin()), parse_known_only(parse_known_only) {} - /// @brief Update the parser-specific parameters of the state object. - /// @param parser The new parser. void set_parser(argument_parser& parser) { this->curr_arg = nullptr; this->curr_pos_arg_it = parser._positional_args.begin(); } - arg_ptr_t curr_arg; ///< The currently processed argument. - arg_ptr_vec_iter_t - curr_pos_arg_it; ///< An iterator pointing to the next positional argument to be processed. - const bool - parse_known_only; ///< A flag indicating whether only known arguments should be parsed. - std::vector unknown_args = {}; ///< A vector of unknown argument values. + arg_ptr_t curr_arg; + arg_ptr_vec_iter_t curr_pos_arg_it; + const bool parse_known_only; + std::vector unknown_args = {}; }; - argument_parser(const std::string_view name, const std::string_view parent_name) + argument_parser( + const std::string_view name, const std::string_view parent_name, const bool dynamic + ) : _name(name), _program_name( - std::format("{}{}{}", parent_name, std::string(not parent_name.empty(), ' '), name) + dynamic + ? "" + : std::format("{}{}{}", parent_name, std::string(not parent_name.empty(), ' '), name) ), _gr_positional_args(add_group("Positional Arguments")), - _gr_optional_args(add_group("Optional Arguments")) { + _gr_optional_args(add_group("Optional Arguments")), + _is_name_resolved(not dynamic) { + if (not this->_is_name_resolved) + return; + if (name.empty()) throw invalid_configuration("The program name cannot be empty!"); @@ -1059,10 +1086,29 @@ class argument_parser { throw invalid_configuration("The program name cannot contain whitespace characters!"); } - /** - * @brief Verifies the pattern of an argument name and if it's invalid, an error is thrown - * @throws argon::invalid_configuration - */ + void _resolve_name(std::string_view path) { + if (this->_is_name_resolved) + return; + + const auto pos = path.find_last_of("/\\"); + this->_name = path.substr(pos == std::string_view::npos ? 0 : pos + 1); + + if (this->_name.empty()) + this->_name = "unknown"; + + this->_program_name = this->_name; + this->_is_name_resolved = true; + + for (auto& sub : this->_subparsers) + sub->_update_program_name(this->_program_name); + } + + void _update_program_name(std::string_view parent_name) { + this->_program_name = std::format("{} {}", parent_name, this->_name); + for (auto& sub : this->_subparsers) + sub->_update_program_name(this->_program_name); + } + void _verify_arg_name_pattern(const std::string_view arg_name) const { if (arg_name.empty()) throw invalid_configuration::invalid_argument_name( @@ -1089,12 +1135,6 @@ class argument_parser { ); } - /** - * @brief Returns a unary predicate function which checks if the given name matches the argument's name - * @param arg_name The name of the argument. - * @param m_type The match type used within the predicate. - * @return Argument predicate based on the provided name. - */ [[nodiscard]] auto _name_match_predicate( const std::string_view arg_name, const argument_name::match_type m_type = argument_name::m_any @@ -1102,20 +1142,10 @@ class argument_parser { return [=](const arg_ptr_t& arg) { return arg->name().match(arg_name, m_type); }; } - /** - * @brief Returns a unary predicate function which checks if the given name matches the argument's name - * @param arg_name The name of the argument. - * @return Argument predicate based on the provided name. - */ [[nodiscard]] auto _name_match_predicate(const argument_name& arg_name) const noexcept { return [&arg_name](const arg_ptr_t& arg) { return arg->name().match(arg_name); }; } - /** - * @brief Check if an argument name is already used. - * @param arg_name The name of the argument. - * @return True if the argument name is already used, false otherwise. - */ [[nodiscard]] bool _is_arg_name_used(const argument_name& arg_name) const noexcept { const auto predicate = this->_name_match_predicate(arg_name); @@ -1128,11 +1158,6 @@ class argument_parser { return false; } - /** - * @brief Check if the given group belongs to the parser. - * @param group The group to validate. - * @throws std::logic_error if the group doesn't belong to the parser. - */ void _validate_group(const argument_group& group) { if (group._parser != this) throw std::logic_error(std::format( @@ -1140,15 +1165,6 @@ class argument_parser { )); } - /** - * @brief Implementation of parsing command-line arguments. - * @tparam AIt The command-line argument value iterator type. - * @note `AIt` must be a `std::forward_iterator` with a value type convertible to `std::string`. - * @param args_begin The begin iterator for the command-line argument value range. - * @param args_end The end iterator for the command-line argument value range. - * @param state The current parsing state. - * @throws argon::invalid_configuration, argon::parsing_failure - */ template AIt> void _parse_args_impl(AIt args_begin, const AIt args_end, parsing_state& state) { this->_invoked = true; @@ -1175,12 +1191,6 @@ class argument_parser { this->_finalized = true; } - /** - * @brief Validate whether the definition/configuration of the parser's arguments is correct. - * - * What is verified: - * 1. No required positional argument can be added after a non-required positional argument. - */ void _validate_argument_configuration() const { // step 1 arg_ptr_t non_required_arg = nullptr; @@ -1200,15 +1210,6 @@ class argument_parser { } } - /** - * @brief Converts the command-line arguments into a list of tokens. - * @tparam AIt The command-line argument value iterator type. - * @note `AIt` must be a `std::forward_iterator` with a value type convertible to `std::string`. - * @param args_begin The begin iterator for the command-line argument value range. - * @param args_end The end iterator for the command-line argument value range. - * @param state The current parsing state. - * @return A list of preprocessed command-line argument tokens. - */ template AIt> [[nodiscard]] arg_token_vec_t _tokenize( AIt args_begin, const AIt args_end, const parsing_state& state @@ -1221,12 +1222,6 @@ class argument_parser { return toks; } - /** - * @brief Appends an argument token(s) created from `arg_value` to the `toks` vector. - * @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 std::string_view arg_value, arg_token_vec_t& toks, const parsing_state& state ) { @@ -1282,15 +1277,11 @@ class argument_parser { } } - /** - * @brief Returns the most appropriate *initial* token type based on a command-line argument's value. - * - * The token's *initial* type is deduced using the following rules: - * - `t_value`: an argument contains whitespace characters or cannot be a flag token - * - `t_flag_primary`: an argument begins with a primary flag prefix (`--`) - * - `t_flag_secondary`: an argument begins with a secondary flag prefix (`-`) - * - `t_flag_compound`: INITIALLY a token can NEVER have a compound flag type (may only be set when a flag token is validated) - */ + // The token's *initial* type is deduced using the following rules: + // - `t_value`: an argument contains whitespace characters or cannot be a flag token + // - `t_flag_primary`: an argument begins with a primary flag prefix (`--`) + // - `t_flag_secondary`: an argument begins with a secondary flag prefix (`-`) + // - `t_flag_compound`: INITIALLY a token can NEVER have a compound flag type (may only be set when a flag token is validated) [[nodiscard]] detail::argument_token::token_type _deduce_token_type( const std::string_view arg_value ) const noexcept { @@ -1306,12 +1297,8 @@ class argument_parser { return detail::argument_token::t_value; } - /** - * @brief Check if a flag token is valid based on its value. - * @attention Extends the `args` member of the token if an argument with the given name (token's value) is present. - * @param tok The argument token to validate. - * @return `true` if the given token represents a valid argument flag. - */ + // 1. Checks if a flag token is valid based on its value. + // 2. Extends the `args` member of the token if an argument(s) with the given name (token's value) is present. [[nodiscard]] bool _validate_flag_token(detail::argument_token& tok) noexcept { const auto opt_arg_it = this->_find_opt_arg(tok); if (opt_arg_it == this->_optional_args.end()) @@ -1321,13 +1308,8 @@ class argument_parser { return true; } - /** - * @brief Check if a flag token is a valid compound argument flag based on its value. - * @attention If the token indeed represents valid compound flag, the token's type is changed to `t_flag_compuund` - * @attention and its `args` list is filled with all the arguments the token represents. - * @param tok The argument token to validate. - * @return `true` if the given token represents a valid compound argument flag. - */ + // NOTE: If the token is a valid compound flag, its type is changed to `t_flag_compound` + // and its `args` list is extended with all the arguments the token represents. bool _validate_compound_flag_token(detail::argument_token& tok) noexcept { if (tok.type != detail::argument_token::t_flag_secondary) return false; @@ -1393,12 +1375,6 @@ class argument_parser { return false; } - /** - * @brief Find an optional argument based on a flag token. - * @param flag_tok An argument_token instance, the value of which will be used to find the argument. - * @return An iterator to the argument's position. - * @note If the `flag_tok.type` is not a valid flag token, then the end iterator will be returned. - */ [[nodiscard]] arg_ptr_vec_iter_t _find_opt_arg(const detail::argument_token& flag_tok ) noexcept { if (not flag_tok.is_flag_token()) @@ -1415,11 +1391,6 @@ class argument_parser { ); } - /** - * @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) { @@ -1432,12 +1403,6 @@ class argument_parser { } } - /** - * @brief Parse a single command-line argument token. - * @param tok The token to be parsed. - * @param state The current parsing state. - * @throws argon::parsing_failure - */ void _parse_token(const detail::argument_token& tok, parsing_state& state) { if (state.curr_arg and state.curr_arg->is_greedy()) { this->_set_argument_value(tok.value, state); @@ -1450,12 +1415,6 @@ class argument_parser { this->_parse_value_token(tok, state); } - /** - * @brief Parse a single command-line argument *flag* token. - * @param tok The token to be parsed. - * @param state The current parsing state. - * @throws argon::parsing_failure - */ void _parse_flag_token(const detail::argument_token& tok, parsing_state& state) { if (not tok.is_valid_flag_token()) { if (state.parse_known_only) { @@ -1477,12 +1436,6 @@ class argument_parser { } } - /** - * @brief Parse a single command-line argument *value* token. - * @param tok The token to be parsed. - * @param state The current parsing state. - * @throws argon::parsing_failure - */ 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()) { @@ -1496,12 +1449,7 @@ class argument_parser { 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 value The value to be set for the current argument. - * @param state The current parsing state. - */ + // NOTE: This function assumes that the current argument is set (i.e. `state.curr_arg != nullptr`) void _set_argument_value(const std::string_view value, parsing_state& state) { if (state.curr_arg->set_value(std::string(value))) return; // argument still accepts values @@ -1514,22 +1462,13 @@ class argument_parser { state.curr_arg.reset(); } - /** - * @brief Verifies the correctness of the parsed command-line arguments. - * @throws argon::parsing_failure if the state of the parsed arguments is invalid. - */ void _verify_final_state() const { const auto [supress_group_checks, suppress_arg_checks] = this->_are_checks_suppressed(); for (const auto& group : this->_argument_groups) this->_verify_group_requirements(*group, supress_group_checks, suppress_arg_checks); } - /** - * @brief Check 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. - */ + // NOTE: Returns pair: [[nodiscard]] std::pair _are_checks_suppressed() const noexcept { bool suppress_group_checks = false; bool suppress_arg_checks = false; @@ -1549,12 +1488,6 @@ class argument_parser { return {suppress_group_checks, suppress_arg_checks}; } - /** - * @brief Verifies whether the requirements of the given argument group are satisfied. - * @param group The argument group to verify. - * @param suppress_arg_checks A flag indicating whether argument checks are suppressed. - * @throws argon::parsing_failure if the requirements are not satistied. - */ void _verify_group_requirements( const argument_group& group, const bool suppress_group_checks, @@ -1597,12 +1530,6 @@ class argument_parser { 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 suppress_arg_checks A flag indicating whether argument checks are suppressed. - * @throws argon::parsing_failure if the requirements are not satistied. - */ void _verify_argument_requirements(const arg_ptr_t& arg, const bool suppress_arg_checks) const { if (suppress_arg_checks) return; @@ -1615,12 +1542,7 @@ class argument_parser { throw parsing_failure::invalid_nvalues(arg->name(), nv_ord); } - /** - * @brief Get the argument with the specified name. - * @param arg_name The name of the argument. - * @return The argument with the specified name, if found; otherwise, std::nullopt. - * @throws argon::lookup_failure if an argument with the given name cannot be found. - */ + // NOTE: Throws if not found [[nodiscard]] arg_ptr_t _get_argument(std::string_view arg_name) const { const auto predicate = this->_name_match_predicate(arg_name); @@ -1659,13 +1581,7 @@ class argument_parser { os << '\n'; } - /** - * @brief Print the given argument list to an output stream. - * @param os The output stream to print to. - * @param group The argument group to print. - * @param verbose A verbosity mode indicator flag. - * @attention If a group has no visible arguments, nothing will be printed. - */ + // NOTE: A group is only printed if it's not hidden and has at least one visible argument void _print_group(std::ostream& os, const argument_group& group, const bool verbose) const noexcept { if (group._hidden) @@ -1713,36 +1629,34 @@ class argument_parser { // --- attributes --- - std::string _name = ""; ///< The name of the parser. + std::string _name = ""; std::string _program_name = - ""; ///< The name of the program in the format "... ". - std::optional _program_version = std::nullopt; ///< The version of the program. - std::optional _program_description = - std::nullopt; ///< The description of the program. - unknown_policy _unknown_policy = unknown_policy::fail; ///< Policy for unknown arguments. + ""; // The name of the program in the format "... ". + std::optional _program_version = std::nullopt; + std::optional _program_description = std::nullopt; + unknown_policy _unknown_policy = unknown_policy::fail; - char _flag_char = '-'; ///< The character used as a flag prefix. - std::string _primary_flag_prefix = "--"; ///< The primary flag prefix. + char _flag_char = '-'; + std::string _primary_flag_prefix = "--"; // --- parsing cfg & state --- - arg_ptr_vec_t _positional_args = {}; ///< The list of positional arguments. - arg_ptr_vec_t _optional_args = {}; ///< The list of optional arguments. - arg_group_ptr_vec_t _argument_groups = {}; ///< The list of argument groups. - argument_group& _gr_positional_args; ///< The positional argument group. - argument_group& _gr_optional_args; ///< The optional argument group. - arg_parser_ptr_vec_t _subparsers = {}; ///< The list of subparsers. + arg_ptr_vec_t _positional_args = {}; + arg_ptr_vec_t _optional_args = {}; + arg_group_ptr_vec_t _argument_groups = {}; + argument_group& _gr_positional_args; + argument_group& _gr_optional_args; + arg_parser_ptr_vec_t _subparsers = {}; // --- cfg flags --- - bool _verbose : 1 = false; ///< Verbosity flag. + bool _is_name_resolved : 1 = false; + bool _verbose : 1 = false; // --- parsing state flags --- - bool _invoked : 1 = - false; ///< A flag indicating whether the parser has been invoked to parse arguments. - bool _finalized : 1 = - false; ///< A flag indicating whether the parsing process has been finalized. + bool _invoked : 1 = false; // Indicates whether the parser has been invoked to parse arguments + bool _finalized : 1 = false; // Indicates whether the parsing process has been finalized. // --- constants --- @@ -1785,11 +1699,6 @@ inline const std::vector& argument_group::values(std::string_view arg_base_na namespace detail { -/** - * @brief Adds a predefined/default positional argument to the parser. - * @param arg_discriminator The default argument discriminator. - * @param arg_parser The argument parser to which the argument will be added. - */ inline void add_default_argument( const default_argument arg_discriminator, argument_parser& arg_parser ) noexcept { @@ -1846,5 +1755,4 @@ inline void add_default_argument( } } // namespace detail - } // namespace argon diff --git a/include/argon/types.hpp b/include/argon/types.hpp index 4714b719..60d44fe2 100644 --- a/include/argon/types.hpp +++ b/include/argon/types.hpp @@ -19,6 +19,14 @@ namespace argon { */ struct none_type {}; +/// @brief Tag type used to enable dynamic program name deduction. +struct dynamic_name_t { + explicit dynamic_name_t() = default; +}; + +/// @brief Tag value that enables dynamic deduction of the program name. +inline constexpr dynamic_name_t dynamic_name{}; + /// @brief A helper structure used to represent a program's version. struct version { std::uint32_t major = 0u; ///< The major version number. diff --git a/tests/source/test_argument_parser_parse_args.cpp b/tests/source/test_argument_parser_parse_args.cpp index d0ce2b51..ab46a458 100644 --- a/tests/source/test_argument_parser_parse_args.cpp +++ b/tests/source/test_argument_parser_parse_args.cpp @@ -1908,4 +1908,102 @@ TEST_CASE_FIXTURE( CHECK_EQ(subparser.value(opt_arg_name), opt_arg_val); } +// dynamic program name + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "range-based parse_args and parse_known_args should throw if the dynamic program name is " + "unresolved" +) { + argon::argument_parser dynamic_parser(argon::dynamic_name); + std::vector args = {"--help"}; + + CHECK_THROWS_WITH_AS( + dynamic_parser.parse_args(args), + "Dynamic program name must be resolved before calling parsing the program's arguments", + std::logic_error + ); + + CHECK_THROWS_WITH_AS( + dynamic_parser.parse_known_args(args), + "Dynamic program name must be resolved before calling parsing the program's arguments", + std::logic_error + ); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args(argc, argv) should correctly resolve the dynamic program name from argv[0] using " + "Unix paths" +) { + argon::argument_parser dynamic_parser(argon::dynamic_name); + dynamic_parser.add_flag("help"); + + std::vector argv_vec{"/usr/local/bin/my_app", "--help"}; + const int argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + REQUIRE_NOTHROW(dynamic_parser.parse_args(argc, argv)); + + CHECK_EQ(dynamic_parser.name(), "my_app"); + CHECK_EQ(dynamic_parser.program_name(), "my_app"); + + free_argv(argc, argv); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args(argc, argv) should correctly resolve the dynamic program name from argv[0] using " + "Windows paths" +) { + argon::argument_parser dynamic_parser(argon::dynamic_name); + dynamic_parser.add_flag("help"); + + std::vector argv_vec{"C:\\Program Files\\App\\my_app.exe", "--help"}; + const int argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + REQUIRE_NOTHROW(dynamic_parser.parse_args(argc, argv)); + + CHECK_EQ(dynamic_parser.name(), "my_app.exe"); + CHECK_EQ(dynamic_parser.program_name(), "my_app.exe"); + + free_argv(argc, argv); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "dynamic program name resolution should propagate to all nested subparsers" +) { + argon::argument_parser dynamic_parser(argon::dynamic_name); + + auto& sub_cmd = dynamic_parser.add_subparser("commit"); + auto& nested_cmd = sub_cmd.add_subparser("now"); + + std::vector argv_vec{"/usr/bin/git", "commit", "now"}; + const int argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + // Initial state check - subparser program names should be incomplete because the parent isn't resolved yet + CHECK_EQ(sub_cmd.program_name(), "commit"); + CHECK_EQ(nested_cmd.program_name(), "commit now"); + + // Parse will trigger the resolution and propagation + REQUIRE_NOTHROW(dynamic_parser.parse_args(argc, argv)); + + // Verify root parser + CHECK_EQ(dynamic_parser.name(), "git"); + CHECK_EQ(dynamic_parser.program_name(), "git"); + + // Verify first-level subparser + CHECK_EQ(sub_cmd.name(), "commit"); + CHECK_EQ(sub_cmd.program_name(), "git commit"); + + // Verify nested subparser + CHECK_EQ(nested_cmd.name(), "now"); + CHECK_EQ(nested_cmd.program_name(), "git commit now"); + + free_argv(argc, argv); +} + TEST_SUITE_END(); // test_argument_parser_parse_args;