2026-10-07 13:16:52 +00:00
|
|
|
package commandkit
|
|
|
|
|
|
|
|
|
|
import (
|
2026-10-11 01:15:13 +00:00
|
|
|
"errors"
|
2026-10-07 13:16:52 +00:00
|
|
|
"fmt"
|
2026-10-19 17:23:32 +00:00
|
|
|
"strings"
|
2026-10-07 13:22:57 +00:00
|
|
|
"unicode"
|
2026-10-07 13:27:00 +00:00
|
|
|
|
2026-10-11 01:25:45 +00:00
|
|
|
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
2026-10-19 15:08:00 +00:00
|
|
|
"gitea.maximumdirect.net/eric/CommandKit/internal/commandpath"
|
2026-10-07 13:16:52 +00:00
|
|
|
)
|
|
|
|
|
|
2026-10-19 19:59:41 +00:00
|
|
|
// Command is an opaque, reusable declaration of canonical commands and groups.
|
|
|
|
|
// NewCommand, Group, RootGroup, and integration command factories return
|
|
|
|
|
// Command values. Capture copies declaration slices but retains callback and
|
|
|
|
|
// caller-owned object references. Installation validates the declaration
|
|
|
|
|
// before changing an App registry.
|
2026-10-11 01:15:13 +00:00
|
|
|
type Command struct{ record *commandRecord }
|
|
|
|
|
|
2026-10-19 19:59:41 +00:00
|
|
|
// Declaration is a captured canonical command or application routing rule
|
|
|
|
|
// accepted by New and App.Register. Use []Declaration when collecting both
|
|
|
|
|
// kinds. Group and RootGroup children remain Commands; install routing rules
|
|
|
|
|
// alongside the completed command tree through New or Register. Only
|
|
|
|
|
// CommandKit declaration types implement Declaration.
|
|
|
|
|
//
|
|
|
|
|
// A zero Command, zero RoutingRule, nil declaration, or typed nil declaration
|
|
|
|
|
// pointer is invalid. Use NoCommand to represent an intentionally absent
|
|
|
|
|
// optional command.
|
2026-10-19 17:23:32 +00:00
|
|
|
type Declaration interface {
|
|
|
|
|
commandkitDeclaration()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (Command) commandkitDeclaration() {}
|
|
|
|
|
|
2026-10-19 19:59:41 +00:00
|
|
|
// RoutingRule is an opaque, reusable command or flag alias declaration. Alias,
|
|
|
|
|
// FlagAlias, and FlagAliasFor capture routing rules eagerly and retain any
|
|
|
|
|
// construction issues for New or App.Register to report. Identical duplicate
|
|
|
|
|
// mappings are accepted; competing targets for the same source are errors.
|
2026-10-19 17:23:32 +00:00
|
|
|
type RoutingRule struct{ record *routingRuleRecord }
|
|
|
|
|
|
|
|
|
|
func (RoutingRule) commandkitDeclaration() {}
|
|
|
|
|
|
|
|
|
|
type routingRuleKind uint8
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
commandAliasRule routingRuleKind = iota + 1
|
|
|
|
|
globalFlagAliasRule
|
|
|
|
|
scopedFlagAliasRule
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type capturedFlagSpelling struct {
|
|
|
|
|
name string
|
|
|
|
|
short bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type routingRuleRecord struct {
|
|
|
|
|
kind routingRuleKind
|
|
|
|
|
sourcePath commandpath.Path
|
|
|
|
|
targetPath commandpath.Path
|
|
|
|
|
scopePath commandpath.Path
|
|
|
|
|
sourceFlag capturedFlagSpelling
|
|
|
|
|
targetFlag capturedFlagSpelling
|
|
|
|
|
sourceText string
|
|
|
|
|
targetText string
|
|
|
|
|
scopeText string
|
|
|
|
|
issues []error
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 01:15:13 +00:00
|
|
|
type commandRecord struct {
|
2026-10-07 13:16:52 +00:00
|
|
|
name string
|
|
|
|
|
summary string
|
|
|
|
|
description string
|
|
|
|
|
children []Command
|
|
|
|
|
payload commandPayload
|
2026-10-11 01:15:13 +00:00
|
|
|
issues []error
|
|
|
|
|
rootOnly bool
|
|
|
|
|
absent bool
|
|
|
|
|
invalid bool
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-11 02:06:06 +00:00
|
|
|
// Supplied carries a value and whether it was explicitly provided. Defaults can
|
|
|
|
|
// have Set=false; an explicitly supplied empty, false, or zero value has Set=true.
|
2026-10-07 13:16:52 +00:00
|
|
|
type Supplied[T any] struct {
|
|
|
|
|
// Value is the associated value.
|
|
|
|
|
Value T
|
|
|
|
|
// Set reports whether a caller explicitly supplied the value.
|
|
|
|
|
Set bool
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 02:06:06 +00:00
|
|
|
// FlagMeta describes a command flag. Required applies when execution prepares
|
|
|
|
|
// the selected command; help reports the metadata without requiring the value.
|
2026-10-07 13:16:52 +00:00
|
|
|
type FlagMeta struct {
|
|
|
|
|
// Name is the flag name without leading dashes.
|
|
|
|
|
Name string
|
|
|
|
|
// Short is an optional one-byte spelling.
|
|
|
|
|
Short string
|
|
|
|
|
// Usage briefly describes the flag.
|
|
|
|
|
Usage string
|
|
|
|
|
// Required reports whether execution requires the flag to be supplied.
|
|
|
|
|
Required bool
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 02:06:06 +00:00
|
|
|
// ArgumentMeta describes a positional command argument. Its name and usage are
|
|
|
|
|
// used in generated help.
|
2026-10-07 13:16:52 +00:00
|
|
|
type ArgumentMeta struct {
|
|
|
|
|
// Name is the argument name shown in command help.
|
|
|
|
|
Name string
|
|
|
|
|
// Usage briefly describes the argument.
|
|
|
|
|
Usage string
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 02:06:06 +00:00
|
|
|
// Flag represents a typed binding for a command flag. Copies retain binding
|
|
|
|
|
// identity; separately constructed flags are distinct even when their metadata
|
|
|
|
|
// and callbacks are equivalent.
|
2026-10-07 13:16:52 +00:00
|
|
|
type Flag[I any] struct {
|
|
|
|
|
binding flagBinding[I]
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 01:18:07 +00:00
|
|
|
type bindingIdentity struct{ marker byte }
|
|
|
|
|
|
|
|
|
|
type preparedValue[I any] struct {
|
|
|
|
|
identity *bindingIdentity
|
|
|
|
|
supplied bool
|
|
|
|
|
apply func(*I)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 13:16:52 +00:00
|
|
|
type flagBinding[I any] struct {
|
2026-10-11 01:39:23 +00:00
|
|
|
identity *bindingIdentity
|
|
|
|
|
meta FlagMeta
|
|
|
|
|
deferredRequired bool
|
|
|
|
|
defaultText string
|
|
|
|
|
repeated bool
|
|
|
|
|
boolean bool
|
|
|
|
|
prepare func([]string) (preparedValue[I], error)
|
|
|
|
|
valid bool
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Argument represents a typed binding for a positional command argument.
|
|
|
|
|
type Argument[I any] struct {
|
|
|
|
|
binding argumentBinding[I]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type argumentBinding[I any] struct {
|
2026-10-11 01:18:07 +00:00
|
|
|
identity *bindingIdentity
|
|
|
|
|
meta ArgumentMeta
|
|
|
|
|
min int
|
|
|
|
|
max int
|
|
|
|
|
prepare func([]string) (preparedValue[I], error)
|
|
|
|
|
valid bool
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CommandSpec declares one command using typed input and handler callbacks.
|
2026-10-11 02:06:06 +00:00
|
|
|
// Capture copies its Flags, Arguments, and Children slices. NewInput runs only
|
|
|
|
|
// when execution reaches input construction; a nil factory yields a fresh zero
|
|
|
|
|
// value. CheckInput runs after supplied values are converted and applied, before
|
|
|
|
|
// Run. Run may be nil for a parent that only groups child commands.
|
2026-10-07 13:16:52 +00:00
|
|
|
type CommandSpec[I any] struct {
|
|
|
|
|
// Name is one command path component.
|
|
|
|
|
Name string
|
|
|
|
|
// Summary is a short description of the command.
|
|
|
|
|
Summary string
|
|
|
|
|
// Description provides additional command detail.
|
|
|
|
|
Description string
|
2026-10-11 01:15:13 +00:00
|
|
|
// Children are relative child declarations captured with this command.
|
|
|
|
|
Children []Command
|
2026-10-07 13:16:52 +00:00
|
|
|
// NewInput creates an input value for execution. A nil factory uses a fresh
|
|
|
|
|
// zero value.
|
|
|
|
|
NewInput func() I
|
|
|
|
|
// Flags declares the command's local flags.
|
|
|
|
|
Flags []Flag[I]
|
|
|
|
|
// Arguments declares the command's positional inputs.
|
|
|
|
|
Arguments []Argument[I]
|
|
|
|
|
// CheckInput validates declared combinations of parsed inputs.
|
|
|
|
|
CheckInput func(I) error
|
|
|
|
|
// Run handles an invocation. It may be nil for a command used as a group.
|
|
|
|
|
Run func(Invocation, I) Outcome
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type commandPayload interface {
|
|
|
|
|
validate() error
|
|
|
|
|
hasRun() bool
|
2026-10-11 01:25:45 +00:00
|
|
|
bridgeFlags() []commandmeta.Flag
|
|
|
|
|
bridgeArguments() []commandmeta.Argument
|
2026-10-19 19:46:22 +00:00
|
|
|
prepare([][]string, [][]string, []string, bool) (func(*execution) Outcome, error)
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type typedCommand[I any] struct {
|
2026-10-11 01:21:52 +00:00
|
|
|
spec CommandSpec[I]
|
|
|
|
|
privateRun func(commandExecution, I) Outcome
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c typedCommand[I]) validate() error {
|
|
|
|
|
return validateBindings(c.spec.Flags, c.spec.Arguments)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c typedCommand[I]) hasRun() bool {
|
2026-10-11 01:21:52 +00:00
|
|
|
return c.spec.Run != nil || c.privateRun != nil
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-11 01:25:45 +00:00
|
|
|
func (c typedCommand[I]) bridgeFlags() []commandmeta.Flag {
|
|
|
|
|
flags := make([]commandmeta.Flag, len(c.spec.Flags))
|
2026-10-07 13:27:00 +00:00
|
|
|
for index, flag := range c.spec.Flags {
|
|
|
|
|
binding := flag.binding
|
2026-10-11 01:25:45 +00:00
|
|
|
flags[index] = commandmeta.Flag{
|
2026-10-07 13:27:00 +00:00
|
|
|
Name: binding.meta.Name,
|
|
|
|
|
Short: binding.meta.Short,
|
|
|
|
|
Usage: binding.meta.Usage,
|
|
|
|
|
Default: binding.defaultText,
|
|
|
|
|
Boolean: binding.boolean,
|
|
|
|
|
Repeated: binding.repeated,
|
|
|
|
|
Required: binding.meta.Required,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return flags
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 01:25:45 +00:00
|
|
|
func (c typedCommand[I]) bridgeArguments() []commandmeta.Argument {
|
|
|
|
|
arguments := make([]commandmeta.Argument, len(c.spec.Arguments))
|
2026-10-07 13:37:57 +00:00
|
|
|
for index, argument := range c.spec.Arguments {
|
|
|
|
|
binding := argument.binding
|
2026-10-11 01:25:45 +00:00
|
|
|
arguments[index] = commandmeta.Argument{
|
2026-10-07 13:37:57 +00:00
|
|
|
Name: binding.meta.Name,
|
|
|
|
|
Usage: binding.meta.Usage,
|
|
|
|
|
Min: binding.min,
|
|
|
|
|
Max: binding.max,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return arguments
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-19 19:46:22 +00:00
|
|
|
func (c typedCommand[I]) prepare(flagValues, flagSpellings [][]string, operands []string, enforceRequired bool) (func(*execution) Outcome, error) {
|
|
|
|
|
prepared, err := prepareInputWithSpellings(c.spec, flagValues, flagSpellings, operands, enforceRequired)
|
2026-10-07 13:22:57 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
2026-10-11 01:21:52 +00:00
|
|
|
return func(current *execution) Outcome {
|
2026-10-07 13:22:57 +00:00
|
|
|
input := prepared.build()
|
2026-10-11 01:21:52 +00:00
|
|
|
if c.spec.CheckInput != nil {
|
|
|
|
|
if err := c.spec.CheckInput(input); err != nil {
|
|
|
|
|
return classifyInputError(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if c.privateRun != nil {
|
|
|
|
|
return c.privateRun(current.commandContext(), input)
|
|
|
|
|
}
|
2026-10-19 19:33:49 +00:00
|
|
|
return c.spec.Run(current.invocation(), input)
|
2026-10-07 13:22:57 +00:00
|
|
|
}, nil
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-19 19:59:41 +00:00
|
|
|
// NewCommand captures a typed canonical command declaration without
|
|
|
|
|
// constructing its input or running callbacks. It copies the spec's flags,
|
|
|
|
|
// arguments, and children slices. Construction issues are retained and
|
|
|
|
|
// reported when the declaration is installed; NewCommand itself does not
|
|
|
|
|
// return an error. The returned declaration can be reused across Apps.
|
2026-10-11 01:15:13 +00:00
|
|
|
func NewCommand[I any](spec CommandSpec[I]) Command {
|
2026-10-11 01:21:52 +00:00
|
|
|
return captureCommand(spec, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func captureCommand[I any](spec CommandSpec[I], privateRun func(commandExecution, I) Outcome) Command {
|
2026-10-07 13:16:52 +00:00
|
|
|
copySpec := spec
|
|
|
|
|
copySpec.Flags = append([]Flag[I](nil), spec.Flags...)
|
|
|
|
|
copySpec.Arguments = append([]Argument[I](nil), spec.Arguments...)
|
2026-10-11 01:15:13 +00:00
|
|
|
copySpec.Children = append([]Command(nil), spec.Children...)
|
2026-10-11 01:21:52 +00:00
|
|
|
payload := typedCommand[I]{spec: copySpec, privateRun: privateRun}
|
2026-10-11 01:15:13 +00:00
|
|
|
record := &commandRecord{
|
|
|
|
|
name: spec.Name, summary: spec.Summary, description: spec.Description,
|
|
|
|
|
children: copySpec.Children, payload: payload,
|
|
|
|
|
}
|
2026-10-07 13:16:52 +00:00
|
|
|
if err := payload.validate(); err != nil {
|
2026-10-11 01:15:13 +00:00
|
|
|
record.issues = append(record.issues, fmt.Errorf("command %q: %w", spec.Name, err))
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
2026-10-11 01:15:13 +00:00
|
|
|
captureComposition(record)
|
|
|
|
|
return Command{record: record}
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-19 19:59:41 +00:00
|
|
|
// Group captures a non-runnable canonical command group and a copy of its
|
|
|
|
|
// relative Command child slice. Child declarations remain reusable and are not
|
|
|
|
|
// recursively copied. Routing rules cannot be children; install them alongside
|
|
|
|
|
// the root command tree through New or App.Register.
|
2026-10-11 01:15:13 +00:00
|
|
|
func Group(name, summary string, children ...Command) Command {
|
|
|
|
|
record := &commandRecord{name: name, summary: summary, children: append([]Command(nil), children...)}
|
|
|
|
|
captureComposition(record)
|
|
|
|
|
return Command{record: record}
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-19 19:59:41 +00:00
|
|
|
// RootGroup captures a group that may only be installed at the application
|
|
|
|
|
// root. Installation rejects it below any other command, including through
|
|
|
|
|
// wrappers. Its children are canonical Commands; install routing rules at the
|
|
|
|
|
// application root separately.
|
2026-10-11 01:15:13 +00:00
|
|
|
func RootGroup(name, summary string, children ...Command) Command {
|
|
|
|
|
command := Group(name, summary, children...)
|
|
|
|
|
command.record.rootOnly = true
|
|
|
|
|
return command
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-19 19:59:41 +00:00
|
|
|
// WithError returns a new Command declaration that also retains a non-nil err
|
|
|
|
|
// for installation. Earlier declaration and child errors remain available,
|
|
|
|
|
// and wrapped error identity is preserved. A nil err returns command unchanged.
|
2026-10-11 01:15:13 +00:00
|
|
|
func WithError(command Command, err error) Command {
|
|
|
|
|
if err == nil {
|
|
|
|
|
return command
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
2026-10-11 01:15:13 +00:00
|
|
|
var record commandRecord
|
|
|
|
|
if command.record == nil {
|
|
|
|
|
record.invalid = true
|
|
|
|
|
} else {
|
|
|
|
|
record = *command.record
|
|
|
|
|
record.issues = append([]error(nil), record.issues...)
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
2026-10-11 01:15:13 +00:00
|
|
|
record.issues = append(record.issues, err)
|
|
|
|
|
return Command{record: &record}
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-19 19:59:41 +00:00
|
|
|
// NoCommand explicitly represents an absent optional command declaration.
|
|
|
|
|
// Installation skips it, but reports errors attached to it; a zero Command is
|
|
|
|
|
// invalid and is not an absence marker. Routing rules have no absence marker.
|
2026-10-11 01:15:13 +00:00
|
|
|
func NoCommand() Command { return Command{record: &commandRecord{absent: true}} }
|
|
|
|
|
|
2026-10-19 17:23:32 +00:00
|
|
|
// Alias captures a command-path alias from path to target. Paths are
|
2026-10-19 19:59:41 +00:00
|
|
|
// whitespace-separated sequences of command names relative to the application
|
|
|
|
|
// root. The target must resolve to a canonical command or group; aliases do not
|
|
|
|
|
// add operands or transform flag values. Construction issues are retained for
|
|
|
|
|
// New or App.Register to report. Identical mappings are accepted, while
|
|
|
|
|
// different targets for the same source path are installation errors.
|
2026-10-19 17:23:32 +00:00
|
|
|
func Alias(path, target string) RoutingRule {
|
|
|
|
|
record := &routingRuleRecord{
|
|
|
|
|
kind: commandAliasRule,
|
|
|
|
|
sourceText: path,
|
|
|
|
|
targetText: target,
|
|
|
|
|
}
|
|
|
|
|
record.sourcePath = captureRoutingPath("source", path, &record.issues)
|
|
|
|
|
record.targetPath = captureRoutingPath("target", target, &record.issues)
|
|
|
|
|
return RoutingRule{record: record}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FlagAlias captures an application-wide flag spelling alias. Spellings must
|
2026-10-19 19:59:41 +00:00
|
|
|
// be one long flag name or one individual short spelling. The mapping applies
|
|
|
|
|
// wherever its canonical target flag is available and contributes nothing on
|
|
|
|
|
// commands without that flag. Identical mappings are accepted; different
|
|
|
|
|
// targets for the same source spelling in this scope are installation errors.
|
2026-10-19 17:23:32 +00:00
|
|
|
func FlagAlias(spelling, target string) RoutingRule {
|
|
|
|
|
record := &routingRuleRecord{
|
|
|
|
|
kind: globalFlagAliasRule,
|
|
|
|
|
sourceText: spelling,
|
|
|
|
|
targetText: target,
|
|
|
|
|
}
|
|
|
|
|
record.sourceFlag = captureFlagSpelling("source", spelling, &record.issues)
|
|
|
|
|
record.targetFlag = captureFlagSpelling("target", target, &record.issues)
|
|
|
|
|
return RoutingRule{record: record}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FlagAliasFor captures a flag spelling alias scoped to a canonical command
|
2026-10-19 19:59:41 +00:00
|
|
|
// path, not an alias path. The command and canonical target flag must be
|
|
|
|
|
// available in the complete installation. Scoped mappings take precedence over
|
|
|
|
|
// application-wide mappings with the same source spelling. Identical mappings
|
|
|
|
|
// are accepted; competing targets in the same scope are installation errors.
|
2026-10-19 17:23:32 +00:00
|
|
|
func FlagAliasFor(commandPath, spelling, target string) RoutingRule {
|
|
|
|
|
record := &routingRuleRecord{
|
|
|
|
|
kind: scopedFlagAliasRule,
|
|
|
|
|
sourceText: spelling,
|
|
|
|
|
targetText: target,
|
|
|
|
|
scopeText: commandPath,
|
|
|
|
|
}
|
|
|
|
|
record.scopePath = captureRoutingPath("command scope", commandPath, &record.issues)
|
|
|
|
|
record.sourceFlag = captureFlagSpelling("source", spelling, &record.issues)
|
|
|
|
|
record.targetFlag = captureFlagSpelling("target", target, &record.issues)
|
|
|
|
|
return RoutingRule{record: record}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func captureRoutingPath(label, value string, issues *[]error) commandpath.Path {
|
|
|
|
|
path, err := commandpath.ParseDeclaration(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
*issues = append(*issues, fmt.Errorf("invalid %s path: %w", label, err))
|
|
|
|
|
}
|
|
|
|
|
return path
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func captureFlagSpelling(label, value string, issues *[]error) capturedFlagSpelling {
|
|
|
|
|
spelling, err := parseFlagSpelling(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
*issues = append(*issues, fmt.Errorf("invalid %s flag spelling %q: %w", label, value, err))
|
|
|
|
|
}
|
|
|
|
|
return spelling
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseFlagSpelling(value string) (capturedFlagSpelling, error) {
|
|
|
|
|
if strings.HasPrefix(value, "--") {
|
|
|
|
|
name := value[2:]
|
|
|
|
|
if validFlagName(name) {
|
|
|
|
|
return capturedFlagSpelling{name: name}, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(value) == 2 && value[0] == '-' && value[1] != '-' {
|
|
|
|
|
return capturedFlagSpelling{name: value[1:], short: true}, nil
|
|
|
|
|
}
|
|
|
|
|
return capturedFlagSpelling{}, fmt.Errorf("must be a long flag or one-byte short flag spelling")
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 01:15:13 +00:00
|
|
|
func captureComposition(record *commandRecord) {
|
|
|
|
|
if err := validateCommandName(record.name); err != nil {
|
|
|
|
|
record.issues = append(record.issues, err)
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
2026-10-11 01:15:13 +00:00
|
|
|
seen := make(map[string]struct{}, len(record.children))
|
|
|
|
|
for _, child := range record.children {
|
|
|
|
|
if child.record == nil {
|
|
|
|
|
record.issues = append(record.issues, fmt.Errorf("command %q has an invalid child declaration", record.name))
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if child.record.absent {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
name := child.record.name
|
|
|
|
|
if _, exists := seen[name]; exists {
|
|
|
|
|
record.issues = append(record.issues, fmt.Errorf("duplicate child command %q under %q", name, record.name))
|
|
|
|
|
}
|
|
|
|
|
seen[name] = struct{}{}
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 01:15:13 +00:00
|
|
|
func validateCommandDeclaration(command Command) error {
|
2026-10-11 01:22:47 +00:00
|
|
|
return validateCommandPlacement(command)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validateCommandPlacement(command Command) error {
|
2026-10-11 01:15:13 +00:00
|
|
|
var issues []error
|
2026-10-11 01:22:47 +00:00
|
|
|
collectDeclarationIssues(command, nil, true, &issues)
|
2026-10-11 01:15:13 +00:00
|
|
|
return errors.Join(issues...)
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-11 01:22:47 +00:00
|
|
|
func collectDeclarationIssues(command Command, parents []string, root bool, issues *[]error) {
|
2026-10-11 01:15:13 +00:00
|
|
|
if command.record == nil {
|
|
|
|
|
*issues = append(*issues, errors.New("invalid zero command declaration"))
|
|
|
|
|
return
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
2026-10-11 01:15:13 +00:00
|
|
|
record := command.record
|
|
|
|
|
path := append(append([]string(nil), parents...), record.name)
|
2026-10-19 15:08:00 +00:00
|
|
|
label := commandpath.FromTokens(path).String()
|
2026-10-11 01:15:13 +00:00
|
|
|
for _, issue := range record.issues {
|
|
|
|
|
if label != "" {
|
|
|
|
|
*issues = append(*issues, fmt.Errorf("command %q: %w", label, issue))
|
|
|
|
|
} else {
|
|
|
|
|
*issues = append(*issues, issue)
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-11 01:22:47 +00:00
|
|
|
if record.rootOnly && !root {
|
|
|
|
|
*issues = append(*issues, fmt.Errorf("root-only command %q is nested beneath another command", label))
|
|
|
|
|
}
|
2026-10-11 01:15:13 +00:00
|
|
|
if record.invalid || record.absent {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for _, child := range record.children {
|
|
|
|
|
if child.record == nil {
|
|
|
|
|
continue
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
2026-10-11 01:22:47 +00:00
|
|
|
collectDeclarationIssues(child, path, false, issues)
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validateCommandName(name string) error {
|
2026-10-19 15:08:00 +00:00
|
|
|
return commandpath.ValidateComponent(name)
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validateBindings[I any](flags []Flag[I], arguments []Argument[I]) error {
|
|
|
|
|
flagNames := make(map[string]struct{}, len(flags))
|
|
|
|
|
shortNames := make(map[string]struct{}, len(flags))
|
|
|
|
|
for _, flag := range flags {
|
|
|
|
|
binding := flag.binding
|
2026-10-07 13:22:57 +00:00
|
|
|
if !validFlagName(binding.meta.Name) {
|
|
|
|
|
return fmt.Errorf("flag name %q must use lowercase hyphen-separated words", binding.meta.Name)
|
2026-10-07 13:16:52 +00:00
|
|
|
}
|
|
|
|
|
if binding.meta.Name == "help" {
|
|
|
|
|
return fmt.Errorf("flag name %q is reserved for help", binding.meta.Name)
|
|
|
|
|
}
|
2026-10-26 02:50:54 +00:00
|
|
|
if commandmeta.IsReportingFlag(binding.meta.Name) {
|
|
|
|
|
return fmt.Errorf("flag name %q is reserved for reporting", binding.meta.Name)
|
|
|
|
|
}
|
2026-10-07 13:16:52 +00:00
|
|
|
if _, exists := flagNames[binding.meta.Name]; exists {
|
|
|
|
|
return fmt.Errorf("duplicate flag name %q", binding.meta.Name)
|
|
|
|
|
}
|
|
|
|
|
flagNames[binding.meta.Name] = struct{}{}
|
|
|
|
|
if binding.meta.Short != "" {
|
|
|
|
|
if len(binding.meta.Short) != 1 || binding.meta.Short == "-" {
|
|
|
|
|
return fmt.Errorf("short spelling %q must be one byte", binding.meta.Short)
|
|
|
|
|
}
|
|
|
|
|
if binding.meta.Short == "h" {
|
|
|
|
|
return fmt.Errorf("short spelling %q is reserved for help", binding.meta.Short)
|
|
|
|
|
}
|
|
|
|
|
if _, exists := shortNames[binding.meta.Short]; exists {
|
|
|
|
|
return fmt.Errorf("duplicate short spelling %q", binding.meta.Short)
|
|
|
|
|
}
|
|
|
|
|
shortNames[binding.meta.Short] = struct{}{}
|
|
|
|
|
}
|
2026-10-11 01:18:07 +00:00
|
|
|
if binding.identity == nil || !binding.valid || binding.prepare == nil {
|
2026-10-07 13:16:52 +00:00
|
|
|
return fmt.Errorf("flag %q is missing parser or setter wiring", binding.meta.Name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, argument := range arguments {
|
|
|
|
|
binding := argument.binding
|
|
|
|
|
if binding.meta.Name == "" {
|
|
|
|
|
return fmt.Errorf("argument name must not be empty")
|
|
|
|
|
}
|
|
|
|
|
if binding.min < 0 || binding.max < -1 || (binding.max >= 0 && binding.min > binding.max) {
|
|
|
|
|
return fmt.Errorf("argument %q has invalid bounds %d..%d", binding.meta.Name, binding.min, binding.max)
|
|
|
|
|
}
|
2026-10-11 01:18:07 +00:00
|
|
|
if binding.identity == nil || !binding.valid || binding.prepare == nil {
|
2026-10-07 13:16:52 +00:00
|
|
|
return fmt.Errorf("argument %q is missing parser or setter wiring", binding.meta.Name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-10-07 13:22:57 +00:00
|
|
|
|
|
|
|
|
func validFlagName(name string) bool {
|
|
|
|
|
if name == "" || name[0] == '-' || name[len(name)-1] == '-' {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
segmentHasCharacter := false
|
|
|
|
|
for _, character := range name {
|
|
|
|
|
if character == '-' {
|
|
|
|
|
if !segmentHasCharacter {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
segmentHasCharacter = false
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if !unicode.IsLower(character) && !unicode.IsDigit(character) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
segmentHasCharacter = true
|
|
|
|
|
}
|
|
|
|
|
return segmentHasCharacter
|
|
|
|
|
}
|