Refactoring
This commit is contained in:
1 parent
d5b7bc9934
commit
718afeaa40
11 files changed
+516
-454
No files matched your search
@@ -20,8 +20,9 @@
|
||||
// const reference all work). Mismatches are RegistrationErrors caught at
|
||||
// startup, not silent surprises at run time.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
@@ -43,37 +44,35 @@ namespace cliforge
|
||||
{
|
||||
// --- small type-trait helpers used only for action() binding ----------
|
||||
|
||||
template <typename T>
|
||||
struct is_optional : std::false_type {};
|
||||
|
||||
template <typename T>
|
||||
struct is_optional<std::optional<T>> : std::true_type {
|
||||
using inner = T;
|
||||
template <typename T> struct IsOptional : std::false_type
|
||||
{
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
inline constexpr bool is_optional_v = is_optional<T>::value;
|
||||
|
||||
template <typename T>
|
||||
struct is_vector : std::false_type {};
|
||||
|
||||
template <typename T>
|
||||
struct is_vector<std::vector<T>> : std::true_type {
|
||||
using inner = T;
|
||||
template <typename T> struct IsOptional<std::optional<T>> : std::true_type
|
||||
{
|
||||
using Inner = T;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
inline constexpr bool is_vector_v = is_vector<T>::value;
|
||||
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 is_builtin_scalar_v =
|
||||
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>;
|
||||
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
|
||||
@@ -87,10 +86,9 @@ namespace cliforge
|
||||
// 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()
|
||||
template <typename T> std::string friendlyTypeName()
|
||||
{
|
||||
if constexpr (is_builtin_scalar_v<T>)
|
||||
if constexpr (IsBuiltinScalarV<T>)
|
||||
{
|
||||
return TypeOf<T>::get().displayName;
|
||||
}
|
||||
@@ -110,8 +108,8 @@ namespace cliforge
|
||||
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::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
|
||||
@@ -126,7 +124,7 @@ namespace cliforge
|
||||
// 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);
|
||||
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
|
||||
@@ -150,55 +148,55 @@ namespace cliforge
|
||||
s.cppType = std::type_index(typeid(T));
|
||||
if constexpr (std::is_enum_v<T>)
|
||||
{
|
||||
s.awaitingChoices = true; // choices<T>(...) must follow immediately
|
||||
s.awaitingChoices = true; // choices<T>(...) must follow immediately
|
||||
}
|
||||
else
|
||||
{
|
||||
static_assert(detail::is_builtin_scalar_v<T>,
|
||||
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 (!unstructuredStarted_)
|
||||
if (!m_unstructuredStarted)
|
||||
{
|
||||
if (structuredSealed_)
|
||||
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");
|
||||
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();
|
||||
m_structured.push_back(std::move(s));
|
||||
m_lastSlot = &m_structured.back();
|
||||
if (variadic)
|
||||
structuredSealed_ = true;
|
||||
m_structuredSealed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (looseSealed_)
|
||||
if (m_looseSealed)
|
||||
{
|
||||
throw RegistrationError(
|
||||
"can't declare parameter('" + s.name +
|
||||
"') after a variadic parameter -- a variadic parameter "
|
||||
"must be the last parameter declared");
|
||||
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;
|
||||
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, std::string description);
|
||||
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)
|
||||
Command& option(std::string longName, char shortName, std::string description,
|
||||
bool variadic = false)
|
||||
{
|
||||
longName = stripDashes(std::move(longName));
|
||||
validateLongName(longName);
|
||||
@@ -217,20 +215,19 @@ namespace cliforge
|
||||
}
|
||||
else
|
||||
{
|
||||
static_assert(detail::is_builtin_scalar_v<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)"
|
||||
);
|
||||
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();
|
||||
}
|
||||
unstructured_.push_back(std::move(s));
|
||||
lastSlot_ = &unstructured_.back();
|
||||
unstructuredStarted_ = true;
|
||||
m_unstructured.push_back(std::move(s));
|
||||
m_lastSlot = &m_unstructured.back();
|
||||
m_unstructuredStarted = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Command& describe(std::string description);
|
||||
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
|
||||
@@ -238,25 +235,26 @@ namespace cliforge
|
||||
// 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 (lastSlot_ == nullptr || (lastSlot_->kind != SlotKind::Parameter && lastSlot_->kind != SlotKind::Option))
|
||||
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"
|
||||
);
|
||||
throw RegistrationError("choices<>() must directly follow the parameter<T>() or "
|
||||
"option<T>() it restricts");
|
||||
}
|
||||
|
||||
if (!lastSlot_->cppType || *lastSlot_->cppType != std::type_index(typeid(T)))
|
||||
if (!m_lastSlot->cppType || *m_lastSlot->cppType != std::type_index(typeid(T)))
|
||||
{
|
||||
throw RegistrationError("choices<>() type doesn't match the type declared for '" +
|
||||
lastSlot_->name + "'"
|
||||
);
|
||||
m_lastSlot->name + "'");
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, T>> table(values);
|
||||
lastSlot_->type = makeChoiceTypeInfo<T>(typeName.empty() ? std::string("enum") : std::move(typeName), table);
|
||||
lastSlot_->awaitingChoices = false;
|
||||
m_lastSlot->type = makeChoiceTypeInfo<T>(
|
||||
typeName.empty() ? std::string("enum") : std::move(typeName), table);
|
||||
m_lastSlot->awaitingChoices = false;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -267,56 +265,55 @@ namespace cliforge
|
||||
// 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)
|
||||
template <typename Func> Command& action(Func f)
|
||||
{
|
||||
using Traits = detail::function_traits<std::decay_t<Func>>;
|
||||
if (structured_.empty() || std::none_of(structured_.begin(), structured_.end(), [](const Slot& s)
|
||||
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; }))
|
||||
{
|
||||
return s.kind == SlotKind::Keyword;
|
||||
}))
|
||||
{
|
||||
throw RegistrationError(
|
||||
"a command must declare at least one keyword() so it can "
|
||||
"be identified on the command line");
|
||||
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);
|
||||
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)
|
||||
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()) +
|
||||
"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>{});
|
||||
sealed_ = true;
|
||||
bindArgs(std::function{f}, ordered, std::make_index_sequence<Traits::Arity>{});
|
||||
m_sealed = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool sealed() const;
|
||||
const std::string& description() const;
|
||||
[[nodiscard]] bool sealed() const;
|
||||
[[nodiscard]] const std::string& description() const;
|
||||
|
||||
// --- matching / parsing entry points, used by Engine -------------
|
||||
|
||||
StructuredMatch tryMatchStructured(const std::vector<std::string>& tokens) const;
|
||||
|
||||
[[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.
|
||||
std::vector<Value> parseUnstructured(const std::vector<std::string>& tokens) const;
|
||||
[[nodiscard]] std::vector<Value> parseUnstructured(
|
||||
const std::vector<std::string>& tokens) const;
|
||||
|
||||
void invoke(std::vector<Value> allValues) const;
|
||||
|
||||
std::size_t structuredSlotCount() 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
|
||||
@@ -326,34 +323,36 @@ namespace cliforge
|
||||
// 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<std::string>& tokens) const;
|
||||
[[nodiscard]] bool couldMatchPrefix(const std::vector<std::string>& tokens) const;
|
||||
|
||||
// --- help / usage --------------------------------------------------
|
||||
|
||||
std::string usageLine() const;
|
||||
[[nodiscard]] std::string usageLine() const;
|
||||
|
||||
std::string helpText(const std::string& progName) const;
|
||||
[[nodiscard]] std::string helpText(const std::string& progName) const;
|
||||
|
||||
std::vector<std::string> flagOptionNames() const;
|
||||
[[nodiscard]] std::vector<std::string> flagOptionNames() const;
|
||||
|
||||
// Fuzzy structural distance used for "did you mean...?" suggestions.
|
||||
struct FuzzyScore {
|
||||
struct FuzzyScore
|
||||
{
|
||||
std::size_t distance = 0;
|
||||
std::size_t keywordChars = 0; // denominator for a relative threshold
|
||||
std::size_t keywordChars = 0; // denominator for a relative threshold
|
||||
};
|
||||
|
||||
FuzzyScore fuzzyStructuralDistance(const std::vector<std::string>& tokens) const;
|
||||
[[nodiscard]] FuzzyScore fuzzyStructuralDistance(
|
||||
const std::vector<std::string>& tokens) const;
|
||||
|
||||
private:
|
||||
std::vector<Slot> structured_;
|
||||
std::vector<Slot> unstructured_;
|
||||
std::string description_;
|
||||
std::function<void(std::vector<Value>&)> invoke_;
|
||||
Slot* lastSlot_ = nullptr;
|
||||
bool unstructuredStarted_ = false;
|
||||
bool structuredSealed_ = false;
|
||||
bool looseSealed_ = false;
|
||||
bool sealed_ = false;
|
||||
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);
|
||||
|
||||
@@ -368,69 +367,94 @@ namespace cliforge
|
||||
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>());
|
||||
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) {
|
||||
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()");
|
||||
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::is_optional_v<Stripped>) {
|
||||
using Inner = typename detail::is_optional<Stripped>::inner;
|
||||
if (slot.kind == SlotKind::Parameter) {
|
||||
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");
|
||||
">' 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<>");
|
||||
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::is_vector_v<Stripped>) {
|
||||
using Inner = typename detail::is_vector<Stripped>::inner;
|
||||
if (!slot.variadic) {
|
||||
throw RegistrationError("'" + slot.name +
|
||||
"' binds to std::vector<> but wasn't declared variadic=true");
|
||||
}
|
||||
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<>");
|
||||
}
|
||||
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");
|
||||
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>) {
|
||||
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::is_optional_v<ArgT>) {
|
||||
using Inner = typename detail::is_optional<ArgT>::inner;
|
||||
}
|
||||
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::is_vector_v<ArgT>) {
|
||||
using Inner = typename detail::is_vector<ArgT>::inner;
|
||||
}
|
||||
else if constexpr (detail::IsVectorV<ArgT>)
|
||||
{
|
||||
using Inner = typename detail::IsVector<ArgT>::Inner;
|
||||
return v.asVector<Inner>();
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
return v.as<ArgT>();
|
||||
}
|
||||
}
|
||||
@@ -442,20 +466,22 @@ namespace cliforge
|
||||
// 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...>) {
|
||||
std::index_sequence<I...>)
|
||||
{
|
||||
(configureSlot<Args>(*ordered[I]), ...);
|
||||
invoke_ = [f = std::move(f)](std::vector<Value>& values) {
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
int findByLong(std::string_view name) const;
|
||||
[[nodiscard]] int findByLong(std::string_view name) const;
|
||||
|
||||
int findByShort(char c) const;
|
||||
[[nodiscard]] int findByShort(char c) const;
|
||||
|
||||
std::string suggestFlagOption(const std::string& given) const;
|
||||
[[nodiscard]] std::string suggestFlagOption(const std::string& given) const;
|
||||
|
||||
[[noreturn]] void throwUnknown(std::string_view tok) const;
|
||||
|
||||
|
||||
@@ -20,11 +20,12 @@
|
||||
namespace cliforge
|
||||
{
|
||||
|
||||
class Engine {
|
||||
class Engine
|
||||
{
|
||||
public:
|
||||
explicit Engine(std::string programName = "");
|
||||
|
||||
Engine& describe(std::string description);
|
||||
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.
|
||||
@@ -34,9 +35,9 @@ namespace cliforge
|
||||
int run(int argc, char** argv);
|
||||
|
||||
private:
|
||||
std::string programName_;
|
||||
std::string description_;
|
||||
std::vector<std::unique_ptr<Command>> commands_;
|
||||
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);
|
||||
@@ -45,7 +46,7 @@ namespace cliforge
|
||||
// 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.
|
||||
int showHelp(const std::vector<std::string>& queryTokens) const;
|
||||
[[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;
|
||||
|
||||
@@ -8,54 +8,54 @@
|
||||
// having to spell the types out twice.
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include <functional>
|
||||
#include <tuple>
|
||||
|
||||
namespace cliforge::detail
|
||||
{
|
||||
|
||||
template <typename T> struct function_traits;
|
||||
template <typename T> struct FunctionTraits;
|
||||
|
||||
// Plain free function: void foo(std::string, int)
|
||||
template <typename Ret, typename... Args> struct function_traits<Ret (*)(Args...)>
|
||||
template <typename Ret, typename... Args> struct FunctionTraits<Ret (*)(Args...)>
|
||||
{
|
||||
using return_type = 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 args_tuple = std::tuple<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 function_traits<std::function<Ret(Args...)>>
|
||||
template <typename Ret, typename... Args> struct FunctionTraits<std::function<Ret(Args...)>>
|
||||
{
|
||||
using return_type = 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 args_tuple = std::tuple<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 function_traits<Ret (C::*)(Args...) const>
|
||||
struct FunctionTraits<Ret (C::*)(Args...) const>
|
||||
{
|
||||
using return_type = 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 args_tuple = std::tuple<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...>;
|
||||
};
|
||||
|
||||
template <typename C, typename Ret, typename... Args>
|
||||
struct function_traits<Ret (C::*)(Args...)>
|
||||
template <typename C, typename Ret, typename... Args> struct FunctionTraits<Ret (C::*)(Args...)>
|
||||
{
|
||||
using return_type = 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 args_tuple = std::tuple<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 function_traits : function_traits<decltype(&F::operator())>
|
||||
template <typename F> struct FunctionTraits : FunctionTraits<decltype(&F::operator())>
|
||||
{
|
||||
};
|
||||
}
|
||||
@@ -16,6 +16,6 @@ namespace cliforge::detail
|
||||
// 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);
|
||||
const std::vector<std::string>& pool,
|
||||
std::size_t limit = 3);
|
||||
}
|
||||
@@ -58,7 +58,7 @@ namespace cliforge
|
||||
// this is false for every slot before binding.
|
||||
bool awaitingChoices = false;
|
||||
|
||||
std::string longFlag() const;
|
||||
std::string shortFlag() const;
|
||||
[[nodiscard]] std::string longFlag() const;
|
||||
[[nodiscard]] std::string shortFlag() const;
|
||||
};
|
||||
}
|
||||
@@ -9,9 +9,10 @@
|
||||
// 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 <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -47,10 +48,10 @@ namespace cliforge
|
||||
{
|
||||
public:
|
||||
explicit CliError(const std::string& msg, ErrorKind kind = ErrorKind::Generic);
|
||||
ErrorKind kind() const noexcept;
|
||||
[[nodiscard]] ErrorKind kind() const noexcept;
|
||||
|
||||
private:
|
||||
ErrorKind kind_;
|
||||
ErrorKind m_kind;
|
||||
};
|
||||
|
||||
// A token couldn't be converted to the type a slot expects -- always
|
||||
@@ -58,7 +59,7 @@ namespace cliforge
|
||||
class ParseError : public CliError
|
||||
{
|
||||
public:
|
||||
explicit ParseError(const std::string& msg);
|
||||
explicit ParseError(const std::string& msg);
|
||||
};
|
||||
|
||||
// Something is wrong with how a command was *built* (mismatched arity
|
||||
@@ -69,7 +70,7 @@ namespace cliforge
|
||||
// them at compile time via static_assert), not at end-user parse time.
|
||||
struct RegistrationError : CliError
|
||||
{
|
||||
explicit RegistrationError(const std::string& msg);
|
||||
explicit RegistrationError(const std::string& msg);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -121,7 +122,7 @@ namespace cliforge
|
||||
{
|
||||
std::vector<std::pair<std::string, Scalar>> entries;
|
||||
|
||||
Scalar parse(std::string_view token, std::string_view label) const;
|
||||
[[nodiscard]] Scalar parse(std::string_view token, std::string_view label) const;
|
||||
};
|
||||
|
||||
struct TypeInfo
|
||||
@@ -134,9 +135,8 @@ namespace cliforge
|
||||
// Populated only for choices<T>()-restricted slots.
|
||||
std::shared_ptr<const ChoiceTable> choiceTable;
|
||||
|
||||
bool valid() const;
|
||||
Scalar parse(std::string_view token, std::string_view label) const;
|
||||
|
||||
[[nodiscard]] bool valid() const;
|
||||
[[nodiscard]] Scalar parse(std::string_view token, std::string_view label) const;
|
||||
};
|
||||
|
||||
namespace detail
|
||||
@@ -326,12 +326,12 @@ namespace cliforge
|
||||
|
||||
static Value ofScalar(Scalar s);
|
||||
static Value ofVector(std::vector<Scalar> v);
|
||||
bool hasValue() const;
|
||||
bool isVector() const;
|
||||
[[nodiscard]] bool hasValue() const;
|
||||
[[nodiscard]] bool isVector() const;
|
||||
|
||||
template <typename T> T as() const
|
||||
template <typename T> [[nodiscard]] T as() const
|
||||
{
|
||||
const Scalar& s = std::get<Scalar>(data_);
|
||||
const Scalar& s = std::get<Scalar>(m_data);
|
||||
if constexpr (std::is_enum_v<T>)
|
||||
{
|
||||
return static_cast<T>(std::get<EnumTag>(s).value);
|
||||
@@ -344,7 +344,7 @@ namespace cliforge
|
||||
|
||||
template <typename T> std::vector<T> asVector() const
|
||||
{
|
||||
const auto& vec = std::get<std::vector<Scalar>>(data_);
|
||||
const auto& vec = std::get<std::vector<Scalar>>(m_data);
|
||||
std::vector<T> out;
|
||||
out.reserve(vec.size());
|
||||
for (const Scalar& s : vec)
|
||||
@@ -362,6 +362,6 @@ namespace cliforge
|
||||
}
|
||||
|
||||
private:
|
||||
std::variant<std::monostate, Scalar, std::vector<Scalar>> data_;
|
||||
std::variant<std::monostate, Scalar, std::vector<Scalar>> m_data;
|
||||
};
|
||||
}
|
||||
@@ -33,15 +33,15 @@ namespace cliforge
|
||||
}
|
||||
}
|
||||
|
||||
Command& Command::keyword(std::string literal)
|
||||
Command& Command::keyword(const std::string& literal)
|
||||
{
|
||||
if (unstructuredStarted_)
|
||||
if (m_unstructuredStarted)
|
||||
{
|
||||
throw RegistrationError("can't declare keyword('" + literal +
|
||||
"') after a flag()/option() was added -- keywords must "
|
||||
"come entirely before the unstructured part");
|
||||
}
|
||||
if (structuredSealed_)
|
||||
if (m_structuredSealed)
|
||||
{
|
||||
throw RegistrationError("can't declare keyword('" + literal +
|
||||
"') after a variadic parameter -- a variadic parameter "
|
||||
@@ -56,13 +56,13 @@ namespace cliforge
|
||||
Slot s;
|
||||
s.kind = SlotKind::Keyword;
|
||||
s.name = std::move(literal);
|
||||
structured_.push_back(std::move(s));
|
||||
lastSlot_ = &structured_.back();
|
||||
m_structured.push_back(std::move(s));
|
||||
m_lastSlot = &m_structured.back();
|
||||
return *this;
|
||||
}
|
||||
|
||||
// shortName is 0 (the default) for "no short form".
|
||||
Command& Command::flag(std::string longName, char shortName, std::string description)
|
||||
Command& Command::flag(std::string longName, char shortName, const std::string& description)
|
||||
{
|
||||
longName = stripDashes(std::move(longName));
|
||||
validateLongName(longName);
|
||||
@@ -73,26 +73,26 @@ namespace cliforge
|
||||
s.name = std::move(longName);
|
||||
s.shortName = shortName;
|
||||
s.description = std::move(description);
|
||||
unstructured_.push_back(std::move(s));
|
||||
lastSlot_ = &unstructured_.back();
|
||||
unstructuredStarted_ = true;
|
||||
m_unstructured.push_back(std::move(s));
|
||||
m_lastSlot = &m_unstructured.back();
|
||||
m_unstructuredStarted = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Command& Command::describe(std::string description)
|
||||
Command& Command::describe(const std::string& description)
|
||||
{
|
||||
description_ = std::move(description);
|
||||
m_description = std::move(description);
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool Command::sealed() const
|
||||
{
|
||||
return sealed_;
|
||||
return m_sealed;
|
||||
}
|
||||
|
||||
const std::string& Command::description() const
|
||||
{
|
||||
return description_;
|
||||
return m_description;
|
||||
}
|
||||
|
||||
// --- matching / parsing entry points, used by Engine -------------
|
||||
@@ -103,10 +103,10 @@ namespace cliforge
|
||||
std::size_t i = 0;
|
||||
try
|
||||
{
|
||||
for (std::size_t s = 0; s < structured_.size(); ++s)
|
||||
for (std::size_t s = 0; s < m_structured.size(); ++s)
|
||||
{
|
||||
const Slot& slot = structured_[s];
|
||||
bool isLast = (s + 1 == structured_.size());
|
||||
const Slot& slot = m_structured[s];
|
||||
bool isLast = (s + 1 == m_structured.size());
|
||||
if (slot.kind == SlotKind::Keyword)
|
||||
{
|
||||
if (i >= tokens.size() || tokens[i] != slot.name)
|
||||
@@ -135,9 +135,9 @@ namespace cliforge
|
||||
// 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);
|
||||
">': expected a value of type " +
|
||||
slot.type.displayName,
|
||||
ErrorKind::MissingArgument);
|
||||
}
|
||||
|
||||
m.paramValues.push_back(
|
||||
@@ -168,10 +168,9 @@ namespace cliforge
|
||||
// missing/extra positional argument, or a type mismatch.
|
||||
std::vector<Value> Command::parseUnstructured(const std::vector<std::string>& tokens) const
|
||||
{
|
||||
std::vector<bool> provided(unstructured_.size(), false);
|
||||
std::vector<std::vector<Scalar>> collected(unstructured_.size());
|
||||
std::vector<std::string_view>
|
||||
looseTokens; // views into `tokens`, which outlives this call
|
||||
std::vector<bool> provided(m_unstructured.size(), false);
|
||||
std::vector<std::vector<Scalar>> collected(m_unstructured.size());
|
||||
std::vector<std::string_view> looseTokens; // views into `tokens`, which outlives this call
|
||||
looseTokens.reserve(tokens.size());
|
||||
|
||||
std::size_t i = 0;
|
||||
@@ -192,8 +191,7 @@ namespace cliforge
|
||||
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{};
|
||||
std::string_view inlineValue = hasInline ? rest.substr(eq + 1) : std::string_view{};
|
||||
int idx = findByLong(longName);
|
||||
if (idx < 0)
|
||||
throwUnknown(tok);
|
||||
@@ -222,16 +220,16 @@ namespace cliforge
|
||||
if (idx < 0)
|
||||
{
|
||||
throw CliError(std::string("unknown flag '-") + c +
|
||||
"' in combined group '" + std::string(tok) +
|
||||
"'" + suggestFlagOption(std::string("-") + c),
|
||||
ErrorKind::UnknownOption);
|
||||
"' in combined group '" + std::string(tok) + "'" +
|
||||
suggestFlagOption(std::string("-") + c),
|
||||
ErrorKind::UnknownOption);
|
||||
}
|
||||
if (unstructured_[static_cast<size_t>(idx)].kind != SlotKind::Flag)
|
||||
if (m_unstructured[static_cast<size_t>(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");
|
||||
std::string(tok) +
|
||||
"' is an option (needs a value), so it can't "
|
||||
"be combined with other short flags");
|
||||
}
|
||||
provided[static_cast<size_t>(idx)] = true;
|
||||
}
|
||||
@@ -244,16 +242,16 @@ namespace cliforge
|
||||
// Parameter slots, positionally, in declaration order -- this is
|
||||
// independent of where those tokens fell relative to flags/options.
|
||||
std::vector<std::size_t> looseParamIdx;
|
||||
for (std::size_t idx = 0; idx < unstructured_.size(); ++idx)
|
||||
for (std::size_t idx = 0; idx < m_unstructured.size(); ++idx)
|
||||
{
|
||||
if (unstructured_[idx].kind == SlotKind::Parameter)
|
||||
if (m_unstructured[idx].kind == SlotKind::Parameter)
|
||||
looseParamIdx.push_back(idx);
|
||||
}
|
||||
std::vector<Value> looseValues(looseParamIdx.size());
|
||||
std::size_t li = 0;
|
||||
for (std::size_t k = 0; k < looseParamIdx.size(); ++k)
|
||||
{
|
||||
const Slot& slot = unstructured_[looseParamIdx[k]];
|
||||
const Slot& slot = m_unstructured[looseParamIdx[k]];
|
||||
bool isLastLoose = (k + 1 == looseParamIdx.size());
|
||||
if (slot.variadic && isLastLoose)
|
||||
{
|
||||
@@ -271,8 +269,8 @@ namespace cliforge
|
||||
if (li >= looseTokens.size())
|
||||
{
|
||||
throw CliError("missing required argument '<" + slot.name +
|
||||
">': expected a value of type " + slot.type.displayName,
|
||||
ErrorKind::MissingArgument);
|
||||
">': expected a value of type " + slot.type.displayName,
|
||||
ErrorKind::MissingArgument);
|
||||
}
|
||||
looseValues[k] = Value::ofScalar(slot.type.parse(looseTokens[li], slot.name));
|
||||
++li;
|
||||
@@ -288,14 +286,14 @@ namespace cliforge
|
||||
throw CliError(msg, ErrorKind::TooManyArguments);
|
||||
}
|
||||
|
||||
// Assemble the final values in unstructured_'s declaration order --
|
||||
// Assemble the final values in m_unstructured's declaration order --
|
||||
// this is what gets paired with the bound function's arguments.
|
||||
std::vector<Value> out;
|
||||
out.reserve(unstructured_.size());
|
||||
out.reserve(m_unstructured.size());
|
||||
std::size_t looseCursor = 0;
|
||||
for (std::size_t idx = 0; idx < unstructured_.size(); ++idx)
|
||||
for (std::size_t idx = 0; idx < m_unstructured.size(); ++idx)
|
||||
{
|
||||
const Slot& slot = unstructured_[idx];
|
||||
const Slot& slot = m_unstructured[idx];
|
||||
if (slot.kind == SlotKind::Parameter)
|
||||
{
|
||||
out.push_back(std::move(looseValues[looseCursor++]));
|
||||
@@ -322,23 +320,23 @@ namespace cliforge
|
||||
|
||||
void Command::invoke(std::vector<Value> allValues) const
|
||||
{
|
||||
invoke_(allValues);
|
||||
m_invoke(allValues);
|
||||
}
|
||||
|
||||
std::size_t Command::structuredSlotCount() const
|
||||
{
|
||||
return structured_.size();
|
||||
return m_structured.size();
|
||||
}
|
||||
|
||||
bool Command::couldMatchPrefix(const std::vector<std::string>& tokens) const
|
||||
{
|
||||
std::size_t i = 0;
|
||||
for (std::size_t s = 0; s < structured_.size(); ++s)
|
||||
for (std::size_t s = 0; s < m_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());
|
||||
const Slot& slot = m_structured[s];
|
||||
bool isLast = (s + 1 == m_structured.size());
|
||||
if (slot.kind == SlotKind::Keyword)
|
||||
{
|
||||
if (tokens[i] != slot.name)
|
||||
@@ -360,7 +358,7 @@ namespace cliforge
|
||||
// Whatever tokens remain must be plausible flags/options (assumed
|
||||
// fine without deep validation) or fit a declared loose parameter.
|
||||
std::vector<const Slot*> looseParams;
|
||||
for (const Slot& s : unstructured_)
|
||||
for (const Slot& s : m_unstructured)
|
||||
{
|
||||
if (s.kind == SlotKind::Parameter)
|
||||
looseParams.push_back(&s);
|
||||
@@ -383,9 +381,9 @@ namespace cliforge
|
||||
std::string Command::usageLine() const
|
||||
{
|
||||
std::ostringstream os;
|
||||
for (std::size_t s = 0; s < structured_.size(); ++s)
|
||||
for (std::size_t s = 0; s < m_structured.size(); ++s)
|
||||
{
|
||||
const Slot& slot = structured_[s];
|
||||
const Slot& slot = m_structured[s];
|
||||
if (s)
|
||||
os << ' ';
|
||||
if (slot.kind == SlotKind::Keyword)
|
||||
@@ -398,11 +396,11 @@ namespace cliforge
|
||||
}
|
||||
}
|
||||
bool anyFlagOrOption =
|
||||
std::any_of(unstructured_.begin(), unstructured_.end(),
|
||||
std::any_of(m_unstructured.begin(), m_unstructured.end(),
|
||||
[](const Slot& s) { return s.kind != SlotKind::Parameter; });
|
||||
if (anyFlagOrOption)
|
||||
os << " [OPTIONS]";
|
||||
for (const Slot& slot : unstructured_)
|
||||
for (const Slot& slot : m_unstructured)
|
||||
{
|
||||
if (slot.kind != SlotKind::Parameter)
|
||||
continue;
|
||||
@@ -415,49 +413,45 @@ namespace cliforge
|
||||
{
|
||||
std::ostringstream os;
|
||||
os << "Usage: " << progName << " " << usageLine() << "\n";
|
||||
if (!description_.empty())
|
||||
os << "\n" << description_ << "\n";
|
||||
if (!m_description.empty())
|
||||
os << "\n" << m_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; });
|
||||
bool anyParams = std::any_of(m_structured.begin(), m_structured.end(),
|
||||
[](const Slot& s) { return s.kind == SlotKind::Parameter; }) ||
|
||||
std::any_of(m_unstructured.begin(), m_unstructured.end(),
|
||||
[](const Slot& s) { return s.kind == SlotKind::Parameter; });
|
||||
if (anyParams)
|
||||
{
|
||||
os << "\nArguments:\n";
|
||||
for (const Slot& slot : structured_)
|
||||
for (const Slot& slot : m_structured)
|
||||
{
|
||||
if (slot.kind != SlotKind::Parameter)
|
||||
continue;
|
||||
std::string label = "<" + slot.name + (slot.variadic ? "..." : "") + ">";
|
||||
os << " " << padRight(label, 22) << describeType(slot) << slot.description
|
||||
<< "\n";
|
||||
os << " " << padRight(label, 22) << describeType(slot) << slot.description << "\n";
|
||||
}
|
||||
for (const Slot& slot : unstructured_)
|
||||
for (const Slot& slot : m_unstructured)
|
||||
{
|
||||
if (slot.kind != SlotKind::Parameter)
|
||||
continue;
|
||||
std::string label = "<" + slot.name + (slot.variadic ? "..." : "") + ">";
|
||||
os << " " << padRight(label, 22) << describeType(slot) << slot.description
|
||||
<< "\n";
|
||||
os << " " << padRight(label, 22) << describeType(slot) << slot.description << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
bool anyFlagOrOption =
|
||||
std::any_of(unstructured_.begin(), unstructured_.end(),
|
||||
std::any_of(m_unstructured.begin(), m_unstructured.end(),
|
||||
[](const Slot& s) { return s.kind != SlotKind::Parameter; });
|
||||
os << "\nOptions:\n";
|
||||
if (anyFlagOrOption)
|
||||
{
|
||||
for (const Slot& slot : unstructured_)
|
||||
for (const Slot& slot : m_unstructured)
|
||||
{
|
||||
if (slot.kind == SlotKind::Parameter)
|
||||
continue;
|
||||
std::string names =
|
||||
slot.shortName == '\0'
|
||||
? (" --" + slot.name)
|
||||
: (std::string("-") + slot.shortName + ", --" + slot.name);
|
||||
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 ? "...>" : ">");
|
||||
@@ -472,7 +466,7 @@ namespace cliforge
|
||||
std::vector<std::string> Command::flagOptionNames() const
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
for (const Slot& s : unstructured_)
|
||||
for (const Slot& s : m_unstructured)
|
||||
{
|
||||
if (s.kind == SlotKind::Parameter)
|
||||
continue;
|
||||
@@ -483,14 +477,15 @@ namespace cliforge
|
||||
return out;
|
||||
}
|
||||
|
||||
Command::FuzzyScore Command::fuzzyStructuralDistance(const std::vector<std::string>& tokens) const
|
||||
Command::FuzzyScore Command::fuzzyStructuralDistance(
|
||||
const std::vector<std::string>& tokens) const
|
||||
{
|
||||
std::size_t i = 0;
|
||||
FuzzyScore score;
|
||||
for (std::size_t s = 0; s < structured_.size(); ++s)
|
||||
for (std::size_t s = 0; s < m_structured.size(); ++s)
|
||||
{
|
||||
const Slot& slot = structured_[s];
|
||||
bool isLast = (s + 1 == structured_.size());
|
||||
const Slot& slot = m_structured[s];
|
||||
bool isLast = (s + 1 == m_structured.size());
|
||||
if (slot.kind == SlotKind::Keyword)
|
||||
{
|
||||
score.keywordChars += slot.name.size();
|
||||
@@ -555,7 +550,7 @@ namespace cliforge
|
||||
|
||||
void Command::checkNameFree(const std::string& longName, char shortName) const
|
||||
{
|
||||
for (const Slot& s : unstructured_)
|
||||
for (const Slot& s : m_unstructured)
|
||||
{
|
||||
if (s.kind == SlotKind::Parameter)
|
||||
continue; // different namespace (bare, not "--name")
|
||||
@@ -565,8 +560,7 @@ namespace cliforge
|
||||
}
|
||||
if (shortName != '\0' && s.shortName == shortName)
|
||||
{
|
||||
throw RegistrationError(std::string("duplicate short name '-") + shortName +
|
||||
"'");
|
||||
throw RegistrationError(std::string("duplicate short name '-") + shortName + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -584,21 +578,21 @@ namespace cliforge
|
||||
std::vector<Slot*> Command::orderedBindableSlots()
|
||||
{
|
||||
std::vector<Slot*> out;
|
||||
for (Slot& s : structured_)
|
||||
for (Slot& s : m_structured)
|
||||
{
|
||||
if (s.kind == SlotKind::Parameter)
|
||||
out.push_back(&s);
|
||||
}
|
||||
for (Slot& s : unstructured_)
|
||||
for (Slot& s : m_unstructured)
|
||||
out.push_back(&s);
|
||||
return out;
|
||||
}
|
||||
|
||||
int Command::findByLong(std::string_view name) const
|
||||
{
|
||||
for (std::size_t i = 0; i < unstructured_.size(); ++i)
|
||||
for (std::size_t i = 0; i < m_unstructured.size(); ++i)
|
||||
{
|
||||
if (unstructured_[i].kind != SlotKind::Parameter && unstructured_[i].name == name)
|
||||
if (m_unstructured[i].kind != SlotKind::Parameter && m_unstructured[i].name == name)
|
||||
{
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
@@ -610,9 +604,9 @@ namespace cliforge
|
||||
{
|
||||
if (c == '\0')
|
||||
return -1;
|
||||
for (std::size_t i = 0; i < unstructured_.size(); ++i)
|
||||
for (std::size_t i = 0; i < m_unstructured.size(); ++i)
|
||||
{
|
||||
if (unstructured_[i].kind != SlotKind::Parameter && unstructured_[i].shortName == c)
|
||||
if (m_unstructured[i].kind != SlotKind::Parameter && m_unstructured[i].shortName == c)
|
||||
{
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
@@ -634,8 +628,8 @@ namespace cliforge
|
||||
[[noreturn]] void Command::throwUnknown(std::string_view tok) const
|
||||
{
|
||||
throw CliError("unknown flag/option '" + std::string(tok) + "' for this command" +
|
||||
suggestFlagOption(std::string(tok)),
|
||||
ErrorKind::UnknownOption);
|
||||
suggestFlagOption(std::string(tok)),
|
||||
ErrorKind::UnknownOption);
|
||||
}
|
||||
|
||||
std::string Command::describeType(const Slot& slot)
|
||||
@@ -651,14 +645,13 @@ namespace cliforge
|
||||
}
|
||||
|
||||
void Command::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)
|
||||
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)
|
||||
{
|
||||
std::size_t idx = static_cast<std::size_t>(idxIn);
|
||||
const Slot& slot = self.unstructured_[idx];
|
||||
const Slot& slot = self.m_unstructured[idx];
|
||||
if (slot.kind == SlotKind::Flag)
|
||||
{
|
||||
if (hasInline)
|
||||
@@ -692,8 +685,8 @@ namespace cliforge
|
||||
if (i == before)
|
||||
{
|
||||
throw CliError("option '" + std::string(tokForError) +
|
||||
"' requires at least one value",
|
||||
ErrorKind::MissingValue);
|
||||
"' requires at least one value",
|
||||
ErrorKind::MissingValue);
|
||||
}
|
||||
provided[idx] = true;
|
||||
}
|
||||
@@ -702,7 +695,7 @@ namespace cliforge
|
||||
if (i >= tokens.size() || detail::looksLikeOptionStart(tokens[i]))
|
||||
{
|
||||
throw CliError("option '" + std::string(tokForError) + "' requires a value",
|
||||
ErrorKind::MissingValue);
|
||||
ErrorKind::MissingValue);
|
||||
}
|
||||
collected[idx] = {slot.type.parse(tokens[i], slot.name)};
|
||||
provided[idx] = true;
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
namespace cliforge
|
||||
{
|
||||
Engine::Engine(std::string programName) : programName_(std::move(programName)) {}
|
||||
Engine::Engine(std::string programName) : m_programName(std::move(programName)) {}
|
||||
|
||||
Engine& Engine::describe(std::string description)
|
||||
Engine& Engine::describe(const std::string& description)
|
||||
{
|
||||
description_ = std::move(description);
|
||||
m_description = std::move(description);
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -16,15 +16,15 @@ namespace cliforge
|
||||
// The Engine owns it for the rest of the program's lifetime.
|
||||
Command& Engine::command()
|
||||
{
|
||||
commands_.push_back(std::make_unique<Command>());
|
||||
return *commands_.back();
|
||||
m_commands.push_back(std::make_unique<Command>());
|
||||
return *m_commands.back();
|
||||
}
|
||||
|
||||
int Engine::run(int argc, char** argv)
|
||||
{
|
||||
if (programName_.empty() && argc > 0)
|
||||
programName_ = basename(argv[0]);
|
||||
for (auto& cmd : commands_)
|
||||
if (m_programName.empty() && argc > 0)
|
||||
m_programName = basename(argv[0]);
|
||||
for (auto& cmd : m_commands)
|
||||
{
|
||||
if (!cmd->sealed())
|
||||
{
|
||||
@@ -69,14 +69,14 @@ namespace cliforge
|
||||
|
||||
struct Candidate
|
||||
{
|
||||
Command* cmd;
|
||||
Command* cmd{};
|
||||
StructuredMatch match;
|
||||
};
|
||||
std::vector<Candidate> candidates;
|
||||
std::string bestPartialError;
|
||||
std::vector<Command*> bestPartialCmds; // every command tied for the best partial score
|
||||
int bestPartialScore = -1;
|
||||
for (auto& cmd : commands_)
|
||||
for (auto& cmd : m_commands)
|
||||
{
|
||||
StructuredMatch m = cmd->tryMatchStructured(tokens);
|
||||
if (m.success)
|
||||
@@ -115,7 +115,7 @@ namespace cliforge
|
||||
std::cerr << "Error: " << bestPartialError << "\n";
|
||||
if (bestPartialCmds.size() == 1)
|
||||
{
|
||||
std::cerr << "Usage: " << programName_ << " "
|
||||
std::cerr << "Usage: " << m_programName << " "
|
||||
<< bestPartialCmds.front()->usageLine() << "\n";
|
||||
}
|
||||
else
|
||||
@@ -124,7 +124,7 @@ namespace cliforge
|
||||
<< " commands:\n";
|
||||
for (auto* cmd : bestPartialCmds)
|
||||
{
|
||||
std::string line = programName_ + " " + cmd->usageLine();
|
||||
std::string line = m_programName + " " + cmd->usageLine();
|
||||
std::cerr << " " << padRight(line, 46) << firstLine(cmd->description())
|
||||
<< "\n";
|
||||
}
|
||||
@@ -177,7 +177,7 @@ namespace cliforge
|
||||
|
||||
std::cerr << "Error: " << firstError << "\n";
|
||||
if (firstErrorCmd)
|
||||
std::cerr << "Usage: " << programName_ << " " << firstErrorCmd->usageLine() << "\n";
|
||||
std::cerr << "Usage: " << m_programName << " " << firstErrorCmd->usageLine() << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ namespace cliforge
|
||||
return 0;
|
||||
}
|
||||
std::vector<Command*> matches;
|
||||
for (auto& cmd : commands_)
|
||||
for (auto& cmd : m_commands)
|
||||
{
|
||||
if (cmd->couldMatchPrefix(queryTokens))
|
||||
matches.push_back(cmd.get());
|
||||
@@ -224,7 +224,7 @@ namespace cliforge
|
||||
}
|
||||
if (matches.size() == 1)
|
||||
{
|
||||
std::cout << matches.front()->helpText(programName_);
|
||||
std::cout << matches.front()->helpText(m_programName);
|
||||
return 0;
|
||||
}
|
||||
std::cout << "'" << join(queryTokens) << "' matches " << matches.size() << " commands:\n\n";
|
||||
@@ -232,30 +232,30 @@ namespace cliforge
|
||||
{
|
||||
if (i)
|
||||
std::cout << "\n";
|
||||
std::cout << matches[i]->helpText(programName_);
|
||||
std::cout << matches[i]->helpText(m_programName);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Engine::printGlobalHelp(std::ostream& os) const
|
||||
{
|
||||
os << "Usage: " << programName_ << " <command> [ARGUMENTS] [OPTIONS]\n";
|
||||
if (!description_.empty())
|
||||
os << "\n" << description_ << "\n";
|
||||
os << "Usage: " << m_programName << " <command> [ARGUMENTS] [OPTIONS]\n";
|
||||
if (!m_description.empty())
|
||||
os << "\n" << m_description << "\n";
|
||||
os << "\nCommands:\n";
|
||||
for (auto& cmd : commands_)
|
||||
for (auto& cmd : m_commands)
|
||||
{
|
||||
std::string line = programName_ + " " + cmd->usageLine();
|
||||
std::string line = m_programName + " " + cmd->usageLine();
|
||||
os << " " << padRight(line, 46) << firstLine(cmd->description()) << "\n";
|
||||
}
|
||||
os << "\nRun '" << programName_ << " help <command>' or add --help/-h to any command "
|
||||
os << "\nRun '" << m_programName << " help <command>' or add --help/-h to any command "
|
||||
<< "for details.\n";
|
||||
}
|
||||
|
||||
void Engine::suggestCommands(const std::vector<std::string>& tokens, std::ostream& os) const
|
||||
{
|
||||
std::vector<std::pair<double, Command*>> scored;
|
||||
for (auto& cmd : commands_)
|
||||
for (auto& cmd : m_commands)
|
||||
{
|
||||
Command::FuzzyScore s = cmd->fuzzyStructuralDistance(tokens);
|
||||
double ratio = s.keywordChars ? static_cast<double>(s.distance) /
|
||||
@@ -277,7 +277,7 @@ namespace cliforge
|
||||
return;
|
||||
os << "\nDid you mean:\n";
|
||||
for (auto* cmd : close)
|
||||
os << " " << programName_ << " " << cmd->usageLine() << "\n";
|
||||
os << " " << m_programName << " " << cmd->usageLine() << "\n";
|
||||
}
|
||||
|
||||
std::string Engine::firstLine(const std::string& s)
|
||||
|
||||
@@ -26,8 +26,7 @@ namespace cliforge::detail
|
||||
// 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)
|
||||
const std::vector<std::string>& pool, std::size_t limit)
|
||||
{
|
||||
std::vector<std::pair<std::size_t, std::string>> scored;
|
||||
scored.reserve(pool.size());
|
||||
|
||||
@@ -4,16 +4,20 @@
|
||||
namespace cliforge
|
||||
{
|
||||
CliError::CliError(const std::string& msg, ErrorKind kind)
|
||||
: std::runtime_error(msg), kind_(kind) {}
|
||||
: std::runtime_error(msg), m_kind(kind)
|
||||
{
|
||||
}
|
||||
|
||||
ErrorKind CliError::kind() const noexcept
|
||||
{
|
||||
return kind_;
|
||||
return kind();
|
||||
}
|
||||
|
||||
ParseError::ParseError(const std::string& msg) : CliError(msg, ErrorKind::TypeMismatch) {}
|
||||
|
||||
RegistrationError::RegistrationError(const std::string& msg) : CliError(msg, ErrorKind::Generic) {}
|
||||
RegistrationError::RegistrationError(const std::string& msg) : CliError(msg, ErrorKind::Generic)
|
||||
{
|
||||
}
|
||||
|
||||
Scalar ChoiceTable::parse(std::string_view token, std::string_view label) const
|
||||
{
|
||||
@@ -59,22 +63,22 @@ namespace cliforge
|
||||
Value Value::ofScalar(Scalar s)
|
||||
{
|
||||
Value v;
|
||||
v.data_ = std::move(s);
|
||||
v.m_data = std::move(s);
|
||||
return v;
|
||||
}
|
||||
Value Value::ofVector(std::vector<Scalar> v)
|
||||
{
|
||||
Value out;
|
||||
out.data_ = std::move(v);
|
||||
out.m_data = std::move(v);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool Value::hasValue() const
|
||||
{
|
||||
return !std::holds_alternative<std::monostate>(data_);
|
||||
return !std::holds_alternative<std::monostate>(m_data);
|
||||
}
|
||||
bool Value::isVector() const
|
||||
{
|
||||
return std::holds_alternative<std::vector<Scalar>>(data_);
|
||||
return std::holds_alternative<std::vector<Scalar>>(m_data);
|
||||
}
|
||||
}
|
||||
+160
-121
@@ -8,6 +8,7 @@
|
||||
// take their arguments by value, by reference, or by const reference,
|
||||
// whichever fits.
|
||||
#include <cstdio>
|
||||
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -20,10 +21,30 @@ using cliforge::Engine;
|
||||
// ---------------------------------------------------------------------
|
||||
// Custom enums -- plain C++ enum class, nothing cliforge-specific here.
|
||||
// ---------------------------------------------------------------------
|
||||
enum class ProjectTemplate { Cpp, Python, Node };
|
||||
enum class Editor { Vim, Emacs, Nano };
|
||||
enum class TwoFactorMethod { App, Sms, Email };
|
||||
enum class UpdateChannel { Stable, Beta, Nightly };
|
||||
enum class ProjectTemplate
|
||||
{
|
||||
Cpp,
|
||||
Python,
|
||||
Node
|
||||
};
|
||||
enum class Editor
|
||||
{
|
||||
Vim,
|
||||
Emacs,
|
||||
Nano
|
||||
};
|
||||
enum class TwoFactorMethod
|
||||
{
|
||||
App,
|
||||
Sms,
|
||||
Email
|
||||
};
|
||||
enum class UpdateChannel
|
||||
{
|
||||
Stable,
|
||||
Beta,
|
||||
Nightly
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Every command handler is a completely ordinary free function. There is
|
||||
@@ -33,56 +54,70 @@ enum class UpdateChannel { Stable, Beta, Nightly };
|
||||
// const reference, or reference, whichever avoids unnecessary copies.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
void projectCreate(const std::string& name, std::optional<ProjectTemplate> tmpl, bool git) {
|
||||
static const char* names[] = {"cpp", "python", "node"};
|
||||
ProjectTemplate t = tmpl.value_or(ProjectTemplate::Cpp);
|
||||
std::printf("[create] project '%s' from template '%s' (git init: %s)\n", name.c_str(),
|
||||
names[static_cast<int>(t)], git ? "yes" : "no");
|
||||
void projectCreate(const std::string& name, std::optional<ProjectTemplate> tmpl, bool git)
|
||||
{
|
||||
static const char* s_names[] = {"cpp", "python", "node"};
|
||||
ProjectTemplate t = tmpl.value_or(ProjectTemplate::Cpp);
|
||||
std::printf("[create] project '%s' from template '%s' (git init: %s)\n", name.c_str(),
|
||||
s_names[static_cast<int>(t)], git ? "yes" : "no");
|
||||
}
|
||||
|
||||
void projectDelete(const std::string& name, bool force, const std::optional<std::string>& reason) {
|
||||
std::printf("[delete] project '%s' (force=%s, reason=%s)\n", name.c_str(),
|
||||
force ? "yes" : "no", reason ? reason->c_str() : "<none given>");
|
||||
void projectDelete(const std::string& name, bool force, const std::optional<std::string>& reason)
|
||||
{
|
||||
std::printf("[delete] project '%s' (force=%s, reason=%s)\n", name.c_str(), force ? "yes" : "no",
|
||||
reason ? reason->c_str() : "<none given>");
|
||||
}
|
||||
|
||||
void projectInfo(const std::string& name, bool json) {
|
||||
if (json) {
|
||||
std::printf("{\"project\": \"%s\", \"status\": \"ok\"}\n", name.c_str());
|
||||
} else {
|
||||
std::printf("[info] project '%s': status ok\n", name.c_str());
|
||||
}
|
||||
void projectInfo(const std::string& name, bool json)
|
||||
{
|
||||
if (json)
|
||||
{
|
||||
std::printf("{\"project\": \"%s\", \"status\": \"ok\"}\n", name.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
std::printf("[info] project '%s': status ok\n", name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void build(const std::vector<int32_t>& targets, bool noAssets, bool debug, bool verbose,
|
||||
const std::vector<std::string>& architectures, std::optional<Editor> editor,
|
||||
std::optional<int32_t> threshold) {
|
||||
static const char* editorNames[] = {"vim", "emacs", "nano"};
|
||||
std::printf("[build] targets=[");
|
||||
for (size_t i = 0; i < targets.size(); ++i) std::printf("%s%d", i ? "," : "", targets[i]);
|
||||
std::printf("] noAssets=%d debug=%d verbose=%d architectures=[", noAssets, debug, verbose);
|
||||
for (size_t i = 0; i < architectures.size(); ++i)
|
||||
std::printf("%s%s", i ? "," : "", architectures[i].c_str());
|
||||
std::printf("] editor=%s threshold=%s\n",
|
||||
editor ? editorNames[static_cast<int>(*editor)] : "<none>",
|
||||
threshold ? std::to_string(*threshold).c_str() : "<default>");
|
||||
const std::vector<std::string>& architectures, std::optional<Editor> editor,
|
||||
std::optional<int32_t> threshold)
|
||||
{
|
||||
static const char* s_editorNames[] = {"vim", "emacs", "nano"};
|
||||
std::printf("[build] targets=[");
|
||||
for (size_t i = 0; i < targets.size(); ++i)
|
||||
std::printf("%s%d", i ? "," : "", targets[i]);
|
||||
std::printf("] noAssets=%d debug=%d verbose=%d architectures=[", noAssets, debug, verbose);
|
||||
for (size_t i = 0; i < architectures.size(); ++i)
|
||||
std::printf("%s%s", i ? "," : "", architectures[i].c_str());
|
||||
std::printf("] editor=%s threshold=%s\n",
|
||||
editor ? s_editorNames[static_cast<int>(*editor)] : "<none>",
|
||||
threshold ? std::to_string(*threshold).c_str() : "<default>");
|
||||
}
|
||||
|
||||
void enable2fa(std::optional<TwoFactorMethod> method) {
|
||||
static const char* names[] = {"app", "sms", "email"};
|
||||
TwoFactorMethod m = method.value_or(TwoFactorMethod::App);
|
||||
std::printf("[2fa] enabled via %s\n", names[static_cast<int>(m)]);
|
||||
void enable2fa(std::optional<TwoFactorMethod> method)
|
||||
{
|
||||
static const char* s_names[] = {"app", "sms", "email"};
|
||||
TwoFactorMethod m = method.value_or(TwoFactorMethod::App);
|
||||
std::printf("[2fa] enabled via %s\n", s_names[static_cast<int>(m)]);
|
||||
}
|
||||
|
||||
void disable2fa() { std::printf("[2fa] disabled\n"); }
|
||||
|
||||
void selfUpdate(std::optional<UpdateChannel> channel, bool checkOnly) {
|
||||
static const char* names[] = {"stable", "beta", "nightly"};
|
||||
std::printf("[self-update] channel=%s checkOnly=%d\n",
|
||||
channel ? names[static_cast<int>(*channel)] : "stable(default)", checkOnly);
|
||||
void disable2fa()
|
||||
{
|
||||
std::printf("[2fa] disabled\n");
|
||||
}
|
||||
|
||||
void status(bool json) {
|
||||
std::printf(json ? "{\"status\":\"ok\"}\n" : "All systems operational.\n");
|
||||
void selfUpdate(std::optional<UpdateChannel> channel, bool checkOnly)
|
||||
{
|
||||
static const char* s_names[] = {"stable", "beta", "nightly"};
|
||||
std::printf("[self-update] channel=%s checkOnly=%d\n",
|
||||
channel ? s_names[static_cast<int>(*channel)] : "stable(default)", checkOnly);
|
||||
}
|
||||
|
||||
void status(bool json)
|
||||
{
|
||||
std::printf(json ? "{\"status\":\"ok\"}\n" : "All systems operational.\n");
|
||||
}
|
||||
|
||||
// `deploy` showcases a *loose* parameter: `hosts` is declared after
|
||||
@@ -91,10 +126,13 @@ void status(bool json) {
|
||||
// packctl deploy web1 --env prod web2 -n web3
|
||||
// still collects hosts=[web1,web2,web3], matched by position among
|
||||
// themselves, not by where they fall relative to the option/flag.
|
||||
void deploy(bool dryRun, const std::optional<std::string>& env, const std::vector<std::string>& hosts) {
|
||||
std::printf("[deploy] dryRun=%d env=%s hosts=[", dryRun, env ? env->c_str() : "<none>");
|
||||
for (size_t i = 0; i < hosts.size(); ++i) std::printf("%s%s", i ? "," : "", hosts[i].c_str());
|
||||
std::printf("]\n");
|
||||
void deploy(bool dryRun, const std::optional<std::string>& env,
|
||||
const std::vector<std::string>& hosts)
|
||||
{
|
||||
std::printf("[deploy] dryRun=%d env=%s hosts=[", dryRun, env ? env->c_str() : "<none>");
|
||||
for (size_t i = 0; i < hosts.size(); ++i)
|
||||
std::printf("%s%s", i ? "," : "", hosts[i].c_str());
|
||||
std::printf("]\n");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -103,90 +141,91 @@ void deploy(bool dryRun, const std::optional<std::string>& env, const std::vecto
|
||||
// the bound function's arguments are consistent with what was declared.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
Engine cli("packctl");
|
||||
cli.describe("A demo package/project CLI built with cliforge.");
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
Engine cli("packctl");
|
||||
cli.describe("A demo package/project CLI built with cliforge.");
|
||||
|
||||
cli.command()
|
||||
.keyword("project")
|
||||
.parameter<std::string>("name", "Project name")
|
||||
.keyword("create")
|
||||
.option<ProjectTemplate>("template", 't', "Project template to scaffold (default: cpp)")
|
||||
.choices<ProjectTemplate>({{"cpp", ProjectTemplate::Cpp},
|
||||
{"python", ProjectTemplate::Python},
|
||||
{"node", ProjectTemplate::Node}})
|
||||
.flag("git", 'g', "Initialize a git repository")
|
||||
.describe("Scaffold a new project from a template")
|
||||
.action(&projectCreate);
|
||||
cli.command()
|
||||
.keyword("project")
|
||||
.parameter<std::string>("name", "Project name")
|
||||
.keyword("create")
|
||||
.option<ProjectTemplate>("template", 't', "Project template to scaffold (default: cpp)")
|
||||
.choices<ProjectTemplate>({{"cpp", ProjectTemplate::Cpp},
|
||||
{"python", ProjectTemplate::Python},
|
||||
{"node", ProjectTemplate::Node}})
|
||||
.flag("git", 'g', "Initialize a git repository")
|
||||
.describe("Scaffold a new project from a template")
|
||||
.action(&projectCreate);
|
||||
|
||||
cli.command()
|
||||
.keyword("project")
|
||||
.parameter<std::string>("name", "Project name")
|
||||
.keyword("delete")
|
||||
.flag("force", 'f', "Skip the confirmation prompt")
|
||||
.option<std::string>("reason", 'r', "Why the project is being deleted")
|
||||
.describe("Permanently delete a project by name")
|
||||
.action(&projectDelete);
|
||||
cli.command()
|
||||
.keyword("project")
|
||||
.parameter<std::string>("name", "Project name")
|
||||
.keyword("delete")
|
||||
.flag("force", 'f', "Skip the confirmation prompt")
|
||||
.option<std::string>("reason", 'r', "Why the project is being deleted")
|
||||
.describe("Permanently delete a project by name")
|
||||
.action(&projectDelete);
|
||||
|
||||
cli.command()
|
||||
.keyword("project")
|
||||
.parameter<std::string>("name", "Project name")
|
||||
.flag("json", 'j', "Print machine-readable JSON")
|
||||
.describe("Show information about a project")
|
||||
.action(&projectInfo);
|
||||
cli.command()
|
||||
.keyword("project")
|
||||
.parameter<std::string>("name", "Project name")
|
||||
.flag("json", 'j', "Print machine-readable JSON")
|
||||
.describe("Show information about a project")
|
||||
.action(&projectInfo);
|
||||
|
||||
cli.command()
|
||||
.keyword("build")
|
||||
.parameter<int32_t>("targets", "Target ids to build", /*variadic=*/true)
|
||||
.flag("no-assets", 'n', "Skip packaging assets")
|
||||
.flag("debug", 'd', "Build with debug symbols")
|
||||
.flag("verbose", 'i', "Verbose build output")
|
||||
.option<std::string>("architectures", 'a', "Target architectures", /*variadic=*/true)
|
||||
.option<Editor>("editor", 'e', "Editor to open on completion")
|
||||
.choices<Editor>({{"vim", Editor::Vim}, {"emacs", Editor::Emacs}, {"nano", Editor::Nano}})
|
||||
.option<int32_t>("threshold", 't', "Warning threshold (0 = strict)")
|
||||
.describe("Build the given target ids")
|
||||
.action(&build);
|
||||
cli.command()
|
||||
.keyword("build")
|
||||
.parameter<int32_t>("targets", "Target ids to build", /*variadic=*/true)
|
||||
.flag("no-assets", 'n', "Skip packaging assets")
|
||||
.flag("debug", 'd', "Build with debug symbols")
|
||||
.flag("verbose", 'i', "Verbose build output")
|
||||
.option<std::string>("architectures", 'a', "Target architectures", /*variadic=*/true)
|
||||
.option<Editor>("editor", 'e', "Editor to open on completion")
|
||||
.choices<Editor>({{"vim", Editor::Vim}, {"emacs", Editor::Emacs}, {"nano", Editor::Nano}})
|
||||
.option<int32_t>("threshold", 't', "Warning threshold (0 = strict)")
|
||||
.describe("Build the given target ids")
|
||||
.action(&build);
|
||||
|
||||
cli.command()
|
||||
.keyword("2fa")
|
||||
.keyword("enable")
|
||||
.option<TwoFactorMethod>("method", 'm', "Verification method (default: app)")
|
||||
.choices<TwoFactorMethod>({{"app", TwoFactorMethod::App},
|
||||
{"sms", TwoFactorMethod::Sms},
|
||||
{"email", TwoFactorMethod::Email}})
|
||||
.describe("Turn on two-factor authentication")
|
||||
.action(&enable2fa);
|
||||
cli.command()
|
||||
.keyword("2fa")
|
||||
.keyword("enable")
|
||||
.option<TwoFactorMethod>("method", 'm', "Verification method (default: app)")
|
||||
.choices<TwoFactorMethod>({{"app", TwoFactorMethod::App},
|
||||
{"sms", TwoFactorMethod::Sms},
|
||||
{"email", TwoFactorMethod::Email}})
|
||||
.describe("Turn on two-factor authentication")
|
||||
.action(&enable2fa);
|
||||
|
||||
cli.command()
|
||||
.keyword("2fa")
|
||||
.keyword("disable")
|
||||
.describe("Turn off two-factor authentication")
|
||||
.action(&disable2fa);
|
||||
cli.command()
|
||||
.keyword("2fa")
|
||||
.keyword("disable")
|
||||
.describe("Turn off two-factor authentication")
|
||||
.action(&disable2fa);
|
||||
|
||||
cli.command()
|
||||
.keyword("self-update")
|
||||
.option<UpdateChannel>("channel", 'c', "Release channel to update from")
|
||||
.choices<UpdateChannel>({{"stable", UpdateChannel::Stable},
|
||||
{"beta", UpdateChannel::Beta},
|
||||
{"nightly", UpdateChannel::Nightly}})
|
||||
.flag("check-only", 'k', "Check for updates without installing")
|
||||
.describe("Update packctl to the latest version")
|
||||
.action(&selfUpdate);
|
||||
cli.command()
|
||||
.keyword("self-update")
|
||||
.option<UpdateChannel>("channel", 'c', "Release channel to update from")
|
||||
.choices<UpdateChannel>({{"stable", UpdateChannel::Stable},
|
||||
{"beta", UpdateChannel::Beta},
|
||||
{"nightly", UpdateChannel::Nightly}})
|
||||
.flag("check-only", 'k', "Check for updates without installing")
|
||||
.describe("Update packctl to the latest version")
|
||||
.action(&selfUpdate);
|
||||
|
||||
cli.command()
|
||||
.keyword("status")
|
||||
.flag("json", 'j', "Print machine-readable JSON")
|
||||
.describe("Show whether everything is operational")
|
||||
.action(&status);
|
||||
cli.command()
|
||||
.keyword("status")
|
||||
.flag("json", 'j', "Print machine-readable JSON")
|
||||
.describe("Show whether everything is operational")
|
||||
.action(&status);
|
||||
|
||||
cli.command()
|
||||
.keyword("deploy")
|
||||
.flag("dry-run", 'n', "Show what would happen without doing it")
|
||||
.option<std::string>("env", 'e', "Environment name")
|
||||
.parameter<std::string>("hosts", "Target hostnames", /*variadic=*/true) // loose!
|
||||
.describe("Deploy to the given hosts, in any order relative to --env/-n")
|
||||
.action(&deploy);
|
||||
cli.command()
|
||||
.keyword("deploy")
|
||||
.flag("dry-run", 'n', "Show what would happen without doing it")
|
||||
.option<std::string>("env", 'e', "Environment name")
|
||||
.parameter<std::string>("hosts", "Target hostnames", /*variadic=*/true) // loose!
|
||||
.describe("Deploy to the given hosts, in any order relative to --env/-n")
|
||||
.action(&deploy);
|
||||
|
||||
return cli.run(argc, argv);
|
||||
return cli.run(argc, argv);
|
||||
}
|
||||
Reference in new issue
Block a user