Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -1294,6 +1295,43 @@ The available policies are:
> unknown = --unknown
> ```

<br />

#### 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<int>("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.

<br />
<br />

Expand Down
32 changes: 29 additions & 3 deletions include/argon/argument_parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ class argument_parser {
if (not std::isprint(static_cast<unsigned char>(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;
Expand Down Expand Up @@ -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<std::string_view> 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;
}
Expand Down Expand Up @@ -1596,6 +1620,7 @@ class argument_parser {
std::optional<std::string> _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.

Expand All @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions tests/source/test_argument_parser_cfg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
112 changes: 112 additions & 0 deletions tests/source/test_argument_parser_parse_args.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>("number", "n");
sut.add_optional_argument("string", "s");

std::vector<std::string> argv_vec{"program", "--number=42", "-s=hello world"};

const int argc = static_cast<int>(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<int>("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<std::string> argv_vec{"program", "--names=Kowalski", "Wisniewski", "Nowak"};

const int argc = static_cast<int>(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<std::string> 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<int>("level", "l");

std::vector<std::string> argv_vec{"program", "-vvl=5"};

const int argc = static_cast<int>(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<int>("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<std::string> argv_vec{"program", "--known=valid_value", unknown_arg};

const int argc = static_cast<int>(argv_vec.size());
auto argv = to_char_2d_array(argv_vec);

std::vector<std::string> 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<int>("number", "n");

std::vector<std::string> argv_vec{"program", "--number", "=", "42"};

const int argc = static_cast<int>(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(
Expand Down
Loading