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
33 changes: 33 additions & 0 deletions include/rfl/cli/parse_argv.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <vector>

#include "../Result.hpp"
#include "../internal/strings/utf8_conversions.hpp"
#include "resolve_args.hpp"

namespace rfl::cli {
Expand Down Expand Up @@ -120,6 +121,38 @@ inline rfl::Result<ParsedArgs> 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<ParsedArgs> 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<std::string> narrow_argv;
narrow_argv.reserve(argc);
std::vector<char*> 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
30 changes: 30 additions & 0 deletions include/rfl/cli/read.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -32,6 +33,35 @@ rfl::Result<T> 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 <class T, class... Ps>
rfl::Result<T> read(int argc, wchar_t* argv[]) {
if (argc < 0 || (argc > 0 && !argv)) {
return error("Invalid argc/argv.");
}
std::vector<std::string> narrow_argv;
narrow_argv.reserve(argc);
std::vector<char*> 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<T, Ps...>(argc, narrow_argv_ptrs.data());
}

} // namespace rfl::cli

#endif
10 changes: 5 additions & 5 deletions tests/cli/test_regression_bugs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Config>(2, const_cast<char**>(args));
EXPECT_FALSE(result)
Expand All @@ -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<Config>(2, const_cast<char**>(args));
EXPECT_FALSE(result)
Expand All @@ -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<SignedConfig>(2, const_cast<char**>(args));
Expand All @@ -67,7 +67,7 @@ TEST(regression, cli_rejects_out_of_range_for_signed_narrow_type) {
<< static_cast<int>(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<SignedConfig>(2, const_cast<char**>(args));
Expand All @@ -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";
Expand Down
12 changes: 6 additions & 6 deletions tests/cli/test_settings_macro.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>(';');
Expand All @@ -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>('|')
Expand All @@ -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";
Expand All @@ -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')
Expand All @@ -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);
Expand Down
Loading
Loading