From 2c186a9a91916828f63edd8e8c654be8356326c4 Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Mon, 11 May 2026 14:41:14 +0900 Subject: [PATCH 01/11] feat: add ignore state to ActionResult and nargs context to ActionCtx ActionResult gains a 3rd state (ok / error / ignore): - ignore stops downstream pipeline steps (no-op to storage) - error now passes through to downstream steps instead of bypassing - have_value(), is_ignored(), ignore() factory, propagate() added ActionCtx gains nargs, value_index, total_values fields (invoke_count removed). Action::invoke bypasses on ignore, passes error through to each step. All built-in steps guard with have_value() to propagate error/ignore correctly. Adds conversion::DefaultMissingValue for optional-value option support. Co-Authored-By: Claude Sonnet 4.6 --- include/cli/action.hh | 302 ++++++++++++++++++++++++++++++---------- include/cli/argument.hh | 61 +++++--- include/cli/parser.hh | 12 +- 3 files changed, 279 insertions(+), 96 deletions(-) diff --git a/include/cli/action.hh b/include/cli/action.hh index f1c5639..80ced7f 100644 --- a/include/cli/action.hh +++ b/include/cli/action.hh @@ -30,13 +30,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 +46,78 @@ template struct ActionResult { using value_type = T; + enum class State : 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}; + } + + /** + * @brief Propagates the non-ok state to a different type. + * + * @tparam U The target value type. + * @return An `ActionResult` carrying the same error or ignore state. + */ + template + [[nodiscard]] constexpr auto propagate() const -> ActionResult { + if (state == State::error) return ActionResult::fail(error); + return ActionResult::ignore(); } }; @@ -102,28 +127,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 : 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,23 +169,39 @@ 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 the non-ok state to a different type. + * + * @tparam U The target value type. + * @return An `ActionResult` carrying the same error or ignore state. + */ + template + [[nodiscard]] constexpr auto propagate() const -> ActionResult { + if (state == State::error) return ActionResult::fail(error); + return ActionResult::ignore(); } }; @@ -155,7 +209,8 @@ struct ActionResult { * @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 @@ -163,11 +218,11 @@ struct ActionResult { */ template 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 index{}; ///< Zero-based index into `argv` of the current token. + size_t occurrences{}; ///< Number of times the parent option token has appeared. + 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. }; @@ -183,11 +238,11 @@ struct ActionCtx { */ template <> 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 index{}; ///< Zero-based index into `argv` of the current token. + size_t occurrences{}; ///< Number of times the parent option token has appeared. + 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 +254,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; }; @@ -353,12 +410,12 @@ 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)); + ctx, ActionResult::ignore()); } } @@ -381,12 +438,12 @@ 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)); + ctx, ActionResult::ignore()); } } @@ -423,13 +480,32 @@ struct Action { } } + private: + template + struct first_fn_type { + using type = std::remove_cvref_t; + }; + + public: + static constexpr bool entry_is_optional = + first_fn_type::type::template accepts_input< + std::optional>; + static constexpr auto validate() -> auto { - return validate_impl(); + using FirstFn = typename first_fn_type::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 +617,7 @@ struct Integer { constexpr auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input.template propagate(); T result{}; auto r = std::from_chars(input.value.data(), input.value.data() + input.value.size(), result); @@ -592,6 +669,7 @@ struct Floating { constexpr auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input.template propagate(); T result{}; auto r = std::from_chars(input.value.data(), input.value.data() + input.value.size(), result); @@ -634,6 +712,7 @@ struct String { constexpr auto operator()(ActionCtx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input.template propagate(); return ActionResult::ok(std::string(input.value)); } }; @@ -659,6 +738,7 @@ struct Bool { constexpr auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input.template propagate(); const std::string_view val = input.value; if (val == "true" || val == "1") { return ActionResult::ok(true); @@ -688,6 +768,7 @@ struct Path { auto operator()(ActionCtx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input.template propagate(); return ActionResult::ok( std::filesystem::path{input.value}); } @@ -713,6 +794,7 @@ struct ExistingFile { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input.template propagate(); auto path = std::filesystem::path{input.value}; if (!std::filesystem::exists(path) || !std::filesystem::is_regular_file(path)) { @@ -745,6 +827,7 @@ struct ExistingDirectory { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input.template propagate(); auto path = std::filesystem::path{input.value}; if (!std::filesystem::exists(path) || !std::filesystem::is_directory(path)) { return ActionResult::fail( @@ -781,6 +864,7 @@ struct Choice { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input.template propagate(); if (auto value = Mapper(input.value); value.has_value()) { return ActionResult::ok(*std::move(value)); } @@ -837,6 +921,7 @@ struct Negatable { constexpr auto operator()(ActionCtx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input.template propagate(); constexpr std::string_view prefix = Prefix.view(); const std::string_view val = input.value; if (val.starts_with(prefix)) { @@ -856,6 +941,51 @@ 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 input.template propagate(); + 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 +1023,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 +1053,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 +1086,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 +1111,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 +1136,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 +1162,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 +1188,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 +1222,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 +1255,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 +1281,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 +1305,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 +1329,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 +1354,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 +1389,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 +1449,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 input.template propagate(); ctx.arg.get() = true; return ActionResult::ok(); } @@ -1324,8 +1469,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 input.template propagate(); ctx.arg.get() = false; return ActionResult::ok(); } @@ -1343,8 +1489,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 input.template propagate(); ctx.arg.get() = !ctx.arg.get(); return ActionResult::ok(); } @@ -1362,8 +1509,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 input.template propagate(); ++ctx.arg.get(); return ActionResult::ok(); } @@ -1390,6 +1538,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))); @@ -1420,6 +1569,7 @@ struct SetOnce { template auto operator()(ActionCtx>> ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input.template propagate(); if (ctx.arg.get().has_value() || ctx.occurrences > 1) { return ActionResult::fail(detail::duplicate_argument_error( ctx.index, detail::to_error_subject(input.value))); @@ -1449,6 +1599,7 @@ struct PushUnique { template auto operator()(ActionCtx>> ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input.template propagate(); auto& values = ctx.arg.get(); if (std::ranges::find(values, input.value) != values.end()) { return ActionResult::fail(detail::duplicate_argument_error( @@ -1479,6 +1630,7 @@ struct Push { template auto operator()(ActionCtx>> ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input.template propagate(); ctx.arg.get().push_back(std::move(input.value)); return ActionResult::ok(); } @@ -1505,6 +1657,7 @@ struct Insert { template auto operator()(ActionCtx>> ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input.template propagate(); ctx.arg.get().insert(std::move(input.value)); return ActionResult::ok(); } @@ -1534,6 +1687,7 @@ struct InsertOrAssign { template auto operator()(ActionCtx> ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input.template propagate(); auto&& [key, value] = input.value; ctx.arg.get().insert_or_assign(key, value); return ActionResult::ok(); @@ -1556,6 +1710,7 @@ struct Extend { template auto operator()(ActionCtx> ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input.template propagate(); auto& out = ctx.arg.get(); for (auto&& value : input.value) { out.emplace_back(value); @@ -1582,8 +1737,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 input.template propagate(); ctx.arg.get() = true; return ActionResult::ok(); } @@ -1611,6 +1767,7 @@ struct StoreInto { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input.template propagate(); if (T* ptr = ctx.arg.get(); ptr != nullptr) { *ptr = std::move(input.value); return ActionResult::ok(); @@ -1649,6 +1806,7 @@ struct Callback { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { + if (!input.have_value()) return input.template propagate(); if constexpr (requires { Fn(ctx, input.value); }) { Fn(ctx, input.value); } else if constexpr (requires { Fn(input.value); }) { @@ -1704,6 +1862,7 @@ struct PrintHelp { template auto operator()(ActionCtx, ActionResult input) const -> ActionResult> { + if (!input.have_value()) return input.template propagate>(); return ActionResult>::ok( after_type{.value = std::move(input.value)}); } @@ -1727,7 +1886,8 @@ 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{ diff --git a/include/cli/argument.hh b/include/cli/argument.hh index c6f4b0f..e51c6ba 100644 --- a/include/cli/argument.hh +++ b/include/cli/argument.hh @@ -445,38 +445,57 @@ struct ArgImpl : public OptionTag { 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) + auto invoke_action(std::string_view text, std::size_t arg_index, + std::size_t nargs, std::size_t value_index) -> ActionResult { provided_ = true; ActionCtx ctx{ .index = arg_index, .occurrences = occurrence_count_, - .invoke_count = invoke_count_, + .nargs = 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)); + ++total_values_; + auto invoke_result = [&] { + if constexpr (decltype(A)::entry_is_optional) { + return decltype(A)::invoke( + ctx, ActionResult>::ok( + std::optional{text})); + } else { + return decltype(A)::invoke(ctx, ActionResult::ok(text)); + } + }(); + if (invoke_result.has_error()) { + return ActionResult::fail(std::move(invoke_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 { + auto invoke_flag(std::size_t arg_index, std::size_t nargs) -> ActionResult { provided_ = true; ActionCtx ctx{ .index = arg_index, .occurrences = occurrence_count_, - .invoke_count = invoke_count_, + .nargs = nargs, + .value_index = 0, + .total_values = total_values_, .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)); + auto invoke_result = [&] { + if constexpr (decltype(A)::entry_is_optional) { + return decltype(A)::invoke( + ctx, ActionResult>::ok( + std::optional{std::nullopt})); + } else { + return decltype(A)::invoke( + ctx, ActionResult::ok(std::string_view{})); + } + }(); + if (invoke_result.has_error()) { + return ActionResult::fail(std::move(invoke_result.error)); } return ActionResult::ok(); } @@ -497,7 +516,7 @@ struct ArgImpl : public OptionTag { 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 + std::size_t total_values_{}; // total values across all occurrences bool provided_{}; }; @@ -571,17 +590,20 @@ struct PositionalImpl : public PositionalTag { static constexpr auto nargs = N; // Called once per value token for this positional. - auto invoke_action(std::string_view text, std::size_t arg_index) + auto invoke_action(std::string_view text, std::size_t arg_index, + std::size_t nargs = 1, std::size_t value_index = 0) -> ActionResult { provided_ = true; ActionCtx ctx{ .index = arg_index, .occurrences = occurrence_count_, - .invoke_count = invoke_count_, + .nargs = 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()) { @@ -606,8 +628,7 @@ struct PositionalImpl : public PositionalTag { value_type value_{}; std::function on_parse_{}; 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_{}; }; diff --git a/include/cli/parser.hh b/include/cli/parser.hh index 27d6ddc..4040fc1 100644 --- a/include/cli/parser.hh +++ b/include/cli/parser.hh @@ -819,11 +819,13 @@ struct Parser { found = true; f.notify_option_seen(); if constexpr (F::nargs.min == 0 && F::nargs.max == 0) { - res = f.invoke_flag(option_position + first_index); + res = f.invoke_flag(option_position + first_index, vals.size()); } else { - for (const auto& vt : vals) { + 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(vt.text, vt.position + first_index); + res = f.invoke_action(vals[vi].text, vals[vi].position + first_index, + nargs, vi); } } if (!res.has_error()) f.fire_on_parse(); @@ -946,9 +948,9 @@ struct Parser { 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_flag(0, 0); } else { - res = f.invoke_action(std::string_view(raw), 0); + res = f.invoke_action(std::string_view(raw), 0, 1, 0); } if (!res.has_error()) f.fire_on_parse(); } From b39bb6074ac0d93c17d1c0ba21387a39c605a753 Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Mon, 11 May 2026 14:43:02 +0900 Subject: [PATCH 02/11] style: apply clang-format --- include/cli/action.hh | 89 ++++++++++++++++++++++------------------- include/cli/argument.hh | 6 ++- 2 files changed, 52 insertions(+), 43 deletions(-) diff --git a/include/cli/action.hh b/include/cli/action.hh index 80ced7f..ea9f34b 100644 --- a/include/cli/action.hh +++ b/include/cli/action.hh @@ -218,11 +218,13 @@ struct ActionResult { */ template 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 nargs{}; ///< Number of values in this option occurrence. + 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 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. + size_t + total_values{}; ///< Total values across all occurrences of this option. std::reference_wrapper arg{}; ///< Reference to the argument's storage value. }; @@ -238,11 +240,13 @@ struct ActionCtx { */ template <> 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 nargs{}; ///< Number of values in this option occurrence. + 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 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. + size_t + total_values{}; ///< Total values across all occurrences of this option. /** * @brief Constructs from a typed `ActionCtx`, copying the counters. @@ -404,28 +408,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.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) { + 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 @@ -442,8 +445,7 @@ struct Action { if constexpr (sizeof...(FnTail) == 0) { return ActionResult::ignore(); } else { - return Action::invoke( - ctx, ActionResult::ignore()); + return Action::invoke(ctx, ActionResult::ignore()); } } @@ -487,9 +489,8 @@ struct Action { }; public: - static constexpr bool entry_is_optional = - first_fn_type::type::template accepts_input< - std::optional>; + static constexpr bool entry_is_optional = first_fn_type< + Fns...>::type::template accepts_input>; static constexpr auto validate() -> auto { using FirstFn = typename first_fn_type::type; @@ -768,7 +769,8 @@ struct Path { auto operator()(ActionCtx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) + return input.template propagate(); return ActionResult::ok( std::filesystem::path{input.value}); } @@ -794,7 +796,8 @@ struct ExistingFile { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) + return input.template propagate(); auto path = std::filesystem::path{input.value}; if (!std::filesystem::exists(path) || !std::filesystem::is_regular_file(path)) { @@ -827,7 +830,8 @@ struct ExistingDirectory { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) + return input.template propagate(); auto path = std::filesystem::path{input.value}; if (!std::filesystem::exists(path) || !std::filesystem::is_directory(path)) { return ActionResult::fail( @@ -946,7 +950,8 @@ inline constexpr auto existing_directory = Action{}; * flag (no value provided). * * Accepts `std::optional` as input: - * - If the optional has a value (actual value provided), unwrap and pass through. + * - 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. @@ -984,7 +989,8 @@ struct DefaultMissingValue { }; template -inline constexpr auto default_missing_value = Action{}>{}; +inline constexpr auto default_missing_value = + Action{}>{}; } // namespace conversion @@ -1886,7 +1892,8 @@ struct ExitSuccess { using storage_type = std::monostate; template - auto operator()(ActionCtx ctx, ActionResult input) 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) { diff --git a/include/cli/argument.hh b/include/cli/argument.hh index e51c6ba..87a0ef8 100644 --- a/include/cli/argument.hh +++ b/include/cli/argument.hh @@ -464,7 +464,8 @@ struct ArgImpl : public OptionTag { ctx, ActionResult>::ok( std::optional{text})); } else { - return decltype(A)::invoke(ctx, ActionResult::ok(text)); + return decltype(A)::invoke(ctx, + ActionResult::ok(text)); } }(); if (invoke_result.has_error()) { @@ -474,7 +475,8 @@ struct ArgImpl : public OptionTag { } // Called once for flags (nargs = {0,0}) after the option token is seen. - auto invoke_flag(std::size_t arg_index, std::size_t nargs) -> ActionResult { + auto invoke_flag(std::size_t arg_index, std::size_t nargs) + -> ActionResult { provided_ = true; ActionCtx ctx{ .index = arg_index, From 3b3b8194ba1f192fd6b0c4bf871b5551774f1b8f Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Mon, 11 May 2026 14:44:58 +0900 Subject: [PATCH 03/11] fix: rename invoke_action/invoke_flag nargs param to avoid MSVC C4458 Parameter 'nargs' was shadowing static member 'nargs', causing MSVC warning C4458 (treated as error with /WX). Co-Authored-By: Claude Sonnet 4.6 --- include/cli/argument.hh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/include/cli/argument.hh b/include/cli/argument.hh index 87a0ef8..503c615 100644 --- a/include/cli/argument.hh +++ b/include/cli/argument.hh @@ -446,13 +446,13 @@ struct ArgImpl : public OptionTag { // Called once per value token associated with this option. auto invoke_action(std::string_view text, std::size_t arg_index, - std::size_t nargs, std::size_t value_index) + std::size_t occurrence_nargs, std::size_t value_index) -> ActionResult { provided_ = true; ActionCtx ctx{ .index = arg_index, .occurrences = occurrence_count_, - .nargs = nargs, + .nargs = occurrence_nargs, .value_index = value_index, .total_values = total_values_, .arg = std::ref(value_), @@ -475,13 +475,13 @@ struct ArgImpl : public OptionTag { } // Called once for flags (nargs = {0,0}) after the option token is seen. - auto invoke_flag(std::size_t arg_index, std::size_t nargs) + auto invoke_flag(std::size_t arg_index, std::size_t occurrence_nargs) -> ActionResult { provided_ = true; ActionCtx ctx{ .index = arg_index, .occurrences = occurrence_count_, - .nargs = nargs, + .nargs = occurrence_nargs, .value_index = 0, .total_values = total_values_, .arg = std::ref(value_), @@ -593,13 +593,13 @@ struct PositionalImpl : public PositionalTag { // Called once per value token for this positional. auto invoke_action(std::string_view text, std::size_t arg_index, - std::size_t nargs = 1, std::size_t value_index = 0) - -> ActionResult { + std::size_t occurrence_nargs = 1, + std::size_t value_index = 0) -> ActionResult { provided_ = true; ActionCtx ctx{ .index = arg_index, .occurrences = occurrence_count_, - .nargs = nargs, + .nargs = occurrence_nargs, .value_index = value_index, .total_values = total_values_, .arg = std::ref(value_), From 74309e925e9f79a55f2b1242c13bbd1f12e23b1a Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Mon, 11 May 2026 16:33:49 +0900 Subject: [PATCH 04/11] fix: support optional missing values in options --- include/cli/action.hh | 106 +++++++++++++++++++++------------------- include/cli/argument.hh | 4 ++ include/cli/parser.hh | 14 ++++-- modules/cli20.cppm | 1 + tests/test_sugar.cc | 30 ++++++++++++ 5 files changed, 99 insertions(+), 56 deletions(-) diff --git a/include/cli/action.hh b/include/cli/action.hh index ea9f34b..38dc50f 100644 --- a/include/cli/action.hh +++ b/include/cli/action.hh @@ -107,18 +107,6 @@ struct ActionResult { [[nodiscard]] static constexpr auto ignore() -> ActionResult { return {.state = State::ignore}; } - - /** - * @brief Propagates the non-ok state to a different type. - * - * @tparam U The target value type. - * @return An `ActionResult` carrying the same error or ignore state. - */ - template - [[nodiscard]] constexpr auto propagate() const -> ActionResult { - if (state == State::error) return ActionResult::fail(error); - return ActionResult::ignore(); - } }; /** @@ -191,20 +179,39 @@ struct ActionResult { [[nodiscard]] static constexpr auto ignore() -> ActionResult { return {.state = State::ignore}; } - - /** - * @brief Propagates the non-ok state to a different type. - * - * @tparam U The target value type. - * @return An `ActionResult` carrying the same error or ignore state. - */ - template - [[nodiscard]] constexpr auto propagate() const -> ActionResult { - if (state == State::error) return ActionResult::fail(error); - return ActionResult::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. * @@ -618,7 +625,7 @@ struct Integer { constexpr auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) return propagate(input); T result{}; auto r = std::from_chars(input.value.data(), input.value.data() + input.value.size(), result); @@ -670,7 +677,7 @@ struct Floating { constexpr auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) return propagate(input); T result{}; auto r = std::from_chars(input.value.data(), input.value.data() + input.value.size(), result); @@ -713,7 +720,7 @@ struct String { constexpr auto operator()(ActionCtx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) return propagate(input); return ActionResult::ok(std::string(input.value)); } }; @@ -739,7 +746,7 @@ struct Bool { constexpr auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) return propagate(input); const std::string_view val = input.value; if (val == "true" || val == "1") { return ActionResult::ok(true); @@ -769,8 +776,7 @@ struct Path { auto operator()(ActionCtx, ActionResult input) const -> ActionResult { - if (!input.have_value()) - return input.template propagate(); + if (!input.have_value()) return propagate(input); return ActionResult::ok( std::filesystem::path{input.value}); } @@ -796,8 +802,7 @@ struct ExistingFile { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) - return input.template propagate(); + 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)) { @@ -830,8 +835,7 @@ struct ExistingDirectory { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) - return input.template propagate(); + 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( @@ -868,7 +872,7 @@ struct Choice { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) return propagate(input); if (auto value = Mapper(input.value); value.has_value()) { return ActionResult::ok(*std::move(value)); } @@ -925,7 +929,7 @@ struct Negatable { constexpr auto operator()(ActionCtx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + 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)) { @@ -973,7 +977,7 @@ struct DefaultMissingValue { auto operator()(ActionCtx ctx, ActionResult> input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) return propagate(input); if (input.value.has_value()) { // actual value provided: unwrap and pass through return ActionResult::ok(*input.value); @@ -1457,7 +1461,7 @@ struct SetTrue { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) return propagate(input); ctx.arg.get() = true; return ActionResult::ok(); } @@ -1477,7 +1481,7 @@ struct SetFalse { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) return propagate(input); ctx.arg.get() = false; return ActionResult::ok(); } @@ -1497,7 +1501,7 @@ struct Toggle { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) return propagate(input); ctx.arg.get() = !ctx.arg.get(); return ActionResult::ok(); } @@ -1517,7 +1521,7 @@ struct Increment { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) return propagate(input); ++ctx.arg.get(); return ActionResult::ok(); } @@ -1575,7 +1579,7 @@ struct SetOnce { template auto operator()(ActionCtx>> ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) return propagate(input); if (ctx.arg.get().has_value() || ctx.occurrences > 1) { return ActionResult::fail(detail::duplicate_argument_error( ctx.index, detail::to_error_subject(input.value))); @@ -1605,7 +1609,7 @@ struct PushUnique { template auto operator()(ActionCtx>> ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + 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( @@ -1636,7 +1640,7 @@ struct Push { template auto operator()(ActionCtx>> ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) return propagate(input); ctx.arg.get().push_back(std::move(input.value)); return ActionResult::ok(); } @@ -1663,7 +1667,7 @@ struct Insert { template auto operator()(ActionCtx>> ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) return propagate(input); ctx.arg.get().insert(std::move(input.value)); return ActionResult::ok(); } @@ -1693,7 +1697,7 @@ struct InsertOrAssign { template auto operator()(ActionCtx> ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) return propagate(input); auto&& [key, value] = input.value; ctx.arg.get().insert_or_assign(key, value); return ActionResult::ok(); @@ -1716,7 +1720,7 @@ struct Extend { template auto operator()(ActionCtx> ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) return propagate(input); auto& out = ctx.arg.get(); for (auto&& value : input.value) { out.emplace_back(value); @@ -1745,7 +1749,7 @@ struct MarkPresent { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) return propagate(input); ctx.arg.get() = true; return ActionResult::ok(); } @@ -1773,7 +1777,7 @@ struct StoreInto { auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + if (!input.have_value()) return propagate(input); if (T* ptr = ctx.arg.get(); ptr != nullptr) { *ptr = std::move(input.value); return ActionResult::ok(); @@ -1812,7 +1816,7 @@ struct Callback { template auto operator()(ActionCtx ctx, ActionResult input) const -> ActionResult { - if (!input.have_value()) return input.template propagate(); + 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); }) { @@ -1868,7 +1872,7 @@ struct PrintHelp { template auto operator()(ActionCtx, ActionResult input) const -> ActionResult> { - if (!input.have_value()) return input.template propagate>(); + if (!input.have_value()) return propagate>(input); return ActionResult>::ok( after_type{.value = std::move(input.value)}); } diff --git a/include/cli/argument.hh b/include/cli/argument.hh index 503c615..1e2bbc9 100644 --- a/include/cli/argument.hh +++ b/include/cli/argument.hh @@ -440,6 +440,8 @@ struct ArgImpl : public OptionTag { static constexpr auto name = Name; static constexpr auto short_name = ShortName; static constexpr auto nargs = N; + 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_; } @@ -590,6 +592,8 @@ struct PositionalImpl : public PositionalTag { value_type default_value{}; static constexpr auto nargs = N; + 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, diff --git a/include/cli/parser.hh b/include/cli/parser.hh index 51a338c..1504815 100644 --- a/include/cli/parser.hh +++ b/include/cli/parser.hh @@ -831,11 +831,15 @@ struct Parser { if constexpr (F::nargs.min == 0 && F::nargs.max == 0) { res = f.invoke_flag(option_position + first_index, vals.size()); } else { - 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 (vals.empty() && F::entry_is_optional) { + res = f.invoke_flag(option_position + first_index, 0); + } else { + 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(); 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_sugar.cc b/tests/test_sugar.cc index 4093e02..b4b9fdc 100644 --- a/tests/test_sugar.cc +++ b/tests/test_sugar.cc @@ -106,3 +106,33 @@ 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.value().has_value()); + EXPECT_EQ(*result.value.mode.value(), "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.value().has_value()); + EXPECT_EQ(*result.value.mode.value(), "manual"); + } +} From 5553f3d53513e3404d032dd1da9a3393387fefc4 Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Mon, 11 May 2026 16:46:58 +0900 Subject: [PATCH 05/11] docs: clarify ignore propagation semantics --- docs/docs/actions.mdx | 5 +++-- include/cli/action.hh | 9 +++++---- tests/test_conversion.cc | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/docs/docs/actions.mdx b/docs/docs/actions.mdx index 53eb1df..6dc6378 100644 --- a/docs/docs/actions.mdx +++ b/docs/docs/actions.mdx @@ -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. --- diff --git a/include/cli/action.hh b/include/cli/action.hh index 38dc50f..c3e08e6 100644 --- a/include/cli/action.hh +++ b/include/cli/action.hh @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -46,7 +47,7 @@ template struct ActionResult { using value_type = T; - enum class State : uint8_t { ok, error, ignore }; + enum class State : std::uint8_t { ok, error, ignore }; State state{State::ok}; ParseError error{}; @@ -122,7 +123,7 @@ template <> struct ActionResult { using value_type = void; - enum class State : uint8_t { ok, error, ignore }; + enum class State : std::uint8_t { ok, error, ignore }; State state{State::ok}; ParseError error{}; @@ -399,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. */ diff --git a/tests/test_conversion.cc b/tests/test_conversion.cc index 46eb585..8c86646 100644 --- a/tests/test_conversion.cc +++ b/tests/test_conversion.cc @@ -13,8 +13,10 @@ namespace { using cli::ActionCtx; using cli::ActionResult; +using cli::Action; 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 { From baa4f0e661a9a85d19f85e74a293e067602a369e Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Mon, 11 May 2026 16:51:39 +0900 Subject: [PATCH 06/11] docs: update advanced jobs example --- docs/docs/examples/advanced.mdx | 15 ++++++++----- examples/advanced.cc | 40 ++++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/docs/docs/examples/advanced.mdx b/docs/docs/examples/advanced.mdx index d4609db..dc19cdc 100644 --- a/docs/docs/examples/advanced.mdx +++ b/docs/docs/examples/advanced.mdx @@ -28,9 +28,11 @@ 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` (or a +`nproc_dummy()` value when `-j` is provided without a value), plus collected +`includes` and `defines`. ## Try It @@ -44,16 +46,19 @@ 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 +- **Explicit jobs** — `--config config.toml --jobs 8` to override the runtime default - **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 +- **Missing jobs value** — `--config config.toml --jobs` 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/examples/advanced.cc b/examples/advanced.cc index 29894ca..06f2871 100644 --- a/examples/advanced.cc +++ b/examples/advanced.cc @@ -1,11 +1,43 @@ #include #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 = + std::same_as, std::optional>; + + template + using after_type = int; + + 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{}( + ctx, cli::ActionResult::ok(*input.value)); + } + return cli::ActionResult::ok(nproc_dummy()); + } +}; + struct Args { cli::Description description{ "Action-pipeline example using Arg, filesystem conversion, and" @@ -21,9 +53,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{{.help = "Parallel job count (default 1; -j uses nproc_dummy())", + .default_value = 1}}; cli::Arg<"include", 'I', cli::conversion::path | cli::validation::parent_exists | From f5a4d7214a03164dcfa736cd7a1ca10808320b27 Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Mon, 11 May 2026 18:35:32 +0900 Subject: [PATCH 07/11] Refine single-value option storage --- docs/docs/actions.mdx | 27 +++--- docs/docs/examples/advanced.mdx | 13 +-- docs/docs/intro.mdx | 6 +- docs/docs/tutorial.md | 14 +-- examples/advanced.cc | 19 ++-- examples/callback_entry_point.cc | 7 +- examples/compiler.cc | 2 +- examples/env_fallback.cc | 8 +- examples/module.cc | 2 +- examples/simple_api.cc | 6 +- examples/single_header.cc | 6 +- examples/subcommand.cc | 4 +- examples/unicode_echo.cc | 4 +- examples/validation.cc | 8 +- include/cli/action.hh | 35 ++++---- include/cli/argument.hh | 85 +++++++++++++----- include/cli/parser.hh | 7 +- tests/test_callback.cc | 8 +- tests/test_conversion.cc | 2 +- tests/test_parse.cc | 150 +++++++++++++++++++++++++------ tests/test_sugar.cc | 54 ++++++++--- 21 files changed, 310 insertions(+), 157 deletions(-) diff --git a/docs/docs/actions.mdx b/docs/docs/actions.mdx index 6dc6378..f6cad35 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 @@ -144,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 | @@ -215,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 } @@ -242,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 dc19cdc..e6bf829 100644 --- a/docs/docs/examples/advanced.mdx +++ b/docs/docs/examples/advanced.mdx @@ -30,9 +30,8 @@ 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, 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` (or a -`nproc_dummy()` value when `-j` is provided without a value), plus collected -`includes` and `defines`. +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 @@ -54,11 +53,13 @@ Things to try: - **Happy path** — `--config config.toml -I /include -D DEBUG` - **Default jobs** — `--config config.toml` to see the `1` default -- **Explicit jobs** — `--config config.toml --jobs 8` to override the runtime 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 -- **Missing jobs value** — `--config config.toml --jobs` to see the example fall back to `nproc_dummy()` +- **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 06f2871..9cee0d2 100644 --- a/examples/advanced.cc +++ b/examples/advanced.cc @@ -2,7 +2,6 @@ #include #include #include -#include #include "cli/argument.hh" #include "cli/parser.hh" @@ -18,10 +17,10 @@ auto nproc_dummy() -> int { struct JobsOrNproc { template static constexpr bool accepts_input = - std::same_as, std::optional>; + cli::deduce_accepts_input; template - using after_type = int; + using after_type = cli::deduce_after_type; template using storage_type = void; @@ -31,8 +30,8 @@ struct JobsOrNproc { -> cli::ActionResult { if (!input.have_value()) return cli::propagate(input); if (input.value.has_value()) { - return cli::conversion::Integer{}( - ctx, cli::ActionResult::ok(*input.value)); + return cli::conversion::integer.invoke( + ctx, cli::ActionResult::ok(input.value.value())); } return cli::ActionResult::ok(nproc_dummy()); } @@ -56,8 +55,8 @@ struct Args { cli::Action{} | cli::validation::range<1, 64> | cli::pack::set_once, cli::nargs::zero_or_one> - jobs{{.help = "Parallel job count (default 1; -j uses nproc_dummy())", - .default_value = 1}}; + 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 | @@ -75,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..d9f9076 100644 --- a/examples/callback_entry_point.cc +++ b/examples/callback_entry_point.cc @@ -44,12 +44,7 @@ auto main(int argc, char* argv[]) -> int { .on_parse = [&state](const bool& value) { 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) { state.jobs = value; }, }}; args.includes = cli::ListOption>{{ 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 c3e08e6..9c80fbc 100644 --- a/include/cli/action.hh +++ b/include/cli/action.hh @@ -2,10 +2,10 @@ #include #include -#include #include #include #include +#include #include #include #include @@ -492,16 +492,16 @@ struct Action { private: template - struct first_fn_type { + struct FirstFnType { using type = std::remove_cvref_t; }; public: - static constexpr bool entry_is_optional = first_fn_type< + static constexpr bool entry_is_optional = FirstFnType< Fns...>::type::template accepts_input>; static constexpr auto validate() -> auto { - using FirstFn = typename first_fn_type::type; + using FirstFn = typename FirstFnType::type; if constexpr (FirstFn::template accepts_input< std::optional>) { return validate_impl, Fns...>(); @@ -934,11 +934,11 @@ struct Negatable { 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}); } }; @@ -1559,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 { @@ -1575,13 +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 { + auto operator()(ActionCtx> ctx, ActionResult input) const + -> ActionResult { if (!input.have_value()) return propagate(input); - if (ctx.arg.get().has_value() || ctx.occurrences > 1) { + if (ctx.occurrences > 1) { return ActionResult::fail(detail::duplicate_argument_error( ctx.index, detail::to_error_subject(input.value))); } @@ -1682,18 +1681,18 @@ 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 diff --git a/include/cli/argument.hh b/include/cli/argument.hh index 1e2bbc9..683adaf 100644 --- a/include/cli/argument.hh +++ b/include/cli/argument.hh @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -378,7 +379,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 @@ -403,7 +405,8 @@ struct ArgImpl : public OptionTag { : help(param.help), presence(param.presence), env(param.env), - value_(param.default_value), + default_value(param.default_value), + value_(param.default_value.value_or(value_type{})), on_parse_(std::move(param.on_parse)) {} // Convenience constructor for StoreInto variants (value_type == T*). @@ -435,7 +438,7 @@ struct ArgImpl : public OptionTag { // NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes) std::string_view env{}; // NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes) - value_type default_value{}; + std::optional default_value{}; static constexpr auto name = Name; static constexpr auto short_name = ShortName; @@ -448,9 +451,9 @@ struct ArgImpl : public OptionTag { // Called once per value token associated with this option. auto invoke_action(std::string_view text, std::size_t arg_index, - std::size_t occurrence_nargs, std::size_t value_index) - -> ActionResult { - provided_ = true; + 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_, @@ -460,7 +463,7 @@ struct ArgImpl : public OptionTag { .arg = std::ref(value_), }; ++total_values_; - auto invoke_result = [&] { + auto invoke_result = [&]() -> auto { if constexpr (decltype(A)::entry_is_optional) { return decltype(A)::invoke( ctx, ActionResult>::ok( @@ -473,13 +476,14 @@ struct ArgImpl : public OptionTag { 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 for flags (nargs = {0,0}) after the option token is seen. - auto invoke_flag(std::size_t arg_index, std::size_t occurrence_nargs) - -> ActionResult { - provided_ = true; + auto invoke_flag(std::size_t arg_index, std::size_t occurrence_nargs, + bool mark_provided = true) -> ActionResult { + if (mark_provided) provided_ = true; ActionCtx ctx{ .index = arg_index, .occurrences = occurrence_count_, @@ -488,7 +492,7 @@ struct ArgImpl : public OptionTag { .total_values = total_values_, .arg = std::ref(value_), }; - auto invoke_result = [&] { + auto invoke_result = [&]() -> auto { if constexpr (decltype(A)::entry_is_optional) { return decltype(A)::invoke( ctx, ActionResult>::ok( @@ -501,6 +505,7 @@ struct ArgImpl : public OptionTag { if (invoke_result.has_error()) { return ActionResult::fail(std::move(invoke_result.error)); } + if (invoke_result.has_value()) value_present_ = true; return ActionResult::ok(); } @@ -512,6 +517,25 @@ struct ArgImpl : public OptionTag { [[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; + } private: template @@ -522,6 +546,7 @@ struct ArgImpl : public OptionTag { std::size_t occurrence_count_{}; // times the option token appeared std::size_t total_values_{}; // total values across all occurrences bool provided_{}; + bool value_present_{default_value.has_value()}; }; template @@ -582,14 +607,15 @@ struct PositionalImpl : public PositionalTag { PositionalImpl(ArgParameter param) : help(param.help), presence(param.presence), - value_(param.default_value), + default_value(param.default_value), + value_(param.default_value.value_or(value_type{})), 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{}; + std::optional default_value{}; static constexpr auto nargs = N; static constexpr bool entry_is_optional = @@ -600,6 +626,7 @@ struct PositionalImpl : public PositionalTag { 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_, @@ -608,13 +635,13 @@ struct PositionalImpl : public PositionalTag { .total_values = total_values_, .arg = std::ref(value_), }; - ++occurrence_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(); } @@ -626,6 +653,25 @@ struct PositionalImpl : public PositionalTag { [[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; + } private: template @@ -636,6 +682,7 @@ struct PositionalImpl : public PositionalTag { std::size_t occurrence_count_{}; // times a value was provided std::size_t total_values_{}; // total values across all invocations bool provided_{}; + bool value_present_{default_value.has_value()}; }; /** @@ -836,11 +883,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. @@ -867,8 +913,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/parser.hh b/include/cli/parser.hh index 1504815..a5b3cde 100644 --- a/include/cli/parser.hh +++ b/include/cli/parser.hh @@ -926,7 +926,7 @@ struct Parser { if (res.has_error()) { return; } - if (f.presence == Presence::required && !f.provided()) { + if (f.presence == Presence::required && !static_cast(f)) { std::string subject; if constexpr (std::derived_from) { subject = "--" + std::string(F::name.view()); @@ -960,11 +960,10 @@ struct Parser { const char* raw = std::getenv(std::string(f.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, 0); + res = f.invoke_flag(0, 0, false); } else { - res = f.invoke_action(std::string_view(raw), 0, 1, 0); + res = f.invoke_action(std::string_view(raw), 0, 1, 0, false); } if (!res.has_error()) f.fire_on_parse(); } diff --git a/tests/test_callback.cc b/tests/test_callback.cc index 8c91b36..1db10e6 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) { captured = v; }, }}; auto args_vec = argv({"prog", "--count", "42"}); @@ -132,7 +128,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&) { ++call_count; }, }}; auto args_vec = argv({"prog", "--count", "notanint"}); diff --git a/tests/test_conversion.cc b/tests/test_conversion.cc index 8c86646..18b4fcd 100644 --- a/tests/test_conversion.cc +++ b/tests/test_conversion.cc @@ -11,9 +11,9 @@ namespace fs = std::filesystem; namespace { +using cli::Action; using cli::ActionCtx; using cli::ActionResult; -using cli::Action; using cli::ErrorCode; using cli::conversion::Bool; using cli::conversion::DefaultMissingValue; diff --git a/tests/test_parse.cc b/tests/test_parse.cc index 186b095..a70d45d 100644 --- a/tests/test_parse.cc +++ b/tests/test_parse.cc @@ -1,6 +1,7 @@ #include #include +#include #include #include @@ -35,6 +36,42 @@ 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}}; +}; + auto argv(std::initializer_list values) -> std::vector { return {values}; @@ -51,13 +88,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 +142,61 @@ 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); + } +} + #ifdef _WIN32 namespace { @@ -136,8 +228,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 +239,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 +250,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 +261,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 +309,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 +344,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 +356,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 +368,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 +429,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 +442,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 +457,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 +480,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_sugar.cc b/tests/test_sugar.cc index b4b9fdc..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) { @@ -122,8 +122,8 @@ TEST(Sugar, DefaultMissingValueSupportsFlagAndExplicitValue) { std::span(args), 1); ASSERT_TRUE(result.has_value()); - ASSERT_TRUE(result.value.mode.value().has_value()); - EXPECT_EQ(*result.value.mode.value(), "auto"); + ASSERT_TRUE(result.value.mode); + EXPECT_EQ(*result.value.mode, "auto"); } { @@ -132,7 +132,33 @@ TEST(Sugar, DefaultMissingValueSupportsFlagAndExplicitValue) { std::span(args), 1); ASSERT_TRUE(result.has_value()); - ASSERT_TRUE(result.value.mode.value().has_value()); - EXPECT_EQ(*result.value.mode.value(), "manual"); + 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()); +} From a787190f524e0e2830be90ea027c7a383e4293be Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Mon, 11 May 2026 19:59:24 +0900 Subject: [PATCH 08/11] refactor(cli): rename store_into to write_to and encapsulate metadata Rename `pack::store_into` to `pack::write_to` across the library, tests, and documentation. Refactor `ArgImpl` and `PositionalImpl` to store metadata in an internal `ArgParameter` struct instead of flat fields, providing explicit getters for access. Add program name accessors to the `Parser` class. BREAKING CHANGE: pack::store_into is renamed to pack::write_to. --- docs/docs/actions.mdx | 2 +- examples/callback_entry_point.cc | 18 ++- include/cli/action.hh | 4 +- include/cli/argument.hh | 198 +++++++++++++++---------------- include/cli/help.hh | 9 +- include/cli/parser.hh | 47 +++++--- tests/test_store_into.cc | 4 +- 7 files changed, 148 insertions(+), 134 deletions(-) diff --git a/docs/docs/actions.mdx b/docs/docs/actions.mdx index f6cad35..39b0289 100644 --- a/docs/docs/actions.mdx +++ b/docs/docs/actions.mdx @@ -153,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 diff --git a/examples/callback_entry_point.cc b/examples/callback_entry_point.cc index d9f9076..eb62bd6 100644 --- a/examples/callback_entry_point.cc +++ b/examples/callback_entry_point.cc @@ -41,23 +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 int& 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/include/cli/action.hh b/include/cli/action.hh index 9c80fbc..3066dcd 100644 --- a/include/cli/action.hh +++ b/include/cli/action.hh @@ -1783,7 +1783,7 @@ struct StoreInto { 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()?")); } }; @@ -1843,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 diff --git a/include/cli/argument.hh b/include/cli/argument.hh index 683adaf..50cb049 100644 --- a/include/cli/argument.hh +++ b/include/cli/argument.hh @@ -360,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. @@ -400,22 +403,16 @@ template struct ArgImpl : public OptionTag { using value_type = typename std::remove_cvref_t::type; - ArgImpl() = default; - ArgImpl(ArgParameter param) - : help(param.help), - presence(param.presence), - env(param.env), - default_value(param.default_value), - value_(param.default_value.value_or(value_type{})), - on_parse_(std::move(param.on_parse)) {} + ArgImpl() : ArgImpl(ArgParameter{}) {} + ArgImpl(ArgParameter param) : param_(param) {} // 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 @@ -431,18 +428,39 @@ 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) - std::optional 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; @@ -510,43 +528,24 @@ struct ArgImpl : public OptionTag { } // 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 occurrences() const -> std::size_t { - return occurrence_count_; + [[nodiscard]] auto get_param() const -> const ArgParameter& { + return param_; } - [[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; - } - - 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 total_values_{}; // total values across all occurrences bool provided_{}; - bool value_present_{default_value.has_value()}; + bool value_present_{param_.default_value.has_value()}; }; template @@ -604,50 +603,13 @@ struct PositionalImpl : public PositionalTag { typename std::remove_cvref_t::type; PositionalImpl() = default; - PositionalImpl(ArgParameter param) - : help(param.help), - presence(param.presence), - default_value(param.default_value), - value_(param.default_value.value_or(value_type{})), - 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) - std::optional default_value{}; + PositionalImpl(ArgParameter param) : param_(param) {} static constexpr auto nargs = N; - 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, - 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_, - .nargs = occurrence_nargs, - .value_index = value_index, - .total_values = total_values_, - .arg = std::ref(value_), - }; - ++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_); + auto on_parse() -> void { + if (param_.on_parse) param_.on_parse(value_); } [[nodiscard]] auto value() const -> const value_type& { return value_; } @@ -673,16 +635,52 @@ struct PositionalImpl : public PositionalTag { 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, + 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_, + .nargs = occurrence_nargs, + .value_index = value_index, + .total_values = total_values_, + .arg = std::ref(value_), + }; + ++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(); + } + 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 total_values_{}; // total values across all invocations bool provided_{}; - bool value_present_{default_value.has_value()}; + bool value_present_{param_.default_value.has_value()}; }; /** @@ -749,24 +747,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 @@ -774,8 +772,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 @@ -783,8 +781,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 @@ -852,7 +850,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 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 a5b3cde..d16fdff 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(); } }); @@ -842,7 +858,7 @@ struct Parser { } } } - if (!res.has_error()) f.fire_on_parse(); + if (!res.has_error()) f.on_parse(); } }); return res; @@ -895,8 +911,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)); @@ -926,7 +942,8 @@ struct Parser { if (res.has_error()) { return; } - if (f.presence == Presence::required && !static_cast(f)) { + if (f.get_param().presence == Presence::required && + !static_cast(f)) { std::string subject; if constexpr (std::derived_from) { subject = "--" + std::string(F::name.view()); @@ -949,15 +966,15 @@ 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; if constexpr (F::nargs.min == 0 && F::nargs.max == 0) { @@ -965,7 +982,7 @@ struct Parser { } else { 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/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; }; From 280c31b066ebafd2a744337f76d6f038040cc60a Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Mon, 11 May 2026 21:09:33 +0900 Subject: [PATCH 09/11] refactor(cli): unify flag and option action invocation Consolidate invoke_flag into invoke_action by using std::optional for the input text. This simplifies the parser's dispatch logic and provides a single entry point for all option types. Additionally, ensure default values are correctly initialized in ArgImpl and PositionalImpl constructors, and refactor for_each_field to use index sequences and constexpr. --- include/cli/argument.hh | 49 ++++++++------------------------ include/cli/parser.hh | 62 ++++++++++++++++++++++++++--------------- tests/test_callback.cc | 31 ++++++++++----------- 3 files changed, 66 insertions(+), 76 deletions(-) diff --git a/include/cli/argument.hh b/include/cli/argument.hh index 50cb049..5159e8c 100644 --- a/include/cli/argument.hh +++ b/include/cli/argument.hh @@ -404,7 +404,9 @@ struct ArgImpl : public OptionTag { using value_type = typename std::remove_cvref_t::type; ArgImpl() : ArgImpl(ArgParameter{}) {} - ArgImpl(ArgParameter param) : param_(param) {} + ArgImpl(ArgParameter param) + : param_(std::move(param)), + value_(param_.default_value.value_or(value_type{})) {} // Convenience constructor for StoreInto variants (value_type == T*). ArgImpl(std::remove_pointer_t& ref) @@ -468,7 +470,7 @@ struct ArgImpl : public OptionTag { 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, + 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; @@ -481,43 +483,14 @@ struct ArgImpl : public OptionTag { .arg = std::ref(value_), }; ++total_values_; - auto invoke_result = [&]() -> auto { - if constexpr (decltype(A)::entry_is_optional) { - return decltype(A)::invoke( - ctx, ActionResult>::ok( - std::optional{text})); - } else { - return decltype(A)::invoke(ctx, - ActionResult::ok(text)); - } - }(); - 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 for flags (nargs = {0,0}) after the option token is seen. - auto invoke_flag(std::size_t arg_index, std::size_t occurrence_nargs, - bool mark_provided = true) -> ActionResult { - if (mark_provided) provided_ = true; - ActionCtx ctx{ - .index = arg_index, - .occurrences = occurrence_count_, - .nargs = occurrence_nargs, - .value_index = 0, - .total_values = total_values_, - .arg = std::ref(value_), - }; - auto invoke_result = [&]() -> auto { - if constexpr (decltype(A)::entry_is_optional) { + auto invoke_result = [&ctx, &text]() -> auto { + if constexpr (entry_is_optional) { return decltype(A)::invoke( - ctx, ActionResult>::ok( - std::optional{std::nullopt})); + ctx, ActionResult>::ok(text)); } else { return decltype(A)::invoke( - ctx, ActionResult::ok(std::string_view{})); + ctx, ActionResult::ok(text.value_or(""))); } }(); if (invoke_result.has_error()) { @@ -602,8 +575,10 @@ struct PositionalImpl : public PositionalTag { using value_type = typename std::remove_cvref_t::type; - PositionalImpl() = default; - PositionalImpl(ArgParameter param) : param_(param) {} + PositionalImpl() : PositionalImpl(ArgParameter{}) {} + PositionalImpl(ArgParameter param) + : value_(param.default_value.value_or(value_type{})), + param_(std::move(param)) {} static constexpr auto nargs = N; diff --git a/include/cli/parser.hh b/include/cli/parser.hh index d16fdff..1441a03 100644 --- a/include/cli/parser.hh +++ b/include/cli/parser.hh @@ -789,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. @@ -820,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 { @@ -830,33 +842,37 @@ 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, vals.size()); - } else { - if (vals.empty() && F::entry_is_optional) { - res = f.invoke_flag(option_position + first_index, 0); - } else { - 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 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.on_parse(); } @@ -978,7 +994,7 @@ struct Parser { #endif if (!raw) return; if constexpr (F::nargs.min == 0 && F::nargs.max == 0) { - res = f.invoke_flag(0, 0, false); + res = f.invoke_action(std::nullopt, 0, 0, 0, false); } else { res = f.invoke_action(std::string_view(raw), 0, 1, 0, false); } diff --git a/tests/test_callback.cc b/tests/test_callback.cc index 1db10e6..a52ce2d 100644 --- a/tests/test_callback.cc +++ b/tests/test_callback.cc @@ -35,7 +35,7 @@ TEST(Callback, CalledForIntOption) { int captured = -1; CallbackIntArgs args; args.count = cli::IntOption<"count", 'c'>{{ - .on_parse = [&captured](const int& v) { captured = v; }, + .on_parse = [&captured](const int& v) -> void { captured = v; }, }}; auto args_vec = argv({"prog", "--count", "42"}); @@ -50,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"}); @@ -65,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"}); @@ -83,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. @@ -107,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"}); @@ -128,7 +126,7 @@ TEST(Callback, NotCalledOnParseError) { int call_count = 0; CallbackIntArgs args; args.count = cli::IntOption<"count", 'c'>{{ - .on_parse = [&call_count](const int&) { ++call_count; }, + .on_parse = [&call_count](const int&) -> void { ++call_count; }, }}; auto args_vec = argv({"prog", "--count", "notanint"}); @@ -144,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"}); From 3d60757aedfe3bc36f3ecba5f6ddd9c4b8269eb0 Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Mon, 11 May 2026 22:06:15 +0900 Subject: [PATCH 10/11] style(cli): add blank line between get overloads --- include/cli/action.hh | 1 + 1 file changed, 1 insertion(+) diff --git a/include/cli/action.hh b/include/cli/action.hh index 3066dcd..9fdf0a5 100644 --- a/include/cli/action.hh +++ b/include/cli/action.hh @@ -1946,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) From 90dc08fda4215a752c37f9c837841fd10438a4ee Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Mon, 11 May 2026 23:16:29 +0900 Subject: [PATCH 11/11] Fix total_values counting for optional entries --- include/cli/argument.hh | 2 +- tests/test_parse.cc | 46 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/include/cli/argument.hh b/include/cli/argument.hh index 5159e8c..8128593 100644 --- a/include/cli/argument.hh +++ b/include/cli/argument.hh @@ -482,7 +482,7 @@ struct ArgImpl : public OptionTag { .total_values = total_values_, .arg = std::ref(value_), }; - ++total_values_; + if (text.has_value()) ++total_values_; auto invoke_result = [&ctx, &text]() -> auto { if constexpr (entry_is_optional) { diff --git a/tests/test_parse.cc b/tests/test_parse.cc index a70d45d..0d14ad9 100644 --- a/tests/test_parse.cc +++ b/tests/test_parse.cc @@ -72,6 +72,37 @@ struct JobsArgs { .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}; @@ -197,6 +228,21 @@ TEST(Parse, JobsOptionRejectsDuplicateOccurrences) { } } +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 {