diff --git a/docs/tutorial.md b/docs/tutorial.md index e26ca92a..3869f4d7 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. +

diff --git a/include/argon/argument_parser.hpp b/include/argon/argument_parser.hpp index 19589e3e..f0ad3a0c 100644 --- a/include/argon/argument_parser.hpp +++ b/include/argon/argument_parser.hpp @@ -259,6 +259,10 @@ 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; @@ -1124,33 +1128,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,6 +1620,7 @@ 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. std::string _primary_flag_prefix = "--"; ///< The primary flag prefix. @@ -1621,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 d0b6cf31..2a32318f 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 57f773c6..8c333178 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(