diff --git a/TODO.md b/TODO.md index caa9cce..5ae7696 100644 --- a/TODO.md +++ b/TODO.md @@ -2,8 +2,9 @@ - [x] `validate()` — post-parse validation hook on the parsed struct - [ ] `visit_subcommand()` — visitor dispatch over matched subcommands -- [ ] `range` / `choice` — built-in validation constraints -- [ ] pipeline `action` types — validate action types and show clear error messages +- [x] `range` / `choice` — built-in validation constraints +- [x] pipeline `action` types — validate action types and show clear error messages - [x] env fallback — populate unprovided options from environment variables - [ ] completion — shell completion script generation - [x] short cluster / attached value — `-xvf`, `-ofile` +- [ ] `nargs::rest` — consume all remaining tokens as values (`min == max == -1`), disabling further option/subcommand detection for external-command style tails diff --git a/docs/docs/actions.mdx b/docs/docs/actions.mdx index 39b0289..a89a92e 100644 --- a/docs/docs/actions.mdx +++ b/docs/docs/actions.mdx @@ -249,8 +249,47 @@ cli::ActionCtx ctx{.arg = std::ref(storage)}; ## Defining Custom Pipeline Steps -A pipeline step is a `constexpr`-constructible struct with an `operator()` and -three type-level traits. cli20 provides two deduction helpers — +For one-off transformations, use `cli::action::custom`. It wraps a +`constexpr` callable as a non-terminal action step: + +```cpp +constexpr auto jobs_or_nproc = + [](cli::ActionCtx ctx, + cli::ActionResult> input) + -> cli::ActionResult { + if (!input.have_value()) return cli::fail(input.error); + + if (input.value.has_value()) { + return cli::conversion::integer.invoke( + ctx, cli::ok(std::string_view(*input.value))); + } + + return cli::ok(4); // stand-in for a runtime nproc query +}; + +cli::Arg<"jobs", 'j', + cli::action::custom + | cli::validation::range<1, 64> + | cli::pack::set_once, + cli::nargs::zero_or_one> + jobs{{.help = "Parallel job count", .default_value = 1}}; +``` + +The callable must have this shape: + +```cpp +ActionResult fn(ActionCtx ctx, ActionResult input); +``` + +`T` is the current pipeline value and `U` becomes the next pipeline value. Since +`action::custom` is non-terminal, it does not store anything by itself; it must +eventually be followed by a pack action such as `pack::set_once` or +`pack::push`. + +Use a reusable struct when the action is part of your API, needs template +parameters, or should be documented/tested independently. A pipeline step is a +`constexpr`-constructible struct with an `operator()` and three type-level +traits. cli20 provides two deduction helpers — `cli::deduce_accepts_input` and `cli::deduce_after_type` — that derive two of those traits from `operator()`, so you only write what is unique to your step. diff --git a/docs/docs/examples/advanced.mdx b/docs/docs/examples/advanced.mdx index e6bf829..d704e5f 100644 --- a/docs/docs/examples/advanced.mdx +++ b/docs/docs/examples/advanced.mdx @@ -14,7 +14,8 @@ This is the reference example for explicit action-pipeline composition. Use it when the sugar aliases are close but not quite enough. It is the page to open when you need to understand the lower-level building -blocks behind cli20's higher-level aliases. +blocks behind cli20's higher-level aliases. It also demonstrates +`cli::action::custom` for a one-off lambda-based action step. ## Source @@ -66,4 +67,5 @@ Things to try: ## Related Examples - [Validation](/docs/examples/validation) +- [Custom Action](/docs/examples/custom-action) - [Bind](/docs/examples/bind) diff --git a/docs/docs/examples/compiler.mdx b/docs/docs/examples/compiler.mdx index 2370870..f3b8273 100644 --- a/docs/docs/examples/compiler.mdx +++ b/docs/docs/examples/compiler.mdx @@ -1,6 +1,6 @@ --- -sidebar_position: 8 -title: 7. Compiler Wrapper +sidebar_position: 9 +title: 9. Compiler Wrapper --- import {ExampleSource} from '@site/src/components/ExampleDoc'; diff --git a/docs/docs/examples/custom-action.mdx b/docs/docs/examples/custom-action.mdx new file mode 100644 index 0000000..705941c --- /dev/null +++ b/docs/docs/examples/custom-action.mdx @@ -0,0 +1,65 @@ +--- +sidebar_position: 7 +title: 7. Custom Action +--- + +import {ExampleSource} from '@site/src/components/ExampleDoc'; +import {ExamplePlayground} from '@site/src/components/ExamplePlayground'; + +# Custom Action + +This example shows the full struct-based form for defining a reusable action +pipeline step. Use this pattern when a conversion or validation should become +part of your program's reusable vocabulary. + +For one-off transformations, prefer `cli::action::custom` as shown in the +[Advanced](./advanced) example. For reusable actions, define a struct with: + +- `accepts_input` +- `after_type` +- `storage_type` +- `operator()(ActionCtx, ActionResult)` + +## Source + + + +## What This Example Shows + +The example defines `HHMMConversion`, a custom conversion from `HH:MM` strings +to a `MinutesSinceMidnight` value. The action is then wrapped as: + +```cpp +inline constexpr auto hhmm = cli::Action{}; +``` + +and reused by two options: + +```cpp +cli::Arg<"start", 's', hhmm | cli::pack::set_once> start; +cli::Arg<"end", 'e', hhmm | cli::pack::set_once> end; +``` + +## Try It + + + +Things to try: + +- **Happy path** — `--start 09:30 --end 17:45` +- **Invalid format** — `--start 9am --end 17:45` +- **Out of range** — `--start 24:00 --end 17:45` +- **Missing required option** — `--start 09:30` +- **Help** — `-h` or `--help` + +## Related Examples + +- [Advanced](./advanced) +- [Validation](./validation) diff --git a/docs/docs/examples/distribution-and-platform.mdx b/docs/docs/examples/distribution-and-platform.mdx index 4dc121c..aac0f6b 100644 --- a/docs/docs/examples/distribution-and-platform.mdx +++ b/docs/docs/examples/distribution-and-platform.mdx @@ -1,6 +1,6 @@ --- -sidebar_position: 9 -title: 8. Distribution and Platform Examples +sidebar_position: 10 +title: 10. Distribution and Platform Examples --- import {ExampleSource} from '@site/src/components/ExampleDoc'; diff --git a/docs/docs/examples/env-fallback.mdx b/docs/docs/examples/env-fallback.mdx index 182f06b..d849a20 100644 --- a/docs/docs/examples/env-fallback.mdx +++ b/docs/docs/examples/env-fallback.mdx @@ -1,6 +1,6 @@ --- -sidebar_position: 10 -title: 9. Env Fallback +sidebar_position: 11 +title: 11. Env Fallback --- import {ExampleSource} from '@site/src/components/ExampleDoc'; diff --git a/docs/docs/examples/git.mdx b/docs/docs/examples/git.mdx new file mode 100644 index 0000000..2ee0bac --- /dev/null +++ b/docs/docs/examples/git.mdx @@ -0,0 +1,69 @@ +--- +sidebar_position: 8 +title: 8. Git +--- + +import {ExampleSource} from '@site/src/components/ExampleDoc'; +import {ExamplePlayground} from '@site/src/components/ExamplePlayground'; + +# Git + +This example mirrors the core behavior of clap's `git.rs` example using cli20's +typed subcommand model. It demonstrates a command-heavy CLI with nested +subcommands and optional positional interpretation. + +## Source + + + +## What This Example Shows + +The top-level command provides: + +- `clone ` +- `diff [base] [head] [path] [--color[=WHEN]]` +- `push ` +- `add ...` +- `stash`, with nested `push`, `pop`, and `apply` + +The `diff` command follows the clap example's shifting behavior: + +- no positional args: `stage..worktree` +- one positional arg: treat it as `path` +- two positional args: treat them as `base` and `path` +- three positional args: treat them as `base`, `head`, and `path` + +The clap example also allows external subcommands. cli20 does not currently +model external subcommands, so this example focuses on the declared command +tree. + +## Try It + + + +Things to try: + +- **Clone** — `clone https://example.com/repo.git` +- **Diff defaults** — `diff` +- **Diff path only** — `diff src/main.cc` +- **Diff base/path** — `diff main src/main.cc` +- **Diff full form** — `diff main feature src/main.cc --color=always` +- **Push** — `push origin` +- **Add paths** — `add src/main.cc include/cli/action.hh` +- **Default stash push** — `stash -m wip` +- **Explicit stash push** — `stash push -m wip` +- **Stash pop** — `stash pop stash@{0}` +- **Stash apply** — `stash apply stash@{1}` +- **Help** — `--help`, `diff --help`, or `stash --help` + +## Related Examples + +- [Subcommands](./subcommand) +- [Advanced](./advanced) diff --git a/docs/docs/examples/index.mdx b/docs/docs/examples/index.mdx index 23283ce..cd0c31a 100644 --- a/docs/docs/examples/index.mdx +++ b/docs/docs/examples/index.mdx @@ -16,19 +16,23 @@ Each one is small enough to copy into a project, but specific enough to answer The smallest useful typed-schema CLI: help, a flag, an option, and a required positional. 2. [Subcommands](./subcommand) Nested commands with their own help output and behavior. -3. [Validation](./validation) +3. [Validation](./validation) Parse-time checks for numbers, names, and filesystem arguments. -4. [Bind](./bind) +4. [Bind](./bind) Write values directly into existing variables. -5. [Callback Entry Point](./callback-entry-point) +5. [Callback Entry Point](./callback-entry-point) Fill application state during parsing, then call the real entry point. -6. [Advanced](./advanced) +6. [Advanced](./advanced) Lower-level composition with explicit conversion, validation, and packing steps. -7. [Compiler Wrapper](./compiler) +7. [Custom Action](./custom-action) + Full struct-based action definition with `accepts_input`, `after_type`, and `storage_type`. +8. [Git](./git) + A command-heavy CLI inspired by clap's git example, including nested subcommands. +9. [Compiler Wrapper](./compiler) Short-cluster flags, negatable key-value options, and `std::map` storage with `insert_or_assign`. -8. [Distribution and Platform Examples](./distribution-and-platform) +10. [Distribution and Platform Examples](./distribution-and-platform) Single-header usage, C++20 modules, and the Windows Unicode sample used by CI. -9. [Env Fallback](./env-fallback) +11. [Env Fallback](./env-fallback) Populate options from environment variables with the same conversion and validation pipeline. ## Building diff --git a/docs/docs/tutorial.md b/docs/docs/tutorial.md index 323622b..92598eb 100644 --- a/docs/docs/tutorial.md +++ b/docs/docs/tutorial.md @@ -203,3 +203,6 @@ The pipeline is split into three conceptual layers: | Packing | `cli::pack::*` | Store the final value into the field | Use the sugar API for the common case and drop down to `Arg` only when you need stronger validation or custom conversion behavior. + +For one-off lambda-based steps and reusable struct-based actions, see +[Actions & Pipelines: Defining Custom Pipeline Steps](./actions#defining-custom-pipeline-steps). diff --git a/examples/advanced.cc b/examples/advanced.cc index 9cee0d2..0886a11 100644 --- a/examples/advanced.cc +++ b/examples/advanced.cc @@ -14,27 +14,16 @@ auto nproc_dummy() -> int { return 4; } -struct JobsOrNproc { - template - static constexpr bool accepts_input = - cli::deduce_accepts_input; - - template - using after_type = cli::deduce_after_type; - - template - using storage_type = void; - - auto operator()(cli::ActionCtx ctx, - cli::ActionResult> input) const - -> cli::ActionResult { - if (!input.have_value()) return cli::propagate(input); - if (input.value.has_value()) { - return cli::conversion::integer.invoke( - ctx, cli::ActionResult::ok(input.value.value())); - } - return cli::ActionResult::ok(nproc_dummy()); +constexpr auto jobs_or_nproc = + [](cli::ActionCtx ctx, + cli::ActionResult> input) + -> cli::ActionResult { + if (!input.have_value()) return cli::fail(input.error); + if (input.value.has_value()) { + return cli::conversion::integer.invoke( + ctx, cli::ActionResult::ok(input.value.value())); } + return cli::ActionResult::ok(nproc_dummy()); }; struct Args { @@ -52,7 +41,7 @@ struct Args { config{{.help = "Configuration file", .presence = cli::required}}; cli::Arg<"jobs", 'j', - cli::Action{} | cli::validation::range<1, 64> | + cli::action::custom | cli::validation::range<1, 64> | cli::pack::set_once, cli::nargs::zero_or_one> jobs_arg{{.help = "Parallel job count (default 1; -j uses nproc_dummy())", diff --git a/examples/custom_action.cc b/examples/custom_action.cc new file mode 100644 index 0000000..f9aaad7 --- /dev/null +++ b/examples/custom_action.cc @@ -0,0 +1,119 @@ +#include +#include +#include +#include +#include +#include + +#include "cli/argument.hh" +#include "cli/parser.hh" + +struct MinutesSinceMidnight { + int value{}; +}; + +template <> +struct std::formatter { + constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); } + + auto format(MinutesSinceMidnight time, std::format_context& ctx) const { + const int hours = time.value / 60; + const int minutes = time.value % 60; + return std::format_to(ctx.out(), "{:02}:{:02}", hours, minutes); + } +}; + +struct HHMMConversion { + template + static constexpr bool accepts_input = + cli::deduce_accepts_input; + + template + using after_type = cli::deduce_after_type; + + template + using storage_type = void; + + auto operator()(cli::ActionCtx ctx, + cli::ActionResult input) const + -> cli::ActionResult { + if (!input.have_value()) { + return cli::fail(input.error); + } + + const std::string_view text = input.value; + const auto colon = text.find(':'); + if (colon == text.npos) { + return invalid(ctx, text, "expected HH:MM"); + } + + int hours{}; + int minutes{}; + if (!parse_int(text.substr(0, colon), hours) || + !parse_int(text.substr(colon + 1), minutes)) { + return invalid(ctx, text, "hours and minutes must be integers"); + } + if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { + return invalid(ctx, text, "expected a time between 00:00 and 23:59"); + } + + return cli::ok(MinutesSinceMidnight{.value = hours * 60 + minutes}); + } + + private: + static auto parse_int(std::string_view text, int& out) -> bool { + if (text.empty()) return false; + const auto* first = text.data(); + const auto* last = text.data() + text.size(); + const auto [ptr, ec] = std::from_chars(first, last, out); + return ec == std::errc{} && ptr == last; + } + + static auto invalid(cli::ActionCtx ctx, std::string_view subject, + std::string detail) + -> cli::ActionResult { + return cli::fail(cli::ParseError{ + .code = cli::ErrorCode::invalid_value, + .kind = cli::ErrorKind::conversion, + .position = static_cast(ctx.index), + .subject = std::string(subject), + .detail = std::move(detail), + }); + } +}; + +inline constexpr auto hhmm = cli::Action{}; + +struct Args { + cli::Description description{ + "Custom action example using a reusable struct-based conversion."}; + + cli::Arg<"start", 's', hhmm | cli::pack::set_once> start{ + {.help = "Start time in HH:MM", .presence = cli::required}}; + + cli::Arg<"end", 'e', hhmm | cli::pack::set_once> end{ + {.help = "End time in HH:MM", .presence = cli::required}}; +}; + +auto print_time(MinutesSinceMidnight time) -> void { + const int hours = time.value / 60; + const int minutes = time.value % 60; + if (hours < 10) std::cout << '0'; + std::cout << hours << ':'; + if (minutes < 10) std::cout << '0'; + std::cout << minutes; +} + +auto main(int argc, char* argv[]) -> int { + const auto args = cli::parse_or_exit(argc, argv); + + std::cout << "start: "; + print_time(*args.start); + std::cout << " (" << args.start->value << " minutes)\n"; + + std::cout << "end: "; + print_time(*args.end); + std::cout << " (" << args.end->value << " minutes)\n"; + + return 0; +} diff --git a/examples/git.cc b/examples/git.cc new file mode 100644 index 0000000..c47d281 --- /dev/null +++ b/examples/git.cc @@ -0,0 +1,183 @@ +#include +#include +#include +#include +#include +#include + +#include "cli/argument.hh" +#include "cli/parser.hh" + +namespace fs = std::filesystem; + +constexpr auto is_color_when = [](const std::string& value) { + return value == "always" || value == "auto" || value == "never"; +}; + +struct CloneArgs { + cli::Description description{"Clones repos."}; + cli::Help<> help; + cli::Positional remote{ + {.help = "The remote to clone", .presence = cli::required}}; +}; + +struct DiffArgs { + cli::Description description{"Compare two commits."}; + cli::Help<> help; + + cli::Arg<"color", + cli::conversion::default_missing_value<"always"> | + cli::conversion::string | + cli::validation::predicate | cli::pack::set_once, + cli::nargs::zero_or_one> + color{{.help = "When to use color: always, auto, or never", + .default_value = std::string{"auto"}}}; + + cli::Positional base{{.help = "Base commit"}}; + cli::Positional head{{.help = "Head commit"}}; + cli::Positional path{{.help = "Path to diff"}}; +}; + +struct PushArgs { + cli::Description description{"Pushes things."}; + cli::Help<> help; + cli::Positional remote{ + {.help = "The remote to target", .presence = cli::required}}; +}; + +struct AddArgs { + cli::Description description{"Adds things."}; + cli::Help<> help; + cli::Positional paths{ + {.help = "Stuff to add", .presence = cli::required}}; +}; + +struct StashPushArgs { + cli::Description description{"Push a new stash."}; + cli::Help<> help; + cli::StringOption<"message", 'm'> message{{.help = "Stash message"}}; +}; + +struct StashPopArgs { + cli::Description description{"Pop a stash."}; + cli::Help<> help; + cli::Positional stash{{.help = "Stash reference"}}; +}; + +struct StashApplyArgs { + cli::Description description{"Apply a stash."}; + cli::Help<> help; + cli::Positional stash{{.help = "Stash reference"}}; +}; + +struct StashArgs { + cli::Description description{"Manage the stash."}; + cli::Help<> help; + cli::StringOption<"message", 'm'> message{{.help = "Stash message"}}; + cli::Command<"push", StashPushArgs> push{{.help = "Push a new stash"}}; + cli::Command<"pop", StashPopArgs> pop{{.help = "Pop a stash"}}; + cli::Command<"apply", StashApplyArgs> apply{{.help = "Apply a stash"}}; +}; + +struct Args { + cli::Description description{"A fictional versioning CLI."}; + cli::Help<> help; + cli::Command<"clone", CloneArgs> clone{{.help = "Clones repos"}}; + cli::Command<"diff", DiffArgs> diff{{.help = "Compare two commits"}}; + cli::Command<"push", PushArgs> push{{.help = "Pushes things"}}; + cli::Command<"add", AddArgs> add{{.help = "Adds things"}}; + cli::Command<"stash", StashArgs> stash{{.help = "Manage the stash"}}; +}; + +auto optional_string(const auto& arg) -> std::optional { + if (!arg) return std::nullopt; + return std::string_view(*arg); +} + +auto print_paths(const std::vector& paths) -> void { + std::cout << "["; + for (std::size_t i = 0; i < paths.size(); ++i) { + if (i != 0) std::cout << ", "; + std::cout << '"' << paths[i].string() << '"'; + } + std::cout << "]"; +} + +auto main(int argc, char* argv[]) -> int { + const auto args = cli::parse_or_exit(argc, argv); + + if (args.clone.provided()) { + std::cout << "Cloning " << *args.clone.remote << '\n'; + return 0; + } + + if (args.diff.provided()) { + auto base = optional_string(args.diff.base); + auto head = optional_string(args.diff.head); + auto path = optional_string(args.diff.path); + + if (!path) { + path = head; + head = std::nullopt; + if (!path) { + path = base; + base = std::nullopt; + } + } + + std::cout << "Diffing " << base.value_or("stage") << ".." + << head.value_or("worktree") << " " << path.value_or("") + << " (color=" << args.diff.color.value() << ")\n"; + return 0; + } + + if (args.push.provided()) { + std::cout << "Pushing to " << *args.push.remote << '\n'; + return 0; + } + + if (args.add.provided()) { + std::cout << "Adding "; + print_paths(args.add.paths.value()); + std::cout << '\n'; + return 0; + } + + if (args.stash.provided()) { + if (args.stash.apply.provided()) { + std::cout << "Applying "; + if (args.stash.apply.stash) { + std::cout << '"' << *args.stash.apply.stash << '"'; + } else { + std::cout << "null"; + } + std::cout << '\n'; + return 0; + } + + if (args.stash.pop.provided()) { + std::cout << "Popping "; + if (args.stash.pop.stash) { + std::cout << '"' << *args.stash.pop.stash << '"'; + } else { + std::cout << "null"; + } + std::cout << '\n'; + return 0; + } + + const auto& message = args.stash.push.provided() ? args.stash.push.message + : args.stash.message; + std::cout << "Pushing "; + if (message) { + std::cout << '"' << *message << '"'; + } else { + std::cout << "null"; + } + std::cout << '\n'; + return 0; + } + + std::cerr << "No command provided. Try --help.\n"; + return 1; +} diff --git a/examples/unicode_echo.cc b/examples/unicode_echo.cc index fd86802..e5c3a9d 100644 --- a/examples/unicode_echo.cc +++ b/examples/unicode_echo.cc @@ -2,7 +2,8 @@ // Parses --name and positional file arguments, then echoes each value to // stdout in "key=value\n" format so CI scripts can compare output. // -// Build and run via wmain on Windows; falls back to main elsewhere. +// Windows converts wmain's UTF-16 argv explicitly, then uses the normal UTF-8 +// parser path. Non-Windows builds use main directly. #include @@ -17,10 +18,12 @@ struct Args { #ifdef _WIN32 auto wmain(int argc, wchar_t* argv[]) -> int { + auto argv_utf8 = cli::utf16_to_utf8(argc, argv); + const auto args = cli::parse_or_exit(argv_utf8.size(), argv_utf8.data()); #else auto main(int argc, char* argv[]) -> int { -#endif const auto args = cli::parse_or_exit(argc, argv); +#endif if (args.name) { std::cout << "name=" << *args.name << '\n'; diff --git a/include/cli/action.hh b/include/cli/action.hh index 9fdf0a5..1af0f46 100644 --- a/include/cli/action.hh +++ b/include/cli/action.hh @@ -183,34 +183,51 @@ struct ActionResult { }; /** - * @brief Propagates a non-ok ActionResult to a different value type. + * @brief Helper functions for constructing `ActionResult` objects with type + * deduction. * - * This preserves error versus ignore state while adapting the held value type. + * @tparam T The value type of the `ActionResult` to construct. + * @param value The value to store in a success result. + * @return A successful `ActionResult` holding `value`. + */ +template +[[nodiscard]] constexpr auto ok(T&& value) + -> ActionResult> { + return ActionResult>::ok(std::forward(value)); +} + +/** + * @brief Helper function for constructing a successful `ActionResult`. + * + * @return A successful `ActionResult`. + */ +[[nodiscard]] constexpr auto ok() -> ActionResult { + return ActionResult::ok(); +} + +/** + * @brief Helper function for constructing a failure `ActionResult` with type + * deduction. * - * @tparam U The target value type. - * @tparam T The source value type. - * @param input The source result to propagate. - * @return An `ActionResult` carrying the same error or ignore state. + * @tparam T The value type of the `ActionResult` to construct. + * @param error The error to store in the failure result. + * @return A failed `ActionResult` carrying `error`. */ -template -[[nodiscard]] constexpr auto propagate(const ActionResult& input) - -> ActionResult { - if (input.has_error()) return ActionResult::fail(input.error); - return ActionResult::ignore(); +template +[[nodiscard]] constexpr auto fail(ParseError error) -> ActionResult { + return ActionResult::fail(std::move(error)); } /** - * @brief Propagates a non-ok ActionResult to a different value type. + * @brief Helper function for constructing an ignore `ActionResult` with type + * deduction. * - * @tparam U The target value type. - * @param input The source result to propagate. - * @return An `ActionResult` carrying the same error or ignore state. + * @tparam T The value type of the `ActionResult` to construct. + * @return An ignored `ActionResult`. */ -template -[[nodiscard]] constexpr auto propagate(const ActionResult& input) - -> ActionResult { - if (input.has_error()) return ActionResult::fail(input.error); - return ActionResult::ignore(); +template +[[nodiscard]] constexpr auto ignore() -> ActionResult { + return ActionResult::ignore(); } /** @@ -416,27 +433,23 @@ struct Action { template [[nodiscard]] static constexpr auto invoke(ActionCtx& ctx, ActionResult input) { - return []( - ActionCtx& ctx, - ActionResult input) -> decltype(auto) { - using Head = std::remove_cvref_t; - using Next = typename Head::template after_type; - - if (input.is_ignored()) { - if constexpr (sizeof...(FnTail) == 0) { - return ActionResult::ignore(); - } else { - return Action::invoke(ctx, ActionResult::ignore()); - } - } - - auto next = FnHead(ctx, std::move(input)); - if constexpr (sizeof...(FnTail) == 0) { - return next; - } else { - return Action::invoke(ctx, std::move(next)); - } - }.template operator()(ctx, std::move(input)); + return + []( + ActionCtx& ctx, ActionResult input) -> decltype(auto) { + if (input.is_ignored()) { + using FinalIdentity = std::remove_cvref_t( + Action::template validate_impl()))>; + return ActionResult::ignore(); + } + + auto next = FnHead(ctx, std::move(input)); + if constexpr (sizeof...(FnTail) == 0) { + return next; + } else { + return Action::invoke(ctx, std::move(next)); + } + }.template operator()(ctx, std::move(input)); } template @@ -446,15 +459,11 @@ struct Action { return []( const ActionCtx& ctx, ActionResult input) -> decltype(auto) { - using Head = std::remove_cvref_t; - using Next = typename Head::template after_type; - if (input.is_ignored()) { - if constexpr (sizeof...(FnTail) == 0) { - return ActionResult::ignore(); - } else { - return Action::invoke(ctx, ActionResult::ignore()); - } + using FinalIdentity = std::remove_cvref_t( + Action::template validate_impl()))>; + return ActionResult::ignore(); } auto next = FnHead(ctx, std::move(input)); @@ -466,26 +475,31 @@ struct Action { }.template operator()(ctx, std::move(input)); } - template + template static constexpr auto validate_impl() { static_assert( - sizeof...(FnTail) != 0 || - !std::same_as::template storage_type, - void>, + !EnableTailCheck || + (sizeof...(FnTail) != 0 || + !std::same_as::template storage_type, + void>), "The last action must have a non-void storage type"); using Head = std::remove_cvref_t; if constexpr (sizeof...(FnTail) == 0) { - return std::make_pair( + return std::make_tuple( Head::template accepts_input, - std::type_identity>{}); + std::type_identity>{}, + std::type_identity>{}); } else { if constexpr (!Head::template accepts_input) { - return std::make_pair(false, std::type_identity{}); + static_assert(Head::template accepts_input, + "Action step does not accept the previous step's output"); + return std::make_tuple(false, std::type_identity{}, + std::type_identity{}); } else { using Next = typename Head::template after_type; - return validate_impl(); + return validate_impl(); } } } @@ -504,9 +518,9 @@ struct Action { using FirstFn = typename FirstFnType::type; if constexpr (FirstFn::template accepts_input< std::optional>) { - return validate_impl, Fns...>(); + return validate_impl, Fns...>(); } else { - return validate_impl(); + return validate_impl(); } } }; @@ -522,7 +536,8 @@ struct Action<> { } static constexpr auto validate() -> auto { - return std::make_pair(true, std::type_identity{}); + return std::make_tuple(true, std::type_identity{}, + std::type_identity{}); } }; @@ -626,7 +641,7 @@ struct Integer { constexpr auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); T result{}; auto r = std::from_chars(input.value.data(), input.value.data() + input.value.size(), result); @@ -649,7 +664,7 @@ struct Integer { "unexpected trailing characters")); } - return ActionResult::ok(result); + return ok(result); } }; @@ -678,7 +693,7 @@ struct Floating { constexpr auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); T result{}; auto r = std::from_chars(input.value.data(), input.value.data() + input.value.size(), result); @@ -701,7 +716,7 @@ struct Floating { "unexpected trailing characters")); } - return ActionResult::ok(result); + return ok(result); } }; @@ -721,8 +736,8 @@ struct String { constexpr auto operator()(ActionCtx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); - return ActionResult::ok(std::string(input.value)); + if (!input.have_value()) return fail(input.error); + return ok(std::string(input.value)); } }; @@ -747,13 +762,13 @@ struct Bool { constexpr auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); const std::string_view val = input.value; if (val == "true" || val == "1") { - return ActionResult::ok(true); + return ok(true); } if (val == "false" || val == "0") { - return ActionResult::ok(false); + return ok(false); } return ActionResult::fail(detail::invalid_value_error( ErrorKind::conversion, ctx.index, std::string(input.value), @@ -777,9 +792,8 @@ struct Path { auto operator()(ActionCtx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); - return ActionResult::ok( - std::filesystem::path{input.value}); + if (!input.have_value()) return fail(input.error); + return ok(std::filesystem::path{input.value}); } }; @@ -803,7 +817,7 @@ struct ExistingFile { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); auto path = std::filesystem::path{input.value}; if (!std::filesystem::exists(path) || !std::filesystem::is_regular_file(path)) { @@ -812,7 +826,7 @@ struct ExistingFile { path.string(), "expected an existing regular file")); } - return ActionResult::ok(std::move(path)); + return ok(std::move(path)); } }; @@ -836,7 +850,7 @@ struct ExistingDirectory { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); auto path = std::filesystem::path{input.value}; if (!std::filesystem::exists(path) || !std::filesystem::is_directory(path)) { return ActionResult::fail( @@ -844,7 +858,7 @@ struct ExistingDirectory { path.string(), "expected an existing directory")); } - return ActionResult::ok(std::move(path)); + return ok(std::move(path)); } }; @@ -873,9 +887,9 @@ struct Choice { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); if (auto value = Mapper(input.value); value.has_value()) { - return ActionResult::ok(*std::move(value)); + return ok(*std::move(value)); } return ActionResult::fail( detail::invalid_choice_error(ctx.index, std::string(input.value))); @@ -930,15 +944,14 @@ struct Negatable { constexpr auto operator()(ActionCtx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); constexpr std::string_view prefix = Prefix.view(); const std::string_view val = input.value; if (val.starts_with(prefix)) { - return ActionResult::ok(NegatableResult{ - .name = std::string(val.substr(prefix.size())), .enabled = false}); + return ok(NegatableResult{.name = std::string(val.substr(prefix.size())), + .enabled = false}); } - return ActionResult::ok( - NegatableResult{.name = std::string(val), .enabled = true}); + return ok(NegatableResult{.name = std::string(val), .enabled = true}); } }; @@ -978,15 +991,15 @@ struct DefaultMissingValue { auto operator()(ActionCtx ctx, ActionResult> input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); if (input.value.has_value()) { // actual value provided: unwrap and pass through - return ActionResult::ok(*input.value); + return ok(std::string_view(*input.value)); } // nullopt: flag invocation if (ctx.nargs == 0) { // pure flag with no value: inject the compile-time default - return ActionResult::ok(Value.view()); + return ok(Value.view()); } // flag seen but a value is coming via separate value invocation: ignore return ActionResult::ignore(); @@ -1462,9 +1475,9 @@ struct SetTrue { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); ctx.arg.get() = true; - return ActionResult::ok(); + return ok(); } }; @@ -1482,9 +1495,9 @@ struct SetFalse { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); ctx.arg.get() = false; - return ActionResult::ok(); + return ok(); } }; @@ -1502,9 +1515,9 @@ struct Toggle { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); ctx.arg.get() = !ctx.arg.get(); - return ActionResult::ok(); + return ok(); } }; @@ -1522,9 +1535,9 @@ struct Increment { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); ++ctx.arg.get(); - return ActionResult::ok(); + return ok(); } }; @@ -1579,13 +1592,13 @@ struct SetOnce { template auto operator()(ActionCtx> ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); if (ctx.occurrences > 1) { return ActionResult::fail(detail::duplicate_argument_error( ctx.index, detail::to_error_subject(input.value))); } ctx.arg.get() = std::move(input.value); - return ActionResult::ok(); + return ok(); } }; @@ -1609,14 +1622,14 @@ struct PushUnique { template auto operator()(ActionCtx>> ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); auto& values = ctx.arg.get(); if (std::ranges::find(values, input.value) != values.end()) { return ActionResult::fail(detail::duplicate_argument_error( ctx.index, detail::to_error_subject(input.value))); } values.push_back(std::move(input.value)); - return ActionResult::ok(); + return ok(); } }; @@ -1640,9 +1653,9 @@ struct Push { template auto operator()(ActionCtx>> ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); ctx.arg.get().push_back(std::move(input.value)); - return ActionResult::ok(); + return ok(); } }; @@ -1667,9 +1680,9 @@ struct Insert { template auto operator()(ActionCtx>> ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); ctx.arg.get().insert(std::move(input.value)); - return ActionResult::ok(); + return ok(); } }; @@ -1697,10 +1710,10 @@ struct InsertOrAssign { template auto operator()(ActionCtx> ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); auto&& [key, value] = input.value; ctx.arg.get().insert_or_assign(key, value); - return ActionResult::ok(); + return ok(); } }; @@ -1720,12 +1733,12 @@ struct Extend { template auto operator()(ActionCtx> ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); auto& out = ctx.arg.get(); for (auto&& value : input.value) { out.emplace_back(value); } - return ActionResult::ok(); + return ok(); } }; @@ -1749,9 +1762,9 @@ struct MarkPresent { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); ctx.arg.get() = true; - return ActionResult::ok(); + return ok(); } }; @@ -1777,10 +1790,10 @@ struct StoreInto { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); + if (!input.have_value()) return fail(input.error); if (T* ptr = ctx.arg.get(); ptr != nullptr) { *ptr = std::move(input.value); - return ActionResult::ok(); + return ok(); } return ActionResult::fail(detail::validation_failed_error( ctx.index, "write_to", @@ -1788,13 +1801,22 @@ struct StoreInto { } }; +template +concept CallbackInvocable = requires(ActionCtx ctx, T& value) { + { Fn(ctx, value) } -> std::same_as; +} || requires(T& value) { + { Fn(value) } -> std::same_as; +} || requires { + { Fn() } -> std::same_as; +}; + /** * @brief Invokes a compile-time callable `Fn` with the parsed value as a * terminal action. * * `Fn` may have any of the following signatures: - * - `(ActionCtx, T value)` — receives context and value. - * - `(T value)` — receives value only. + * - `(ActionCtx, T&)` — receives context and value. + * - `(T&)` — receives value only. * - `()` — receives nothing. * * Storage type: `std::monostate` (no value is stored; the callback is the only @@ -1805,7 +1827,7 @@ struct StoreInto { template struct Callback { template - static constexpr bool accepts_input = true; + static constexpr bool accepts_input = CallbackInvocable; template using after_type = void; @@ -1814,17 +1836,18 @@ struct Callback { using storage_type = std::monostate; template + requires CallbackInvocable auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return propagate(input); - if constexpr (requires { Fn(ctx, input.value); }) { + if (!input.have_value()) return fail(input.error); + if constexpr (requires(T& value) { Fn(ctx, value); }) { Fn(ctx, input.value); - } else if constexpr (requires { Fn(input.value); }) { + } else if constexpr (requires(T& value) { Fn(value); }) { Fn(input.value); } else { Fn(); } - return ActionResult::ok(); + return ok(); } }; @@ -1852,6 +1875,48 @@ inline constexpr auto write_to = Action{}>{}; */ namespace action { +/** + * @brief Wraps a user-defined callable as a non-terminal action pipeline step. + * + * `Fn` must be invocable as: + * + * @code + * ActionResult Fn(ActionCtx ctx, ActionResult input); + * @endcode + * + * for each input type `T` it supports. The returned `ActionResult` becomes + * the input to the next action in the pipeline. + * + * This is the escape hatch for conversions or transformations that do not fit + * the built-in `conversion` or `validation` actions. Unlike `pack::callback`, + * `Custom` does not terminate the pipeline and does not store a value; its + * `storage_type` is always `void`. + * + * @tparam Fn A constexpr callable used to transform the current pipeline + * value. + */ +template +struct Custom { + template + static constexpr bool accepts_input = + std::invocable&, ActionResult>; + + template + using after_type = + typename std::invoke_result_t&, + ActionResult>::value_type; + + template + using storage_type = void; + + template + requires accepts_input + auto operator()(ActionCtx ctx, ActionResult input) const + -> ActionResult> { + return Fn(ctx, std::move(input)); + } +}; + /** * @brief Wraps the current value in a `HelpRequested` sentinel type. * @@ -1872,9 +1937,8 @@ struct PrintHelp { template auto operator()(ActionCtx, ActionResult input) const -> ActionResult> { - if (!input.have_value()) return propagate>(input); - return ActionResult>::ok( - after_type{.value = std::move(input.value)}); + if (!input.have_value()) return fail>(input.error); + return ok(after_type{.value = std::move(input.value)}); } }; @@ -1918,6 +1982,8 @@ struct ExitSuccess { inline constexpr auto print_help = Action{}; inline constexpr auto exit_success = Action{}; +template +inline constexpr auto custom = Action{}>{}; } // namespace action diff --git a/include/cli/argument.hh b/include/cli/argument.hh index 8128593..19d1990 100644 --- a/include/cli/argument.hh +++ b/include/cli/argument.hh @@ -402,7 +402,7 @@ template } struct ArgImpl : public OptionTag { using value_type = - typename std::remove_cvref_t::type; + typename std::remove_cvref_t(A.validate()))>::type; ArgImpl() : ArgImpl(ArgParameter{}) {} ArgImpl(ArgParameter param) : param_(std::move(param)), @@ -573,7 +573,7 @@ template } struct PositionalImpl : public PositionalTag { using value_type = - typename std::remove_cvref_t::type; + typename std::remove_cvref_t(A.validate()))>::type; PositionalImpl() : PositionalImpl(ArgParameter{}) {} PositionalImpl(ArgParameter param) diff --git a/include/cli/parser.hh b/include/cli/parser.hh index 1441a03..3c8896c 100644 --- a/include/cli/parser.hh +++ b/include/cli/parser.hh @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -29,22 +30,65 @@ namespace detail { * @brief Converts a wide-character string to UTF-8. * * Uses `WideCharToMultiByte` with code page `CP_UTF8`. Returns an empty string - * for a null pointer or a zero-length conversion result. + * for a null pointer, an empty string, or a conversion failure. * * @param wide Null-terminated wide-character string, or `nullptr`. * @return The UTF-8 encoded equivalent, or `""` on failure. */ inline auto wide_to_utf8(const wchar_t* wide) -> std::string { if (!wide) return {}; - int len = - WideCharToMultiByte(CP_UTF8, 0, wide, -1, nullptr, 0, nullptr, nullptr); + constexpr DWORD flags = WC_ERR_INVALID_CHARS; + int len = WideCharToMultiByte(CP_UTF8, flags, wide, -1, nullptr, 0, nullptr, + nullptr); if (len <= 1) return {}; std::string result(static_cast(len) - 1, '\0'); - WideCharToMultiByte(CP_UTF8, 0, wide, -1, result.data(), len, nullptr, - nullptr); + const int written = WideCharToMultiByte(CP_UTF8, flags, wide, -1, + result.data(), len, nullptr, nullptr); + if (written != len) return {}; return result; } } // namespace detail + +/** + * @brief Owns UTF-8 strings converted from a Windows UTF-16 `argv`. + * + * The pointer array returned by `data()` is compatible with APIs that accept + * `char* argv[]`, including `cli::parse(argc, argv)`. + */ +struct Utf8Argv { + std::vector storage; + std::vector pointers; + + [[nodiscard]] auto size() const -> int { + return static_cast(pointers.size()); + } + + [[nodiscard]] auto data() -> char** { return pointers.data(); } +}; + +/** + * @brief Converts a Windows UTF-16 `argv` to an owned UTF-8 `argv` wrapper. + * + * This is intentionally separate from parsing. `wmain` entry points can convert + * once and then call the ordinary UTF-8 parser overload: + * + * @code + * auto argv_utf8 = cli::utf16_to_utf8(argc, argv); + * auto args = cli::parse_or_exit(argv_utf8.size(), argv_utf8.data()); + * @endcode + */ +inline auto utf16_to_utf8(int argc, wchar_t* argv[]) -> Utf8Argv { + Utf8Argv result; + result.storage.reserve(static_cast(argc)); + result.pointers.reserve(static_cast(argc)); + for (int i = 0; i < argc; ++i) { + result.storage.emplace_back(detail::wide_to_utf8(argv[i])); + } + for (std::string& arg : result.storage) { + result.pointers.push_back(arg.data()); + } + return result; +} #endif /** @@ -487,8 +531,8 @@ struct ParseResult { * - `parse(initial, ...)` — pre-initialised variants; useful for `BoundOption` / * `on_parse`. * - * On Windows, `wchar_t*` overloads are also provided; they convert arguments to - * UTF-8 via `detail::wide_to_utf8` before processing. + * On Windows, use `cli::utf16_to_utf8(argc, argv)` in `wmain` and pass the + * converted UTF-8 argv to the standard `char*` overload. * * @tparam T The argument specification type (must satisfy `ArgumentSpec`). */ @@ -543,43 +587,6 @@ struct Parser { std::span(args), 1); } -#ifdef _WIN32 - auto parse(int argc, wchar_t* argv[]) -> ParseResult { - std::vector owned; - owned.reserve(static_cast(argc)); - for (int i = 0; i < argc; ++i) { - owned.emplace_back(detail::wide_to_utf8(argv[i])); - } - std::vector args; - args.reserve(owned.size()); - for (const auto& s : owned) { - args.emplace_back(s); - } - if (argc > 0) { - set_program_name(owned[0]); - } - return this->parse(std::span(args), 1); - } - - auto parse(T initial, int argc, wchar_t* argv[]) -> ParseResult { - std::vector owned; - owned.reserve(static_cast(argc)); - for (int i = 0; i < argc; ++i) { - owned.emplace_back(detail::wide_to_utf8(argv[i])); - } - std::vector args; - args.reserve(owned.size()); - for (const auto& s : owned) { - args.emplace_back(s); - } - if (argc > 0) { - set_program_name(owned[0]); - } - return this->parse(std::move(initial), - std::span(args), 1); - } -#endif - /** * @brief Parses a span of string views, skipping `first_index` elements. * @@ -1062,25 +1069,4 @@ auto parse_or_exit(int argc, char* argv[], std::ostream& out = std::cout, return std::move(result.value); } -#ifdef _WIN32 -template -auto parse(int argc, wchar_t* argv[]) -> ParseResult { - return Parser{}.parse(argc, argv); -} - -template -auto parse_or_exit(int argc, wchar_t* argv[], std::ostream& out = std::cout, - std::ostream& err = std::cerr) -> T { - auto result = parse(argc, argv); - if (!result) { - if (const auto message = result.error.message(); !message.empty()) { - auto& stream = result.error.use_stdout() ? out : err; - stream << message << '\n'; - } - std::exit(result.error.exit_code()); - } - return std::move(result.value); -} -#endif - -}; // namespace cli +} // namespace cli diff --git a/tests/test_parse.cc b/tests/test_parse.cc index 0d14ad9..6cada92 100644 --- a/tests/test_parse.cc +++ b/tests/test_parse.cc @@ -38,34 +38,23 @@ struct ParseArgs { auto nproc_dummy() -> int { return 4; } -struct JobsOrNproc { - template - static constexpr bool accepts_input = - cli::deduce_accepts_input; - - template - using after_type = cli::deduce_after_type; - - template - using storage_type = void; - - auto operator()(cli::ActionCtx ctx, - cli::ActionResult> input) const - -> cli::ActionResult { - if (!input.have_value()) return cli::propagate(input); - if (input.value.has_value()) { - return cli::conversion::integer.invoke( - ctx, cli::ActionResult::ok(input.value.value())); - } - return cli::ActionResult::ok(nproc_dummy()); +constexpr auto jobs_or_nproc = + [](cli::ActionCtx ctx, + cli::ActionResult> input) + -> cli::ActionResult { + if (!input.have_value()) return cli::fail(input.error); + if (input.value.has_value()) { + return cli::conversion::integer.invoke( + ctx, cli::ActionResult::ok(input.value.value())); } + return cli::ActionResult::ok(nproc_dummy()); }; struct JobsArgs { cli::StringOption<"config", 'c'> config{ {.help = "Config file", .presence = cli::Presence::required}}; cli::Arg<"jobs", 'j', - cli::Action{} | cli::validation::range<1, 64> | + cli::action::custom | cli::validation::range<1, 64> | cli::pack::set_once, cli::nargs::zero_or_one> jobs_arg{{.help = "Parallel job count (default 1; -j uses nproc_dummy())", @@ -74,29 +63,18 @@ struct JobsArgs { inline std::vector* observed_total_values = nullptr; -struct RecordTotalValues { - template - static constexpr bool accepts_input = true; - - template - using after_type = Input; - - template - using storage_type = void; - - template - auto operator()(cli::ActionCtx ctx, cli::ActionResult input) const - -> cli::ActionResult { - if (observed_total_values) { - observed_total_values->push_back(ctx.total_values); - } - return input; +constexpr auto record_total_values = + [](cli::ActionCtx ctx, + cli::ActionResult input) -> cli::ActionResult { + if (observed_total_values) { + observed_total_values->push_back(ctx.total_values); } + return input; }; struct TotalValuesArgs { cli::Arg<"mode", 'm', - cli::Action{} | + cli::action::custom | cli::conversion::default_missing_value<"auto"> | cli::pack::set_once, cli::nargs::zero_or_one> @@ -270,8 +248,10 @@ struct UnicodeArgs { // ASCII via wide argv — basic sanity check. TEST(ParseWide, AsciiRoundtrip) { auto args = wide_argv({L"prog", L"--name", L"hello"}); - auto result = cli::Parser{}.parse(static_cast(args.size()), - args.data()); + auto argv_utf8 = + cli::utf16_to_utf8(static_cast(args.size()), args.data()); + auto result = + cli::Parser{}.parse(argv_utf8.size(), argv_utf8.data()); ASSERT_TRUE(result.has_value()); ASSERT_TRUE(result.value.name); @@ -281,8 +261,10 @@ TEST(ParseWide, AsciiRoundtrip) { // Japanese: テスト (U+30C6 U+30B9 U+30C8) — 3-byte UTF-8 sequences. TEST(ParseWide, JapaneseOption) { auto args = wide_argv({L"prog", L"--name", L"\u30C6\u30B9\u30C8"}); - auto result = cli::Parser{}.parse(static_cast(args.size()), - args.data()); + auto argv_utf8 = + cli::utf16_to_utf8(static_cast(args.size()), args.data()); + auto result = + cli::Parser{}.parse(argv_utf8.size(), argv_utf8.data()); ASSERT_TRUE(result.has_value()); ASSERT_TRUE(result.value.name); @@ -292,8 +274,10 @@ TEST(ParseWide, JapaneseOption) { // Chinese: 你好 (U+4F60 U+597D). TEST(ParseWide, ChineseOption) { auto args = wide_argv({L"prog", L"--name", L"\u4F60\u597D"}); - auto result = cli::Parser{}.parse(static_cast(args.size()), - args.data()); + auto argv_utf8 = + cli::utf16_to_utf8(static_cast(args.size()), args.data()); + auto result = + cli::Parser{}.parse(argv_utf8.size(), argv_utf8.data()); ASSERT_TRUE(result.has_value()); ASSERT_TRUE(result.value.name); @@ -303,8 +287,10 @@ TEST(ParseWide, ChineseOption) { // Emoji: 😀 (U+1F600) — 4-byte UTF-8 / surrogate pair in UTF-16. TEST(ParseWide, EmojiOption) { auto args = wide_argv({L"prog", L"--name", L"\U0001F600"}); - auto result = cli::Parser{}.parse(static_cast(args.size()), - args.data()); + auto argv_utf8 = + cli::utf16_to_utf8(static_cast(args.size()), args.data()); + auto result = + cli::Parser{}.parse(argv_utf8.size(), argv_utf8.data()); ASSERT_TRUE(result.has_value()); ASSERT_TRUE(result.value.name); @@ -315,8 +301,10 @@ TEST(ParseWide, EmojiOption) { TEST(ParseWide, UnicodePositionals) { auto args = wide_argv( {L"prog", L"--name", L"x", L"\u30A2\u30A4\u30A6", L"\u6587\u4EF6"}); - auto result = cli::Parser{}.parse(static_cast(args.size()), - args.data()); + auto argv_utf8 = + cli::utf16_to_utf8(static_cast(args.size()), args.data()); + auto result = + cli::Parser{}.parse(argv_utf8.size(), argv_utf8.data()); ASSERT_TRUE(result.has_value()); EXPECT_EQ(result.value.files.value(), @@ -329,8 +317,10 @@ TEST(ParseWide, UnicodePositionals) { // Unicode program name is stored correctly. TEST(ParseWide, UnicodeProgramName) { auto args = wide_argv({L"\u30A2\u30D7\u30EA", L"--name", L"x"}); + auto argv_utf8 = + cli::utf16_to_utf8(static_cast(args.size()), args.data()); cli::Parser parser; - auto result = parser.parse(static_cast(args.size()), args.data()); + auto result = parser.parse(argv_utf8.size(), argv_utf8.data()); ASSERT_TRUE(result.has_value()); // program_name_ is not directly exposed, but parse succeeds without @@ -340,19 +330,22 @@ TEST(ParseWide, UnicodeProgramName) { // Error path: missing required option still works through wide argv. TEST(ParseWide, MissingRequiredOptionFails) { auto args = wide_argv({L"prog"}); - auto result = cli::Parser{}.parse(static_cast(args.size()), - args.data()); + auto argv_utf8 = + cli::utf16_to_utf8(static_cast(args.size()), args.data()); + auto result = + cli::Parser{}.parse(argv_utf8.size(), argv_utf8.data()); ASSERT_TRUE(result.has_error()); EXPECT_EQ(result.error.code, cli::ErrorCode::missing_required); EXPECT_EQ(result.error.subject, "--name"); } -// Free-function parse() overload for wchar_t*. -TEST(ParseWide, FreeFunctionParse) { +// Free-function parse() works after the explicit UTF-16 to UTF-8 conversion. +TEST(ParseWide, Utf16ToUtf8WithFreeFunctionParse) { auto args = wide_argv({L"prog", L"--name", L"\u30C6\u30B9\u30C8"}); - auto result = - cli::parse(static_cast(args.size()), args.data()); + auto argv_utf8 = + cli::utf16_to_utf8(static_cast(args.size()), args.data()); + auto result = cli::parse(argv_utf8.size(), argv_utf8.data()); ASSERT_TRUE(result.has_value()); ASSERT_TRUE(result.value.name);