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(