Add project files
This commit is contained in:
commit
f2fe6b8855
114 files changed
+20525
No files matched your search
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "Command.hpp"
|
||||
#include "Engine.hpp"
|
||||
#include "FunctionTraits.hpp"
|
||||
#include "Levenshtein.hpp"
|
||||
#include "Slot.hpp"
|
||||
#include "Value.hpp"
|
||||
@@ -0,0 +1,499 @@
|
||||
#pragma once
|
||||
|
||||
// Command is the fluent builder AND the parser for one CLI command. A
|
||||
// command is built like:
|
||||
//
|
||||
// engine.command()
|
||||
// .keyword("project")
|
||||
// .parameter<std::string>("name", "Project name")
|
||||
// .keyword("delete")
|
||||
// .flag("force", 'f', "Skip the confirmation prompt")
|
||||
// .option<std::string>("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<T>, or
|
||||
// std::vector<T> 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 <cctype>
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <typeindex>
|
||||
#include <vector>
|
||||
|
||||
#include "FunctionTraits.hpp"
|
||||
#include "Levenshtein.hpp"
|
||||
#include "Slot.hpp"
|
||||
#include "Value.hpp"
|
||||
|
||||
namespace cliforge
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
// --- small type-trait helpers used only for action() binding ----------
|
||||
|
||||
template <typename T> struct IsOptional : std::false_type
|
||||
{
|
||||
};
|
||||
|
||||
template <typename T> struct IsOptional<std::optional<T>> : std::true_type
|
||||
{
|
||||
using Inner = T;
|
||||
};
|
||||
|
||||
template <typename T> inline constexpr bool IsOptionalV = IsOptional<T>::value;
|
||||
|
||||
template <typename T> struct IsVector : std::false_type
|
||||
{
|
||||
};
|
||||
|
||||
template <typename T> struct IsVector<std::vector<T>> : std::true_type
|
||||
{
|
||||
using Inner = T;
|
||||
};
|
||||
|
||||
template <typename T> inline constexpr bool IsVectorV = IsVector<T>::value;
|
||||
|
||||
template <typename T>
|
||||
inline constexpr bool IsBuiltinScalarV =
|
||||
std::is_same_v<T, int8_t> || std::is_same_v<T, uint8_t> || std::is_same_v<T, int16_t> ||
|
||||
std::is_same_v<T, uint16_t> || std::is_same_v<T, int32_t> ||
|
||||
std::is_same_v<T, uint32_t> || std::is_same_v<T, int64_t> ||
|
||||
std::is_same_v<T, uint64_t> || std::is_same_v<T, float> || std::is_same_v<T, double> ||
|
||||
std::is_same_v<T, std::string> || std::is_same_v<T, char> || std::is_same_v<T, bool>;
|
||||
|
||||
// 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 '-'.
|
||||
bool looksLikeOptionStart(std::string_view tok);
|
||||
|
||||
bool isValidKeyword(std::string_view s);
|
||||
|
||||
// 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<T>()'s own
|
||||
// static_assert), so this never needs to handle the general case.
|
||||
template <typename T> std::string friendlyTypeName()
|
||||
{
|
||||
if constexpr (IsBuiltinScalarV<T>)
|
||||
{
|
||||
return TypeOf<T>::get().displayName;
|
||||
}
|
||||
else if constexpr (std::is_enum_v<T>)
|
||||
{
|
||||
return "a different enum type";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "an unsupported type";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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<Value> 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(const std::string& literal);
|
||||
|
||||
// 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 <typename T>
|
||||
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<T>)
|
||||
{
|
||||
s.awaitingChoices = true; // choices<T>(...) must follow immediately
|
||||
}
|
||||
else
|
||||
{
|
||||
static_assert(detail::IsBuiltinScalarV<T>,
|
||||
"cliforge: unsupported parameter type -- use a fixed-width int, "
|
||||
"float/double, std::string, char, bool, or an enum (with "
|
||||
"choices<T>() called immediately after)");
|
||||
s.type = TypeOf<T>::get();
|
||||
}
|
||||
if (!m_unstructuredStarted)
|
||||
{
|
||||
if (m_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");
|
||||
}
|
||||
m_structured.push_back(std::move(s));
|
||||
m_lastSlot = &m_structured.back();
|
||||
if (variadic)
|
||||
m_structuredSealed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_looseSealed)
|
||||
{
|
||||
throw RegistrationError("can't declare parameter('" + s.name +
|
||||
"') after a variadic parameter -- a variadic parameter "
|
||||
"must be the last parameter declared");
|
||||
}
|
||||
|
||||
m_unstructured.push_back(std::move(s));
|
||||
m_lastSlot = &m_unstructured.back();
|
||||
if (variadic)
|
||||
m_looseSealed = true;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// shortName is 0 (the default) for "no short form".
|
||||
Command& flag(std::string longName, char shortName, const std::string& description);
|
||||
|
||||
// T is explicit and required, same as parameter<T>(). Options are
|
||||
// always optional in the calling convention (see action()) -- there
|
||||
// is no way to declare a "required" option, by design.
|
||||
template <typename T>
|
||||
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<T>)
|
||||
{
|
||||
s.awaitingChoices = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
static_assert(detail::IsBuiltinScalarV<T>,
|
||||
"cliforge: unsupported option type -- use a fixed-width int, "
|
||||
"float/double, std::string, char, bool, or an enum (with "
|
||||
"choices<T>() called immediately after)");
|
||||
s.type = TypeOf<T>::get();
|
||||
}
|
||||
m_unstructured.push_back(std::move(s));
|
||||
m_lastSlot = &m_unstructured.back();
|
||||
m_unstructuredStarted = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Command& describe(const std::string& description);
|
||||
|
||||
// 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<T>()/option<T>().
|
||||
template <typename T>
|
||||
Command& choices(std::initializer_list<std::pair<std::string, T>> values,
|
||||
std::string typeName = "")
|
||||
{
|
||||
if (m_lastSlot == nullptr ||
|
||||
(m_lastSlot->kind != SlotKind::Parameter && m_lastSlot->kind != SlotKind::Option))
|
||||
{
|
||||
throw RegistrationError("choices<>() must directly follow the parameter<T>() or "
|
||||
"option<T>() it restricts");
|
||||
}
|
||||
|
||||
if (!m_lastSlot->cppType || *m_lastSlot->cppType != std::type_index(typeid(T)))
|
||||
{
|
||||
throw RegistrationError("choices<>() type doesn't match the type declared for '" +
|
||||
m_lastSlot->name + "'");
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, T>> table(values);
|
||||
m_lastSlot->type = makeChoiceTypeInfo<T>(
|
||||
typeName.empty() ? std::string("enum") : std::move(typeName), table);
|
||||
m_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<T>()/option<T>()/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 <typename Func> Command& action(Func f)
|
||||
{
|
||||
using Traits = detail::FunctionTraits<std::decay_t<Func>>;
|
||||
if (m_structured.empty() ||
|
||||
std::none_of(m_structured.begin(), m_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 : m_structured)
|
||||
checkNotAwaitingChoices(s);
|
||||
for (const Slot& s : m_unstructured)
|
||||
checkNotAwaitingChoices(s);
|
||||
|
||||
std::vector<Slot*> 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<Traits::Arity>{});
|
||||
m_sealed = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool sealed() const;
|
||||
[[nodiscard]] const std::string& description() const;
|
||||
|
||||
// --- matching / parsing entry points, used by Engine -------------
|
||||
|
||||
[[nodiscard]] StructuredMatch tryMatchStructured(
|
||||
const std::vector<std::string>& tokens) const;
|
||||
|
||||
// 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.
|
||||
[[nodiscard]] std::vector<Value> parseUnstructured(
|
||||
const std::vector<std::string>& tokens) const;
|
||||
|
||||
void invoke(std::vector<Value> allValues) const;
|
||||
|
||||
[[nodiscard]] std::size_t structuredSlotCount() const;
|
||||
|
||||
// Lenient prefix check used only for --help / `help <words>` : 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.
|
||||
[[nodiscard]] bool couldMatchPrefix(const std::vector<std::string>& tokens) const;
|
||||
|
||||
// --- help / usage --------------------------------------------------
|
||||
|
||||
[[nodiscard]] std::string usageLine() const;
|
||||
|
||||
[[nodiscard]] std::string helpText(const std::string& progName) const;
|
||||
|
||||
[[nodiscard]] std::vector<std::string> flagOptionNames() const;
|
||||
|
||||
// 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
|
||||
};
|
||||
|
||||
[[nodiscard]] FuzzyScore fuzzyStructuralDistance(
|
||||
const std::vector<std::string>& tokens) const;
|
||||
|
||||
private:
|
||||
std::vector<Slot> m_structured;
|
||||
std::vector<Slot> m_unstructured;
|
||||
std::string m_description;
|
||||
std::function<void(std::vector<Value>&)> m_invoke;
|
||||
Slot* m_lastSlot = nullptr;
|
||||
bool m_unstructuredStarted = false;
|
||||
bool m_structuredSealed = false;
|
||||
bool m_looseSealed = false;
|
||||
bool m_sealed = false;
|
||||
|
||||
static std::string stripDashes(std::string s);
|
||||
|
||||
void validateLongName(const std::string& s) const;
|
||||
|
||||
void validateShortName(char c) const;
|
||||
|
||||
void checkNameFree(const std::string& longName, char shortName) const;
|
||||
|
||||
static void checkNotAwaitingChoices(const Slot& s);
|
||||
|
||||
std::vector<Slot*> orderedBindableSlots();
|
||||
|
||||
// Validates (never invents) that a slot's declared type matches T.
|
||||
template <typename T> 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<T>());
|
||||
}
|
||||
}
|
||||
|
||||
// ArgT is the *raw* action() parameter type, e.g. `const std::string&`
|
||||
// or `std::vector<int32_t>` -- validated after stripping cv/ref.
|
||||
template <typename ArgT> static void configureSlot(Slot& slot)
|
||||
{
|
||||
using Stripped = std::remove_cvref_t<ArgT>;
|
||||
if (slot.kind == SlotKind::Flag)
|
||||
{
|
||||
if constexpr (!std::is_same_v<Stripped, bool>)
|
||||
{
|
||||
throw RegistrationError("flag '--" + slot.name +
|
||||
"' must bind to bool in action()");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if constexpr (detail::IsOptionalV<Stripped>)
|
||||
{
|
||||
using Inner = typename detail::IsOptional<Stripped>::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<Inner>(slot);
|
||||
}
|
||||
else if constexpr (detail::IsVectorV<Stripped>)
|
||||
{
|
||||
using Inner = typename detail::IsVector<Stripped>::Inner;
|
||||
if (!slot.variadic)
|
||||
{
|
||||
throw RegistrationError(
|
||||
"'" + slot.name +
|
||||
"' binds to std::vector<> but wasn't declared variadic=true");
|
||||
}
|
||||
checkTypeMatches<Inner>(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<T> (or std::vector<T> if "
|
||||
"variadic) -- options are always optional, never required");
|
||||
}
|
||||
checkTypeMatches<Stripped>(slot);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename ArgT> static ArgT extractArg(const Value& v)
|
||||
{
|
||||
if constexpr (std::is_same_v<ArgT, bool>)
|
||||
{
|
||||
return v.as<bool>();
|
||||
}
|
||||
else if constexpr (detail::IsOptionalV<ArgT>)
|
||||
{
|
||||
using Inner = typename detail::IsOptional<ArgT>::Inner;
|
||||
return v.hasValue() ? std::optional<Inner>(v.as<Inner>()) : std::nullopt;
|
||||
}
|
||||
else if constexpr (detail::IsVectorV<ArgT>)
|
||||
{
|
||||
using Inner = typename detail::IsVector<ArgT>::Inner;
|
||||
return v.asVector<Inner>();
|
||||
}
|
||||
else
|
||||
{
|
||||
return v.as<ArgT>();
|
||||
}
|
||||
}
|
||||
|
||||
// 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 <typename Ret, typename... Args, std::size_t... I>
|
||||
void bindArgs(std::function<Ret(Args...)> f, std::vector<Slot*>& ordered,
|
||||
std::index_sequence<I...>)
|
||||
{
|
||||
(configureSlot<Args>(*ordered[I]), ...);
|
||||
m_invoke = [f = std::move(f)](std::vector<Value>& values)
|
||||
{
|
||||
std::tuple<std::remove_cvref_t<Args>...> extracted{
|
||||
extractArg<std::remove_cvref_t<Args>>(values[I])...};
|
||||
std::apply(f, extracted);
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] int findByLong(std::string_view name) const;
|
||||
|
||||
[[nodiscard]] int findByShort(char c) const;
|
||||
|
||||
[[nodiscard]] std::string suggestFlagOption(const std::string& given) const;
|
||||
|
||||
[[noreturn]] void throwUnknown(std::string_view tok) const;
|
||||
|
||||
static std::string describeType(const Slot& slot);
|
||||
|
||||
static std::string padRight(const std::string& s, std::size_t width);
|
||||
|
||||
static void consumeSlot(const Command& self, int idxIn, bool hasInline,
|
||||
std::string_view inlineValue,
|
||||
const std::vector<std::string>& tokens, std::size_t& i,
|
||||
std::vector<bool>& provided,
|
||||
std::vector<std::vector<Scalar>>& collected,
|
||||
std::string_view tokForError);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
// Engine owns every registered Command and is the single entry point:
|
||||
//
|
||||
// Engine cli;
|
||||
// cli.describe("A demo CLI");
|
||||
// cli.command()....action(&fn);
|
||||
// return cli.run(argc, argv);
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Command.hpp"
|
||||
#include "Levenshtein.hpp"
|
||||
|
||||
namespace cliforge
|
||||
{
|
||||
|
||||
class Engine
|
||||
{
|
||||
public:
|
||||
explicit Engine(std::string programName = "");
|
||||
|
||||
Engine& describe(const std::string& description);
|
||||
|
||||
// Registers a new, empty command and returns a reference to it so it
|
||||
// can be built up with the fluent .keyword()/.parameter()/... API.
|
||||
// The Engine owns it for the rest of the program's lifetime.
|
||||
Command& command();
|
||||
|
||||
int run(int argc, char** argv);
|
||||
|
||||
private:
|
||||
std::string m_programName;
|
||||
std::string m_description;
|
||||
std::vector<std::unique_ptr<Command>> m_commands;
|
||||
|
||||
static std::string basename(const std::string& path);
|
||||
static std::string join(const std::vector<std::string>& tokens);
|
||||
|
||||
// Finds every command consistent with `queryTokens` as a prefix and
|
||||
// shows its help -- one command's help if there's exactly one match,
|
||||
// all of them (clearly separated) if the prefix is genuinely
|
||||
// ambiguous, or the global command list plus a hint if there's none.
|
||||
[[nodiscard]] int showHelp(const std::vector<std::string>& queryTokens) const;
|
||||
|
||||
void printGlobalHelp(std::ostream& os) const;
|
||||
void suggestCommands(const std::vector<std::string>& tokens, std::ostream& os) const;
|
||||
static std::string firstLine(const std::string& s);
|
||||
static std::string padRight(const std::string& s, std::size_t width);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
// Deduces the return type and argument-type pack of "anything callable":
|
||||
// plain free-function pointers, non-generic lambdas (captureless or
|
||||
// capturing), and std::function instances. This is what lets
|
||||
// Command::action() figure out, at compile time, exactly which C++ types
|
||||
// it needs to convert parsed CLI tokens into -- without the user ever
|
||||
// having to spell the types out twice.
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include <functional>
|
||||
#include <tuple>
|
||||
|
||||
namespace cliforge::detail
|
||||
{
|
||||
|
||||
template <typename T> struct FunctionTraits;
|
||||
|
||||
// Plain free function: void foo(std::string, int)
|
||||
template <typename Ret, typename... Args> struct FunctionTraits<Ret (*)(Args...)>
|
||||
{
|
||||
using ReturnType = Ret;
|
||||
static constexpr std::size_t Arity = sizeof...(Args);
|
||||
template <std::size_t I> using Arg = std::tuple_element_t<I, std::tuple<Args...>>;
|
||||
using ArgsTuple = std::tuple<Args...>;
|
||||
};
|
||||
|
||||
// std::function<void(std::string, int)>
|
||||
template <typename Ret, typename... Args> struct FunctionTraits<std::function<Ret(Args...)>>
|
||||
{
|
||||
using ReturnType = Ret;
|
||||
static constexpr std::size_t Arity = sizeof...(Args);
|
||||
template <std::size_t I> using Arg = std::tuple_element_t<I, std::tuple<Args...>>;
|
||||
using ArgsTuple = std::tuple<Args...>;
|
||||
};
|
||||
|
||||
// Pointer-to-member-function of operator(), const and non-const, used to
|
||||
// peel apart lambdas (which are just anonymous functors under the hood).
|
||||
template <typename C, typename Ret, typename... Args>
|
||||
struct FunctionTraits<Ret (C::*)(Args...) const>
|
||||
{
|
||||
using ReturnType = Ret;
|
||||
static constexpr std::size_t Arity = sizeof...(Args);
|
||||
template <std::size_t I> using Arg = std::tuple_element_t<I, std::tuple<Args...>>;
|
||||
using ArgsTuple = std::tuple<Args...>;
|
||||
};
|
||||
|
||||
template <typename C, typename Ret, typename... Args> struct FunctionTraits<Ret (C::*)(Args...)>
|
||||
{
|
||||
using ReturnType = Ret;
|
||||
static constexpr std::size_t Arity = sizeof...(Args);
|
||||
template <std::size_t I> using Arg = std::tuple_element_t<I, std::tuple<Args...>>;
|
||||
using ArgsTuple = std::tuple<Args...>;
|
||||
};
|
||||
|
||||
// Fallback: any other callable (lambda, functor) -- peel off operator().
|
||||
template <typename F> struct FunctionTraits : FunctionTraits<decltype(&F::operator())>
|
||||
{
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
// Plain edit-distance + a small "closest matches" helper, used to power
|
||||
// the "did you mean...?" suggestions for unknown commands, flags and
|
||||
// options.
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace cliforge::detail
|
||||
{
|
||||
std::size_t levenshtein(const std::string& a, const std::string& b);
|
||||
|
||||
// Returns up to `limit` candidates from `pool` that are close to `query`,
|
||||
// ordered by distance. Anything farther than a generous relative
|
||||
// threshold is dropped so we never suggest something wildly unrelated.
|
||||
std::vector<std::string> closestMatches(const std::string& query,
|
||||
const std::vector<std::string>& pool,
|
||||
std::size_t limit = 3);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
// A `Slot` is one piece of a command's signature: a literal keyword, a
|
||||
// typed positional parameter, a boolean flag, or a typed option. The
|
||||
// same struct is reused for all four so Command can keep them in plain
|
||||
// vectors and the matcher/parser code doesn't need four parallel types.
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <typeindex>
|
||||
|
||||
#include "Value.hpp"
|
||||
|
||||
namespace cliforge
|
||||
{
|
||||
enum class SlotKind
|
||||
{
|
||||
Keyword,
|
||||
Parameter,
|
||||
Flag,
|
||||
Option
|
||||
};
|
||||
|
||||
struct Slot
|
||||
{
|
||||
SlotKind kind{};
|
||||
|
||||
// Keyword: the literal text the user must type (e.g. "delete").
|
||||
// Parameter: the display name (e.g. "name" -> shown as <name>).
|
||||
// Flag/Option: the long name WITHOUT the leading "--" (e.g. "force").
|
||||
std::string name;
|
||||
|
||||
// Flags/Options only: single-letter short name (e.g. 'f'), or '\0'
|
||||
// if no short form was registered. A plain char instead of a string
|
||||
// avoids a heap allocation (and the temptation to compare via
|
||||
// std::string) for what is always exactly zero or one character.
|
||||
char shortName = '\0';
|
||||
|
||||
std::string description;
|
||||
|
||||
// Parameter/Option only: may this slot consume more than one token?
|
||||
// Only the *last* structured parameter, or the *last* loose
|
||||
// parameter, or any option, may be variadic.
|
||||
bool variadic = false;
|
||||
|
||||
// Set immediately by parameter<T>()/option<T>(), or by choices<T>(),
|
||||
// never deferred -- so the type is always known and displayable even
|
||||
// before action() is called.
|
||||
TypeInfo type;
|
||||
|
||||
// The exact C++ type T declared via parameter<T>()/option<T>(), kept
|
||||
// so action() can *validate* (not invent) the bound function's
|
||||
// corresponding argument type. Unset for Keyword/Flag slots.
|
||||
std::optional<std::type_index> cppType;
|
||||
|
||||
// True from parameter<T>()/option<T>() (when T is an enum) until the
|
||||
// matching choices<T>() call supplies the name table; action() checks
|
||||
// this is false for every slot before binding.
|
||||
bool awaitingChoices = false;
|
||||
|
||||
[[nodiscard]] std::string longFlag() const;
|
||||
[[nodiscard]] std::string shortFlag() const;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
#pragma once
|
||||
|
||||
// The type system. A `Scalar` is a closed variant over every primitive
|
||||
// type the engine understands. A `Value` wraps either a single Scalar
|
||||
// (for ordinary parameters/options) or a vector of them (for variadic
|
||||
// parameters/options). `TypeInfo` bundles a display name with a parser:
|
||||
// for the built-in scalar types this is a *plain stateless function
|
||||
// pointer* (no heap allocation, unlike std::function), and only the
|
||||
// rarer enum/choice-restricted case pays for a one-time, shared, heap
|
||||
// allocation to hold its name table.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace cliforge
|
||||
{
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Exceptions
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// Every distinct reason a run-time parse can fail. Exposed on CliError so
|
||||
// callers (and Engine internally, for its own error-selection logic) can
|
||||
// branch on *why* something failed, not just read a free-text message.
|
||||
enum class ErrorKind
|
||||
{
|
||||
Generic,
|
||||
UnknownOption, // a flag/option name isn't recognized for this command
|
||||
TooManyArguments, // more positional values than the command declares
|
||||
MissingArgument, // a required positional value (or option value) was never given
|
||||
MissingValue, // an option/flag was named but its value token is missing
|
||||
TypeMismatch, // a token couldn't convert to the slot's declared type
|
||||
};
|
||||
|
||||
// Base of every error the engine raises while registering commands or
|
||||
// parsing user input. Engine::run() catches CliError and prints it
|
||||
// nicely instead of letting it escape as an unhandled exception.
|
||||
class CliError : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
explicit CliError(const std::string& msg, ErrorKind kind = ErrorKind::Generic);
|
||||
[[nodiscard]] ErrorKind kind() const noexcept;
|
||||
|
||||
private:
|
||||
ErrorKind m_kind;
|
||||
};
|
||||
|
||||
// A token couldn't be converted to the type a slot expects -- always
|
||||
// ErrorKind::TypeMismatch.
|
||||
class ParseError : public CliError
|
||||
{
|
||||
public:
|
||||
explicit ParseError(const std::string& msg);
|
||||
};
|
||||
|
||||
// Something is wrong with how a command was *built* (mismatched arity
|
||||
// between declared slots and the bound function, duplicate names, a
|
||||
// choices<T>() call whose T doesn't match the declared type, an option
|
||||
// bound to a non-optional type, an action() parameter type that doesn't
|
||||
// match what was declared, etc). These fire at program startup (many of
|
||||
// them at compile time via static_assert), not at end-user parse time.
|
||||
struct RegistrationError : CliError
|
||||
{
|
||||
explicit RegistrationError(const std::string& msg);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Enum storage
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// Custom enums aren't a native Scalar alternative (there are infinitely
|
||||
// many enum types), so when a slot is restricted to an enum via
|
||||
// choices<T>(), the underlying value is boxed here. Value::as<T>() knows
|
||||
// to unbox + static_cast back to the caller's real enum type.
|
||||
struct EnumTag
|
||||
{
|
||||
long long value;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Scalar: the closed set of representable primitive values
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
using Scalar = std::variant<int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t,
|
||||
uint64_t, float, double, std::string, char, bool, EnumTag>;
|
||||
|
||||
enum class Kind
|
||||
{
|
||||
I8,
|
||||
U8,
|
||||
I16,
|
||||
U16,
|
||||
I32,
|
||||
U32,
|
||||
I64,
|
||||
U64,
|
||||
F32,
|
||||
F64,
|
||||
Str,
|
||||
Char,
|
||||
Bool,
|
||||
Enum
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// TypeInfo: how to parse + describe one type
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// Holds the name table for an enum/choice-restricted slot. Built once at
|
||||
// registration time (a single, amortized allocation via shared_ptr);
|
||||
// parsing against it never allocates beyond the error path.
|
||||
struct ChoiceTable
|
||||
{
|
||||
std::vector<std::pair<std::string, Scalar>> entries;
|
||||
|
||||
[[nodiscard]] Scalar parse(std::string_view token, std::string_view label) const;
|
||||
};
|
||||
|
||||
struct TypeInfo
|
||||
{
|
||||
Kind kind{};
|
||||
std::string displayName;
|
||||
// Stateless parser for built-in scalar types -- a plain function
|
||||
// pointer costs nothing to store or copy, unlike std::function.
|
||||
Scalar (*parsePlain)(std::string_view token, std::string_view label) = nullptr;
|
||||
// Populated only for choices<T>()-restricted slots.
|
||||
std::shared_ptr<const ChoiceTable> choiceTable;
|
||||
|
||||
[[nodiscard]] bool valid() const;
|
||||
[[nodiscard]] Scalar parse(std::string_view token, std::string_view label) const;
|
||||
};
|
||||
|
||||
namespace detail
|
||||
{
|
||||
template <typename Int>
|
||||
Int parseInteger(std::string_view token, std::string_view label, std::string_view typeName)
|
||||
{
|
||||
Int out{};
|
||||
auto [ptr, ec] = std::from_chars(token.data(), token.data() + token.size(), out);
|
||||
if (ec != std::errc{} || ptr != token.data() + token.size())
|
||||
{
|
||||
throw ParseError("type mismatch for '" + std::string(label) + "': got '" +
|
||||
std::string(token) + "', expected " + std::string(typeName));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
template <typename Float>
|
||||
Float parseFloating(std::string_view token, std::string_view label,
|
||||
std::string_view typeName)
|
||||
{
|
||||
Float out{};
|
||||
auto [ptr, ec] = std::from_chars(token.data(), token.data() + token.size(), out);
|
||||
if (ec != std::errc{} || ptr != token.data() + token.size())
|
||||
{
|
||||
throw ParseError("type mismatch for '" + std::string(label) + "': got '" +
|
||||
std::string(token) + "', expected " + std::string(typeName));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string toLower(std::string_view s);
|
||||
}
|
||||
|
||||
// Maps a C++ type T to its Kind + a ready-to-use TypeInfo. Specialized
|
||||
// below for every built-in type; enums go through choices<T>() instead
|
||||
// since they need a name<->value table supplied by the caller.
|
||||
template <typename T> struct TypeOf; // intentionally undefined for unsupported T
|
||||
|
||||
#define CLIFORGE_INT_TYPE(CPP_T, KIND, NAME) \
|
||||
template <> struct TypeOf<CPP_T> \
|
||||
{ \
|
||||
static TypeInfo get() \
|
||||
{ \
|
||||
TypeInfo t; \
|
||||
t.kind = Kind::KIND; \
|
||||
t.displayName = NAME; \
|
||||
t.parsePlain = [](std::string_view tok, std::string_view lbl) -> Scalar \
|
||||
{ return Scalar{detail::parseInteger<CPP_T>(tok, lbl, NAME)}; }; \
|
||||
return t; \
|
||||
} \
|
||||
};
|
||||
|
||||
CLIFORGE_INT_TYPE(int8_t, I8, "int8")
|
||||
CLIFORGE_INT_TYPE(uint8_t, U8, "uint8")
|
||||
CLIFORGE_INT_TYPE(int16_t, I16, "int16")
|
||||
CLIFORGE_INT_TYPE(uint16_t, U16, "uint16")
|
||||
CLIFORGE_INT_TYPE(int32_t, I32, "int32")
|
||||
CLIFORGE_INT_TYPE(uint32_t, U32, "uint32")
|
||||
CLIFORGE_INT_TYPE(int64_t, I64, "int64")
|
||||
CLIFORGE_INT_TYPE(uint64_t, U64, "uint64")
|
||||
#undef CLIFORGE_INT_TYPE
|
||||
|
||||
template <> struct TypeOf<float>
|
||||
{
|
||||
static TypeInfo get()
|
||||
{
|
||||
TypeInfo t;
|
||||
t.kind = Kind::F32;
|
||||
t.displayName = "float32";
|
||||
t.parsePlain = [](std::string_view tok, std::string_view lbl) -> Scalar
|
||||
{ return Scalar{detail::parseFloating<float>(tok, lbl, "float32")}; };
|
||||
return t;
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct TypeOf<double>
|
||||
{
|
||||
static TypeInfo get()
|
||||
{
|
||||
TypeInfo t;
|
||||
t.kind = Kind::F64;
|
||||
t.displayName = "float64";
|
||||
t.parsePlain = [](std::string_view tok, std::string_view lbl) -> Scalar
|
||||
{ return Scalar{detail::parseFloating<double>(tok, lbl, "float64")}; };
|
||||
return t;
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct TypeOf<std::string>
|
||||
{
|
||||
static TypeInfo get()
|
||||
{
|
||||
TypeInfo t;
|
||||
t.kind = Kind::Str;
|
||||
t.displayName = "string";
|
||||
t.parsePlain = [](std::string_view tok, std::string_view) -> Scalar
|
||||
{ return Scalar{std::string(tok)}; };
|
||||
return t;
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct TypeOf<char>
|
||||
{
|
||||
static TypeInfo get()
|
||||
{
|
||||
TypeInfo t;
|
||||
t.kind = Kind::Char;
|
||||
t.displayName = "char";
|
||||
t.parsePlain = [](std::string_view tok, std::string_view lbl) -> Scalar
|
||||
{
|
||||
if (tok.size() != 1)
|
||||
{
|
||||
throw ParseError("type mismatch for '" + std::string(lbl) + "': got '" +
|
||||
std::string(tok) + "', expected a single char");
|
||||
}
|
||||
return Scalar{tok[0]};
|
||||
};
|
||||
return t;
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct TypeOf<bool>
|
||||
{
|
||||
static TypeInfo get()
|
||||
{
|
||||
TypeInfo t;
|
||||
t.kind = Kind::Bool;
|
||||
t.displayName = "bool";
|
||||
t.parsePlain = [](std::string_view tok, std::string_view lbl) -> Scalar
|
||||
{
|
||||
std::string lower = detail::toLower(tok);
|
||||
if (lower == "true" || lower == "1" || lower == "yes" || lower == "on")
|
||||
{
|
||||
return Scalar{true};
|
||||
}
|
||||
if (lower == "false" || lower == "0" || lower == "no" || lower == "off")
|
||||
{
|
||||
return Scalar{false};
|
||||
}
|
||||
throw ParseError("type mismatch for '" + std::string(lbl) + "': got '" +
|
||||
std::string(tok) +
|
||||
"', expected bool (true/false, 1/0, yes/no, on/off)");
|
||||
};
|
||||
return t;
|
||||
}
|
||||
};
|
||||
|
||||
// Builds a TypeInfo for a `choices<T>()` restricted slot: only the
|
||||
// listed string keys are accepted, each mapped to a T. Works both for
|
||||
// genuine C++ enums (boxed into EnumTag) and for restricting a built-in
|
||||
// type (e.g. a string option limited to a fixed set of values). The
|
||||
// choice table is converted to type-erased Scalars once, here, so
|
||||
// nothing downstream needs to be templated on T again.
|
||||
template <typename T>
|
||||
TypeInfo makeChoiceTypeInfo(std::string typeName,
|
||||
const std::vector<std::pair<std::string, T>>& choices)
|
||||
{
|
||||
auto table = std::make_shared<ChoiceTable>();
|
||||
table->entries.reserve(choices.size());
|
||||
for (const auto& [name, val] : choices)
|
||||
{
|
||||
if constexpr (std::is_enum_v<T>)
|
||||
{
|
||||
table->entries.emplace_back(name, Scalar{EnumTag{static_cast<long long>(val)}});
|
||||
}
|
||||
else
|
||||
{
|
||||
table->entries.emplace_back(name, Scalar{val});
|
||||
}
|
||||
}
|
||||
TypeInfo t;
|
||||
t.kind = Kind::Enum;
|
||||
t.displayName = std::move(typeName);
|
||||
t.choiceTable = std::move(table);
|
||||
return t;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Value: what actually flows out of the parser and into bound functions
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
class Value
|
||||
{
|
||||
public:
|
||||
Value();
|
||||
|
||||
static Value ofScalar(Scalar s);
|
||||
static Value ofVector(std::vector<Scalar> v);
|
||||
[[nodiscard]] bool hasValue() const;
|
||||
[[nodiscard]] bool isVector() const;
|
||||
|
||||
template <typename T> [[nodiscard]] T as() const
|
||||
{
|
||||
const Scalar& s = std::get<Scalar>(m_data);
|
||||
if constexpr (std::is_enum_v<T>)
|
||||
{
|
||||
return static_cast<T>(std::get<EnumTag>(s).value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::get<T>(s);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> std::vector<T> asVector() const
|
||||
{
|
||||
const auto& vec = std::get<std::vector<Scalar>>(m_data);
|
||||
std::vector<T> out;
|
||||
out.reserve(vec.size());
|
||||
for (const Scalar& s : vec)
|
||||
{
|
||||
if constexpr (std::is_enum_v<T>)
|
||||
{
|
||||
out.push_back(static_cast<T>(std::get<EnumTag>(s).value));
|
||||
}
|
||||
else
|
||||
{
|
||||
out.push_back(std::get<T>(s));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
std::variant<std::monostate, Scalar, std::vector<Scalar>> m_data;
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user