#pragma once // // command.hpp // // Command is the fluent builder AND the parser for one CLI command. A // command is built like: // // engine.command() // .keyword("project") // .parameter("name", "Project name") // .keyword("delete") // .flag("force", 'f', "Skip the confirmation prompt") // .option("reason", 'r', "Why it's being deleted") // .describe("Delete a project by name") // .action(&deleteProject); // // Every parameter/option is explicitly typed where it's declared, so the // engine never has to invent a type from the bound function -- `.action()` // instead *validates* that the function's corresponding argument is a // consistent binding (bool for a flag; T, std::optional, or // std::vector for a parameter/option; by value, by reference, or by // const reference all work). Mismatches are RegistrationErrors caught at // startup, not silent surprises at run time. // #include #include #include #include #include #include #include #include #include #include #include #include "function_traits.hpp" #include "levenshtein.hpp" #include "slot.hpp" #include "value.hpp" namespace cliforge { namespace detail { // --- small type-trait helpers used only for action() binding ---------- template struct is_optional : std::false_type {}; template struct is_optional> : std::true_type { using inner = T; }; template inline constexpr bool is_optional_v = is_optional::value; template struct is_vector : std::false_type {}; template struct is_vector> : std::true_type { using inner = T; }; template inline constexpr bool is_vector_v = is_vector::value; template inline constexpr bool is_builtin_scalar_v = std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v; // A token is where the unstructured (flags/options) part begins if it // starts with a dash and isn't shaped like a negative number -- that way // `build -5 -3 -1` (variadic int parameters) still works even though the // tokens start with '-'. inline bool looksLikeOptionStart(std::string_view tok) { if (tok.size() < 2 || tok[0] != '-') return false; if (tok[1] == '-') return true; // "--foo" if (std::isdigit(static_cast(tok[1]))) return false; // "-5" return true; // "-n", "-ndi" } inline bool isValidKeyword(std::string_view s) { if (s.empty()) return false; if (s[0] == '-') return false; // reserved for flags/options for (char c : s) { if (!(std::isalnum(static_cast(c)) || c == '.' || c == '_' || c == '-')) { return false; } } return true; } // A human-readable name for a bound action() argument type, used in // RegistrationError messages. Only built-in scalars and enums are ever // legal here (anything else is already rejected by parameter()'s own // static_assert), so this never needs to handle the general case. template std::string friendlyTypeName() { if constexpr (is_builtin_scalar_v) { return TypeOf::get().displayName; } else if constexpr (std::is_enum_v) { return "a different enum type"; } else { return "an unsupported type"; } } } // namespace detail // Result of trying to line a command's structured (keyword+parameter) // signature up against the front of the user's token list. struct StructuredMatch { bool success = false; std::size_t consumed = 0; // tokens belonging to the structured part int keywordScore = 0; // number of literal keywords matched std::vector paramValues; // Set when every literal keyword matched but a parameter's value // either failed to parse or was never given -- lets the Engine // surface this specific, actionable error instead of a generic // "no match" + fuzzy suggestions. std::string error; }; class Command { public: // Keywords carry no description of their own -- .describe() on the // command is what shows up in help; a keyword is purely a literal // token to match. Command& keyword(std::string literal) { if (unstructuredStarted_) { throw RegistrationError( "can't declare keyword('" + literal + "') after a flag()/option() was added -- keywords must " "come entirely before the unstructured part"); } if (structuredSealed_) { throw RegistrationError( "can't declare keyword('" + literal + "') after a variadic parameter -- a variadic parameter " "must be the last thing in the structured part"); } if (!detail::isValidKeyword(literal)) { throw RegistrationError( "invalid keyword '" + literal + "': keywords may contain letters, digits, '.', '_' and " "'-', and may not start with '-'"); } Slot s; s.kind = SlotKind::Keyword; s.name = std::move(literal); structured_.push_back(std::move(s)); lastSlot_ = &structured_.back(); return *this; } // T is explicit and required: every parameter is typed at the point // it's declared, not inferred later from action(). Called before any // flag()/option(), this is a fixed-position structured parameter; // called after, it's a "loose" parameter matched positionally among // other loose parameters regardless of where flags/options fall // around it. template Command& parameter(std::string name, std::string description, bool variadic = false) { if (name.empty()) { throw RegistrationError("parameter name may not be empty"); } Slot s; s.kind = SlotKind::Parameter; s.name = std::move(name); s.description = std::move(description); s.variadic = variadic; s.cppType = std::type_index(typeid(T)); if constexpr (std::is_enum_v) { s.awaitingChoices = true; // choices(...) must follow immediately } else { static_assert(detail::is_builtin_scalar_v, "cliforge: unsupported parameter type -- use a fixed-width int, " "float/double, std::string, char, bool, or an enum (with " "choices() called immediately after)"); s.type = TypeOf::get(); } if (!unstructuredStarted_) { if (structuredSealed_) { throw RegistrationError( "can't declare parameter('" + s.name + "') after a variadic parameter -- a variadic parameter " "must be the last thing in the structured part"); } structured_.push_back(std::move(s)); lastSlot_ = &structured_.back(); if (variadic) structuredSealed_ = true; } else { if (looseSealed_) { throw RegistrationError( "can't declare parameter('" + s.name + "') after a variadic parameter -- a variadic parameter " "must be the last parameter declared"); } unstructured_.push_back(std::move(s)); lastSlot_ = &unstructured_.back(); if (variadic) looseSealed_ = true; } return *this; } // shortName is 0 (the default) for "no short form". Command& flag(std::string longName, char shortName, std::string description) { longName = stripDashes(std::move(longName)); validateLongName(longName); validateShortName(shortName); checkNameFree(longName, shortName); Slot s; s.kind = SlotKind::Flag; s.name = std::move(longName); s.shortName = shortName; s.description = std::move(description); unstructured_.push_back(std::move(s)); lastSlot_ = &unstructured_.back(); unstructuredStarted_ = true; return *this; } // T is explicit and required, same as parameter(). Options are // always optional in the calling convention (see action()) -- there // is no way to declare a "required" option, by design. template Command& option(std::string longName, char shortName, std::string description, bool variadic = false) { longName = stripDashes(std::move(longName)); validateLongName(longName); validateShortName(shortName); checkNameFree(longName, shortName); Slot s; s.kind = SlotKind::Option; s.name = std::move(longName); s.shortName = shortName; s.description = std::move(description); s.variadic = variadic; s.cppType = std::type_index(typeid(T)); if constexpr (std::is_enum_v) { s.awaitingChoices = true; } else { static_assert(detail::is_builtin_scalar_v, "cliforge: unsupported option type -- use a fixed-width int, " "float/double, std::string, char, bool, or an enum (with " "choices() called immediately after)"); s.type = TypeOf::get(); } unstructured_.push_back(std::move(s)); lastSlot_ = &unstructured_.back(); unstructuredStarted_ = true; return *this; } Command& describe(std::string description) { description_ = std::move(description); return *this; } // Restricts the most-recently-declared parameter/option to a fixed // set of named values -- this is how custom enums plug in (T is // deduced from the initializer list, no need to spell it out again), // and it doubles as a generic "choice" restriction for any type. T // must match the type declared at parameter()/option(). template Command& choices(std::initializer_list> values, std::string typeName = "") { if (lastSlot_ == nullptr || (lastSlot_->kind != SlotKind::Parameter && lastSlot_->kind != SlotKind::Option)) { throw RegistrationError( "choices<>() must directly follow the parameter() or " "option() it restricts"); } if (!lastSlot_->cppType || *lastSlot_->cppType != std::type_index(typeid(T))) { throw RegistrationError("choices<>() type doesn't match the type declared for '" + lastSlot_->name + "'"); } std::vector> table(values); lastSlot_->type = makeChoiceTypeInfo(typeName.empty() ? std::string("enum") : std::move(typeName), table); lastSlot_->awaitingChoices = false; return *this; } // Binds this command to a free function (or captureless/capturing // lambda, or std::function). Argument types are validated against // what was already declared via parameter()/option()/flag() -- // action() never invents a type, it only checks consistency -- and // matched, in order, to [parameters in declaration order] then // [flags/options/loose-parameters in declaration order]. Arguments // may be taken by value, by reference, or by const reference. template Command& action(Func f) { using Traits = detail::function_traits>; if (structured_.empty() || std::none_of(structured_.begin(), structured_.end(), [](const Slot& s) { return s.kind == SlotKind::Keyword; })) { throw RegistrationError( "a command must declare at least one keyword() so it can " "be identified on the command line"); } for (const Slot& s : structured_) checkNotAwaitingChoices(s); for (const Slot& s : unstructured_) checkNotAwaitingChoices(s); std::vector ordered = orderedBindableSlots(); if (ordered.size() != Traits::arity) { throw RegistrationError( "action() function takes " + std::to_string(Traits::arity) + " argument(s) but this command declared " + std::to_string(ordered.size()) + " parameter/flag/option slot(s) -- these must match 1:1, " "in declaration order (parameters first, then flags/options)"); } bindArgs(std::function{f}, ordered, std::make_index_sequence{}); sealed_ = true; return *this; } bool sealed() const { return sealed_; } const std::string& description() const { return description_; } // --- matching / parsing entry points, used by Engine ------------- StructuredMatch tryMatchStructured(const std::vector& tokens) const { StructuredMatch m; std::size_t i = 0; try { for (std::size_t s = 0; s < structured_.size(); ++s) { const Slot& slot = structured_[s]; bool isLast = (s + 1 == structured_.size()); if (slot.kind == SlotKind::Keyword) { if (i >= tokens.size() || tokens[i] != slot.name) return StructuredMatch{}; ++i; ++m.keywordScore; } else { if (slot.variadic && isLast) { std::vector collected; while (i < tokens.size() && !detail::looksLikeOptionStart(tokens[i])) { collected.push_back(slot.type.parse(tokens[i], slot.name)); ++i; } m.paramValues.push_back(Value::ofVector(std::move(collected))); } else { if (i >= tokens.size() || detail::looksLikeOptionStart(tokens[i])) { // Every keyword up to here matched literally, so // this is almost certainly the intended command // -- just missing a required argument. throw CliError( "missing required argument '<" + slot.name + ">': expected a value of type " + slot.type.displayName, ErrorKind::MissingArgument); } m.paramValues.push_back(Value::ofScalar(slot.type.parse(tokens[i], slot.name))); ++i; } } } } catch (const CliError& e) { // Either a type mismatch (ParseError, a CliError) or the // MissingArgument thrown just above -- both mean "this is the // command you meant, but something about the value is wrong", // so remember it instead of treating it as a plain non-match. m.success = false; m.error = e.what(); return m; } m.success = true; m.consumed = i; return m; } // Parses the (order-agnostic) flags/options/loose-parameters tail. // Throws CliError (with a specific ErrorKind) describing exactly what // went wrong: an unrecognized flag/option, a missing option value, a // missing/extra positional argument, or a type mismatch. std::vector parseUnstructured(const std::vector& tokens) const { std::vector provided(unstructured_.size(), false); std::vector> collected(unstructured_.size()); std::vector looseTokens; // views into `tokens`, which outlives this call looseTokens.reserve(tokens.size()); std::size_t i = 0; while (i < tokens.size()) { std::string_view tok = tokens[i]; if (!detail::looksLikeOptionStart(tok)) { // Not flag/option syntax (includes negative numbers like // "-5") -- it's a value for the next loose parameter. looseTokens.push_back(tok); ++i; continue; } if (tok[1] == '-') { std::string_view rest = tok.substr(2); auto eq = rest.find('='); bool hasInline = (eq != std::string_view::npos); std::string_view longName = hasInline ? rest.substr(0, eq) : rest; std::string_view inlineValue = hasInline ? rest.substr(eq + 1) : std::string_view{}; int idx = findByLong(longName); if (idx < 0) throwUnknown(tok); ++i; consumeSlot(*this, idx, hasInline, inlineValue, tokens, i, provided, collected, tok); } else { std::string_view rest = tok.substr(1); if (rest.size() == 1) { int idx = findByShort(rest[0]); if (idx < 0) throwUnknown(tok); ++i; consumeSlot(*this, idx, false, std::string_view{}, tokens, i, provided, collected, tok); } else { // Combined short flags, e.g. "-ndi" == -n -d -i. for (char c : rest) { int idx = findByShort(c); if (idx < 0) { throw CliError(std::string("unknown flag '-") + c + "' in combined group '" + std::string(tok) + "'" + suggestFlagOption(std::string("-") + c), ErrorKind::UnknownOption); } if (unstructured_[static_cast(idx)].kind != SlotKind::Flag) { throw CliError( std::string("'-") + c + "' in combined group '" + std::string(tok) + "' is an option (needs a value), so it can't " "be combined with other short flags"); } provided[static_cast(idx)] = true; } ++i; } } } // Assign the collected loose tokens to any declared loose // Parameter slots, positionally, in declaration order -- this is // independent of where those tokens fell relative to flags/options. std::vector looseParamIdx; for (std::size_t idx = 0; idx < unstructured_.size(); ++idx) { if (unstructured_[idx].kind == SlotKind::Parameter) looseParamIdx.push_back(idx); } std::vector looseValues(looseParamIdx.size()); std::size_t li = 0; for (std::size_t k = 0; k < looseParamIdx.size(); ++k) { const Slot& slot = unstructured_[looseParamIdx[k]]; bool isLastLoose = (k + 1 == looseParamIdx.size()); if (slot.variadic && isLastLoose) { std::vector vals; vals.reserve(looseTokens.size() > li ? looseTokens.size() - li : 0); while (li < looseTokens.size()) { vals.push_back(slot.type.parse(looseTokens[li], slot.name)); ++li; } looseValues[k] = Value::ofVector(std::move(vals)); } else { if (li >= looseTokens.size()) { throw CliError("missing required argument '<" + slot.name + ">': expected a value of type " + slot.type.displayName, ErrorKind::MissingArgument); } looseValues[k] = Value::ofScalar(slot.type.parse(looseTokens[li], slot.name)); ++li; } } if (li < looseTokens.size()) { std::size_t extra = looseTokens.size() - li; std::string msg = "too many arguments: unexpected '" + std::string(looseTokens[li]) + "'"; if (extra > 1) msg += " (and " + std::to_string(extra - 1) + " more)"; throw CliError(msg, ErrorKind::TooManyArguments); } // Assemble the final values in unstructured_'s declaration order -- // this is what gets paired with the bound function's arguments. std::vector out; out.reserve(unstructured_.size()); std::size_t looseCursor = 0; for (std::size_t idx = 0; idx < unstructured_.size(); ++idx) { const Slot& slot = unstructured_[idx]; if (slot.kind == SlotKind::Parameter) { out.push_back(std::move(looseValues[looseCursor++])); } else if (slot.kind == SlotKind::Flag) { out.push_back(Value::ofScalar(Scalar{provided[idx]})); } else if (slot.variadic) { out.push_back(Value::ofVector(collected[idx])); } else if (provided[idx]) { out.push_back(Value::ofScalar(collected[idx][0])); } else { out.push_back(Value{}); // options are always optional -> unset means nullopt } } return out; } void invoke(std::vector allValues) const { invoke_(allValues); } std::size_t structuredSlotCount() const { return structured_.size(); } // Lenient prefix check used only for --help / `help ` : does // `tokens` look like the start of (or a complete instance of) this // command, without requiring every required argument to actually be // present? Keywords still must match exactly; parameter slots accept // any token; running out of input mid-command is fine (that's exactly // what makes it a "prefix"). Used to find *every* command consistent // with what's been typed so far, so an ambiguous prefix can show help // for all of them instead of guessing one. bool couldMatchPrefix(const std::vector& tokens) const { std::size_t i = 0; for (std::size_t s = 0; s < structured_.size(); ++s) { if (i >= tokens.size()) break; // ran out of input -- a valid, incomplete prefix const Slot& slot = structured_[s]; bool isLast = (s + 1 == structured_.size()); if (slot.kind == SlotKind::Keyword) { if (tokens[i] != slot.name) return false; ++i; } else if (slot.variadic && isLast) { while (i < tokens.size() && !detail::looksLikeOptionStart(tokens[i])) ++i; } else { if (detail::looksLikeOptionStart(tokens[i])) break; // structured part left incomplete ++i; } } // Whatever tokens remain must be plausible flags/options (assumed // fine without deep validation) or fit a declared loose parameter. std::vector looseParams; for (const Slot& s : unstructured_) { if (s.kind == SlotKind::Parameter) looseParams.push_back(&s); } std::size_t looseIdx = 0; for (; i < tokens.size(); ++i) { if (detail::looksLikeOptionStart(tokens[i])) continue; if (looseIdx >= looseParams.size()) return false; // nothing left to absorb this token if (!looseParams[looseIdx]->variadic) ++looseIdx; } return true; } // --- help / usage -------------------------------------------------- std::string usageLine() const { std::ostringstream os; for (std::size_t s = 0; s < structured_.size(); ++s) { const Slot& slot = structured_[s]; if (s) os << ' '; if (slot.kind == SlotKind::Keyword) { os << slot.name; } else { os << '<' << slot.name << (slot.variadic ? "..." : "") << '>'; } } bool anyFlagOrOption = std::any_of(unstructured_.begin(), unstructured_.end(), [](const Slot& s) { return s.kind != SlotKind::Parameter; }); if (anyFlagOrOption) os << " [OPTIONS]"; for (const Slot& slot : unstructured_) { if (slot.kind != SlotKind::Parameter) continue; os << " <" << slot.name << (slot.variadic ? "..." : "") << '>'; } return os.str(); } std::string helpText(const std::string& progName) const { std::ostringstream os; os << "Usage: " << progName << " " << usageLine() << "\n"; if (!description_.empty()) os << "\n" << description_ << "\n"; bool anyParams = std::any_of(structured_.begin(), structured_.end(), [](const Slot& s) { return s.kind == SlotKind::Parameter; }) || std::any_of(unstructured_.begin(), unstructured_.end(), [](const Slot& s) { return s.kind == SlotKind::Parameter; }); if (anyParams) { os << "\nArguments:\n"; for (const Slot& slot : structured_) { if (slot.kind != SlotKind::Parameter) continue; std::string label = "<" + slot.name + (slot.variadic ? "..." : "") + ">"; os << " " << padRight(label, 22) << describeType(slot) << slot.description << "\n"; } for (const Slot& slot : unstructured_) { if (slot.kind != SlotKind::Parameter) continue; std::string label = "<" + slot.name + (slot.variadic ? "..." : "") + ">"; os << " " << padRight(label, 22) << describeType(slot) << slot.description << "\n"; } } bool anyFlagOrOption = std::any_of(unstructured_.begin(), unstructured_.end(), [](const Slot& s) { return s.kind != SlotKind::Parameter; }); os << "\nOptions:\n"; if (anyFlagOrOption) { for (const Slot& slot : unstructured_) { if (slot.kind == SlotKind::Parameter) continue; std::string names = slot.shortName == '\0' ? (" --" + slot.name) : (std::string("-") + slot.shortName + ", --" + slot.name); if (slot.kind == SlotKind::Option) { names += " <" + slot.type.displayName + (slot.variadic ? "...>" : ">"); } os << " " << padRight(names, 28) << slot.description << "\n"; } } os << " " << padRight("-h, --help", 28) << "Show this help message\n"; return os.str(); } std::vector flagOptionNames() const { std::vector out; for (const Slot& s : unstructured_) { if (s.kind == SlotKind::Parameter) continue; out.push_back("--" + s.name); if (s.shortName != '\0') out.push_back(std::string("-") + s.shortName); } return out; } // Fuzzy structural distance used for "did you mean...?" suggestions. struct FuzzyScore { std::size_t distance = 0; std::size_t keywordChars = 0; // denominator for a relative threshold }; FuzzyScore fuzzyStructuralDistance(const std::vector& tokens) const { std::size_t i = 0; FuzzyScore score; for (std::size_t s = 0; s < structured_.size(); ++s) { const Slot& slot = structured_[s]; bool isLast = (s + 1 == structured_.size()); if (slot.kind == SlotKind::Keyword) { score.keywordChars += slot.name.size(); if (i >= tokens.size()) { score.distance += slot.name.size(); // command is longer than what was typed continue; } score.distance += detail::levenshtein(tokens[i], slot.name); ++i; } else if (slot.variadic && isLast) { while (i < tokens.size() && !detail::looksLikeOptionStart(tokens[i])) ++i; } else if (i < tokens.size() && !detail::looksLikeOptionStart(tokens[i])) { ++i; // parameters are wildcards: any token "fits", no penalty } } score.distance += (tokens.size() > i) ? (tokens.size() - i) : 0; // leftover tokens return score; } private: std::vector structured_; std::vector unstructured_; std::string description_; std::function&)> invoke_; Slot* lastSlot_ = nullptr; bool unstructuredStarted_ = false; bool structuredSealed_ = false; bool looseSealed_ = false; bool sealed_ = false; static std::string stripDashes(std::string s) { while (!s.empty() && s.front() == '-') s.erase(s.begin()); return s; } void validateLongName(const std::string& s) const { if (s.empty()) throw RegistrationError("a flag/option needs a non-empty long name"); if (s == "help") { throw RegistrationError("'help' is reserved for the built-in --help flag"); } for (char c : s) { if (!(std::isalnum(static_cast(c)) || c == '-' || c == '_')) { throw RegistrationError("invalid long name '--" + s + "'"); } } } void validateShortName(char c) const { if (c == '\0') return; // no short form if (!std::isalnum(static_cast(c))) { throw RegistrationError(std::string("short names must be a single letter/digit, got '-") + c + "'"); } if (c == 'h') throw RegistrationError("'-h' is reserved for the built-in --help flag"); } void checkNameFree(const std::string& longName, char shortName) const { for (const Slot& s : unstructured_) { if (s.kind == SlotKind::Parameter) continue; // different namespace (bare, not "--name") if (s.name == longName) { throw RegistrationError("duplicate long name '--" + longName + "'"); } if (shortName != '\0' && s.shortName == shortName) { throw RegistrationError(std::string("duplicate short name '-") + shortName + "'"); } } } static void checkNotAwaitingChoices(const Slot& s) { if (s.awaitingChoices) { throw RegistrationError("'" + s.name + "' is a custom enum type and needs choices(...) " "called right after it is declared"); } } std::vector orderedBindableSlots() { std::vector out; for (Slot& s : structured_) { if (s.kind == SlotKind::Parameter) out.push_back(&s); } for (Slot& s : unstructured_) out.push_back(&s); return out; } // Validates (never invents) that a slot's declared type matches T. template static void checkTypeMatches(const Slot& slot) { if (!slot.cppType || *slot.cppType != std::type_index(typeid(T))) { throw RegistrationError("'" + slot.name + "' was declared as " + slot.type.displayName + " but action() expects " + detail::friendlyTypeName()); } } // ArgT is the *raw* action() parameter type, e.g. `const std::string&` // or `std::vector` -- validated after stripping cv/ref. template static void configureSlot(Slot& slot) { using Stripped = std::remove_cvref_t; if (slot.kind == SlotKind::Flag) { if constexpr (!std::is_same_v) { throw RegistrationError("flag '--" + slot.name + "' must bind to bool in action()"); } return; } if constexpr (detail::is_optional_v) { using Inner = typename detail::is_optional::inner; if (slot.kind == SlotKind::Parameter) { throw RegistrationError("parameter '<" + slot.name + ">' can't bind to std::optional<> -- positional " "parameters are always required"); } if (slot.variadic) { throw RegistrationError("option '--" + slot.name + "' is variadic; bind it to std::vector<>, not std::optional<>"); } checkTypeMatches(slot); } else if constexpr (detail::is_vector_v) { using Inner = typename detail::is_vector::inner; if (!slot.variadic) { throw RegistrationError("'" + slot.name + "' binds to std::vector<> but wasn't declared variadic=true"); } checkTypeMatches(slot); } else { if (slot.variadic) { throw RegistrationError("'" + slot.name + "' was declared variadic=true; bind it to std::vector<>"); } if (slot.kind == SlotKind::Option) { throw RegistrationError("option '--" + slot.name + "' must bind to std::optional (or std::vector if " "variadic) -- options are always optional, never required"); } checkTypeMatches(slot); } } template static ArgT extractArg(const Value& v) { if constexpr (std::is_same_v) { return v.as(); } else if constexpr (detail::is_optional_v) { using Inner = typename detail::is_optional::inner; return v.hasValue() ? std::optional(v.as()) : std::nullopt; } else if constexpr (detail::is_vector_v) { using Inner = typename detail::is_vector::inner; return v.asVector(); } else { return v.as(); } } // Extracted values are materialized into a tuple of *value* types // first, then handed to f via std::apply -- std::get on that (named, // lvalue) tuple yields lvalues, which is what lets f's parameters be // declared by value, by reference, or by const reference and all // still bind correctly. template void bindArgs(std::function f, std::vector& ordered, std::index_sequence) { (configureSlot(*ordered[I]), ...); invoke_ = [f = std::move(f)](std::vector& values) { std::tuple...> extracted{ extractArg>(values[I])...}; std::apply(f, extracted); }; } int findByLong(std::string_view name) const { for (std::size_t i = 0; i < unstructured_.size(); ++i) { if (unstructured_[i].kind != SlotKind::Parameter && unstructured_[i].name == name) { return static_cast(i); } } return -1; } int findByShort(char c) const { if (c == '\0') return -1; for (std::size_t i = 0; i < unstructured_.size(); ++i) { if (unstructured_[i].kind != SlotKind::Parameter && unstructured_[i].shortName == c) { return static_cast(i); } } return -1; } std::string suggestFlagOption(const std::string& given) const { auto names = flagOptionNames(); names.push_back("--help"); names.push_back("-h"); auto close = detail::closestMatches(given, names, 1); if (close.empty()) return ""; return " -- did you mean '" + close.front() + "'?"; } [[noreturn]] void throwUnknown(std::string_view tok) const { throw CliError("unknown flag/option '" + std::string(tok) + "' for this command" + suggestFlagOption(std::string(tok)), ErrorKind::UnknownOption); } static std::string describeType(const Slot& slot) { return "<" + slot.type.displayName + "> "; } static std::string padRight(const std::string& s, std::size_t width) { if (s.size() >= width) return s + " "; return s + std::string(width - s.size(), ' '); } static void consumeSlot(const Command& self, int idxIn, bool hasInline, std::string_view inlineValue, const std::vector& tokens, std::size_t& i, std::vector& provided, std::vector>& collected, std::string_view tokForError) { std::size_t idx = static_cast(idxIn); const Slot& slot = self.unstructured_[idx]; if (slot.kind == SlotKind::Flag) { if (hasInline) throw CliError("flag '--" + slot.name + "' does not take a value"); provided[idx] = true; return; } // Option if (hasInline) { collected[idx].push_back(slot.type.parse(inlineValue, slot.name)); provided[idx] = true; if (slot.variadic) { while (i < tokens.size() && !detail::looksLikeOptionStart(tokens[i])) { collected[idx].push_back(slot.type.parse(tokens[i], slot.name)); ++i; } } return; } if (slot.variadic) { std::size_t before = i; while (i < tokens.size() && !detail::looksLikeOptionStart(tokens[i])) { collected[idx].push_back(slot.type.parse(tokens[i], slot.name)); ++i; } if (i == before) { throw CliError("option '" + std::string(tokForError) + "' requires at least one value", ErrorKind::MissingValue); } provided[idx] = true; } else { if (i >= tokens.size() || detail::looksLikeOptionStart(tokens[i])) { throw CliError("option '" + std::string(tokForError) + "' requires a value", ErrorKind::MissingValue); } collected[idx] = {slot.type.parse(tokens[i], slot.name)}; provided[idx] = true; ++i; } } }; } // namespace cliforge