From e79ce896d72527c0a6a284082b4dae5acf4e4e96 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Tue, 28 Jul 2026 18:35:25 +0200 Subject: [PATCH 1/3] Added support for widestrings --- include/rfl/cli/parse_argv.hpp | 33 ++++++++ include/rfl/cli/read.hpp | 30 +++++++ tests/cli/test_wide_char.cpp | 147 +++++++++++++++++++++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 tests/cli/test_wide_char.cpp diff --git a/include/rfl/cli/parse_argv.hpp b/include/rfl/cli/parse_argv.hpp index 721a68193..fce6f016d 100644 --- a/include/rfl/cli/parse_argv.hpp +++ b/include/rfl/cli/parse_argv.hpp @@ -9,6 +9,7 @@ #include #include "../Result.hpp" +#include "../internal/strings/utf8_conversions.hpp" #include "resolve_args.hpp" namespace rfl::cli { @@ -120,6 +121,38 @@ inline rfl::Result parse_argv(int argc, char* argv[]) { return result; } +/// Parses wide-character command-line arguments into categorized buckets. +/// Converts each wide string to UTF-8 and delegates to the narrow parse_argv. +/// @param argc Number of wide-character command-line arguments +/// @param argv Array of wide-character command-line argument strings +/// @return A Result containing ParsedArgs with UTF-8 encoded arguments +inline rfl::Result parse_argv(int argc, wchar_t* argv[]) { + if (argc < 0 || (argc > 0 && !argv)) { + return error("Invalid argc/argv."); + } + if (argc <= 1) { + return ParsedArgs{}; + } + + std::vector narrow_argv; + narrow_argv.reserve(argc); + std::vector narrow_argv_ptrs; + narrow_argv_ptrs.reserve(argc); + + for (int i = 0; i < argc; ++i) { + const auto str = rfl::internal::strings::wstring_to_utf8(argv[i]); + if (!str) { + return error( + "Could not convert argument " + std::to_string(i) + + " from wide to UTF-8."); + } + narrow_argv.emplace_back(std::move(*str)); + narrow_argv_ptrs.push_back(narrow_argv.back().data()); + } + + return parse_argv(argc, narrow_argv_ptrs.data()); +} + } // namespace rfl::cli #endif diff --git a/include/rfl/cli/read.hpp b/include/rfl/cli/read.hpp index f2a7c5853..13b07ac3e 100644 --- a/include/rfl/cli/read.hpp +++ b/include/rfl/cli/read.hpp @@ -3,6 +3,7 @@ #include "../Processors.hpp" #include "../SnakeCaseToKebabCase.hpp" +#include "../internal/strings/utf8_conversions.hpp" #include "Parser.hpp" #include "Reader.hpp" #include "parse_argv.hpp" @@ -32,6 +33,35 @@ rfl::Result read(int argc, char* argv[]) { }); } +/// Parses wide-character command-line arguments into a struct using reflection. +/// Field names are automatically converted from snake_case to kebab-case for CLI arguments. +/// Wide strings are converted to UTF-8 before parsing. +/// @tparam T The struct type to parse into +/// @tparam Ps Optional processors to apply during parsing +/// @param argc Number of wide-character command-line arguments +/// @param argv Array of wide-character argument strings +/// @return A Result containing the parsed struct or an error +template +rfl::Result read(int argc, wchar_t* argv[]) { + if (argc < 0 || (argc > 0 && !argv)) { + return error("Invalid argc/argv."); + } + std::vector narrow_argv; + narrow_argv.reserve(argc); + std::vector narrow_argv_ptrs; + narrow_argv_ptrs.reserve(argc); + for (int i = 0; i < argc; ++i) { + const auto str = rfl::internal::strings::wstring_to_utf8(argv[i]); + if (!str) { + return error("Could not convert argument " + std::to_string(i) + + " from wide to UTF-8."); + } + narrow_argv.emplace_back(std::move(*str)); + narrow_argv_ptrs.push_back(narrow_argv.back().data()); + } + return read(argc, narrow_argv_ptrs.data()); +} + } // namespace rfl::cli #endif diff --git a/tests/cli/test_wide_char.cpp b/tests/cli/test_wide_char.cpp new file mode 100644 index 000000000..0ceb09094 --- /dev/null +++ b/tests/cli/test_wide_char.cpp @@ -0,0 +1,147 @@ +#include + +#include +#include +#include + +namespace test_wide_char { + +struct Config { + std::string host; + int port; + std::optional rate; + std::optional verbose; +}; + +struct ShortConfig { + std::string host; + rfl::Short<"p", int> port; + rfl::Short<"v", bool> verbose; +}; + +struct PositionalConfig { + rfl::Positional input_file; + rfl::Positional output_file; + bool verbose; +}; + +TEST(cli, test_read_wide_basic) { + const wchar_t* args[] = {L"program", L"--host=localhost", L"--port=8080", + L"--rate=1.5", L"--verbose"}; + const auto result = rfl::cli::read(5, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + EXPECT_EQ(result.value().host, "localhost"); + EXPECT_EQ(result.value().port, 8080); + ASSERT_TRUE(result.value().rate); + EXPECT_DOUBLE_EQ(*result.value().rate, 1.5); + ASSERT_TRUE(result.value().verbose); + EXPECT_TRUE(*result.value().verbose); +} + +TEST(cli, test_read_wide_empty) { + const wchar_t* args[] = {L"program"}; + const auto result = rfl::cli::read(1, const_cast(args)); + ASSERT_FALSE(result); +} + +TEST(cli, test_read_wide_bool_true) { + const wchar_t* args[] = {L"program", L"--host=localhost", L"--port=1", + L"--rate=0", L"--verbose"}; + const auto result = rfl::cli::read(5, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + ASSERT_TRUE(result.value().verbose); + EXPECT_TRUE(*result.value().verbose); +} + +TEST(cli, test_read_wide_bool_false) { + const wchar_t* args[] = {L"program", L"--host=localhost", L"--port=1", + L"--rate=0", L"--verbose=false"}; + const auto result = rfl::cli::read(5, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + ASSERT_TRUE(result.value().verbose); + EXPECT_FALSE(*result.value().verbose); +} + +TEST(cli, test_read_wide_short_option) { + const wchar_t* args[] = {L"program", L"--host=localhost", L"-p", L"9090", + L"-v"}; + const auto result = + rfl::cli::read(5, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + EXPECT_EQ(result.value().host, "localhost"); + EXPECT_EQ(result.value().port(), 9090); + EXPECT_TRUE(result.value().verbose()); +} + +TEST(cli, test_read_wide_short_equals) { + const wchar_t* args[] = {L"program", L"--host=localhost", L"-p=443", L"-v"}; + const auto result = + rfl::cli::read(4, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + EXPECT_EQ(result.value().port(), 443); +} + +TEST(cli, test_read_wide_positional) { + const wchar_t* args[] = {L"program", L"in.txt", L"out.txt", L"--verbose"}; + const auto result = + rfl::cli::read(4, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + EXPECT_EQ(result.value().input_file(), "in.txt"); + EXPECT_EQ(result.value().output_file(), "out.txt"); + EXPECT_TRUE(result.value().verbose); +} + +TEST(cli, test_read_wide_conflict_named_short) { + const wchar_t* args[] = {L"program", L"--host=localhost", L"--port=8080", + L"-p", L"9090"}; + const auto result = + rfl::cli::read(5, const_cast(args)); + ASSERT_FALSE(result); +} + +TEST(cli, test_read_wide_negative_value) { + const wchar_t* args[] = {L"program", L"--host=localhost", L"-p", L"-42", + L"-v"}; + const auto result = + rfl::cli::read(5, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + EXPECT_EQ(result.value().port(), -42); +} + +TEST(cli, test_read_wide_valid_ascii) { + const wchar_t* args[] = {L"program", L"--host=localhost", L"--port=8080"}; + const auto result = rfl::cli::read(3, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + EXPECT_EQ(result.value().host, "localhost"); + EXPECT_EQ(result.value().port, 8080); +} + +TEST(cli, test_parse_argv_wide_basic) { + const wchar_t* args[] = {L"program", L"--host=localhost", L"-p=8080", + L"positional_arg"}; + const auto result = rfl::cli::parse_argv(4, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + // parse_argv(wchar_t*) converts to UTF-8 and delegates, so keys are + // std::string + EXPECT_EQ(result.value().named.at("host"), "localhost"); + EXPECT_EQ(result.value().short_args.at("p"), "8080"); + EXPECT_EQ(result.value().positional[0], "positional_arg"); +} + +TEST(cli, test_parse_argv_wide_double_dash) { + const wchar_t* args[] = {L"program", L"--", L"--not-a-flag", L"positional"}; + const auto result = rfl::cli::parse_argv(4, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + EXPECT_EQ(result.value().positional[0], "--not-a-flag"); + EXPECT_EQ(result.value().positional[1], "positional"); +} + +TEST(cli, test_parse_argv_wide_empty) { + const wchar_t* args[] = {L"program"}; + const auto result = rfl::cli::parse_argv(1, const_cast(args)); + ASSERT_TRUE(result); + EXPECT_TRUE(result.value().named.empty()); + EXPECT_TRUE(result.value().positional.empty()); +} + +} // namespace test_wide_char From 4c085b4c5643c53f2c72200cf6ee1440ec8ed497 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Tue, 28 Jul 2026 18:39:48 +0200 Subject: [PATCH 2/3] Mark all tests cli --- tests/cli/test_regression_bugs.cpp | 10 +++++----- tests/cli/test_settings_macro.cpp | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/cli/test_regression_bugs.cpp b/tests/cli/test_regression_bugs.cpp index d3e68ff0a..6247a60d8 100644 --- a/tests/cli/test_regression_bugs.cpp +++ b/tests/cli/test_regression_bugs.cpp @@ -17,7 +17,7 @@ struct Config { uint16_t port; }; -TEST(regression, cli_rejects_negative_for_unsigned) { +TEST(cli, cli_rejects_negative_for_unsigned) { const char* args[] = {"program", "--port=-1"}; const auto result = rfl::cli::read(2, const_cast(args)); EXPECT_FALSE(result) @@ -37,7 +37,7 @@ struct Config { uint16_t port; }; -TEST(regression, cli_rejects_out_of_range_for_narrow_type) { +TEST(cli, cli_rejects_out_of_range_for_narrow_type) { const char* args[] = {"program", "--port=99999"}; const auto result = rfl::cli::read(2, const_cast(args)); EXPECT_FALSE(result) @@ -57,7 +57,7 @@ struct SignedConfig { int8_t level; }; -TEST(regression, cli_rejects_out_of_range_for_signed_narrow_type) { +TEST(cli, cli_rejects_out_of_range_for_signed_narrow_type) { const char* args[] = {"program", "--level=200"}; const auto result = rfl::cli::read(2, const_cast(args)); @@ -67,7 +67,7 @@ TEST(regression, cli_rejects_out_of_range_for_signed_narrow_type) { << static_cast(result.value().level); } -TEST(regression, cli_rejects_large_negative_for_signed_narrow_type) { +TEST(cli, cli_rejects_large_negative_for_signed_narrow_type) { const char* args[] = {"program", "--level=-200"}; const auto result = rfl::cli::read(2, const_cast(args)); @@ -89,7 +89,7 @@ struct FloatConfig { double rate; }; -TEST(regression, cli_float_parsing_ignores_locale) { +TEST(cli, cli_float_parsing_ignores_locale) { // Save current locale const char* old_locale = std::setlocale(LC_NUMERIC, nullptr); std::string saved_locale = old_locale ? old_locale : "C"; diff --git a/tests/cli/test_settings_macro.cpp b/tests/cli/test_settings_macro.cpp index d30d28e27..5aecdbaf0 100644 --- a/tests/cli/test_settings_macro.cpp +++ b/tests/cli/test_settings_macro.cpp @@ -13,7 +13,7 @@ struct DemoSettings { RFL_SETTINGS_OPS(DemoSettings) }; -TEST(settings_macro, single_field_replace_returns_new_copy) { +TEST(cli, single_field_replace_returns_new_copy) { const DemoSettings original{}; const auto modified = original.with<&DemoSettings::delimiter>(';'); @@ -25,7 +25,7 @@ TEST(settings_macro, single_field_replace_returns_new_copy) { EXPECT_EQ(modified.quoting, original.quoting); } -TEST(settings_macro, chained_with_calls_apply_all_replacements) { +TEST(cli, chained_with_calls_apply_all_replacements) { const auto modified = DemoSettings{} .with<&DemoSettings::batch_size>(2048) .with<&DemoSettings::delimiter>('|') @@ -38,7 +38,7 @@ TEST(settings_macro, chained_with_calls_apply_all_replacements) { EXPECT_FALSE(modified.quoting); } -TEST(settings_macro, replace_string_field_moves_value) { +TEST(cli, replace_string_field_moves_value) { // The with<> parameter is passed by value, so passing an rvalue lets the // implementation move into make_field. Result must equal the source. std::string moved_in = "moved-in-value"; @@ -47,13 +47,13 @@ TEST(settings_macro, replace_string_field_moves_value) { EXPECT_EQ(modified.null_string, "moved-in-value"); } -TEST(settings_macro, by_name_replace_works_for_each_field) { +TEST(cli, by_name_replace_works_for_each_field) { const auto modified = DemoSettings{}.with<"delimiter">(';'); EXPECT_EQ(modified.delimiter, ';'); EXPECT_EQ(modified.batch_size, 1024); } -TEST(settings_macro, by_name_chained_calls_apply_all_replacements) { +TEST(cli, by_name_chained_calls_apply_all_replacements) { const auto modified = DemoSettings{} .with<"batch_size">(4096) .with<"delimiter">('\t') @@ -65,7 +65,7 @@ TEST(settings_macro, by_name_chained_calls_apply_all_replacements) { EXPECT_FALSE(modified.quoting); } -TEST(settings_macro, by_name_and_by_ptm_are_interchangeable) { +TEST(cli, by_name_and_by_ptm_are_interchangeable) { const auto by_ptm = DemoSettings{}.with<&DemoSettings::delimiter>(';'); const auto by_name = DemoSettings{}.with<"delimiter">(';'); EXPECT_EQ(by_ptm.delimiter, by_name.delimiter); From 1af6f8c741c03c7402282e0365e4ca9d13458d23 Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Tue, 28 Jul 2026 18:47:51 +0200 Subject: [PATCH 3/3] Added tests for wstring --- tests/cli/test_wide_char.cpp | 110 +++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/tests/cli/test_wide_char.cpp b/tests/cli/test_wide_char.cpp index 0ceb09094..f2c74e22e 100644 --- a/tests/cli/test_wide_char.cpp +++ b/tests/cli/test_wide_char.cpp @@ -13,18 +13,37 @@ struct Config { std::optional verbose; }; +struct ConfigWstring { + std::wstring host; + int port; + std::optional rate; + std::optional verbose; +}; + struct ShortConfig { std::string host; rfl::Short<"p", int> port; rfl::Short<"v", bool> verbose; }; +struct ShortConfigWstring { + std::wstring host; + rfl::Short<"p", int> port; + rfl::Short<"v", bool> verbose; +}; + struct PositionalConfig { rfl::Positional input_file; rfl::Positional output_file; bool verbose; }; +struct PositionalConfigWstring { + rfl::Positional input_file; + rfl::Positional output_file; + bool verbose; +}; + TEST(cli, test_read_wide_basic) { const wchar_t* args[] = {L"program", L"--host=localhost", L"--port=8080", L"--rate=1.5", L"--verbose"}; @@ -116,6 +135,97 @@ TEST(cli, test_read_wide_valid_ascii) { EXPECT_EQ(result.value().port, 8080); } +TEST(cli, test_read_wstring_basic) { + const wchar_t* args[] = {L"program", L"--host=localhost", L"--port=8080", + L"--rate=1.5", L"--verbose"}; + const auto result = rfl::cli::read(5, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + EXPECT_EQ(result.value().host, L"localhost"); + EXPECT_EQ(result.value().port, 8080); + ASSERT_TRUE(result.value().rate); + EXPECT_DOUBLE_EQ(*result.value().rate, 1.5); + ASSERT_TRUE(result.value().verbose); + EXPECT_TRUE(*result.value().verbose); +} + +TEST(cli, test_read_wstring_empty) { + const wchar_t* args[] = {L"program"}; + const auto result = rfl::cli::read(1, const_cast(args)); + ASSERT_FALSE(result); +} + +TEST(cli, test_read_wstring_bool_true) { + const wchar_t* args[] = {L"program", L"--host=localhost", L"--port=1", + L"--rate=0", L"--verbose"}; + const auto result = rfl::cli::read(5, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + ASSERT_TRUE(result.value().verbose); + EXPECT_TRUE(*result.value().verbose); +} + +TEST(cli, test_read_wstring_bool_false) { + const wchar_t* args[] = {L"program", L"--host=localhost", L"--port=1", + L"--rate=0", L"--verbose=false"}; + const auto result = rfl::cli::read(5, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + ASSERT_TRUE(result.value().verbose); + EXPECT_FALSE(*result.value().verbose); +} + +TEST(cli, test_read_wstring_short_option) { + const wchar_t* args[] = {L"program", L"--host=localhost", L"-p", L"9090", + L"-v"}; + const auto result = + rfl::cli::read(5, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + EXPECT_EQ(result.value().host, L"localhost"); + EXPECT_EQ(result.value().port(), 9090); + EXPECT_TRUE(result.value().verbose()); +} + +TEST(cli, test_read_wstring_short_equals) { + const wchar_t* args[] = {L"program", L"--host=localhost", L"-p=443", L"-v"}; + const auto result = + rfl::cli::read(4, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + EXPECT_EQ(result.value().port(), 443); +} + +TEST(cli, test_read_wstring_positional) { + const wchar_t* args[] = {L"program", L"in.txt", L"out.txt", L"--verbose"}; + const auto result = + rfl::cli::read(4, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + EXPECT_EQ(result.value().input_file(), L"in.txt"); + EXPECT_EQ(result.value().output_file(), L"out.txt"); + EXPECT_TRUE(result.value().verbose); +} + +TEST(cli, test_read_wstring_conflict_named_short) { + const wchar_t* args[] = {L"program", L"--host=localhost", L"--port=8080", + L"-p", L"9090"}; + const auto result = + rfl::cli::read(5, const_cast(args)); + ASSERT_FALSE(result); +} + +TEST(cli, test_read_wstring_negative_value) { + const wchar_t* args[] = {L"program", L"--host=localhost", L"-p", L"-42", + L"-v"}; + const auto result = + rfl::cli::read(5, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + EXPECT_EQ(result.value().port(), -42); +} + +TEST(cli, test_read_wstring_valid_ascii) { + const wchar_t* args[] = {L"program", L"--host=localhost", L"--port=8080"}; + const auto result = rfl::cli::read(3, const_cast(args)); + ASSERT_TRUE(result) << result.error().what(); + EXPECT_EQ(result.value().host, L"localhost"); + EXPECT_EQ(result.value().port, 8080); +} + TEST(cli, test_parse_argv_wide_basic) { const wchar_t* args[] = {L"program", L"--host=localhost", L"-p=8080", L"positional_arg"};