package commandkit import ( "fmt" "strings" ) // Command is an opaque command declaration. Use NewCommand or Group to create // declarations and WithChildren to compose them. type Command struct { name string summary string description string children []Command payload commandPayload } // Supplied carries a value and whether it was explicitly provided. type Supplied[T any] struct { // Value is the associated value. Value T // Set reports whether a caller explicitly supplied the value. Set bool } // FlagMeta describes a command flag. 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 } // ArgumentMeta describes a positional command argument. type ArgumentMeta struct { // Name is the argument name shown in command help. Name string // Usage briefly describes the argument. Usage string } // Flag represents a typed binding for a command flag. type Flag[I any] struct { binding flagBinding[I] } type flagBinding[I any] struct { meta FlagMeta prepare func([]string) (func(*I), error) valid bool } // Argument represents a typed binding for a positional command argument. type Argument[I any] struct { binding argumentBinding[I] } type argumentBinding[I any] struct { meta ArgumentMeta min int max int prepare func([]string) (func(*I), error) valid bool } // CommandSpec declares one command using typed input and handler callbacks. 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 // 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 newInput() any run(Invocation, any) Outcome } type typedCommand[I any] struct { spec CommandSpec[I] } func (c typedCommand[I]) validate() error { return validateBindings(c.spec.Flags, c.spec.Arguments) } func (c typedCommand[I]) hasRun() bool { return c.spec.Run != nil } func (c typedCommand[I]) newInput() any { var input I if c.spec.NewInput != nil { input = c.spec.NewInput() } return &input } func (c typedCommand[I]) run(invocation Invocation, input any) Outcome { return c.spec.Run(invocation, *input.(*I)) } // NewCommand validates and creates a typed command declaration. It retains // callbacks without invoking them and copies the declarative flag and argument // slices. func NewCommand[I any](spec CommandSpec[I]) (Command, error) { if err := validateCommandName(spec.Name); err != nil { return Command{}, err } copySpec := spec copySpec.Flags = append([]Flag[I](nil), spec.Flags...) copySpec.Arguments = append([]Argument[I](nil), spec.Arguments...) payload := typedCommand[I]{spec: copySpec} if err := payload.validate(); err != nil { return Command{}, fmt.Errorf("command %q: %w", spec.Name, err) } return Command{ name: spec.Name, summary: spec.Summary, description: spec.Description, payload: payload, }, nil } // Group creates a non-runnable command group with the supplied children. func Group(name, summary string, children ...Command) (Command, error) { if err := validateCommandName(name); err != nil { return Command{}, err } group := Command{name: name, summary: summary, children: cloneCommands(children)} if err := validateCommandDeclaration(group); err != nil { return Command{}, err } return group, nil } // WithChildren returns a copy of parent with the supplied relative child // declarations. The parent's optional handler and input bindings are retained. func WithChildren(parent Command, children ...Command) (Command, error) { if err := validateCommandDeclaration(parent); err != nil { return Command{}, fmt.Errorf("parent command: %w", err) } combined := make([]Command, 0, len(parent.children)+len(children)) combined = append(combined, parent.children...) combined = append(combined, children...) result := parent result.children = cloneCommands(combined) if err := validateCommandDeclaration(result); err != nil { return Command{}, err } return result, nil } func cloneCommands(commands []Command) []Command { if len(commands) == 0 { return nil } cloned := make([]Command, len(commands)) for i, command := range commands { cloned[i] = cloneCommand(command) } return cloned } func cloneCommand(command Command) Command { command.children = cloneCommands(command.children) return command } func validateCommandDeclaration(command Command) error { if err := validateCommandName(command.name); err != nil { return err } if command.payload != nil { if err := command.payload.validate(); err != nil { return fmt.Errorf("command %q: %w", command.name, err) } } children := make(map[string]struct{}, len(command.children)) for _, child := range command.children { if err := validateCommandDeclaration(child); err != nil { return err } if _, exists := children[child.name]; exists { return fmt.Errorf("duplicate child command %q under %q", child.name, command.name) } children[child.name] = struct{}{} } return nil } func validateCommandName(name string) error { fields := strings.Fields(name) if len(fields) != 1 || fields[0] != name || strings.HasPrefix(name, "-") || name == "--" { return fmt.Errorf("command name %q must be one non-option token", name) } return nil } 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 if binding.meta.Name == "" || strings.ContainsAny(binding.meta.Name, " \t\r\n") || strings.HasPrefix(binding.meta.Name, "-") { return fmt.Errorf("flag name %q must be a nonempty token without leading dashes", binding.meta.Name) } if binding.meta.Name == "help" { return fmt.Errorf("flag name %q is reserved for help", binding.meta.Name) } 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{}{} } if !binding.valid || binding.prepare == nil { 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) } if !binding.valid || binding.prepare == nil { return fmt.Errorf("argument %q is missing parser or setter wiring", binding.meta.Name) } } return nil }