diff --git a/docs/docs/actions.mdx b/docs/docs/actions.mdx index 53eb1df..39b0289 100644 --- a/docs/docs/actions.mdx +++ b/docs/docs/actions.mdx @@ -48,14 +48,14 @@ For **terminal (pack) steps** the story is different. A generic pack step like ```cpp template -auto operator()(ActionCtx>&, ActionResult) -> ActionResult; +auto operator()(ActionCtx&, ActionResult) -> ActionResult; ``` -The storage type `std::optional` is embedded in the first parameter, -and `Prev` itself is not known until the pipeline is validated. There is no -way to extract that type alias without an explicit `storage_type` -declaration — you would need to solve for the template parameter of -`ActionCtx`, which requires a trait or an additional overload. +The storage type `Prev` is embedded in the first parameter, and `Prev` +itself is not known until the pipeline is validated. There is no way to +extract that type alias without an explicit `storage_type` declaration +— you would need to solve for the template parameter of `ActionCtx`, +which requires a trait or an additional overload. So the explicit traits are load-bearing for pack steps. Once `storage_type` must exist anyway, keeping `accepts_input` and `after_type` explicit costs @@ -88,8 +88,9 @@ string_view ──► [Conversion] ──► T ──► [Validation] ──► └──────────────────────┴─────────────────────► ParseError ``` -Any step can short-circuit the chain by returning a `ParseError`. Downstream -steps are skipped automatically. +`ignore` short-circuits the remaining steps in the chain. `error` still +flows through downstream steps, which lets terminal actions preserve or +reformat the failure instead of bypassing it. --- @@ -143,7 +144,7 @@ action. | Expression | Storage type | Behaviour | |---|---|---| -| `pack::set_once` | `std::optional` | stores once; rejects duplicates | +| `pack::set_once` | `T` | stores once; rejects duplicates | | `pack::push` | `std::vector` | appends on each occurrence | | `pack::push_unique` | `std::vector` | appends; rejects duplicate values | | `pack::insert` | `std::set` | inserts into set | @@ -152,7 +153,7 @@ action. | `pack::set_false` | `bool` | always stores `false` | | `pack::toggle` | `bool` | flips on each occurrence | | `pack::increment` | `std::size_t` | increments on each occurrence | -| `pack::store_into` | `T*` | writes through a pointer | +| `pack::write_to` | `T*` | writes through a pointer | | `pack::callback` | `std::monostate` | calls `fn` with the value | ### Examples @@ -214,16 +215,15 @@ constexpr auto pipeline = | cli::validation::range<1, 32> | cli::pack::set_once; -// pack::set_once writes into std::optional, so the ctx must carry -// a reference to that storage. -std::optional storage; -cli::ActionCtx> ctx{.arg = std::ref(storage)}; +// pack::set_once writes into int, so the ctx must carry a reference to it. +int storage{}; +cli::ActionCtx ctx{.arg = std::ref(storage)}; auto result = pipeline.invoke(ctx, cli::ActionResult::ok("8")); if (result.has_value()) { // The packed value is in storage, not in result. - // storage == std::optional{8} + // storage == 8 } else { // result.error.code, .subject, .detail carry the failure reason } @@ -241,8 +241,8 @@ For pack actions (which need the storage reference) use `ActionCtx` initialised with designated initialisers: ```cpp -std::optional storage; -cli::ActionCtx> ctx{.arg = std::ref(storage)}; +int storage{}; +cli::ActionCtx ctx{.arg = std::ref(storage)}; ``` --- diff --git a/docs/docs/examples/advanced.mdx b/docs/docs/examples/advanced.mdx index d4609db..e6bf829 100644 --- a/docs/docs/examples/advanced.mdx +++ b/docs/docs/examples/advanced.mdx @@ -28,9 +28,10 @@ blocks behind cli20's higher-level aliases. This example builds parsing behavior from explicit conversion, validation, and storage steps instead of the higher-level aliases. It is the pattern to copy -when you need lists, uniqueness, or filesystem checks with precise control over -how values are stored. On success, it produces optional `config` and `jobs` -values plus collected `includes` and `defines`. +when you need lists, uniqueness, filesystem checks, or a runtime default for +an option value with precise control over how values are stored. On success, it +produces optional `config`, `jobs` with a default of `1` (`-j` with no value +uses a `nproc_dummy()` value), plus collected `includes` and `defines`. ## Try It @@ -44,16 +45,21 @@ The virtual filesystem is configured like this in this environment: Things to try: -- **Happy path** — `--config config.toml --jobs 4 -I /include -D DEBUG` +- **Happy path** — `--config config.toml -I /include -D DEBUG` +- **Default jobs** — `--config config.toml` to see the `1` default +- **Runtime default** — `--config config.toml -j` to see `nproc_dummy()` return `4` +- **Explicit jobs** — `--config config.toml -j 8` to override the runtime default +- **Duplicate jobs** — `--config config.toml -j -j` to see duplicate detection fail - **Multiple defines** — `--config config.toml -D FOO -D BAR -D FOO` to see `push_unique` deduplicate - **Directory as config** — `--config /configs` to see `is_regular_file` reject it -- **Out-of-range jobs** — `--config config.toml --jobs 99` to see range validation fail +- **Out-of-range jobs** — `--config config.toml -j 99` to see range validation fail +- **Missing jobs value** — `--config config.toml -j` to see the example fall back to `nproc_dummy()` - **Missing parent** — `-I /usr/include` to see `parent_exists` fail (`/usr` doesn't exist in the virtual FS) - **Help** — `-h` or `--help` diff --git a/docs/docs/intro.mdx b/docs/docs/intro.mdx index 5abd95b..51e8a6f 100644 --- a/docs/docs/intro.mdx +++ b/docs/docs/intro.mdx @@ -102,11 +102,11 @@ auto main(int argc, char* argv[]) -> int { std::cout << "verbose mode on\n"; } - if (args.output.value()) { - std::cout << "output: " << *args.output.value() << '\n'; + if (args.output) { + std::cout << "output: " << *args.output << '\n'; } - std::cout << "input: " << *args.input.value() << '\n'; + std::cout << "input: " << *args.input << '\n'; return 0; } ``` diff --git a/docs/docs/tutorial.md b/docs/docs/tutorial.md index d5fd08c..323622b 100644 --- a/docs/docs/tutorial.md +++ b/docs/docs/tutorial.md @@ -45,11 +45,11 @@ cli::ListOption include_dirs; | `cli::DoubleOption<"ratio">` | `cli::Option` | | `cli::PathOption<"output">` | `cli::Option` | -Scalar options store `std::optional`. List options store `std::vector`. +Scalar options store `T`. List options store `std::vector`. ```cpp -if (args.output.value()) { - std::cout << "output: " << *args.output.value() << '\n'; +if (args.output) { + std::cout << "output: " << *args.output << '\n'; } ``` @@ -58,12 +58,12 @@ if (args.output.value()) { Positional arguments are matched by position rather than name. ```cpp -cli::Positional src; // optional +cli::Positional src; // string cli::Positional files; // vector cli::Positional count; ``` -If `nargs.max == 1`, the storage type is `std::optional`. +If `nargs.max == 1`, the storage type is `T`. If `nargs.max != 1`, the storage type is `std::vector`. ```cpp @@ -111,7 +111,7 @@ Check whether the subcommand was provided with `.provided()`: ```cpp if (args.build.provided()) { - std::cout << "jobs: " << *args.build.jobs.value() << '\n'; + std::cout << "jobs: " << *args.build.jobs << '\n'; } ``` @@ -158,7 +158,7 @@ auto main(int argc, char* argv[]) -> int { } if (args.build.provided()) { - std::cout << "jobs: " << *args.build.jobs.value() << '\n'; + std::cout << "jobs: " << *args.build.jobs << '\n'; } return 0; diff --git a/examples/advanced.cc b/examples/advanced.cc index 29894ca..9cee0d2 100644 --- a/examples/advanced.cc +++ b/examples/advanced.cc @@ -1,11 +1,42 @@ #include #include +#include +#include #include "cli/argument.hh" #include "cli/parser.hh" namespace fs = std::filesystem; +auto nproc_dummy() -> int { + // Example-only stand-in for a runtime nproc query. Kept deterministic so it + // works in environments where querying host CPU count is awkward. + 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()); + } +}; + struct Args { cli::Description description{ "Action-pipeline example using Arg, filesystem conversion, and" @@ -21,9 +52,11 @@ struct Args { config{{.help = "Configuration file", .presence = cli::required}}; cli::Arg<"jobs", 'j', - cli::conversion::integer | cli::validation::range<1, 64> | - cli::pack::set_once> - jobs{{.help = "Parallel job count (1-64)"}}; + cli::Action{} | 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())", + .default_value = 1}}; cli::Arg<"include", 'I', cli::conversion::path | cli::validation::parent_exists | @@ -41,10 +74,8 @@ struct Args { auto main(int argc, char* argv[]) -> int { const auto args = cli::parse_or_exit(argc, argv); - std::cout << "config: " << args.config.value()->string() << '\n'; - if (args.jobs.value()) { - std::cout << "jobs: " << *args.jobs.value() << '\n'; - } + std::cout << "config: " << args.config->string() << '\n'; + std::cout << "jobs: " << *args.jobs_arg << '\n'; for (const fs::path& include_dir : args.includes.value()) { std::cout << "include: " << include_dir.string() << '\n'; } diff --git a/examples/callback_entry_point.cc b/examples/callback_entry_point.cc index 0903ac6..eb62bd6 100644 --- a/examples/callback_entry_point.cc +++ b/examples/callback_entry_point.cc @@ -41,28 +41,21 @@ auto main(int argc, char* argv[]) -> int { Args args; args.verbose = cli::Flag<"verbose", 'v'>{{ - .on_parse = [&state](const bool& value) { state.verbose = value; }, + .on_parse = [&state](const bool& value) -> void { state.verbose = value; }, }}; args.jobs = cli::IntOption<"jobs", 'j'>{{ - .on_parse = - [&state](const std::optional& value) { - if (value) { - state.jobs = *value; - } - }, + .on_parse = [&state](const int& value) -> void { state.jobs = value; }, }}; args.includes = cli::ListOption>{{ - .on_parse = - [&state](const std::vector& value) { - state.includes = value; - }, + .on_parse = [&state](const std::vector& value) -> void { + state.includes = value; + }, }}; args.inputs = cli::Positional{{ - .on_parse = - [&state](const std::vector& value) { - state.inputs = value; - }, + .on_parse = [&state](const std::vector& value) -> void { + state.inputs = value; + }, }}; const auto result = cli::Parser{}.parse(std::move(args), argc, argv); diff --git a/examples/compiler.cc b/examples/compiler.cc index 8159f8f..f32f808 100644 --- a/examples/compiler.cc +++ b/examples/compiler.cc @@ -46,7 +46,7 @@ auto main(int argc, char* argv[]) -> int { const auto args = cli::parse_or_exit(argc, argv); if (args.optimize.value()) { - std::cout << "optimize: -O" << *args.optimize.value() << '\n'; + std::cout << "optimize: -O" << *args.optimize << '\n'; } if (const std::size_t v = args.verbose.value(); v > 0) { diff --git a/examples/env_fallback.cc b/examples/env_fallback.cc index eca9fb2..3de765c 100644 --- a/examples/env_fallback.cc +++ b/examples/env_fallback.cc @@ -38,10 +38,10 @@ auto main(int argc, char* argv[]) -> int { if (args.verbose.value()) { std::cout << "verbose: true\n"; } - std::cout << "threads: " << *args.threads.value() << '\n'; - if (const auto& cfg = args.config.value()) { - std::cout << "config: " << *cfg << '\n'; + std::cout << "threads: " << *args.threads << '\n'; + if (args.config) { + std::cout << "config: " << *args.config << '\n'; } - std::cout << "job: " << *args.job.value() << '\n'; + std::cout << "job: " << *args.job << '\n'; return 0; } diff --git a/examples/module.cc b/examples/module.cc index 54c7136..546a5a3 100644 --- a/examples/module.cc +++ b/examples/module.cc @@ -24,7 +24,7 @@ struct Args { auto main(int argc, char* argv[]) -> int { const Args args = cli::parse_or_exit(argc, argv); if (args.port.value()) { - std::cout << "port: " << *args.port.value() << '\n'; + std::cout << "port: " << *args.port << '\n'; } for (const string& file : args.files.value()) { std::cout << "file: " << file << '\n'; diff --git a/examples/simple_api.cc b/examples/simple_api.cc index f80d81f..bde8fd1 100644 --- a/examples/simple_api.cc +++ b/examples/simple_api.cc @@ -19,9 +19,9 @@ auto main(int argc, char* argv[]) -> int { if (args.verbose.value()) { std::cout << "verbose: true\n"; } - if (args.output.value()) { - std::cout << "output: " << *args.output.value() << '\n'; + if (args.output) { + std::cout << "output: " << *args.output << '\n'; } - std::cout << "input: " << *args.input.value() << '\n'; + std::cout << "input: " << *args.input << '\n'; return 0; } diff --git a/examples/single_header.cc b/examples/single_header.cc index d7f9a94..286fe5b 100644 --- a/examples/single_header.cc +++ b/examples/single_header.cc @@ -14,9 +14,9 @@ struct Args { auto main(int argc, char* argv[]) -> int { const auto args = cli::parse_or_exit(argc, argv); std::cout << "verbose: " << std::boolalpha << args.verbose.value() << '\n'; - if (args.config.value()) { - std::cout << "config: " << *args.config.value() << '\n'; + if (args.config) { + std::cout << "config: " << *args.config << '\n'; } - std::cout << "input: " << *args.input.value() << '\n'; + std::cout << "input: " << *args.input << '\n'; return 0; } diff --git a/examples/subcommand.cc b/examples/subcommand.cc index a47ebf5..6b9689d 100644 --- a/examples/subcommand.cc +++ b/examples/subcommand.cc @@ -41,7 +41,7 @@ auto main(int argc, char* argv[]) -> int { std::cout << "release: true\n"; } if (args.build.jobs.value()) { - std::cout << "jobs: " << *args.build.jobs.value() << '\n'; + std::cout << "jobs: " << *args.build.jobs << '\n'; } for (const std::string& input : args.build.inputs.value()) { std::cout << "input: " << input << '\n'; @@ -50,7 +50,7 @@ auto main(int argc, char* argv[]) -> int { if (args.publish.provided()) { std::cout << "command: publish\n"; - std::cout << "registry: " << *args.publish.registry.value() << '\n'; + std::cout << "registry: " << *args.publish.registry << '\n'; std::cout << "dry-run: " << std::boolalpha << args.publish.dry_run.value() << '\n'; } diff --git a/examples/unicode_echo.cc b/examples/unicode_echo.cc index 1842ae1..fd86802 100644 --- a/examples/unicode_echo.cc +++ b/examples/unicode_echo.cc @@ -22,8 +22,8 @@ auto main(int argc, char* argv[]) -> int { #endif const auto args = cli::parse_or_exit(argc, argv); - if (const auto& v = args.name.value(); v.has_value()) { - std::cout << "name=" << *v << '\n'; + if (args.name) { + std::cout << "name=" << *args.name << '\n'; } for (const auto& f : args.files.value()) { std::cout << "file=" << f << '\n'; diff --git a/examples/validation.cc b/examples/validation.cc index b5cc197..207bdf3 100644 --- a/examples/validation.cc +++ b/examples/validation.cc @@ -28,10 +28,10 @@ struct Args { auto main(int argc, char* argv[]) -> int { const auto args = cli::parse_or_exit(argc, argv); - std::cout << "port: " << *args.port.value() << '\n'; - std::cout << "profile: " << *args.profile.value() << '\n'; - if (args.output.value()) { - std::cout << "output: " << args.output.value()->string() << '\n'; + std::cout << "port: " << *args.port << '\n'; + std::cout << "profile: " << *args.profile << '\n'; + if (args.output) { + std::cout << "output: " << args.output->string() << '\n'; } return 0; } diff --git a/include/cli/action.hh b/include/cli/action.hh index f1c5639..9fdf0a5 100644 --- a/include/cli/action.hh +++ b/include/cli/action.hh @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -30,13 +31,15 @@ namespace cli { * @brief The result type produced and consumed at each step of an action * pipeline. * - * An `ActionResult` is either a *success* carrying a value of type `T`, or - * a *failure* carrying a `ParseError`. The `operator bool()`, `has_value()`, - * and `has_error()` methods make it easy to check the state inline. + * An `ActionResult` is a 3-state type: ok (carrying a value of type `T`), + * error (carrying a `ParseError`), or ignore (silent skip). The + * `operator bool()`, `have_value()`, `has_value()`, `has_error()`, and + * `is_ignored()` methods make it easy to check the state inline. * * Factory methods: * - `ActionResult::ok(value)` — create a success result. * - `ActionResult::fail(error)` — create a failure result. + * - `ActionResult::ignore()` — create an ignore result. * * @tparam T The value type held on success. */ @@ -44,55 +47,66 @@ template struct ActionResult { using value_type = T; + enum class State : std::uint8_t { ok, error, ignore }; + + State state{State::ok}; ParseError error{}; T value{}; /** @brief Returns `true` when the result represents success. */ - [[nodiscard]] - constexpr explicit operator bool() const noexcept { - return !error.has_error(); + [[nodiscard]] constexpr explicit operator bool() const noexcept { + return state == State::ok; } /** @brief Returns `true` when the result represents success. */ - [[nodiscard]] - constexpr auto has_value() const noexcept -> bool { - return !error.has_error(); + [[nodiscard]] constexpr auto have_value() const noexcept -> bool { + return state == State::ok; + } + + /** @brief Returns `true` when the result represents success. */ + [[nodiscard]] constexpr auto has_value() const noexcept -> bool { + return have_value(); } /** @brief Returns `true` when the result represents failure. */ - [[nodiscard]] - constexpr auto has_error() const noexcept -> bool { - return error.has_error(); + [[nodiscard]] constexpr auto has_error() const noexcept -> bool { + return state == State::error; + } + + /** @brief Returns `true` when the result represents an ignored step. */ + [[nodiscard]] constexpr auto is_ignored() const noexcept -> bool { + return state == State::ignore; } /** * @brief Creates a success result holding `value`. * * @tparam U Type of the value (deduced). - * @param value The value to store. + * @param v The value to store. * @return A successful `ActionResult`. */ template - [[nodiscard]] - static constexpr auto ok(U&& value) -> ActionResult { - return { - .error = {}, - .value = std::forward(value), - }; + [[nodiscard]] static constexpr auto ok(U&& v) -> ActionResult { + return {.state = State::ok, .value = std::forward(v)}; } /** * @brief Creates a failure result carrying `error`. * - * @param error The error to store. + * @param e The error to store. * @return A failed `ActionResult`. */ - [[nodiscard]] - static constexpr auto fail(ParseError error) -> ActionResult { - return { - .error = std::move(error), - .value = {}, - }; + [[nodiscard]] static constexpr auto fail(ParseError e) -> ActionResult { + return {.state = State::error, .error = std::move(e)}; + } + + /** + * @brief Creates an ignore result. + * + * @return An ignored `ActionResult`. + */ + [[nodiscard]] static constexpr auto ignore() -> ActionResult { + return {.state = State::ignore}; } }; @@ -102,28 +116,41 @@ struct ActionResult { * * Pack actions such as `SetTrue` or `Push` write their result directly into the * argument storage referenced by `ActionCtx::arg`; they do not pass a value - * downstream, so `ActionResult` carries only an error or success state. + * downstream, so `ActionResult` carries only an error, success, or ignore + * state. */ template <> struct ActionResult { + using value_type = void; + + enum class State : std::uint8_t { ok, error, ignore }; + + State state{State::ok}; ParseError error{}; /** @brief Returns `true` when the result represents success. */ - [[nodiscard]] - constexpr explicit operator bool() const noexcept { - return !error.has_error(); + [[nodiscard]] constexpr explicit operator bool() const noexcept { + return state == State::ok; } /** @brief Returns `true` when the result represents success. */ - [[nodiscard]] - constexpr auto has_value() const noexcept -> bool { - return !error.has_error(); + [[nodiscard]] constexpr auto have_value() const noexcept -> bool { + return state == State::ok; + } + + /** @brief Returns `true` when the result represents success. */ + [[nodiscard]] constexpr auto has_value() const noexcept -> bool { + return have_value(); } /** @brief Returns `true` when the result represents failure. */ - [[nodiscard]] - constexpr auto has_error() const noexcept -> bool { - return error.has_error(); + [[nodiscard]] constexpr auto has_error() const noexcept -> bool { + return state == State::error; + } + + /** @brief Returns `true` when the result represents an ignored step. */ + [[nodiscard]] constexpr auto is_ignored() const noexcept -> bool { + return state == State::ignore; } /** @@ -131,31 +158,67 @@ struct ActionResult { * * @return A successful `ActionResult`. */ - static constexpr auto ok() -> ActionResult { - return { - .error = {}, - }; + [[nodiscard]] static constexpr auto ok() -> ActionResult { + return {.state = State::ok}; } /** * @brief Creates a failure result carrying `error`. * - * @param error The error to store. + * @param e The error to store. * @return A failed `ActionResult`. */ - [[nodiscard]] - static constexpr auto fail(ParseError error) -> ActionResult { - return { - .error = std::move(error), - }; + [[nodiscard]] static constexpr auto fail(ParseError e) -> ActionResult { + return {.state = State::error, .error = std::move(e)}; + } + + /** + * @brief Creates an ignore result. + * + * @return An ignored `ActionResult`. + */ + [[nodiscard]] static constexpr auto ignore() -> ActionResult { + return {.state = State::ignore}; } }; +/** + * @brief Propagates a non-ok ActionResult to a different value type. + * + * This preserves error versus ignore state while adapting the held value type. + * + * @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. + */ +template +[[nodiscard]] constexpr auto propagate(const ActionResult& input) + -> ActionResult { + if (input.has_error()) return ActionResult::fail(input.error); + return ActionResult::ignore(); +} + +/** + * @brief Propagates a non-ok ActionResult to a different value type. + * + * @tparam U The target value type. + * @param input The source result to propagate. + * @return An `ActionResult` carrying the same error or ignore state. + */ +template +[[nodiscard]] constexpr auto propagate(const ActionResult& input) + -> ActionResult { + if (input.has_error()) return ActionResult::fail(input.error); + return ActionResult::ignore(); +} + /** * @brief Context passed to each action function during pipeline execution. * * Provides access to the current `argv` position, how many times the parent - * option has been seen, how many times the action has been invoked so far, and + * option has been seen, how many values are in this occurrence, the 0-based + * index of the current value, the total values across all occurrences, and * a reference to the argument's storage value. * * @tparam T The storage type of the argument; `void` for intermediate @@ -166,8 +229,10 @@ struct ActionCtx { size_t index{}; ///< Zero-based index into `argv` of the current token. size_t occurrences{}; ///< Number of times the parent option token has appeared. - size_t invoke_count{}; ///< Number of times this action has been invoked for - ///< the current option. + size_t nargs{}; ///< Number of values in this option occurrence. + size_t value_index{}; ///< 0-based index of current value in this occurrence. + size_t + total_values{}; ///< Total values across all occurrences of this option. std::reference_wrapper arg{}; ///< Reference to the argument's storage value. }; @@ -186,8 +251,10 @@ struct ActionCtx { size_t index{}; ///< Zero-based index into `argv` of the current token. size_t occurrences{}; ///< Number of times the parent option token has appeared. - size_t invoke_count{}; ///< Number of times this action has been invoked for - ///< the current option. + size_t nargs{}; ///< Number of values in this option occurrence. + size_t value_index{}; ///< 0-based index of current value in this occurrence. + size_t + total_values{}; ///< Total values across all occurrences of this option. /** * @brief Constructs from a typed `ActionCtx`, copying the counters. @@ -199,7 +266,9 @@ struct ActionCtx { ActionCtx(const ActionCtx& other) : index(other.index), occurrences(other.occurrences), - invoke_count(other.invoke_count) {} + nargs(other.nargs), + value_index(other.value_index), + total_values(other.total_values) {} ActionCtx() = default; }; @@ -331,8 +400,8 @@ concept StringLike = std::same_as, std::string> || * | cli::pack::set_once; * @endcode * - * At pipeline invocation, if any step returns a failure the error is - * short-circuited to the end without invoking subsequent steps. + * At pipeline invocation, `ignore` short-circuits the remaining steps while + * `error` is passed through so downstream steps can preserve or react to it. * * @tparam Fns Pack of constexpr action objects forming the pipeline. */ @@ -347,28 +416,27 @@ 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) { - if constexpr (sizeof...(FnTail) == 0) { - return ActionResult::fail(input.error); - } else { - return Action::invoke( - ctx, ActionResult::fail(input.error)); - } - } - - 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) { + 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)); } template @@ -381,12 +449,11 @@ struct Action { using Head = std::remove_cvref_t; using Next = typename Head::template after_type; - if (!input) { + if (input.is_ignored()) { if constexpr (sizeof...(FnTail) == 0) { - return ActionResult::fail(input.error); + return ActionResult::ignore(); } else { - return Action::invoke( - ctx, ActionResult::fail(input.error)); + return Action::invoke(ctx, ActionResult::ignore()); } } @@ -423,13 +490,31 @@ struct Action { } } + private: + template + struct FirstFnType { + using type = std::remove_cvref_t; + }; + + public: + static constexpr bool entry_is_optional = FirstFnType< + Fns...>::type::template accepts_input>; + static constexpr auto validate() -> auto { - return validate_impl(); + using FirstFn = typename FirstFnType::type; + if constexpr (FirstFn::template accepts_input< + std::optional>) { + return validate_impl, Fns...>(); + } else { + return validate_impl(); + } } }; template <> struct Action<> { + static constexpr bool entry_is_optional = false; + template [[nodiscard]] static constexpr auto invoke(ActionCtx&, ActionResult input) { @@ -541,6 +626,7 @@ struct Integer { constexpr auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); T result{}; auto r = std::from_chars(input.value.data(), input.value.data() + input.value.size(), result); @@ -592,6 +678,7 @@ struct Floating { constexpr auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); T result{}; auto r = std::from_chars(input.value.data(), input.value.data() + input.value.size(), result); @@ -634,6 +721,7 @@ struct String { constexpr auto operator()(ActionCtx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); return ActionResult::ok(std::string(input.value)); } }; @@ -659,6 +747,7 @@ struct Bool { constexpr auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); const std::string_view val = input.value; if (val == "true" || val == "1") { return ActionResult::ok(true); @@ -688,6 +777,7 @@ struct Path { auto operator()(ActionCtx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); return ActionResult::ok( std::filesystem::path{input.value}); } @@ -713,6 +803,7 @@ struct ExistingFile { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); auto path = std::filesystem::path{input.value}; if (!std::filesystem::exists(path) || !std::filesystem::is_regular_file(path)) { @@ -745,6 +836,7 @@ struct ExistingDirectory { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); auto path = std::filesystem::path{input.value}; if (!std::filesystem::exists(path) || !std::filesystem::is_directory(path)) { return ActionResult::fail( @@ -781,6 +873,7 @@ struct Choice { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); if (auto value = Mapper(input.value); value.has_value()) { return ActionResult::ok(*std::move(value)); } @@ -837,14 +930,15 @@ struct Negatable { constexpr auto operator()(ActionCtx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); constexpr std::string_view prefix = Prefix.view(); const std::string_view val = input.value; if (val.starts_with(prefix)) { - return ActionResult::ok( - NegatableResult{std::string(val.substr(prefix.size())), false}); + return ActionResult::ok(NegatableResult{ + .name = std::string(val.substr(prefix.size())), .enabled = false}); } return ActionResult::ok( - NegatableResult{std::string(val), true}); + NegatableResult{.name = std::string(val), .enabled = true}); } }; @@ -856,6 +950,53 @@ inline constexpr auto path = Action{}; inline constexpr auto existing_file = Action{}; inline constexpr auto existing_directory = Action{}; +/** + * @brief Injects a compile-time default value when an option is invoked as a + * flag (no value provided). + * + * Accepts `std::optional` as input: + * - If the optional has a value (actual value provided), unwrap and pass + * through. + * - If nullopt (flag-only invocation) and `nargs == 0`, inject the compile-time + * default string. + * - If nullopt but nargs > 0 (value coming separately), return ignore. + * + * @tparam Value The default string value to inject when invoked as a flag. + */ +template +struct DefaultMissingValue { + template + static constexpr bool accepts_input = + std::same_as, std::optional>; + + template + using after_type = std::string_view; + + template + using storage_type = void; + + auto operator()(ActionCtx ctx, + ActionResult> input) const + -> ActionResult { + if (!input.have_value()) return propagate(input); + if (input.value.has_value()) { + // actual value provided: unwrap and pass through + return ActionResult::ok(*input.value); + } + // nullopt: flag invocation + if (ctx.nargs == 0) { + // pure flag with no value: inject the compile-time default + return ActionResult::ok(Value.view()); + } + // flag seen but a value is coming via separate value invocation: ignore + return ActionResult::ignore(); + } +}; + +template +inline constexpr auto default_missing_value = + Action{}>{}; + } // namespace conversion /** @@ -893,6 +1034,7 @@ struct Min { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input; if (input.value < MinValue) { return ActionResult::fail(detail::validation_failed_error( ctx.index, detail::to_error_subject(input.value), @@ -922,6 +1064,7 @@ struct Max { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input; if (input.value > MaxValue) { return ActionResult::fail(detail::validation_failed_error( ctx.index, detail::to_error_subject(input.value), @@ -954,6 +1097,7 @@ struct Range { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input; if (input.value < MinValue || input.value > MaxValue) { return ActionResult::fail(detail::validation_failed_error( ctx.index, detail::to_error_subject(input.value), @@ -978,6 +1122,7 @@ struct Positive { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input; if (!(input.value > 0)) { return ActionResult::fail(detail::validation_failed_error( ctx.index, detail::to_error_subject(input.value), @@ -1002,6 +1147,7 @@ struct NonNegative { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input; if (input.value < 0) { return ActionResult::fail(detail::validation_failed_error( ctx.index, detail::to_error_subject(input.value), @@ -1027,6 +1173,7 @@ struct NonEmpty { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input; if (input.value.empty()) { return ActionResult::fail(detail::validation_failed_error( ctx.index, detail::to_error_subject(input.value), @@ -1052,6 +1199,7 @@ struct NotBlank { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input; std::string_view text = input.value; if (detail::is_blank(text)) { return ActionResult::fail(detail::validation_failed_error( @@ -1085,6 +1233,7 @@ struct OneOf { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input; if (((input.value == Allowed) || ...)) { return input; } @@ -1117,6 +1266,7 @@ struct Matches { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input; const auto regex = std::regex{std::string(Pattern.view())}; if (!std::regex_match(input.value.begin(), input.value.end(), regex)) { return ActionResult::fail(detail::validation_failed_error( @@ -1142,6 +1292,7 @@ struct Exists { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input; if (!std::filesystem::exists(input.value)) { return ActionResult::fail(detail::validation_failed_error( ctx.index, input.value.string(), "path does not exist")); @@ -1165,6 +1316,7 @@ struct IsRegularFile { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input; if (!std::filesystem::is_regular_file(input.value)) { return ActionResult::fail(detail::validation_failed_error( ctx.index, input.value.string(), "path is not a regular file")); @@ -1188,6 +1340,7 @@ struct IsDirectory { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input; if (!std::filesystem::is_directory(input.value)) { return ActionResult::fail(detail::validation_failed_error( ctx.index, input.value.string(), "path is not a directory")); @@ -1212,6 +1365,7 @@ struct ParentExists { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input; const auto parent = input.value.parent_path(); if (!parent.empty() && !std::filesystem::exists(parent)) { return ActionResult::fail(detail::validation_failed_error( @@ -1246,6 +1400,7 @@ struct Predicate { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input; if (!Pred(input.value)) { return ActionResult::fail(detail::validation_failed_error( ctx.index, detail::to_error_subject(input.value), @@ -1305,8 +1460,9 @@ struct SetTrue { using storage_type = bool; template - auto operator()(ActionCtx ctx, ActionResult) const + auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); ctx.arg.get() = true; return ActionResult::ok(); } @@ -1324,8 +1480,9 @@ struct SetFalse { using storage_type = bool; template - auto operator()(ActionCtx ctx, ActionResult) const + auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); ctx.arg.get() = false; return ActionResult::ok(); } @@ -1343,8 +1500,9 @@ struct Toggle { using storage_type = bool; template - auto operator()(ActionCtx ctx, ActionResult) const + auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); ctx.arg.get() = !ctx.arg.get(); return ActionResult::ok(); } @@ -1362,8 +1520,9 @@ struct Increment { using storage_type = std::size_t; template - auto operator()(ActionCtx ctx, ActionResult) const + auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); ++ctx.arg.get(); return ActionResult::ok(); } @@ -1390,6 +1549,7 @@ struct RejectDuplicate { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input; if (ctx.occurrences > 1) { return ActionResult::fail(detail::duplicate_argument_error( ctx.index, detail::to_error_subject(input.value))); @@ -1399,11 +1559,10 @@ struct RejectDuplicate { }; /** - * @brief Stores the value in `std::optional` and rejects subsequent - * occurrences. + * @brief Stores the input value and rejects subsequent occurrences. * - * Storage type: `std::optional`. Returns `ErrorCode::duplicate_argument` if - * the option appears more than once. This is the default terminal action for + * Storage type: `T`. Returns `ErrorCode::duplicate_argument` if the option + * appears more than once. This is the default terminal action for * `Option`. */ struct SetOnce { @@ -1415,12 +1574,13 @@ struct SetOnce { using after_type = void; template - using storage_type = std::optional>; + using storage_type = detail::decay_t; template - auto operator()(ActionCtx>> ctx, - ActionResult input) const -> ActionResult { - if (ctx.arg.get().has_value() || ctx.occurrences > 1) { + auto operator()(ActionCtx> ctx, ActionResult input) const + -> ActionResult { + if (!input.have_value()) return propagate(input); + if (ctx.occurrences > 1) { return ActionResult::fail(detail::duplicate_argument_error( ctx.index, detail::to_error_subject(input.value))); } @@ -1449,6 +1609,7 @@ struct PushUnique { template auto operator()(ActionCtx>> ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); auto& values = ctx.arg.get(); if (std::ranges::find(values, input.value) != values.end()) { return ActionResult::fail(detail::duplicate_argument_error( @@ -1479,6 +1640,7 @@ struct Push { template auto operator()(ActionCtx>> ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); ctx.arg.get().push_back(std::move(input.value)); return ActionResult::ok(); } @@ -1505,6 +1667,7 @@ struct Insert { template auto operator()(ActionCtx>> ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); ctx.arg.get().insert(std::move(input.value)); return ActionResult::ok(); } @@ -1518,22 +1681,23 @@ struct InsertOrAssign { using after_type = void; template > - struct storage_type_impl { + struct StorageTypeImpl { using type = void; }; template - struct storage_type_impl { + struct StorageTypeImpl { using type = std::map< std::remove_cvref_t>>, std::remove_cvref_t>>>; }; template - using storage_type = typename storage_type_impl::type; + using storage_type = typename StorageTypeImpl::type; template auto operator()(ActionCtx> ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); auto&& [key, value] = input.value; ctx.arg.get().insert_or_assign(key, value); return ActionResult::ok(); @@ -1556,6 +1720,7 @@ struct Extend { template auto operator()(ActionCtx> ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); auto& out = ctx.arg.get(); for (auto&& value : input.value) { out.emplace_back(value); @@ -1582,8 +1747,9 @@ struct MarkPresent { using storage_type = bool; template - auto operator()(ActionCtx ctx, ActionResult) const + auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); ctx.arg.get() = true; return ActionResult::ok(); } @@ -1611,12 +1777,13 @@ struct StoreInto { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); if (T* ptr = ctx.arg.get(); ptr != nullptr) { *ptr = std::move(input.value); return ActionResult::ok(); } return ActionResult::fail(detail::validation_failed_error( - ctx.index, "store_into", + ctx.index, "write_to", "target pointer is null; did you forget to call bind()?")); } }; @@ -1649,6 +1816,7 @@ struct Callback { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return propagate(input); if constexpr (requires { Fn(ctx, input.value); }) { Fn(ctx, input.value); } else if constexpr (requires { Fn(input.value); }) { @@ -1675,7 +1843,7 @@ inline constexpr auto mark_present = Action{}; template inline constexpr auto callback = Action{}>{}; template -inline constexpr auto store_into = Action{}>{}; +inline constexpr auto write_to = Action{}>{}; } // namespace pack @@ -1704,6 +1872,7 @@ 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)}); } @@ -1727,7 +1896,9 @@ struct ExitSuccess { using storage_type = std::monostate; template - auto operator()(ActionCtx ctx, ActionResult) const -> ActionResult { + auto operator()(ActionCtx ctx, ActionResult input) const + -> ActionResult { + if (!input.have_value()) return input; using U = std::remove_cvref_t; if constexpr (detail::IsHelpRequested::value) { return ActionResult::fail(ParseError{ @@ -1775,6 +1946,7 @@ auto get(NegatableResult& r) -> decltype(auto) { else return (r.enabled); } + template auto get(const NegatableResult& r) -> decltype(auto) { if constexpr (I == 0) diff --git a/include/cli/argument.hh b/include/cli/argument.hh index c6f4b0f..8128593 100644 --- a/include/cli/argument.hh +++ b/include/cli/argument.hh @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -359,6 +360,9 @@ concept ArgumentSpec = requires { requires detail::positionals_have_variadic_at_end(); }; +template +struct Parser; + /** * @brief Parameter bag used when constructing `ArgImpl` or `PositionalImpl` with * named fields. @@ -378,7 +382,8 @@ template struct ArgParameter { std::string_view help{}; ///< Help text shown in `--help` output. Presence presence{Presence::optional}; ///< Whether the argument is required. - T default_value{}; ///< Default value when the argument is absent. + std::optional + default_value{}; ///< Default value when the argument is absent. std::function on_parse{}; ///< Callback invoked once after parsing completes. std::string_view env{}; ///< Environment variable name to fall back to when @@ -398,21 +403,18 @@ template struct ArgImpl : public OptionTag { using value_type = typename std::remove_cvref_t::type; - ArgImpl() = default; + ArgImpl() : ArgImpl(ArgParameter{}) {} ArgImpl(ArgParameter param) - : help(param.help), - presence(param.presence), - env(param.env), - value_(param.default_value), - on_parse_(std::move(param.on_parse)) {} + : param_(std::move(param)), + value_(param_.default_value.value_or(value_type{})) {} // Convenience constructor for StoreInto variants (value_type == T*). - explicit ArgImpl(std::remove_pointer_t& ref) + ArgImpl(std::remove_pointer_t& ref) requires std::is_pointer_v : value_(&ref) {} - ArgImpl(std::remove_pointer_t& ref, std::string_view help_text) + ArgImpl(std::remove_pointer_t& ref, ArgParameter param) requires std::is_pointer_v - : help(help_text), value_(&ref) {} + : param_(param), value_(&ref) {} // Bind to an external variable after construction. // Allows: opt.bind(var) or opt = &var @@ -428,77 +430,95 @@ struct ArgImpl : public OptionTag { value_ = ptr; return *this; } - // NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes) - std::string_view help{}; - // NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes) - cli::Presence presence{Presence::optional}; - // NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes) - std::string_view env{}; - // NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes) - value_type default_value{}; static constexpr auto name = Name; static constexpr auto short_name = ShortName; static constexpr auto nargs = N; + [[nodiscard]] auto value() const -> const value_type& { return value_; } + [[nodiscard]] auto value() -> value_type& { return value_; } + [[nodiscard]] auto provided() const -> bool { return provided_; } + [[nodiscard]] auto occurrences() const -> std::size_t { + return occurrence_count_; + } + [[nodiscard]] explicit operator bool() const { return value_present_; } + [[nodiscard]] auto operator*() -> value_type& { return value_; } + [[nodiscard]] auto operator*() const -> const value_type& { return value_; } + [[nodiscard]] auto operator->() -> value_type* { return &value_; } + [[nodiscard]] auto operator->() const -> const value_type* { return &value_; } + + template + [[nodiscard]] auto value_or(U&& fallback) const -> value_type { + if (value_present_) return value_; + return static_cast(std::forward(fallback)); + } + + [[nodiscard]] explicit operator std::optional() const { + if (value_present_) return value_; + return std::nullopt; + } + + [[nodiscard]] auto help_text() const -> const std::string_view { + return param_.help; + } + + protected: + static constexpr bool entry_is_optional = + std::remove_cvref_t::entry_is_optional; + // Called once when the option token is seen (before processing its values). auto notify_option_seen() -> void { ++occurrence_count_; } // Called once per value token associated with this option. - auto invoke_action(std::string_view text, std::size_t arg_index) - -> ActionResult { - provided_ = true; + auto invoke_action(std::optional text, std::size_t arg_index, + std::size_t occurrence_nargs, std::size_t value_index, + bool mark_provided = true) -> ActionResult { + if (mark_provided) provided_ = true; ActionCtx ctx{ .index = arg_index, .occurrences = occurrence_count_, - .invoke_count = invoke_count_, + .nargs = occurrence_nargs, + .value_index = value_index, + .total_values = total_values_, .arg = std::ref(value_), }; - ++invoke_count_; - auto result = - decltype(A)::invoke(ctx, ActionResult::ok(text)); - if (result.has_error()) { - return ActionResult::fail(std::move(result.error)); - } - return ActionResult::ok(); - } - - // Called once for flags (nargs = {0,0}) after the option token is seen. - auto invoke_flag(std::size_t arg_index) -> ActionResult { - provided_ = true; - ActionCtx ctx{ - .index = arg_index, - .occurrences = occurrence_count_, - .invoke_count = invoke_count_, - .arg = std::ref(value_), - }; - ++invoke_count_; - auto result = decltype(A)::invoke( - ctx, ActionResult::ok(std::string_view{})); - if (result.has_error()) { - return ActionResult::fail(std::move(result.error)); + if (text.has_value()) ++total_values_; + + auto invoke_result = [&ctx, &text]() -> auto { + if constexpr (entry_is_optional) { + return decltype(A)::invoke( + ctx, ActionResult>::ok(text)); + } else { + return decltype(A)::invoke( + ctx, ActionResult::ok(text.value_or(""))); + } + }(); + if (invoke_result.has_error()) { + return ActionResult::fail(std::move(invoke_result.error)); } + if (invoke_result.has_value()) value_present_ = true; return ActionResult::ok(); } // Called once after all value tokens for one option occurrence are processed. - auto fire_on_parse() -> void { - if (on_parse_) on_parse_(value_); + auto on_parse() -> void { + if (param_.on_parse) param_.on_parse(value_); } - [[nodiscard]] auto value() const -> const value_type& { return value_; } - [[nodiscard]] auto value() -> value_type& { return value_; } - [[nodiscard]] auto provided() const -> bool { return provided_; } + [[nodiscard]] auto get_param() const -> const ArgParameter& { + return param_; + } - private: template - friend struct Parser; + friend struct ::cli::Parser; + private: + std::size_t total_values_{}; // total values across all occurrences + ArgParameter param_; value_type value_{}; - std::function on_parse_{}; std::size_t occurrence_count_{}; // times the option token appeared - std::size_t invoke_count_{}; // times invoke_action/invoke_flag called bool provided_{}; + bool value_present_{param_.default_value.has_value()}; }; template @@ -555,60 +575,87 @@ struct PositionalImpl : public PositionalTag { using value_type = typename std::remove_cvref_t::type; - PositionalImpl() = default; + PositionalImpl() : PositionalImpl(ArgParameter{}) {} PositionalImpl(ArgParameter param) - : help(param.help), - presence(param.presence), - value_(param.default_value), - on_parse_(std::move(param.on_parse)) {} - // NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes) - std::string_view help{}; - // NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes) - cli::Presence presence{Presence::optional}; - // NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes) - value_type default_value{}; + : value_(param.default_value.value_or(value_type{})), + param_(std::move(param)) {} static constexpr auto nargs = N; + // Called once after all tokens for this positional are consumed. + auto on_parse() -> void { + if (param_.on_parse) param_.on_parse(value_); + } + + [[nodiscard]] auto value() const -> const value_type& { return value_; } + [[nodiscard]] auto value() -> value_type& { return value_; } + [[nodiscard]] auto provided() const -> bool { return provided_; } + [[nodiscard]] auto occurrences() const -> std::size_t { + return occurrence_count_; + } + [[nodiscard]] explicit operator bool() const { return value_present_; } + [[nodiscard]] auto operator*() -> value_type& { return value_; } + [[nodiscard]] auto operator*() const -> const value_type& { return value_; } + [[nodiscard]] auto operator->() -> value_type* { return &value_; } + [[nodiscard]] auto operator->() const -> const value_type* { return &value_; } + + template + [[nodiscard]] auto value_or(U&& fallback) const -> value_type { + if (value_present_) return value_; + return static_cast(std::forward(fallback)); + } + + [[nodiscard]] explicit operator std::optional() const { + if (value_present_) return value_; + return std::nullopt; + } + + [[nodiscard]] auto help_text() const -> const std::string_view { + return param_.help; + } + + protected: + [[nodiscard]] auto get_param() const -> const ArgParameter& { + return param_; + } + + static constexpr bool entry_is_optional = + std::remove_cvref_t::entry_is_optional; + // Called once per value token for this positional. - auto invoke_action(std::string_view text, std::size_t arg_index) - -> ActionResult { + auto invoke_action(std::string_view text, std::size_t arg_index, + std::size_t occurrence_nargs = 1, + std::size_t value_index = 0) -> ActionResult { provided_ = true; + ++occurrence_count_; ActionCtx ctx{ .index = arg_index, .occurrences = occurrence_count_, - .invoke_count = invoke_count_, + .nargs = occurrence_nargs, + .value_index = value_index, + .total_values = total_values_, .arg = std::ref(value_), }; - ++occurrence_count_; - ++invoke_count_; + ++total_values_; auto result = decltype(A)::invoke(ctx, ActionResult::ok(text)); if (result.has_error()) { return ActionResult::fail(std::move(result.error)); } + if (result.has_value()) value_present_ = true; return ActionResult::ok(); } - // Called once after all tokens for this positional are consumed. - auto fire_on_parse() -> void { - if (on_parse_) on_parse_(value_); - } - - [[nodiscard]] auto value() const -> const value_type& { return value_; } - [[nodiscard]] auto value() -> value_type& { return value_; } - [[nodiscard]] auto provided() const -> bool { return provided_; } - private: template friend struct Parser; value_type value_{}; - std::function on_parse_{}; + ArgParameter param_; std::size_t occurrence_count_{}; // times a value was provided - std::size_t - invoke_count_{}; // times invoke_action was called (same for positionals) + std::size_t total_values_{}; // total values across all invocations bool provided_{}; + bool value_present_{param_.default_value.has_value()}; }; /** @@ -675,24 +722,24 @@ template <> struct ActionFor { inline static constexpr auto set_once = conversion::string | pack::set_once; inline static constexpr auto push = conversion::string | pack::push; - inline static constexpr auto store_into = - conversion::string | pack::store_into; + inline static constexpr auto write_to = + conversion::string | pack::write_to; }; template <> struct ActionFor { inline static constexpr auto set_once = conversion::boolean | pack::set_once; inline static constexpr auto push = conversion::boolean | pack::push; - inline static constexpr auto store_into = - conversion::boolean | pack::store_into; + inline static constexpr auto write_to = + conversion::boolean | pack::write_to; }; template <> struct ActionFor { inline static constexpr auto set_once = conversion::path | pack::set_once; inline static constexpr auto push = conversion::path | pack::push; - inline static constexpr auto store_into = - conversion::path | pack::store_into; + inline static constexpr auto write_to = + conversion::path | pack::write_to; }; template @@ -700,8 +747,8 @@ struct ActionFor { inline static constexpr auto set_once = conversion::integer | pack::set_once; inline static constexpr auto push = conversion::integer | pack::push; - inline static constexpr auto store_into = - conversion::integer | pack::store_into; + inline static constexpr auto write_to = + conversion::integer | pack::write_to; }; template @@ -709,8 +756,8 @@ struct ActionFor { inline static constexpr auto set_once = conversion::floating | pack::set_once; inline static constexpr auto push = conversion::floating | pack::push; - inline static constexpr auto store_into = - conversion::floating | pack::store_into; + inline static constexpr auto write_to = + conversion::floating | pack::write_to; }; template @@ -778,7 +825,7 @@ using Flag = Arg; * @tparam ShortName Short option character, or `'\0'`. */ template -using BoundOption = Arg::store_into>; +using BoundOption = Arg::write_to>; /** @brief `BoundOption` specialisation for `std::string`. */ template @@ -809,11 +856,10 @@ using Help = Arg; /** - * @brief A single-value option that stores the parsed result in - * `std::optional`. + * @brief A single-value option that stores the parsed result in `T`. * - * Storage type: `std::optional`. Present when the option was supplied on - * the command line, absent (nullopt) otherwise. + * Storage type: `T`. Use `operator bool()` to check whether a value is present + * and `provided()` / `occurrences()` to check command-line occurrences. * * @tparam T The value type to parse (e.g. `int`, `std::string`). * @tparam Name Long option name. @@ -840,8 +886,7 @@ using ListOption = Arg::push, N>; /** * @brief A typed positional argument. * - * Storage type: `std::optional` for `N.max == 1`, `std::vector` for - * multi-value. + * Storage type: `T` for `N.max == 1`, `std::vector` for multi-value. * * @tparam T The value type to parse. * @tparam N `Nargs` descriptor. Default: `nargs::one`. diff --git a/include/cli/help.hh b/include/cli/help.hh index 726b740..cac02e2 100644 --- a/include/cli/help.hh +++ b/include/cli/help.hh @@ -343,9 +343,9 @@ auto render_help_row(Field& field, std::string_view label, std::size_t width) -> std::string { std::string out = " "; out += label; - if (!field.help.empty()) { + if (!field.help_text().empty()) { out.append(width - label.size() + 2, ' '); - append_wrapped_description(out, field.help, width + 4); + append_wrapped_description(out, field.help_text(), width + 4); } else { out += '\n'; } @@ -445,11 +445,12 @@ auto format_help_impl(T& value, std::string_view program_name, if constexpr (std::derived_from) { auto label = field_usage_label(); option_width = std::max(option_width, label.size()); - option_rows.emplace_back(std::move(label), fields.help); + option_rows.emplace_back(std::move(label), fields.help_text()); } else if constexpr (std::derived_from) { auto label = field_usage_label(); positional_width = std::max(positional_width, label.size()); - positional_rows.emplace_back(std::move(label), fields.help); + positional_rows.emplace_back(std::move(label), + fields.help_text()); } else if constexpr (std::derived_from) { auto label = field_usage_label(); command_width = std::max(command_width, label.size()); diff --git a/include/cli/parser.hh b/include/cli/parser.hh index 2c6e69e..1441a03 100644 --- a/include/cli/parser.hh +++ b/include/cli/parser.hh @@ -513,7 +513,7 @@ struct Parser { args.emplace_back(argv[i]); } if (argc > 0) { - program_name_ = argv[0]; + set_program_name(argv[0]); } return this->parse(std::span(args), 1); } @@ -537,7 +537,7 @@ struct Parser { args.emplace_back(argv[i]); } if (argc > 0) { - program_name_ = argv[0]; + set_program_name(argv[0]); } return this->parse(std::move(initial), std::span(args), 1); @@ -556,7 +556,7 @@ struct Parser { args.emplace_back(s); } if (argc > 0) { - program_name_ = owned[0]; + set_program_name(owned[0]); } return this->parse(std::span(args), 1); } @@ -573,7 +573,7 @@ struct Parser { args.emplace_back(s); } if (argc > 0) { - program_name_ = owned[0]; + set_program_name(owned[0]); } return this->parse(std::move(initial), std::span(args), 1); @@ -606,7 +606,7 @@ struct Parser { auto parse(std::span args, std::string_view program_name, std::size_t first_index = 0) -> ParseResult { - program_name_ = std::string(program_name); + set_program_name(std::string(program_name)); ParseResult result; parse_body(result, args, first_index); return result; @@ -644,13 +644,29 @@ struct Parser { auto parse(T initial, std::span args, std::string_view program_name, std::size_t first_index = 0) -> ParseResult { - program_name_ = std::string(program_name); + set_program_name(program_name); ParseResult result; result.value = std::move(initial); parse_body(result, args, first_index); return result; } + /** + * @brief Get the program name used in help output. + * @return The program name string. + */ + [[nodiscard]] auto program_name() const -> const std::string& { + return program_name_; + } + + /** + * @brief Set the program name used in help output. + * @param name The program name string to set. + */ + auto set_program_name(std::string name) -> void { + program_name_ = std::move(name); + } + private: TokenizerConfig cfg_; std::string program_name_ = "program"; @@ -714,7 +730,7 @@ struct Parser { for_each_field(result.value, [](auto& f) -> auto { using F = std::remove_cvref_t; if constexpr (std::derived_from) { - if (f.provided()) f.fire_on_parse(); + if (f.provided()) f.on_parse(); } }); @@ -773,12 +789,23 @@ struct Parser { // Iterate every field of val, calling fn(field) for each. template - static auto for_each_field(T& val, Fn fn) -> void { - std::apply([&fn](auto&... f) -> auto { (..., fn(f)); }, as_tuple(val)); + static constexpr void for_each_field(T& val, Fn fn) { + auto&& tup = as_tuple(val); + [&](std::index_sequence) + -> auto { + (fn(std::get(tup)), ...); + }(std::make_index_sequence< + std::tuple_size_v>>{}); } + template - static auto for_each_field(const T& val, Fn fn) -> void { - std::apply([&fn](const auto&... f) -> auto { (..., fn(f)); }, as_tuple(val)); + static constexpr void for_each_field(const T& val, Fn fn) { + auto&& tup = as_tuple(val); + [&](std::index_sequence) + -> auto { + (fn(std::get(tup)), ...); + }(std::make_index_sequence< + std::tuple_size_v>>{}); } // Build the option spec-map and command-name set from the field types of val. @@ -804,8 +831,9 @@ struct Parser { } // Find the option field matching (prefix, bare), notify it, and invoke its - // action on each value token. notify_option_seen() is called once; - // invoke_action() (or invoke_flag() for nargs={0,0}) once per value token. + // action on each value token. notify_option_seen() is called once; + // invoke_action() is called once for flags / missing optional entries, or + // once per value token otherwise. auto dispatch_option(T& val, std::string_view prefix, std::string_view bare, std::span vals, std::size_t first_index, std::size_t option_position) -> ActionResult { @@ -814,29 +842,39 @@ struct Parser { for_each_field(val, [&](auto& f) -> auto { using F = std::remove_cvref_t; if constexpr (std::derived_from) { - if (found) return; + if (found) { + return; + } bool match = false; - for (const auto& p : cfg_.option_prefixes) + for (const auto& p : cfg_.option_prefixes) { if (p == prefix && F::name.view() == bare) { match = true; break; } + } if (!match && F::short_name != '\0' && cfg_.short_option_prefix == prefix && bare.size() == 1 && - bare[0] == F::short_name) + bare[0] == F::short_name) { match = true; - if (!match) return; + } + if (!match) { + return; + } found = true; f.notify_option_seen(); - if constexpr (F::nargs.min == 0 && F::nargs.max == 0) { - res = f.invoke_flag(option_position + first_index); - } else { - for (const auto& vt : vals) { - if (res.has_error()) break; - res = f.invoke_action(vt.text, vt.position + first_index); + if constexpr (F::entry_is_optional) { + res = f.invoke_action(std::nullopt, option_position + first_index, + vals.size(), 0); + } + const std::size_t nargs = vals.size(); + for (std::size_t vi = 0; vi < vals.size(); ++vi) { + if (res.has_error()) { + break; } + res = f.invoke_action(vals[vi].text, vals[vi].position + first_index, + nargs, vi); } - if (!res.has_error()) f.fire_on_parse(); + if (!res.has_error()) f.on_parse(); } }); return res; @@ -889,8 +927,8 @@ struct Parser { found = true; f.mark_provided(); auto sub_parser = Parser{}; - sub_parser.program_name_ = - std::format("{} {}", program_name_, F::command_name()); + sub_parser.set_program_name( + std::format("{} {}", program_name_, F::command_name())); auto sub = sub_parser.parse(args, first_index); if (sub.has_error()) { res = ActionResult::fail(std::move(sub.error)); @@ -920,7 +958,8 @@ struct Parser { if (res.has_error()) { return; } - if (f.presence == Presence::required && !f.provided()) { + if (f.get_param().presence == Presence::required && + !static_cast(f)) { std::string subject; if constexpr (std::derived_from) { subject = "--" + std::string(F::name.view()); @@ -943,24 +982,23 @@ struct Parser { for_each_field(val, [&](auto& f) -> auto { using F = std::remove_cvref_t; if constexpr (std::derived_from) { - if (res.has_error() || f.provided() || f.env.empty()) return; + if (res.has_error() || f.provided() || f.get_param().env.empty()) return; #ifdef _WIN32 char* raw_buf = nullptr; std::size_t raw_len = 0; - _dupenv_s(&raw_buf, &raw_len, std::string(f.env).c_str()); + _dupenv_s(&raw_buf, &raw_len, std::string(f.get_param().env).c_str()); std::unique_ptr raw_guard(raw_buf, &free); const char* raw = raw_buf; #else - const char* raw = std::getenv(std::string(f.env).c_str()); + const char* raw = std::getenv(std::string(f.get_param().env).c_str()); #endif if (!raw) return; - f.notify_option_seen(); if constexpr (F::nargs.min == 0 && F::nargs.max == 0) { - res = f.invoke_flag(0); + res = f.invoke_action(std::nullopt, 0, 0, 0, false); } else { - res = f.invoke_action(std::string_view(raw), 0); + res = f.invoke_action(std::string_view(raw), 0, 1, 0, false); } - if (!res.has_error()) f.fire_on_parse(); + if (!res.has_error()) f.on_parse(); } }); return res; diff --git a/modules/cli20.cppm b/modules/cli20.cppm index a85e8b1..c7318f3 100644 --- a/modules/cli20.cppm +++ b/modules/cli20.cppm @@ -68,6 +68,7 @@ using cli::conversion::String; using cli::conversion::boolean; using cli::conversion::choice; using cli::conversion::enumeration; +using cli::conversion::default_missing_value; using cli::conversion::existing_directory; using cli::conversion::existing_file; using cli::conversion::floating; diff --git a/tests/test_callback.cc b/tests/test_callback.cc index 8c91b36..a52ce2d 100644 --- a/tests/test_callback.cc +++ b/tests/test_callback.cc @@ -1,6 +1,5 @@ #include -#include #include #include @@ -36,10 +35,7 @@ TEST(Callback, CalledForIntOption) { int captured = -1; CallbackIntArgs args; args.count = cli::IntOption<"count", 'c'>{{ - .on_parse = - [&captured](const std::optional& v) { - if (v) captured = *v; - }, + .on_parse = [&captured](const int& v) -> void { captured = v; }, }}; auto args_vec = argv({"prog", "--count", "42"}); @@ -54,7 +50,7 @@ TEST(Callback, CalledForFlag) { bool fired = false; CallbackFlagArgs args; args.verbose = cli::Flag<"verbose", 'v'>{{ - .on_parse = [&fired](const bool& v) { fired = v; }, + .on_parse = [&fired](const bool& v) -> void { fired = v; }, }}; auto args_vec = argv({"prog", "--verbose"}); @@ -69,7 +65,7 @@ TEST(Callback, NotCalledWhenFlagAbsent) { bool fired = false; CallbackFlagArgs args; args.verbose = cli::Flag<"verbose", 'v'>{{ - .on_parse = [&fired](const bool& v) { fired = v; }, + .on_parse = [&fired](const bool& v) -> void { fired = v; }, }}; auto args_vec = argv({"prog"}); @@ -87,11 +83,10 @@ TEST(Callback, CalledOncePerOccurrenceForList) { std::vector last_seen; CallbackListArgs args; args.files = cli::ListOption{{ - .on_parse = - [&](const std::vector& v) { - ++call_count; - last_seen = v; - }, + .on_parse = [&](const std::vector& v) -> void { + ++call_count; + last_seen = v; + }, }}; // One occurrence of --file with three values. @@ -111,11 +106,10 @@ TEST(Callback, CalledOnceForPositional) { std::vector final_value; CallbackPositionalArgs args; args.inputs = cli::Positional{{ - .on_parse = - [&](const std::vector& v) { - ++call_count; - final_value = v; - }, + .on_parse = [&](const std::vector& v) -> void { + ++call_count; + final_value = v; + }, }}; auto args_vec = argv({"prog", "foo.cc", "bar.cc", "baz.cc"}); @@ -132,7 +126,7 @@ TEST(Callback, NotCalledOnParseError) { int call_count = 0; CallbackIntArgs args; args.count = cli::IntOption<"count", 'c'>{{ - .on_parse = [&call_count](const std::optional&) { ++call_count; }, + .on_parse = [&call_count](const int&) -> void { ++call_count; }, }}; auto args_vec = argv({"prog", "--count", "notanint"}); @@ -148,8 +142,9 @@ TEST(Callback, CallbackCountMatchesOccurrenceCount) { int call_count = 0; CallbackListArgs args; args.files = cli::ListOption{{ - .on_parse = - [&call_count](const std::vector&) { ++call_count; }, + .on_parse = [&call_count](const std::vector&) -> void { + ++call_count; + }, }}; auto args_vec = argv({"prog", "--file", "a", "--file", "b", "--file", "c"}); diff --git a/tests/test_conversion.cc b/tests/test_conversion.cc index 46eb585..18b4fcd 100644 --- a/tests/test_conversion.cc +++ b/tests/test_conversion.cc @@ -11,10 +11,12 @@ namespace fs = std::filesystem; namespace { +using cli::Action; using cli::ActionCtx; using cli::ActionResult; using cli::ErrorCode; using cli::conversion::Bool; +using cli::conversion::DefaultMissingValue; using cli::conversion::ExistingDirectory; using cli::conversion::ExistingFile; using cli::conversion::Floating; @@ -31,6 +33,22 @@ constexpr auto parse_mode = [](std::string_view value) -> std::optional { return std::nullopt; }; +struct IgnoreStep { + template + static constexpr bool accepts_input = true; + + template + using after_type = Prev; + + template + using storage_type = void; + + template + auto operator()(ActionCtx, ActionResult) const -> ActionResult { + return ActionResult::ignore(); + } +}; + class TempPathGuard { public: TempPathGuard(const TempPathGuard&) = default; @@ -166,6 +184,28 @@ TEST(Conversion, NegatablePrefixInMiddleIsNotStripped) { EXPECT_TRUE(r.value.enabled); } +TEST(Conversion, IgnorePropagatesThroughPipelineWithoutWritingStorage) { + bool storage = false; + ActionCtx storage_ctx{.arg = std::ref(storage)}; + + constexpr auto pipeline = Action{} | cli::pack::mark_present; + auto result = decltype(pipeline)::invoke( + storage_ctx, ActionResult::ignore()); + + EXPECT_TRUE(result.is_ignored()); + EXPECT_FALSE(storage); +} + +TEST(Conversion, DefaultMissingValueIgnoresWhenValueWillArriveLater) { + ActionCtx missing_ctx{}; + missing_ctx.nargs = 1; + auto result = DefaultMissingValue<"auto">{}( + missing_ctx, + ActionResult>::ok(std::nullopt)); + + EXPECT_TRUE(result.is_ignored()); +} + // ── SimpleAction / PackAction CRTP helpers ──────────────────────────────── namespace { diff --git a/tests/test_parse.cc b/tests/test_parse.cc index 186b095..0d14ad9 100644 --- a/tests/test_parse.cc +++ b/tests/test_parse.cc @@ -1,6 +1,7 @@ #include #include +#include #include #include @@ -35,6 +36,73 @@ struct ParseArgs { cli::Command<"build", BuildArgs> build{{.help = "Build subcommand"}}; }; +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()); + } +}; + +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::pack::set_once, + cli::nargs::zero_or_one> + jobs_arg{{.help = "Parallel job count (default 1; -j uses nproc_dummy())", + .default_value = 1}}; +}; + +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; + } +}; + +struct TotalValuesArgs { + cli::Arg<"mode", 'm', + cli::Action{} | + cli::conversion::default_missing_value<"auto"> | + cli::pack::set_once, + cli::nargs::zero_or_one> + mode; +}; + auto argv(std::initializer_list values) -> std::vector { return {values}; @@ -51,13 +119,13 @@ TEST(Parse, ParsesOptionsPositionalsAndSubcommand) { ASSERT_TRUE(result.has_value()); EXPECT_TRUE(result.value.verbose.value()); - ASSERT_TRUE(result.value.count.value().has_value()); - EXPECT_EQ(*result.value.count.value(), 2); + ASSERT_TRUE(result.value.count); + EXPECT_EQ(*result.value.count, 2); EXPECT_EQ((result.value.files.value()), (std::vector{"a.txt", "b.txt"})); EXPECT_TRUE(result.value.build.provided()); - ASSERT_TRUE(result.value.build.threads.value().has_value()); - EXPECT_EQ(*result.value.build.threads.value(), 4); + ASSERT_TRUE(result.value.build.threads); + EXPECT_EQ(*result.value.build.threads, 4); } TEST(Parse, MissingRequiredOptionFails) { @@ -105,6 +173,76 @@ TEST(Parse, MissingRequiredSubcommandOptionFailsInsideCommand) { EXPECT_EQ(result.error.subject, "--threads"); } +TEST(Parse, JobsOptionUsesDefaultValueAndAvoidsDuplicateOnFlagStyleInvocation) { + { + auto args = argv({"prog", "--config", "flake.lock"}); + auto result = cli::Parser{}.parse( + std::span(args), 1); + + ASSERT_TRUE(result.has_value()); + ASSERT_TRUE(result.value.config); + EXPECT_EQ(*result.value.config, "flake.lock"); + EXPECT_EQ(result.value.jobs_arg.value(), 1); + } + + { + auto args = argv({"prog", "-j", "--config", "flake.lock"}); + auto result = cli::Parser{}.parse( + std::span(args), 1); + + ASSERT_TRUE(result.has_value()); + ASSERT_TRUE(result.value.config); + EXPECT_EQ(*result.value.config, "flake.lock"); + EXPECT_EQ(result.value.jobs_arg.value(), 4); + } + + { + auto args = argv({"prog", "-j", "6", "--config", "flake.lock"}); + auto result = cli::Parser{}.parse( + std::span(args), 1); + + ASSERT_TRUE(result.has_value()); + ASSERT_TRUE(result.value.config); + EXPECT_EQ(*result.value.config, "flake.lock"); + EXPECT_EQ(result.value.jobs_arg.value(), 6); + } +} + +TEST(Parse, JobsOptionRejectsDuplicateOccurrences) { + { + auto args = argv({"prog", "-j", "-j", "--config", "flake.lock"}); + auto result = cli::Parser{}.parse( + std::span(args), 1); + + ASSERT_TRUE(result.has_error()); + EXPECT_EQ(result.error.code, cli::ErrorCode::duplicate_argument); + } + + { + auto args = argv({"prog", "-j", "6", "-j", "--config", "flake.lock"}); + auto result = cli::Parser{}.parse( + std::span(args), 1); + + ASSERT_TRUE(result.has_error()); + EXPECT_EQ(result.error.code, cli::ErrorCode::duplicate_argument); + } +} + +TEST(Parse, MissingOptionalEntryDoesNotIncrementTotalValues) { + std::vector seen; + observed_total_values = &seen; + + auto args = argv({"prog", "--mode", "fast"}); + auto result = cli::Parser{}.parse( + std::span(args), 1); + + observed_total_values = nullptr; + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value.mode.value(), "fast"); + EXPECT_EQ(seen, (std::vector{0, 0})); +} + #ifdef _WIN32 namespace { @@ -136,8 +274,8 @@ TEST(ParseWide, AsciiRoundtrip) { args.data()); ASSERT_TRUE(result.has_value()); - ASSERT_TRUE(result.value.name.value().has_value()); - EXPECT_EQ(*result.value.name.value(), "hello"); + ASSERT_TRUE(result.value.name); + EXPECT_EQ(*result.value.name, "hello"); } // Japanese: テスト (U+30C6 U+30B9 U+30C8) — 3-byte UTF-8 sequences. @@ -147,8 +285,8 @@ TEST(ParseWide, JapaneseOption) { args.data()); ASSERT_TRUE(result.has_value()); - ASSERT_TRUE(result.value.name.value().has_value()); - EXPECT_EQ(*result.value.name.value(), "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"); + ASSERT_TRUE(result.value.name); + EXPECT_EQ(*result.value.name, "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"); } // Chinese: 你好 (U+4F60 U+597D). @@ -158,8 +296,8 @@ TEST(ParseWide, ChineseOption) { args.data()); ASSERT_TRUE(result.has_value()); - ASSERT_TRUE(result.value.name.value().has_value()); - EXPECT_EQ(*result.value.name.value(), "\xe4\xbd\xa0\xe5\xa5\xbd"); + ASSERT_TRUE(result.value.name); + EXPECT_EQ(*result.value.name, "\xe4\xbd\xa0\xe5\xa5\xbd"); } // Emoji: 😀 (U+1F600) — 4-byte UTF-8 / surrogate pair in UTF-16. @@ -169,8 +307,8 @@ TEST(ParseWide, EmojiOption) { args.data()); ASSERT_TRUE(result.has_value()); - ASSERT_TRUE(result.value.name.value().has_value()); - EXPECT_EQ(*result.value.name.value(), "\xf0\x9f\x98\x80"); + ASSERT_TRUE(result.value.name); + EXPECT_EQ(*result.value.name, "\xf0\x9f\x98\x80"); } // Unicode in positional arguments. @@ -217,8 +355,8 @@ TEST(ParseWide, FreeFunctionParse) { cli::parse(static_cast(args.size()), args.data()); ASSERT_TRUE(result.has_value()); - ASSERT_TRUE(result.value.name.value().has_value()); - EXPECT_EQ(*result.value.name.value(), "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"); + ASSERT_TRUE(result.value.name); + EXPECT_EQ(*result.value.name, "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"); } #endif @@ -252,8 +390,8 @@ TEST(ParseCluster, AttachedValue) { auto result = cli::Parser{}.parse( std::span(args), 1); ASSERT_TRUE(result.has_value()); - ASSERT_TRUE(result.value.output.value().has_value()); - EXPECT_EQ(*result.value.output.value(), "file"); + ASSERT_TRUE(result.value.output); + EXPECT_EQ(*result.value.output, "file"); } TEST(ParseCluster, FlagsThenAttachedValue) { @@ -264,8 +402,8 @@ TEST(ParseCluster, FlagsThenAttachedValue) { ASSERT_TRUE(result.has_value()); EXPECT_TRUE(result.value.verbose.value()); EXPECT_TRUE(result.value.xray.value()); - ASSERT_TRUE(result.value.output.value().has_value()); - EXPECT_EQ(*result.value.output.value(), "file"); + ASSERT_TRUE(result.value.output); + EXPECT_EQ(*result.value.output, "file"); } TEST(ParseCluster, FlagsThenValueNextToken) { @@ -276,8 +414,8 @@ TEST(ParseCluster, FlagsThenValueNextToken) { ASSERT_TRUE(result.has_value()); EXPECT_TRUE(result.value.verbose.value()); EXPECT_TRUE(result.value.xray.value()); - ASSERT_TRUE(result.value.output.value().has_value()); - EXPECT_EQ(*result.value.output.value(), "out.txt"); + ASSERT_TRUE(result.value.output); + EXPECT_EQ(*result.value.output, "out.txt"); } // ---- cluster with nargs=2 option ------------------------------------------- @@ -337,8 +475,10 @@ TEST(ParseEnv, FallsBackToEnvWhenOptionAbsent) { cli::Parser{}.parse(std::span(args), 1); unsetenv("TEST_OUTPUT"); ASSERT_TRUE(result.has_value()); - ASSERT_TRUE(result.value.output.value().has_value()); - EXPECT_EQ(*result.value.output.value(), "from_env"); + ASSERT_TRUE(result.value.output); + EXPECT_EQ(*result.value.output, "from_env"); + EXPECT_FALSE(result.value.output.provided()); + EXPECT_EQ(result.value.output.occurrences(), 0U); } TEST(ParseEnv, CliTakesPrecedenceOverEnv) { @@ -348,8 +488,8 @@ TEST(ParseEnv, CliTakesPrecedenceOverEnv) { cli::Parser{}.parse(std::span(args), 1); unsetenv("TEST_OUTPUT"); ASSERT_TRUE(result.has_value()); - ASSERT_TRUE(result.value.output.value().has_value()); - EXPECT_EQ(*result.value.output.value(), "from_cli"); + ASSERT_TRUE(result.value.output); + EXPECT_EQ(*result.value.output, "from_cli"); } TEST(ParseEnv, EnvSatisfiesRequiredOption) { @@ -363,7 +503,7 @@ TEST(ParseEnv, EnvSatisfiesRequiredOption) { std::span(args), 1); unsetenv("TEST_OUTPUT"); ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result.value.output.value(), "env_val"); + EXPECT_EQ(*result.value.output, "env_val"); } TEST(ParseEnv, MissingEnvStillFailsForRequired) { @@ -386,8 +526,10 @@ TEST(ParseEnv, IntOptionFromEnv) { cli::Parser{}.parse(std::span(args), 1); unsetenv("TEST_COUNT"); ASSERT_TRUE(result.has_value()); - ASSERT_TRUE(result.value.count.value().has_value()); - EXPECT_EQ(*result.value.count.value(), 42); + ASSERT_TRUE(result.value.count); + EXPECT_EQ(*result.value.count, 42); + EXPECT_FALSE(result.value.count.provided()); + EXPECT_EQ(result.value.count.occurrences(), 0U); } TEST(ParseEnv, FlagFromEnv) { diff --git a/tests/test_store_into.cc b/tests/test_store_into.cc index c472fd9..07bf73f 100644 --- a/tests/test_store_into.cc +++ b/tests/test_store_into.cc @@ -121,7 +121,7 @@ TEST(StoreInto, UnboundOptionFailsValidation) { ASSERT_TRUE(result.has_error()); EXPECT_EQ(result.error.code, cli::ErrorCode::validation_failed); EXPECT_EQ(result.error.kind, cli::ErrorKind::validation); - EXPECT_EQ(result.error.subject, "store_into"); + EXPECT_EQ(result.error.subject, "write_to"); EXPECT_EQ(result.error.detail, "target pointer is null; did you forget to call bind()?"); EXPECT_EQ(result.error.position, 2); @@ -146,7 +146,7 @@ TEST(StoreInto, MultipleVariablesBoundToSameArgs) { TEST(StoreInto, DirectArgWithStoreInto) { struct DirectArgs { cli::Arg<"port", 'p', - cli::conversion::integer | cli::pack::store_into> + cli::conversion::integer | cli::pack::write_to> port_opt; }; diff --git a/tests/test_sugar.cc b/tests/test_sugar.cc index 4093e02..912d16d 100644 --- a/tests/test_sugar.cc +++ b/tests/test_sugar.cc @@ -45,11 +45,11 @@ using FilesField = std::remove_cvref_t().files)>; static_assert(std::same_as); -static_assert(std::same_as>); +static_assert(std::same_as); static_assert(std::same_as>); static_assert(std::same_as>); -static_assert(std::same_as::value_type, - std::optional>); +static_assert( + std::same_as::value_type, std::string>); static_assert( std::same_as::value_type, std::vector>); @@ -65,19 +65,19 @@ TEST(Sugar, ParsesConvenienceAliases) { ASSERT_TRUE(result.has_value()) << result.error.message(); EXPECT_TRUE(result.value.verbose.value()); - ASSERT_TRUE(result.value.count.value().has_value()); - EXPECT_EQ(*result.value.count.value(), 3); + ASSERT_TRUE(result.value.count); + EXPECT_EQ(*result.value.count, 3); EXPECT_EQ((result.value.includes.value()), (std::vector{"inc/a", "inc/b"})); EXPECT_EQ((result.value.files.value()), (std::vector{"main.cc", "util.cc"})); EXPECT_TRUE(result.value.build.provided()); EXPECT_TRUE(result.value.build.release.value()); - ASSERT_TRUE(result.value.build.jobs.value().has_value()); - EXPECT_EQ(*result.value.build.jobs.value(), 8); + ASSERT_TRUE(result.value.build.jobs); + EXPECT_EQ(*result.value.build.jobs, 8); } -TEST(Sugar, SinglePositionalStoresOptionalValue) { +TEST(Sugar, SinglePositionalStoresValue) { struct SinglePositionalArgs { cli::Positional file{ {.help = "Input file", .presence = cli::required}}; @@ -88,8 +88,8 @@ TEST(Sugar, SinglePositionalStoresOptionalValue) { std::span(args), 1); ASSERT_TRUE(result.has_value()); - ASSERT_TRUE(result.value.file.value().has_value()); - EXPECT_EQ(*result.value.file.value(), "input.txt"); + ASSERT_TRUE(result.value.file); + EXPECT_EQ(*result.value.file, "input.txt"); } TEST(Sugar, RequiredAliasStillEnforcesPresence) { @@ -106,3 +106,59 @@ TEST(Sugar, RequiredAliasStillEnforcesPresence) { EXPECT_EQ(result.error.code, cli::ErrorCode::missing_required); EXPECT_EQ(result.error.subject, "--port"); } + +TEST(Sugar, DefaultMissingValueSupportsFlagAndExplicitValue) { + struct DefaultArgs { + cli::Arg<"mode", + cli::conversion::default_missing_value<"auto"> | + cli::conversion::string | cli::pack::set_once, + cli::nargs::zero_or_one> + mode{{.help = "Mode", .presence = cli::Presence::optional}}; + }; + + { + auto args = argv({"prog", "--mode"}); + auto result = cli::Parser{}.parse( + std::span(args), 1); + + ASSERT_TRUE(result.has_value()); + ASSERT_TRUE(result.value.mode); + EXPECT_EQ(*result.value.mode, "auto"); + } + + { + auto args = argv({"prog", "--mode", "manual"}); + auto result = cli::Parser{}.parse( + std::span(args), 1); + + ASSERT_TRUE(result.has_value()); + ASSERT_TRUE(result.value.mode); + EXPECT_EQ(*result.value.mode, "manual"); + } +} + +TEST(Sugar, ValueOptionExposesOptionalLikeApi) { + struct DefaultArgs { + cli::IntOption<"jobs", 'j'> jobs{{.default_value = 4}}; + cli::StringOption<"name", 'n'> name; + }; + + auto args = argv({"prog"}); + auto result = cli::Parser{}.parse( + std::span(args), 1); + + ASSERT_TRUE(result.has_value()); + EXPECT_FALSE(result.value.jobs.provided()); + EXPECT_EQ(result.value.jobs.occurrences(), 0U); + EXPECT_TRUE(result.value.jobs); + EXPECT_EQ(*result.value.jobs, 4); + EXPECT_EQ(result.value.jobs.value_or(1), 4); + auto as_optional = static_cast>(result.value.jobs); + ASSERT_TRUE(as_optional.has_value()); + EXPECT_EQ(*as_optional, 4); + + EXPECT_FALSE(result.value.name); + EXPECT_EQ(result.value.name.value_or("fallback"), "fallback"); + auto missing = static_cast>(result.value.name); + EXPECT_FALSE(missing.has_value()); +}