From bb472522078cb106798a1fc4578df515fe497338 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Tue, 22 Sep 2026 14:16:03 +0200 Subject: [PATCH 1/3] initial assign char impl --- include/argon/argument_parser.hpp | 55 +++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/include/argon/argument_parser.hpp b/include/argon/argument_parser.hpp index 19589e3..9d6a5f1 100644 --- a/include/argon/argument_parser.hpp +++ b/include/argon/argument_parser.hpp @@ -264,6 +264,33 @@ class argument_parser { return *this; } + /** + * @brief Set the assignment character. + * @param chr The assignment character. + * @return Reference to the argument parser. + * @throws argon::invalid_configuration if the assignment character is not a printable + * ASCII character, is a space, is identical to the flag character, or if any + * arguments have already been added to the parser. + * @note The default assignment character is '='. + */ + argument_parser& assign_char(const char chr) { + if (not this->_positional_args.empty() or not this->_optional_args.empty()) + throw invalid_configuration("The assignment character must be set before adding any " + "arguments!"); + + if (not std::isprint(static_cast(chr)) + or std::isspace(static_cast(chr))) + throw invalid_configuration("The assignment character must be a non-space printable " + "ASCII character!"); + + if (chr == this->_flag_char) + throw invalid_configuration("The assignment character cannot be the same as the flag " + "prefix character!"); + + this->_assign_char = chr; + return *this; + } + /** * @brief Add default arguments to the argument parser. * @tparam ArgvRange Type of the positional argument discriminator range. @@ -1124,33 +1151,53 @@ class argument_parser { void _tokenize_arg( const std::string_view arg_value, arg_token_vec_t& toks, const parsing_state& state ) { + std::string_view flag_str = arg_value; + std::optional inline_value = std::nullopt; + + // Split the token if it starts with a flag and contains the assignment character + if (arg_value.starts_with(this->_flag_char)) { + if (const auto assign_pos = arg_value.find(this->_assign_char); + assign_pos != std::string_view::npos) { + flag_str = arg_value.substr(0, assign_pos); + inline_value = arg_value.substr(assign_pos + 1); + } + } + detail::argument_token tok{ - .type = this->_deduce_token_type(arg_value), .value = std::string(arg_value) + .type = this->_deduce_token_type(flag_str), .value = std::string(flag_str) }; if (not tok.is_flag_token() or this->_validate_flag_token(tok)) { toks.emplace_back(std::move(tok)); + if (inline_value.has_value()) { // push the additional value token + toks.emplace_back(detail::argument_token{ + .type = detail::argument_token::t_value, + .value = std::string(inline_value.value()) + }); + } return; } // not a value token -> flag token // flag token could not be validated -> unknown flag if (state.parse_known_only) { // do nothing (will be handled during parsing) + tok.value = std::string(arg_value); // Restore the original argument value toks.emplace_back(std::move(tok)); return; } switch (this->_unknown_policy) { case unknown_policy::fail: - throw parsing_failure::unknown_argument(tok.value); + throw parsing_failure::unknown_argument(arg_value); case unknown_policy::warn: - std::cerr << "[argon::warning] Unknown argument '" << tok.value << "' will be ignored." + std::cerr << "[argon::warning] Unknown argument '" << arg_value << "' will be ignored." << std::endl; [[fallthrough]]; case unknown_policy::ignore: return; case unknown_policy::as_values: tok.type = detail::argument_token::t_value; + tok.value = std::string(arg_value); toks.emplace_back(std::move(tok)); break; } @@ -1596,7 +1643,9 @@ class argument_parser { std::optional _program_description = std::nullopt; ///< The description of the program. unknown_policy _unknown_policy = unknown_policy::fail; ///< Policy for unknown arguments. + char _flag_char = '-'; ///< The character used as a flag prefix. + char _assign_char = '='; ///< The character used to assign values inline. std::string _primary_flag_prefix = "--"; ///< The primary flag prefix. // --- parsing cfg & state --- From 314306cdeef3d54fa474d0a9e056de3ef65fe2df Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Tue, 22 Sep 2026 14:38:21 +0200 Subject: [PATCH 2/3] tests; removed assign_char method --- include/argon/argument_parser.hpp | 33 +----- tests/source/test_argument_parser_cfg.cpp | 11 ++ .../test_argument_parser_parse_args.cpp | 112 ++++++++++++++++++ 3 files changed, 128 insertions(+), 28 deletions(-) diff --git a/include/argon/argument_parser.hpp b/include/argon/argument_parser.hpp index 9d6a5f1..f0ad3a0 100644 --- a/include/argon/argument_parser.hpp +++ b/include/argon/argument_parser.hpp @@ -259,38 +259,15 @@ class argument_parser { if (not std::isprint(static_cast(chr))) throw invalid_configuration("The flag character must be a printable ASCII character!"); + if (chr == this->_assign_char) + throw invalid_configuration("The flag character cannot be the same as the assignment " + "character!"); + this->_flag_char = chr; this->_primary_flag_prefix = std::string(this->_primary_flag_prefix_length, chr); return *this; } - /** - * @brief Set the assignment character. - * @param chr The assignment character. - * @return Reference to the argument parser. - * @throws argon::invalid_configuration if the assignment character is not a printable - * ASCII character, is a space, is identical to the flag character, or if any - * arguments have already been added to the parser. - * @note The default assignment character is '='. - */ - argument_parser& assign_char(const char chr) { - if (not this->_positional_args.empty() or not this->_optional_args.empty()) - throw invalid_configuration("The assignment character must be set before adding any " - "arguments!"); - - if (not std::isprint(static_cast(chr)) - or std::isspace(static_cast(chr))) - throw invalid_configuration("The assignment character must be a non-space printable " - "ASCII character!"); - - if (chr == this->_flag_char) - throw invalid_configuration("The assignment character cannot be the same as the flag " - "prefix character!"); - - this->_assign_char = chr; - return *this; - } - /** * @brief Add default arguments to the argument parser. * @tparam ArgvRange Type of the positional argument discriminator range. @@ -1645,7 +1622,6 @@ class argument_parser { unknown_policy _unknown_policy = unknown_policy::fail; ///< Policy for unknown arguments. char _flag_char = '-'; ///< The character used as a flag prefix. - char _assign_char = '='; ///< The character used to assign values inline. std::string _primary_flag_prefix = "--"; ///< The primary flag prefix. // --- parsing cfg & state --- @@ -1670,6 +1646,7 @@ class argument_parser { // --- constants --- + static constexpr char _assign_char = '='; static constexpr std::uint8_t _primary_flag_prefix_length = 2u; static constexpr std::uint8_t _secondary_flag_prefix_length = 1u; static constexpr std::uint8_t _indent_width = 2u; diff --git a/tests/source/test_argument_parser_cfg.cpp b/tests/source/test_argument_parser_cfg.cpp index d0b6cf3..2a32318 100644 --- a/tests/source/test_argument_parser_cfg.cpp +++ b/tests/source/test_argument_parser_cfg.cpp @@ -118,4 +118,15 @@ TEST_CASE_FIXTURE( ); } +TEST_CASE_FIXTURE( + test_argument_parser_cfg, + "flag_char() should throw if the given character is the same as the assignment character" +) { + CHECK_THROWS_WITH_AS( + sut.flag_char('='), + "The flag character cannot be the same as the assignment character!", + invalid_configuration + ); +} + TEST_SUITE_END(); // test_argument_parser_cfg diff --git a/tests/source/test_argument_parser_parse_args.cpp b/tests/source/test_argument_parser_parse_args.cpp index 57f773c..8c33317 100644 --- a/tests/source/test_argument_parser_parse_args.cpp +++ b/tests/source/test_argument_parser_parse_args.cpp @@ -1521,6 +1521,118 @@ TEST_CASE_FIXTURE( free_argv(argc, argv); } +// assignment character + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should correctly assign values using the assignment character (=)" +) { + sut.add_optional_argument("number", "n"); + sut.add_optional_argument("string", "s"); + + std::vector argv_vec{"program", "--number=42", "-s=hello world"}; + + const int argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + REQUIRE_NOTHROW(sut.parse_args(argc, argv)); + + CHECK(sut.has_value("number")); + CHECK_EQ(sut.value("number"), 42); + + CHECK(sut.has_value("string")); + CHECK_EQ(sut.value("string"), "hello world"); + + free_argv(argc, argv); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should correctly handle multiple values when the first is assigned inline" +) { + sut.add_optional_argument("names").nargs(argon::nargs::at_least(1)); + + std::vector argv_vec{"program", "--names=Kowalski", "Wisniewski", "Nowak"}; + + const int argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + REQUIRE_NOTHROW(sut.parse_args(argc, argv)); + + CHECK(sut.has_value("names")); + CHECK_EQ(sut.count("names"), 1ull); + + const std::vector expected_names{"Kowalski", "Wisniewski", "Nowak"}; + CHECK_EQ(sut.values("names"), expected_names); + + free_argv(argc, argv); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should correctly assign inline values to the final argument of a compound flag" +) { + sut.add_flag("verbose", "v"); + sut.add_optional_argument("level", "l"); + + std::vector argv_vec{"program", "-vvl=5"}; + + const int argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + REQUIRE_NOTHROW(sut.parse_args(argc, argv)); + + CHECK_EQ(sut.count("verbose"), 2ull); + CHECK(sut.has_value("level")); + CHECK_EQ(sut.value("level"), 5); + + free_argv(argc, argv); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_known_args should preserve the full assignment string for unknown arguments" +) { + sut.add_optional_argument("known", "k"); + + const std::string unknown_arg = "--unknown=invalid_value"; + std::vector argv_vec{"program", "--known=valid_value", unknown_arg}; + + const int argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + std::vector unknown_args; + REQUIRE_NOTHROW(unknown_args = sut.parse_known_args(argc, argv)); + + CHECK(sut.has_value("known")); + CHECK_EQ(sut.value("known"), "valid_value"); + + REQUIRE_EQ(unknown_args.size(), 1ull); + CHECK_EQ(unknown_args.front(), unknown_arg); + + free_argv(argc, argv); +} + +TEST_CASE_FIXTURE( + test_argument_parser_parse_args, + "parse_args should throw when spaces around the assignment character lead to invalid values" +) { + sut.add_optional_argument("number", "n"); + + std::vector argv_vec{"program", "--number", "=", "42"}; + + const int argc = static_cast(argv_vec.size()); + auto argv = to_char_2d_array(argv_vec); + + CHECK_THROWS_WITH_AS( + sut.parse_args(argc, argv), + "Cannot parse value `=` for argument [--number, -n].", + parsing_failure + ); + + free_argv(argc, argv); +} + // argument groups TEST_CASE_FIXTURE( From 72dd29323d706e8fe871f1450273830e6bb008b4 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Tue, 22 Sep 2026 14:49:22 +0200 Subject: [PATCH 3/3] docs update --- docs/tutorial.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/tutorial.md b/docs/tutorial.md index e26ca92..3869f4d 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -42,6 +42,7 @@ - [2. Positional arguments are parsed in the order of definition](#2-positional-arguments-are-parsed-in-the-order-of-definition) - [3. Positional arguments consume free values](#3-positional-arguments-consume-free-values) - [4. Unknown Argument Flag Handling](#4-unknown-argument-flag-handling) + - [5. Inline Value Assignment](#5-inline-value-assignment) - [Compound Arguments](#compound-arguments) - [Compound Flags within Argument Groups](#compound-flags-within-argument-groups) - [Parsing Known Arguments](#parsing-known-arguments) @@ -1294,6 +1295,43 @@ The available policies are: > unknown = --unknown > ``` +
+ +#### 5. Inline Value Assignment + +By default, optional arguments accept values separated by spaces (e.g., `--number 42`). CPP-ARGON also natively supports inline value assignment using the `=` character. + +```cpp +parser.add_optional_argument("number", "n"); +parser.add_optional_argument("string", "s"); +parser.add_optional_argument("names").nargs(argon::nargs::at_least(1)); +``` + +You can use the assignment character with primary flags, secondary flags, and even at the end of [compound flags](#compound-arguments) (where the assigned value is automatically passed to the argument represented by the last character in the compound flag): + +```txt +> ./program --number=42 -s=hello +> ./program -vvn=5 +``` + +> [!WARNING] +> * **Do not place spaces around the assignment character.** Command-line shells (like Bash, Zsh, or PowerShell) split arguments by spaces before the program ever processes them. Typing `--number = 42` will cause the parser to treat `=` as the value for `--number` (which will fail during integer conversion). +> +> * If your assigned value contains spaces, quote the value directly after the assignment character: +> ```txt +> > ./program --string="hello world" +> ``` + +**Multiple Values** + +If an argument is configured to accept multiple values (e.g., via `.nargs(argon::nargs::at_least(1))`), you can seamlessly combine inline assignment for the first value with standard space-separated values for the rest: + +```txt +> ./program --names=Kowalski Wisniewski Nowak +``` + +In this case, the parser automatically assigns all three names to the `--names` argument. +