diff --git a/pkg/core/cli.go b/pkg/core/cli.go index 03fb869..61375ec 100644 --- a/pkg/core/cli.go +++ b/pkg/core/cli.go @@ -1,200 +1,152 @@ // SPDX-License-Identifier: EUPL-1.2 -// CLI command framework for the Core framework. -// Based on leaanthony/clir — zero-dependency command line interface. - +// Cli is the CLI surface layer for the Core command tree. +// It reads commands from Core's registry and wires them to terminal I/O. +// +// Run the CLI: +// +// c := core.New(core.Options{{K: "name", V: "myapp"}}) +// c.Command("deploy", handler) +// c.Cli().Run() +// +// The Cli resolves os.Args to a command path, parses flags, +// and calls the command's action with parsed options. package core import ( "fmt" "os" + "strings" ) -// CliAction represents a function called when a command is invoked. -type CliAction func() error - -// CliOpts configures a Cli. -type CliOpts struct { - Version string - Name string - Description string -} - -// Cli is the CLI command framework. +// Cli is the CLI surface for the Core command tree. type Cli struct { - opts *CliOpts - rootCommand *Command - defaultCommand *Command - preRunCommand func(*Cli) error - postRunCommand func(*Cli) error - bannerFunction func(*Cli) string - errorHandler func(string, error) error + core *Core + banner func(*Cli) string } -// defaultBannerFunction prints a banner for the application. -func defaultBannerFunction(c *Cli) string { - version := "" - if c.opts != nil && c.opts.Version != "" { - version = " " + c.opts.Version - } - name := "" - description := "" - if c.opts != nil { - name = c.opts.Name - description = c.opts.Description - } - if description != "" { - return fmt.Sprintf("%s%s - %s", name, version, description) - } - return fmt.Sprintf("%s%s", name, version) -} - - -// Command returns the root command. -func (c *Cli) Command() *Command { - return c.rootCommand -} - -// Version returns the application version string. -func (c *Cli) Version() string { - if c.opts != nil { - return c.opts.Version - } - return "" -} - -// Name returns the application name. -func (c *Cli) Name() string { - if c.opts != nil { - return c.opts.Name - } - return c.rootCommand.name -} - -// ShortDescription returns the application short description. -func (c *Cli) ShortDescription() string { - if c.opts != nil { - return c.opts.Description - } - return c.rootCommand.shortdescription -} - -// SetBannerFunction sets the function that generates the banner string. -func (c *Cli) SetBannerFunction(fn func(*Cli) string) { - c.bannerFunction = fn -} - -// SetErrorFunction sets a custom error handler for undefined flags. -func (c *Cli) SetErrorFunction(fn func(string, error) error) { - c.errorHandler = fn -} - -// AddCommand adds a command to the application. -func (c *Cli) AddCommand(command *Command) { - c.rootCommand.AddCommand(command) -} - -// PrintBanner prints the application banner. -func (c *Cli) PrintBanner() { - fmt.Println(c.bannerFunction(c)) - fmt.Println("") -} - -// PrintHelp prints the application help. -func (c *Cli) PrintHelp() { - c.rootCommand.PrintHelp() -} - -// Run runs the application with the given arguments. -func (c *Cli) Run(args ...string) error { - if c.preRunCommand != nil { - if err := c.preRunCommand(c); err != nil { - return err - } - } +// Run resolves os.Args to a command path and executes it. +// +// c.Cli().Run() +// c.Cli().Run("deploy", "to", "homelab") +func (cl *Cli) Run(args ...string) Result[any] { if len(args) == 0 { args = os.Args[1:] } - if err := c.rootCommand.run(args); err != nil { - return err - } - if c.postRunCommand != nil { - if err := c.postRunCommand(c); err != nil { - return err + + // Filter out empty args and test flags + var clean []string + for _, a := range args { + if a != "" && !strings.HasPrefix(a, "-test.") { + clean = append(clean, a) } } - return nil + + if cl.core == nil || cl.core.commands == nil || len(cl.core.commands.commands) == 0 { + // No commands registered — print banner and exit + if cl.banner != nil { + fmt.Println(cl.banner(cl)) + } + return Result[any]{} + } + + // Resolve command path from args + // "deploy to homelab" → try "deploy/to/homelab", then "deploy/to", then "deploy" + var cmd *Command + var remaining []string + + for i := len(clean); i > 0; i-- { + path := strings.Join(clean[:i], "/") + if c, ok := cl.core.commands.commands[path]; ok { + cmd = c + remaining = clean[i:] + break + } + } + + if cmd == nil { + // No matching command — try root-level action or print help + if cl.banner != nil { + fmt.Println(cl.banner(cl)) + } + cl.PrintHelp() + return Result[any]{} + } + + // Build options from remaining args (flags become Options) + opts := Options{} + for _, arg := range remaining { + if strings.HasPrefix(arg, "--") { + parts := strings.SplitN(strings.TrimPrefix(arg, "--"), "=", 2) + if len(parts) == 2 { + opts = append(opts, Option{K: parts[0], V: parts[1]}) + } else { + opts = append(opts, Option{K: parts[0], V: true}) + } + } else if strings.HasPrefix(arg, "-") { + parts := strings.SplitN(strings.TrimPrefix(arg, "-"), "=", 2) + if len(parts) == 2 { + opts = append(opts, Option{K: parts[0], V: parts[1]}) + } else { + opts = append(opts, Option{K: parts[0], V: true}) + } + } else { + opts = append(opts, Option{K: "_arg", V: arg}) + } + } + + return cmd.Run(opts) } -// DefaultCommand sets the command to run when no other commands are given. -func (c *Cli) DefaultCommand(defaultCommand *Command) *Cli { - c.defaultCommand = defaultCommand - return c +// PrintHelp prints available commands. +// +// c.Cli().PrintHelp() +func (cl *Cli) PrintHelp() { + if cl.core == nil || cl.core.commands == nil { + return + } + + name := "" + if cl.core.app != nil { + name = cl.core.app.Name + } + if name != "" { + fmt.Printf("%s commands:\n\n", name) + } else { + fmt.Println("Commands:\n") + } + + cl.core.commands.mu.RLock() + defer cl.core.commands.mu.RUnlock() + + for path, cmd := range cl.core.commands.commands { + if cmd.hidden { + continue + } + desc := cl.core.I18n().T(cmd.I18nKey()) + // If i18n returned the key itself (no translation), show path only + if desc == cmd.I18nKey() { + fmt.Printf(" %s\n", path) + } else { + fmt.Printf(" %-30s %s\n", path, desc) + } + } } -// NewChildCommand creates a new subcommand. -func (c *Cli) NewChildCommand(name string, description ...string) *Command { - return c.rootCommand.NewChildCommand(name, description...) +// SetBanner sets the banner function. +// +// c.Cli().SetBanner(func(_ *core.Cli) string { return "My App v1.0" }) +func (cl *Cli) SetBanner(fn func(*Cli) string) { + cl.banner = fn } -// NewChildCommandInheritFlags creates a new subcommand that inherits parent flags. -func (c *Cli) NewChildCommandInheritFlags(name string, description ...string) *Command { - return c.rootCommand.NewChildCommandInheritFlags(name, description...) -} - -// PreRun sets a function to call before running the command. -func (c *Cli) PreRun(callback func(*Cli) error) { - c.preRunCommand = callback -} - -// PostRun sets a function to call after running the command. -func (c *Cli) PostRun(callback func(*Cli) error) { - c.postRunCommand = callback -} - -// BoolFlag adds a boolean flag to the root command. -func (c *Cli) BoolFlag(name, description string, variable *bool) *Cli { - c.rootCommand.BoolFlag(name, description, variable) - return c -} - -// StringFlag adds a string flag to the root command. -func (c *Cli) StringFlag(name, description string, variable *string) *Cli { - c.rootCommand.StringFlag(name, description, variable) - return c -} - -// IntFlag adds an int flag to the root command. -func (c *Cli) IntFlag(name, description string, variable *int) *Cli { - c.rootCommand.IntFlag(name, description, variable) - return c -} - -// AddFlags adds struct-tagged flags to the root command. -func (c *Cli) AddFlags(flags any) *Cli { - c.rootCommand.AddFlags(flags) - return c -} - -// Action defines an action for the root command. -func (c *Cli) Action(callback CliAction) *Cli { - c.rootCommand.Action(callback) - return c -} - -// LongDescription sets the long description for the root command. -func (c *Cli) LongDescription(longdescription string) *Cli { - c.rootCommand.LongDescription(longdescription) - return c -} - -// OtherArgs returns the non-flag arguments passed to the CLI. -func (c *Cli) OtherArgs() []string { - return c.rootCommand.flags.Args() -} - -// NewChildCommandFunction creates a subcommand from a function with struct flags. -func (c *Cli) NewChildCommandFunction(name string, description string, fn any) *Cli { - c.rootCommand.NewChildCommandFunction(name, description, fn) - return c +// Banner returns the banner string. +func (cl *Cli) Banner() string { + if cl.banner != nil { + return cl.banner(cl) + } + if cl.core != nil && cl.core.app != nil && cl.core.app.Name != "" { + return cl.core.app.Name + } + return "" } diff --git a/pkg/core/command.go b/pkg/core/command.go index 7d24711..177ecf0 100644 --- a/pkg/core/command.go +++ b/pkg/core/command.go @@ -1,1339 +1,225 @@ +// SPDX-License-Identifier: EUPL-1.2 + +// Command is a DTO representing an executable operation. +// Commands don't know if they're root, child, or nested — the tree +// structure comes from composition via path-based registration. +// +// Register a command: +// +// c.Command("deploy", func(opts core.Options) core.Result[any] { +// return core.Result[any]{Value: "deployed", OK: true} +// }) +// +// Register a nested command: +// +// c.Command("deploy/to/homelab", handler) +// +// Description is an i18n key — derived from path if omitted: +// +// "deploy" → "cmd.deploy.description" +// "deploy/to/homelab" → "cmd.deploy.to.homelab.description" package core import ( - "flag" - "fmt" - "io" - "os" - "reflect" - "strconv" "strings" + "sync" ) -// Command represents a command that may be run by the user +// CommandAction is the function signature for command handlers. +// +// func(opts core.Options) core.Result[any] +type CommandAction func(Options) Result[any] + +// CommandLifecycle is implemented by commands that support managed lifecycle. +// Basic commands only need an action. Daemon commands implement Start/Stop/Signal +// via go-process. +type CommandLifecycle interface { + Start(Options) Result[any] + Stop() Result[any] + Restart() Result[any] + Reload() Result[any] + Signal(string) Result[any] +} + +// Command is the DTO for an executable operation. type Command struct { - name string - commandPath string - shortdescription string - longdescription string - subCommands []*Command - subCommandsMap map[string]*Command - longestSubcommand int - actionCallback CliAction - app *Cli - flags *flag.FlagSet - flagCount int - helpFlag bool - hidden bool - positionalArgsMap map[string]reflect.Value - sliceSeparator map[string]string + name string + description string // i18n key — derived from path if empty + path string // "deploy/to/homelab" + commands map[string]*Command // child commands + action CommandAction // business logic + lifecycle CommandLifecycle // optional — provided by go-process + flags Options // declared flags + hidden bool + mu sync.RWMutex } -// NewCommand creates a new Command. -// Description is optional — if omitted, i18n resolves it from the command path. -func NewCommand(name string, description ...string) *Command { - desc := "" - if len(description) > 0 { - desc = description[0] +// I18nKey returns the i18n key for this command's description. +// +// cmd with path "deploy/to/homelab" → "cmd.deploy.to.homelab.description" +func (cmd *Command) I18nKey() string { + if cmd.description != "" { + return cmd.description } - result := &Command{ - name: name, - shortdescription: desc, - subCommandsMap: make(map[string]*Command), - hidden: false, - positionalArgsMap: make(map[string]reflect.Value), - sliceSeparator: make(map[string]string), + path := cmd.path + if path == "" { + path = cmd.name } - - // Init flagset so flags can be added before Run - result.setParentCommandPath("") - - return result + return "cmd." + strings.ReplaceAll(path, "/", ".") + ".description" } -func (c *Command) setParentCommandPath(parentCommandPath string) { - // Set up command path - if parentCommandPath != "" { - c.commandPath += parentCommandPath + " " +// Run executes the command's action with the given options. +// +// result := cmd.Run(core.Options{{K: "target", V: "homelab"}}) +func (cmd *Command) Run(opts Options) Result[any] { + if cmd.action == nil { + return Result[any]{} } - c.commandPath += c.name - - // Set up flag set - c.flags = flag.NewFlagSet(c.commandPath, flag.ContinueOnError) - c.BoolFlag("help", "Get help on the '"+strings.ToLower(c.commandPath)+"' command.", &c.helpFlag) - - // result.Flags.Usage = result.PrintHelp - + return cmd.action(opts) } -func (c *Command) inheritFlags(inheritFlags *flag.FlagSet) { - // inherit flags - inheritFlags.VisitAll(func(f *flag.Flag) { - if f.Name != "help" { - c.flags.Var(f.Value, f.Name, f.Usage) - } - }) +// Start delegates to the lifecycle implementation if available. +func (cmd *Command) Start(opts Options) Result[any] { + if cmd.lifecycle != nil { + return cmd.lifecycle.Start(opts) + } + return cmd.Run(opts) } -func (c *Command) setApp(app *Cli) { - c.app = app +// Stop delegates to the lifecycle implementation. +func (cmd *Command) Stop() Result[any] { + if cmd.lifecycle != nil { + return cmd.lifecycle.Stop() + } + return Result[any]{} } -// parseFlags parses the given flags -func (c *Command) parseFlags(args []string) error { - // Parse flags - // Suppress flag parse errors to stderr - - c.flags.SetOutput(io.Discard) - - // Credit: https://stackoverflow.com/a/74146375 - var positionalArgs []string - for { - if err := c.flags.Parse(args); err != nil { - return err - } - // Consume all the flags that were parsed as flags. - args = args[len(args)-c.flags.NArg():] - if len(args) == 0 { - break - } - // There's at least one flag remaining and it must be a positional arg since - // we consumed all args that were parsed as flags. Consume just the first - // one, and retry parsing, since subsequent args may be flags. - positionalArgs = append(positionalArgs, args[0]) - args = args[1:] +// Restart delegates to the lifecycle implementation. +func (cmd *Command) Restart() Result[any] { + if cmd.lifecycle != nil { + return cmd.lifecycle.Restart() } - - // Parse just the positional args so that flagset.Args()/flagset.NArgs() - // return the expected value. - // Note: This should never return an error. - err := c.flags.Parse(positionalArgs) - if err != nil { - return err - } - - if len(positionalArgs) > 0 { - return c.parsePositionalArgs(positionalArgs) - } - return nil + return Result[any]{} } -// Run - Runs the Command with the given arguments -func (c *Command) run(args []string) error { - - // If we have arguments, process them - if len(args) > 0 { - // Check for subcommand - subcommand := c.subCommandsMap[args[0]] - if subcommand != nil { - return subcommand.run(args[1:]) - } - - // Parse flags - err := c.parseFlags(args) - if err != nil { - if c.app.errorHandler != nil { - return c.app.errorHandler(c.commandPath, err) - } - return E("cli.Run", fmt.Sprintf("see '%s --help' for usage", c.commandPath), err) - } - - // Help takes precedence - if c.helpFlag { - c.PrintHelp() - return nil - } +// Reload delegates to the lifecycle implementation. +func (cmd *Command) Reload() Result[any] { + if cmd.lifecycle != nil { + return cmd.lifecycle.Reload() } - - // Do we have an action? - if c.actionCallback != nil { - return c.actionCallback() - } - - // If we haven't specified a subcommand - // check for an app level default command - if c.app.defaultCommand != nil { - // Prevent recursion! - if c.app.defaultCommand != c { - // only run default command if no args passed - if len(args) == 0 { - return c.app.defaultCommand.run(args) - } - } - } - - // Nothing left we can do - c.PrintHelp() - - return nil + return Result[any]{} } -// Action - Define an action from this command -func (c *Command) Action(callback CliAction) *Command { - c.actionCallback = callback - return c +// Signal delegates to the lifecycle implementation. +func (cmd *Command) Signal(sig string) Result[any] { + if cmd.lifecycle != nil { + return cmd.lifecycle.Signal(sig) + } + return Result[any]{} } -// PrintHelp - Output the help text for this command -func (c *Command) PrintHelp() { - c.app.PrintBanner() +// --- Command Registry (on Core) --- - commandTitle := c.commandPath - if c.shortdescription != "" { - commandTitle += " - " + c.shortdescription - } - // Ignore root command - if c.commandPath != c.name { - fmt.Println(commandTitle) - } - if c.longdescription != "" { - fmt.Println(c.longdescription + "\n") - } - if len(c.subCommands) > 0 { - fmt.Println("Available commands:") - fmt.Println("") - for _, subcommand := range c.subCommands { - if subcommand.isHidden() { - continue - } - spacer := strings.Repeat(" ", 3+c.longestSubcommand-len(subcommand.name)) - isDefault := "" - if subcommand.isDefaultCommand() { - isDefault = "[default]" - } - fmt.Printf(" %s%s%s %s\n", subcommand.name, spacer, subcommand.shortdescription, isDefault) - } - fmt.Println("") - } - if c.flagCount > 0 { - fmt.Println("Flags:") - fmt.Println() - c.flags.SetOutput(os.Stdout) - c.flags.PrintDefaults() - c.flags.SetOutput(os.Stderr) - - } - fmt.Println() +// commandRegistry holds the command tree. +type commandRegistry struct { + commands map[string]*Command + mu sync.RWMutex } -// isDefaultCommand returns true if called on the default command -func (c *Command) isDefaultCommand() bool { - return c.app.defaultCommand == c -} - -// isHidden returns true if the command is a hidden command -func (c *Command) isHidden() bool { - return c.hidden -} - -// Hidden hides the command from the Help system -func (c *Command) Hidden() { - c.hidden = true -} - -// NewChildCommand - Creates a new subcommand -func (c *Command) NewChildCommand(name string, description ...string) *Command { - result := NewCommand(name, description...) - c.AddCommand(result) - return result -} - -// AddCommand - Adds a subcommand -func (c *Command) AddCommand(command *Command) { - command.setApp(c.app) - command.setParentCommandPath(c.commandPath) - name := command.name - c.subCommands = append(c.subCommands, command) - c.subCommandsMap[name] = command - if len(name) > c.longestSubcommand { - c.longestSubcommand = len(name) - } -} - -// NewChildCommandInheritFlags - Creates a new subcommand, inherits flags from command -func (c *Command) NewChildCommandInheritFlags(name string, description ...string) *Command { - result := c.NewChildCommand(name, description...) - result.inheritFlags(c.flags) - return result -} - -func (c *Command) AddFlags(optionStruct any) *Command { - // use reflection to determine if this is a pointer to a struct - // if not, panic - - t := reflect.TypeOf(optionStruct) - - // Check for a pointer to a struct - if t.Kind() != reflect.Ptr { - panic("AddFlags() requires a pointer to a struct") - } - if t.Elem().Kind() != reflect.Struct { - panic("AddFlags() requires a pointer to a struct") +// CommandHandler registers or retrieves commands on Core. +// Same pattern as Service() — zero args returns registry, one arg gets, two args registers. +// +// c.Command("deploy", handler) // register +// c.Command("deploy/to/homelab", handler) // register nested +// cmd := c.Command("deploy") // get +func (c *Core) Command(args ...any) any { + if c.commands == nil { + c.commands = &commandRegistry{commands: make(map[string]*Command)} } - // Iterate through the fields of the struct reading the struct tags - // and adding the flags - v := reflect.ValueOf(optionStruct).Elem() - for i := 0; i < v.NumField(); i++ { - field := v.Field(i) - fieldType := t.Elem().Field(i) - if !fieldType.IsExported() { - continue - } - // If this is an embedded struct, recurse - if fieldType.Type.Kind() == reflect.Struct { - c.AddFlags(field.Addr().Interface()) - continue - } - - tag := t.Elem().Field(i).Tag - name := tag.Get("name") - description := tag.Get("description") - defaultValue := tag.Get("default") - pos := tag.Get("pos") - sep := tag.Get("sep") - c.positionalArgsMap[pos] = field - if sep != "" { - c.sliceSeparator[pos] = sep - } - if name == "" { - name = strings.ToLower(t.Elem().Field(i).Name) - } - switch field.Kind() { - case reflect.Bool: - var defaultValueBool bool - if defaultValue != "" { - var err error - defaultValueBool, err = strconv.ParseBool(defaultValue) - if err != nil { - panic("Invalid default value for bool flag") - } - } - field.SetBool(defaultValueBool) - c.BoolFlag(name, description, field.Addr().Interface().(*bool)) - case reflect.String: - if defaultValue != "" { - // set value of field to default value - field.SetString(defaultValue) - } - c.StringFlag(name, description, field.Addr().Interface().(*string)) - case reflect.Int: - if defaultValue != "" { - // set value of field to default value - value, err := strconv.Atoi(defaultValue) - if err != nil { - panic("Invalid default value for int flag") - } - field.SetInt(int64(value)) - } - c.IntFlag(name, description, field.Addr().Interface().(*int)) - case reflect.Int8: - if defaultValue != "" { - // set value of field to default value - value, err := strconv.Atoi(defaultValue) - if err != nil { - panic("Invalid default value for int8 flag") - } - field.SetInt(int64(value)) - } - c.Int8Flag(name, description, field.Addr().Interface().(*int8)) - case reflect.Int16: - if defaultValue != "" { - // set value of field to default value - value, err := strconv.Atoi(defaultValue) - if err != nil { - panic("Invalid default value for int16 flag") - } - field.SetInt(int64(value)) - } - c.Int16Flag(name, description, field.Addr().Interface().(*int16)) - case reflect.Int32: - if defaultValue != "" { - // set value of field to default value - value, err := strconv.Atoi(defaultValue) - if err != nil { - panic("Invalid default value for int32 flag") - } - field.SetInt(int64(value)) - } - c.Int32Flag(name, description, field.Addr().Interface().(*int32)) - case reflect.Int64: - if defaultValue != "" { - // set value of field to default value - value, err := strconv.Atoi(defaultValue) - if err != nil { - panic("Invalid default value for int64 flag") - } - field.SetInt(int64(value)) - } - c.Int64Flag(name, description, field.Addr().Interface().(*int64)) - case reflect.Uint: - if defaultValue != "" { - // set value of field to default value - value, err := strconv.Atoi(defaultValue) - if err != nil { - panic("Invalid default value for uint flag") - } - field.SetUint(uint64(value)) - } - c.UintFlag(name, description, field.Addr().Interface().(*uint)) - case reflect.Uint8: - if defaultValue != "" { - // set value of field to default value - value, err := strconv.Atoi(defaultValue) - if err != nil { - panic("Invalid default value for uint8 flag") - } - field.SetUint(uint64(value)) - } - c.Uint8Flag(name, description, field.Addr().Interface().(*uint8)) - case reflect.Uint16: - if defaultValue != "" { - // set value of field to default value - value, err := strconv.Atoi(defaultValue) - if err != nil { - panic("Invalid default value for uint16 flag") - } - field.SetUint(uint64(value)) - } - c.Uint16Flag(name, description, field.Addr().Interface().(*uint16)) - case reflect.Uint32: - if defaultValue != "" { - // set value of field to default value - value, err := strconv.Atoi(defaultValue) - if err != nil { - panic("Invalid default value for uint32 flag") - } - field.SetUint(uint64(value)) - } - c.Uint32Flag(name, description, field.Addr().Interface().(*uint32)) - case reflect.Uint64: - if defaultValue != "" { - // set value of field to default value - value, err := strconv.Atoi(defaultValue) - if err != nil { - panic("Invalid default value for uint64 flag") - } - field.SetUint(uint64(value)) - } - c.UInt64Flag(name, description, field.Addr().Interface().(*uint64)) - case reflect.Float32: - if defaultValue != "" { - // set value of field to default value - value, err := strconv.ParseFloat(defaultValue, 64) - if err != nil { - panic("Invalid default value for float32 flag") - } - field.SetFloat(value) - } - c.Float32Flag(name, description, field.Addr().Interface().(*float32)) - case reflect.Float64: - if defaultValue != "" { - // set value of field to default value - value, err := strconv.ParseFloat(defaultValue, 64) - if err != nil { - panic("Invalid default value for float64 flag") - } - field.SetFloat(value) - } - c.Float64Flag(name, description, field.Addr().Interface().(*float64)) - case reflect.Slice: - c.addSliceField(field, defaultValue, sep) - c.addSliceFlags(name, description, field) - default: - if pos != "" { - fmt.Fprintf(os.Stderr, "WARNING: unsupported type for flag: %s %s\n", fieldType.Type.Kind(), name) - } - } - } - - return c -} - -func (c *Command) addSliceFlags(name, description string, field reflect.Value) *Command { - if field.Kind() != reflect.Slice { - panic("addSliceFlags() requires a pointer to a slice") - } - t := reflect.TypeOf(field.Addr().Interface()) - if t.Kind() != reflect.Ptr { - panic("addSliceFlags() requires a pointer to a slice") - } - if t.Elem().Kind() != reflect.Slice { - panic("addSliceFlags() requires a pointer to a slice") - } - switch t.Elem().Elem().Kind() { - case reflect.Bool: - c.BoolsFlag(name, description, field.Addr().Interface().(*[]bool)) - case reflect.String: - c.StringsFlag(name, description, field.Addr().Interface().(*[]string)) - case reflect.Int: - c.IntsFlag(name, description, field.Addr().Interface().(*[]int)) - case reflect.Int8: - c.Int8sFlag(name, description, field.Addr().Interface().(*[]int8)) - case reflect.Int16: - c.Int16sFlag(name, description, field.Addr().Interface().(*[]int16)) - case reflect.Int32: - c.Int32sFlag(name, description, field.Addr().Interface().(*[]int32)) - case reflect.Int64: - c.Int64sFlag(name, description, field.Addr().Interface().(*[]int64)) - case reflect.Uint: - c.UintsFlag(name, description, field.Addr().Interface().(*[]uint)) - case reflect.Uint8: - c.Uint8sFlag(name, description, field.Addr().Interface().(*[]uint8)) - case reflect.Uint16: - c.Uint16sFlag(name, description, field.Addr().Interface().(*[]uint16)) - case reflect.Uint32: - c.Uint32sFlag(name, description, field.Addr().Interface().(*[]uint32)) - case reflect.Uint64: - c.Uint64sFlag(name, description, field.Addr().Interface().(*[]uint64)) - case reflect.Float32: - c.Float32sFlag(name, description, field.Addr().Interface().(*[]float32)) - case reflect.Float64: - c.Float64sFlag(name, description, field.Addr().Interface().(*[]float64)) + switch len(args) { + case 0: + return c.commands + case 1: + path, _ := args[0].(string) + c.commands.mu.RLock() + cmd := c.commands.commands[path] + c.commands.mu.RUnlock() + return cmd default: - panic(fmt.Sprintf("addSliceFlags() not supported slice type %s", t.Elem().Elem().Kind().String())) - } - return c -} - -func (c *Command) addSliceField(field reflect.Value, defaultValue, separator string) *Command { - if defaultValue == "" { - return c - } - if field.Kind() != reflect.Slice { - panic("addSliceField() requires a pointer to a slice") - } - t := reflect.TypeOf(field.Addr().Interface()) - if t.Kind() != reflect.Ptr { - panic("addSliceField() requires a pointer to a slice") - } - if t.Elem().Kind() != reflect.Slice { - panic("addSliceField() requires a pointer to a slice") - } - defaultSlice := []string{defaultValue} - if separator != "" { - defaultSlice = strings.Split(defaultValue, separator) - } - switch t.Elem().Elem().Kind() { - case reflect.Bool: - defaultValues := make([]bool, 0, len(defaultSlice)) - for _, value := range defaultSlice { - val, err := strconv.ParseBool(value) - if err != nil { - panic("Invalid default value for bool flag") - } - defaultValues = append(defaultValues, val) + path, _ := args[0].(string) + if path == "" { + return E("core.Command", "command path cannot be empty", nil) } - field.Set(reflect.ValueOf(defaultValues)) - case reflect.String: - field.Set(reflect.ValueOf(defaultSlice)) - case reflect.Int: - defaultValues := make([]int, 0, len(defaultSlice)) - for _, value := range defaultSlice { - val, err := strconv.Atoi(value) - if err != nil { - panic("Invalid default value for int flag") - } - defaultValues = append(defaultValues, val) + + c.commands.mu.Lock() + defer c.commands.mu.Unlock() + + cmd := &Command{ + name: pathName(path), + path: path, + commands: make(map[string]*Command), } - field.Set(reflect.ValueOf(defaultValues)) - case reflect.Int8: - defaultValues := make([]int8, 0, len(defaultSlice)) - for _, value := range defaultSlice { - val, err := strconv.Atoi(value) - if err != nil { - panic("Invalid default value for int8 flag") - } - defaultValues = append(defaultValues, int8(val)) + + // Second arg: action function or Options + switch v := args[1].(type) { + case CommandAction: + cmd.action = v + case func(Options) Result[any]: + cmd.action = v + case Options: + cmd.description = v.String("description") + cmd.hidden = v.Bool("hidden") } - field.Set(reflect.ValueOf(defaultValues)) - case reflect.Int16: - defaultValues := make([]int16, 0, len(defaultSlice)) - for _, value := range defaultSlice { - val, err := strconv.Atoi(value) - if err != nil { - panic("Invalid default value for int16 flag") + + // Third arg if present: Options for metadata + if len(args) > 2 { + if opts, ok := args[2].(Options); ok { + cmd.description = opts.String("description") + cmd.hidden = opts.Bool("hidden") } - defaultValues = append(defaultValues, int16(val)) } - field.Set(reflect.ValueOf(defaultValues)) - case reflect.Int32: - defaultValues := make([]int32, 0, len(defaultSlice)) - for _, value := range defaultSlice { - val, err := strconv.ParseInt(value, 10, 32) - if err != nil { - panic("Invalid default value for int32 flag") + + c.commands.commands[path] = cmd + + // Build parent chain — "deploy/to/homelab" creates "deploy" and "deploy/to" if missing + parts := strings.Split(path, "/") + for i := len(parts) - 1; i > 0; i-- { + parentPath := strings.Join(parts[:i], "/") + if _, exists := c.commands.commands[parentPath]; !exists { + c.commands.commands[parentPath] = &Command{ + name: parts[i-1], + path: parentPath, + commands: make(map[string]*Command), + } } - defaultValues = append(defaultValues, int32(val)) + c.commands.commands[parentPath].commands[parts[i]] = cmd + cmd = c.commands.commands[parentPath] } - field.Set(reflect.ValueOf(defaultValues)) - case reflect.Int64: - defaultValues := make([]int64, 0, len(defaultSlice)) - for _, value := range defaultSlice { - val, err := strconv.ParseInt(value, 10, 64) - if err != nil { - panic("Invalid default value for int64 flag") - } - defaultValues = append(defaultValues, val) - } - field.Set(reflect.ValueOf(defaultValues)) - case reflect.Uint: - defaultValues := make([]uint, 0, len(defaultSlice)) - for _, value := range defaultSlice { - val, err := strconv.Atoi(value) - if err != nil { - panic("Invalid default value for uint flag") - } - defaultValues = append(defaultValues, uint(val)) - } - field.Set(reflect.ValueOf(defaultValues)) - case reflect.Uint8: - defaultValues := make([]uint8, 0, len(defaultSlice)) - for _, value := range defaultSlice { - val, err := strconv.Atoi(value) - if err != nil { - panic("Invalid default value for uint8 flag") - } - defaultValues = append(defaultValues, uint8(val)) - } - field.Set(reflect.ValueOf(defaultValues)) - case reflect.Uint16: - defaultValues := make([]uint16, 0, len(defaultSlice)) - for _, value := range defaultSlice { - val, err := strconv.Atoi(value) - if err != nil { - panic("Invalid default value for uint16 flag") - } - defaultValues = append(defaultValues, uint16(val)) - } - field.Set(reflect.ValueOf(defaultValues)) - case reflect.Uint32: - defaultValues := make([]uint32, 0, len(defaultSlice)) - for _, value := range defaultSlice { - val, err := strconv.Atoi(value) - if err != nil { - panic("Invalid default value for uint32 flag") - } - defaultValues = append(defaultValues, uint32(val)) - } - field.Set(reflect.ValueOf(defaultValues)) - case reflect.Uint64: - defaultValues := make([]uint64, 0, len(defaultSlice)) - for _, value := range defaultSlice { - val, err := strconv.Atoi(value) - if err != nil { - panic("Invalid default value for uint64 flag") - } - defaultValues = append(defaultValues, uint64(val)) - } - field.Set(reflect.ValueOf(defaultValues)) - case reflect.Float32: - defaultValues := make([]float32, 0, len(defaultSlice)) - for _, value := range defaultSlice { - val, err := strconv.ParseFloat(value, 32) - if err != nil { - panic("Invalid default value for float32 flag") - } - defaultValues = append(defaultValues, float32(val)) - } - field.Set(reflect.ValueOf(defaultValues)) - case reflect.Float64: - defaultValues := make([]float64, 0, len(defaultSlice)) - for _, value := range defaultSlice { - val, err := strconv.ParseFloat(value, 64) - if err != nil { - panic("Invalid default value for float64 flag") - } - defaultValues = append(defaultValues, float64(val)) - } - field.Set(reflect.ValueOf(defaultValues)) - default: - panic(fmt.Sprintf("addSliceField() not supported slice type %s", t.Elem().Elem().Kind().String())) - } - return c -} -// BoolFlag - Adds a boolean flag to the command -func (c *Command) BoolFlag(name, description string, variable *bool) *Command { - c.flags.BoolVar(variable, name, *variable, description) - c.flagCount++ - return c -} - -// BoolsFlag - Adds a booleans flag to the command -func (c *Command) BoolsFlag(name, description string, variable *[]bool) *Command { - c.flags.Var(newBoolsValue(*variable, variable), name, description) - c.flagCount++ - return c -} - -// StringFlag - Adds a string flag to the command -func (c *Command) StringFlag(name, description string, variable *string) *Command { - c.flags.StringVar(variable, name, *variable, description) - c.flagCount++ - return c -} - -// StringsFlag - Adds a strings flag to the command -func (c *Command) StringsFlag(name, description string, variable *[]string) *Command { - c.flags.Var(newStringsValue(*variable, variable), name, description) - c.flagCount++ - return c -} - -// IntFlag - Adds an int flag to the command -func (c *Command) IntFlag(name, description string, variable *int) *Command { - c.flags.IntVar(variable, name, *variable, description) - c.flagCount++ - return c -} - -// IntsFlag - Adds an ints flag to the command -func (c *Command) IntsFlag(name, description string, variable *[]int) *Command { - c.flags.Var(newIntsValue(*variable, variable), name, description) - c.flagCount++ - return c -} - -// Int8Flag - Adds an int8 flag to the command -func (c *Command) Int8Flag(name, description string, variable *int8) *Command { - c.flags.Var(newInt8Value(*variable, variable), name, description) - c.flagCount++ - return c -} - -// Int8sFlag - Adds an int8 s flag to the command -func (c *Command) Int8sFlag(name, description string, variable *[]int8) *Command { - c.flags.Var(newInt8sValue(*variable, variable), name, description) - c.flagCount++ - return c -} - -// Int16Flag - Adds an int16 flag to the command -func (c *Command) Int16Flag(name, description string, variable *int16) *Command { - c.flags.Var(newInt16Value(*variable, variable), name, description) - c.flagCount++ - return c -} - -// Int16sFlag - Adds an int16s flag to the command -func (c *Command) Int16sFlag(name, description string, variable *[]int16) *Command { - c.flags.Var(newInt16sValue(*variable, variable), name, description) - c.flagCount++ - return c -} - -// Int32Flag - Adds an int32 flag to the command -func (c *Command) Int32Flag(name, description string, variable *int32) *Command { - c.flags.Var(newInt32Value(*variable, variable), name, description) - c.flagCount++ - return c -} - -// Int32sFlag - Adds an int32s flag to the command -func (c *Command) Int32sFlag(name, description string, variable *[]int32) *Command { - c.flags.Var(newInt32sValue(*variable, variable), name, description) - c.flagCount++ - return c -} - -// Int64Flag - Adds an int64 flag to the command -func (c *Command) Int64Flag(name, description string, variable *int64) *Command { - c.flags.Int64Var(variable, name, *variable, description) - c.flagCount++ - return c -} - -// Int64sFlag - Adds an int64s flag to the command -func (c *Command) Int64sFlag(name, description string, variable *[]int64) *Command { - c.flags.Var(newInt64sValue(*variable, variable), name, description) - c.flagCount++ - return c -} - -// UintFlag - Adds an uint flag to the command -func (c *Command) UintFlag(name, description string, variable *uint) *Command { - c.flags.UintVar(variable, name, *variable, description) - c.flagCount++ - return c -} - -// UintsFlag - Adds an uints flag to the command -func (c *Command) UintsFlag(name, description string, variable *[]uint) *Command { - c.flags.Var(newUintsValue(*variable, variable), name, description) - c.flagCount++ - return c -} - -// Uint8Flag - Adds an uint8 flag to the command -func (c *Command) Uint8Flag(name, description string, variable *uint8) *Command { - c.flags.Var(newUint8Value(*variable, variable), name, description) - c.flagCount++ - return c -} - -// Uint8sFlag - Adds an uint8 s flag to the command -func (c *Command) Uint8sFlag(name, description string, variable *[]uint8) *Command { - c.flags.Var(newUint8sValue(*variable, variable), name, description) - c.flagCount++ - return c -} - -// Uint16Flag - Adds an uint16 flag to the command -func (c *Command) Uint16Flag(name, description string, variable *uint16) *Command { - c.flags.Var(newUint16Value(*variable, variable), name, description) - c.flagCount++ - return c -} - -// Uint16sFlag - Adds an uint16s flag to the command -func (c *Command) Uint16sFlag(name, description string, variable *[]uint16) *Command { - c.flags.Var(newUint16sValue(*variable, variable), name, description) - c.flagCount++ - return c -} - -// Uint32Flag - Adds an uint32 flag to the command -func (c *Command) Uint32Flag(name, description string, variable *uint32) *Command { - c.flags.Var(newUint32Value(*variable, variable), name, description) - c.flagCount++ - return c -} - -// Uint32sFlag - Adds an uint32s flag to the command -func (c *Command) Uint32sFlag(name, description string, variable *[]uint32) *Command { - c.flags.Var(newUint32sValue(*variable, variable), name, description) - c.flagCount++ - return c -} - -// UInt64Flag - Adds an uint64 flag to the command -func (c *Command) UInt64Flag(name, description string, variable *uint64) *Command { - c.flags.Uint64Var(variable, name, *variable, description) - c.flagCount++ - return c -} - -// Uint64sFlag - Adds an uint64s flag to the command -func (c *Command) Uint64sFlag(name, description string, variable *[]uint64) *Command { - c.flags.Var(newUint64sValue(*variable, variable), name, description) - c.flagCount++ - return c -} - -// Float64Flag - Adds a float64 flag to the command -func (c *Command) Float64Flag(name, description string, variable *float64) *Command { - c.flags.Float64Var(variable, name, *variable, description) - c.flagCount++ - return c -} - -// Float32Flag - Adds a float32 flag to the command -func (c *Command) Float32Flag(name, description string, variable *float32) *Command { - c.flags.Var(newFloat32Value(*variable, variable), name, description) - c.flagCount++ - return c -} - -// Float32sFlag - Adds a float32s flag to the command -func (c *Command) Float32sFlag(name, description string, variable *[]float32) *Command { - c.flags.Var(newFloat32sValue(*variable, variable), name, description) - c.flagCount++ - return c -} - -// Float64sFlag - Adds a float64s flag to the command -func (c *Command) Float64sFlag(name, description string, variable *[]float64) *Command { - c.flags.Var(newFloat64sValue(*variable, variable), name, description) - c.flagCount++ - return c -} - -type boolsFlagVar []bool - -func (f *boolsFlagVar) String() string { return fmt.Sprint([]bool(*f)) } - -func (f *boolsFlagVar) Set(value string) error { - if value == "" { - *f = append(*f, false) return nil } - b, err := strconv.ParseBool(value) - if err != nil { - return err - } - *f = append(*f, b) - return nil } -func (f *boolsFlagVar) IsBoolFlag() bool { - return true -} - -func newBoolsValue(val []bool, p *[]bool) *boolsFlagVar { - *p = val - return (*boolsFlagVar)(p) -} - -type stringsFlagVar []string - -func (f *stringsFlagVar) String() string { return fmt.Sprint([]string(*f)) } - -func (f *stringsFlagVar) Set(value string) error { - *f = append(*f, value) - return nil -} - -func newStringsValue(val []string, p *[]string) *stringsFlagVar { - *p = val - return (*stringsFlagVar)(p) -} - -type intsFlagVar []int - -func (f *intsFlagVar) String() string { return fmt.Sprint([]int(*f)) } - -func (f *intsFlagVar) Set(value string) error { - i, err := strconv.Atoi(value) - if err != nil { - return err - } - *f = append(*f, i) - return nil -} - -func newIntsValue(val []int, p *[]int) *intsFlagVar { - *p = val - return (*intsFlagVar)(p) -} - -type int8Value int8 - -func newInt8Value(val int8, p *int8) *int8Value { - *p = val - return (*int8Value)(p) -} - -func (f *int8Value) Set(value string) error { - i, err := strconv.Atoi(value) - if err != nil { - return err - } - *f = int8Value(i) - return nil -} - -func (f *int8Value) String() string { return fmt.Sprint(int8(*f)) } - -type int8sFlagVar []int8 - -func (f *int8sFlagVar) String() string { return fmt.Sprint([]int8(*f)) } - -func (f *int8sFlagVar) Set(value string) error { - i, err := strconv.Atoi(value) - if err != nil { - return err - } - *f = append(*f, int8(i)) - return nil -} - -func newInt8sValue(val []int8, p *[]int8) *int8sFlagVar { - *p = val - return (*int8sFlagVar)(p) -} - -type int16Value int16 - -func newInt16Value(val int16, p *int16) *int16Value { - *p = val - return (*int16Value)(p) -} - -func (f *int16Value) Set(value string) error { - i, err := strconv.Atoi(value) - if err != nil { - return err - } - *f = int16Value(i) - return nil -} - -func (f *int16Value) String() string { return fmt.Sprint(int16(*f)) } - -type int16sFlagVar []int16 - -func (f *int16sFlagVar) String() string { return fmt.Sprint([]int16(*f)) } - -func (f *int16sFlagVar) Set(value string) error { - i, err := strconv.Atoi(value) - if err != nil { - return err - } - *f = append(*f, int16(i)) - return nil -} - -func newInt16sValue(val []int16, p *[]int16) *int16sFlagVar { - *p = val - return (*int16sFlagVar)(p) -} - -type int32Value int32 - -func newInt32Value(val int32, p *int32) *int32Value { - *p = val - return (*int32Value)(p) -} - -func (f *int32Value) Set(value string) error { - i, err := strconv.Atoi(value) - if err != nil { - return err - } - *f = int32Value(i) - return nil -} - -func (f *int32Value) String() string { return fmt.Sprint(int32(*f)) } - -type int32sFlagVar []int32 - -func (f *int32sFlagVar) String() string { return fmt.Sprint([]int32(*f)) } - -func (f *int32sFlagVar) Set(value string) error { - i, err := strconv.Atoi(value) - if err != nil { - return err - } - *f = append(*f, int32(i)) - return nil -} - -func newInt32sValue(val []int32, p *[]int32) *int32sFlagVar { - *p = val - return (*int32sFlagVar)(p) -} - -type int64sFlagVar []int64 - -func (f *int64sFlagVar) String() string { return fmt.Sprint([]int64(*f)) } - -func (f *int64sFlagVar) Set(value string) error { - i, err := strconv.ParseInt(value, 10, 64) - if err != nil { - return err - } - *f = append(*f, i) - return nil -} - -func newInt64sValue(val []int64, p *[]int64) *int64sFlagVar { - *p = val - return (*int64sFlagVar)(p) -} - -type uintsFlagVar []uint - -func (f *uintsFlagVar) String() string { - return fmt.Sprint([]uint(*f)) -} - -func (f *uintsFlagVar) Set(value string) error { - i, err := strconv.Atoi(value) - if err != nil { - return err - } - *f = append(*f, uint(i)) - return nil -} - -func newUintsValue(val []uint, p *[]uint) *uintsFlagVar { - *p = val - return (*uintsFlagVar)(p) -} - -type uint8FlagVar uint8 - -func newUint8Value(val uint8, p *uint8) *uint8FlagVar { - *p = val - return (*uint8FlagVar)(p) -} - -func (f *uint8FlagVar) String() string { - return fmt.Sprint(uint8(*f)) -} - -func (f *uint8FlagVar) Set(value string) error { - i, err := strconv.Atoi(value) - if err != nil { - return err - } - *f = uint8FlagVar(i) - return nil -} - -type uint8sFlagVar []uint8 - -func (f *uint8sFlagVar) String() string { - return fmt.Sprint([]uint8(*f)) -} - -func (f *uint8sFlagVar) Set(value string) error { - i, err := strconv.Atoi(value) - if err != nil { - return err - } - *f = append(*f, uint8(i)) - return nil -} - -func newUint8sValue(val []uint8, p *[]uint8) *uint8sFlagVar { - *p = val - return (*uint8sFlagVar)(p) -} - -type uint16FlagVar uint16 - -func newUint16Value(val uint16, p *uint16) *uint16FlagVar { - *p = val - return (*uint16FlagVar)(p) -} - -func (f *uint16FlagVar) String() string { - return fmt.Sprint(uint16(*f)) -} - -func (f *uint16FlagVar) Set(value string) error { - i, err := strconv.Atoi(value) - if err != nil { - return err - } - *f = uint16FlagVar(i) - return nil -} - -type uint16sFlagVar []uint16 - -func (f *uint16sFlagVar) String() string { - return fmt.Sprint([]uint16(*f)) -} - -func (f *uint16sFlagVar) Set(value string) error { - i, err := strconv.Atoi(value) - if err != nil { - return err - } - *f = append(*f, uint16(i)) - return nil -} - -func newUint16sValue(val []uint16, p *[]uint16) *uint16sFlagVar { - *p = val - return (*uint16sFlagVar)(p) -} - -type uint32FlagVar uint32 - -func newUint32Value(val uint32, p *uint32) *uint32FlagVar { - *p = val - return (*uint32FlagVar)(p) -} - -func (f *uint32FlagVar) String() string { - return fmt.Sprint(uint32(*f)) -} - -func (f *uint32FlagVar) Set(value string) error { - i, err := strconv.Atoi(value) - if err != nil { - return err - } - *f = uint32FlagVar(i) - return nil -} - -type uint32sFlagVar []uint32 - -func (f *uint32sFlagVar) String() string { - return fmt.Sprint([]uint32(*f)) -} - -func (f *uint32sFlagVar) Set(value string) error { - i, err := strconv.Atoi(value) - if err != nil { - return err - } - *f = append(*f, uint32(i)) - return nil -} - -func newUint32sValue(val []uint32, p *[]uint32) *uint32sFlagVar { - *p = val - return (*uint32sFlagVar)(p) -} - -type uint64sFlagVar []uint64 - -func (f *uint64sFlagVar) String() string { return fmt.Sprint([]uint64(*f)) } - -func (f *uint64sFlagVar) Set(value string) error { - i, err := strconv.ParseUint(value, 10, 64) - if err != nil { - return err - } - *f = append(*f, i) - return nil -} - -func newUint64sValue(val []uint64, p *[]uint64) *uint64sFlagVar { - *p = val - return (*uint64sFlagVar)(p) -} - -type float32sFlagVar []float32 - -func (f *float32sFlagVar) String() string { return fmt.Sprint([]float32(*f)) } - -func (f *float32sFlagVar) Set(value string) error { - i, err := strconv.ParseFloat(value, 64) - if err != nil { - return err - } - *f = append(*f, float32(i)) - return nil -} - -func newFloat32sValue(val []float32, p *[]float32) *float32sFlagVar { - *p = val - return (*float32sFlagVar)(p) -} - -type float32FlagVar float32 - -func (f *float32FlagVar) String() string { return fmt.Sprint(float32(*f)) } - -func (f *float32FlagVar) Set(value string) error { - i, err := strconv.ParseFloat(value, 64) - if err != nil { - return err - } - *f = float32FlagVar(i) - return nil -} - -func newFloat32Value(val float32, p *float32) *float32FlagVar { - *p = val - return (*float32FlagVar)(p) -} - -type float64sFlagVar []float64 - -func (f *float64sFlagVar) String() string { return fmt.Sprint([]float64(*f)) } - -func (f *float64sFlagVar) Set(value string) error { - i, err := strconv.ParseFloat(value, 64) - if err != nil { - return err - } - *f = append(*f, i) - return nil -} - -func newFloat64sValue(val []float64, p *[]float64) *float64sFlagVar { - *p = val - return (*float64sFlagVar)(p) -} - -// LongDescription - Sets the long description for the command -func (c *Command) LongDescription(longdescription string) *Command { - c.longdescription = longdescription - return c -} - -// OtherArgs - Returns the non-flag arguments passed to the subcommand. NOTE: This should only be called within the context of an action. -func (c *Command) OtherArgs() []string { - return c.flags.Args() -} - -func (c *Command) NewChildCommandFunction(name string, description string, fn any) *Command { - result := c.NewChildCommand(name, description) - // use reflection to determine if this is a function - // if not, panic - t := reflect.TypeOf(fn) - if t.Kind() != reflect.Func { - panic("NewChildCommandFunction '" + name + "' requires a function with the signature 'func(*struct) error'") - } - - // Check the function has 1 input ant it's a struct pointer - fnValue := reflect.ValueOf(fn) - if t.NumIn() != 1 { - panic("NewChildCommandFunction '" + name + "' requires a function with the signature 'func(*struct) error'") - } - // Check the input is a struct pointer - if t.In(0).Kind() != reflect.Ptr { - panic("NewChildCommandFunction '" + name + "' requires a function with the signature 'func(*struct) error'") - } - if t.In(0).Elem().Kind() != reflect.Struct { - panic("NewChildCommandFunction '" + name + "' requires a function with the signature 'func(*struct) error'") - } - // Check only 1 output and it's an error - if t.NumOut() != 1 { - panic("NewChildCommandFunction '" + name + "' requires a function with the signature 'func(*struct) error'") - } - if t.Out(0) != reflect.TypeOf((*error)(nil)).Elem() { - panic("NewChildCommandFunction '" + name + "' requires a function with the signature 'func(*struct) error'") - } - flags := reflect.New(t.In(0).Elem()) - result.Action(func() error { - result := fnValue.Call([]reflect.Value{flags})[0].Interface() - if result != nil { - return result.(error) - } +// Commands returns all registered command paths. +// +// paths := c.Commands() +func (c *Core) Commands() []string { + if c.commands == nil { return nil - }) - result.AddFlags(flags.Interface()) - return result + } + c.commands.mu.RLock() + defer c.commands.mu.RUnlock() + var paths []string + for k := range c.commands.commands { + paths = append(paths, k) + } + return paths } -func (c *Command) parsePositionalArgs(args []string) error { - for index, posArg := range args { - // Check the map for a field for this arg - key := strconv.Itoa(index + 1) - field, ok := c.positionalArgsMap[key] - if !ok { - continue - } - fieldType := field.Type() - switch fieldType.Kind() { - case reflect.Bool: - // set value of field to true - field.SetBool(true) - case reflect.String: - field.SetString(posArg) - case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int: - value, err := strconv.ParseInt(posArg, 10, 64) - if err != nil { - return err - } - field.SetInt(value) - case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint: - value, err := strconv.ParseUint(posArg, 10, 64) - if err != nil { - return err - } - field.SetUint(value) - case reflect.Float64, reflect.Float32: - value, err := strconv.ParseFloat(posArg, 64) - if err != nil { - return err - } - field.SetFloat(value) - case reflect.Slice: - c.addSliceField(field, posArg, c.sliceSeparator[key]) - default: - return E("cli.parsePositionalArgs", "unsupported type for positional argument: "+fieldType.Name(), nil) - } - } - return nil +// pathName extracts the last segment of a path. +// "deploy/to/homelab" → "homelab" +func pathName(path string) string { + parts := strings.Split(path, "/") + return parts[len(parts)-1] } diff --git a/pkg/core/contract.go b/pkg/core/contract.go index c039cf3..143f757 100644 --- a/pkg/core/contract.go +++ b/pkg/core/contract.go @@ -80,7 +80,6 @@ func New(opts ...Options) *Core { config: &Config{ConfigOpts: &ConfigOpts{}}, error: &ErrorPanic{}, log: &ErrorLog{log: defaultLog}, - cli: &Cli{opts: &CliOpts{}}, service: &Service{}, lock: &Lock{}, ipc: &Ipc{}, @@ -95,10 +94,8 @@ func New(opts ...Options) *Core { } } - // Init Cli root command from app name - c.cli.rootCommand = NewCommand(c.app.Name) - c.cli.rootCommand.setParentCommandPath("") - c.cli.rootCommand.setApp(c.cli) + // Init Cli surface with Core reference + c.cli = &Cli{core: c} return c } diff --git a/pkg/core/core.go b/pkg/core/core.go index 2be4716..e2ea084 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -22,8 +22,9 @@ type Core struct { config *Config // c.Config() — Configuration, settings, feature flags error *ErrorPanic // c.Error() — Panic recovery and crash reporting log *ErrorLog // c.Log() — Structured logging + error wrapping - cli *Cli // c.Cli() — CLI command framework - service *Service // c.Service("name") — Service registry and lifecycle + cli *Cli // c.Cli() — CLI surface layer + commands *commandRegistry // c.Command("path") — Command tree + service *Service // c.Service("name") — Service registry and lifecycle lock *Lock // c.Lock("name") — Named mutexes ipc *Ipc // c.IPC() — Message bus for IPC i18n *I18n // c.I18n() — Internationalisation and locale collection diff --git a/tests/cli_test.go b/tests/cli_test.go index 1875be7..d3c5be7 100644 --- a/tests/cli_test.go +++ b/tests/cli_test.go @@ -7,70 +7,72 @@ import ( "github.com/stretchr/testify/assert" ) -// --- Cli --- +// --- Cli Surface --- func TestCli_Good(t *testing.T) { c := New() assert.NotNil(t, c.Cli()) - assert.NotNil(t, c.Cli().Command()) } -func TestCli_Named_Good(t *testing.T) { +func TestCli_Banner_Good(t *testing.T) { c := New(Options{{K: "name", V: "myapp"}}) - assert.NotNil(t, c.Cli().Command()) + assert.Equal(t, "myapp", c.Cli().Banner()) } -func TestCli_NewChildCommand_Good(t *testing.T) { - c := New(Options{{K: "name", V: "myapp"}}) - child := c.Cli().NewChildCommand("test", "a test command") - assert.NotNil(t, child) -} - -func TestCli_AddCommand_Good(t *testing.T) { +func TestCli_SetBanner_Good(t *testing.T) { c := New() - cmd := NewCommand("hello", "says hello") - c.Cli().AddCommand(cmd) -} - -func TestCli_Flags_Good(t *testing.T) { - c := New() - var name string - var debug bool - var port int - c.Cli().StringFlag("name", "app name", &name) - c.Cli().BoolFlag("debug", "enable debug", &debug) - c.Cli().IntFlag("port", "port number", &port) + c.Cli().SetBanner(func(_ *Cli) string { return "Custom Banner" }) + assert.Equal(t, "Custom Banner", c.Cli().Banner()) } func TestCli_Run_Good(t *testing.T) { c := New() executed := false - c.Cli().Command().Action(func() error { + c.Command("hello", func(_ Options) Result[any] { executed = true - return nil + return Result[any]{Value: "world", OK: true} }) - err := c.Cli().Run("") - assert.NoError(t, err) + r := c.Cli().Run("hello") + assert.True(t, r.OK) + assert.Equal(t, "world", r.Value) assert.True(t, executed) } -// --- Command --- - -func TestCommand_New_Good(t *testing.T) { - cmd := NewCommand("test", "a test command") - assert.NotNil(t, cmd) +func TestCli_Run_Nested_Good(t *testing.T) { + c := New() + executed := false + c.Command("deploy/to/homelab", func(_ Options) Result[any] { + executed = true + return Result[any]{OK: true} + }) + r := c.Cli().Run("deploy", "to", "homelab") + assert.True(t, r.OK) + assert.True(t, executed) } -func TestCommand_Child_Good(t *testing.T) { - parent := NewCommand("root") - child := parent.NewChildCommand("sub", "a subcommand") - assert.NotNil(t, child) +func TestCli_Run_WithFlags_Good(t *testing.T) { + c := New() + var received Options + c.Command("serve", func(opts Options) Result[any] { + received = opts + return Result[any]{OK: true} + }) + c.Cli().Run("serve", "--port=8080", "--debug") + assert.Equal(t, "8080", received.String("port")) + assert.True(t, received.Bool("debug")) } -func TestCommand_Flags_Good(t *testing.T) { - cmd := NewCommand("test") - var name string - var debug bool - cmd.StringFlag("name", "app name", &name) - cmd.BoolFlag("debug", "enable debug", &debug) +func TestCli_Run_NoCommand_Good(t *testing.T) { + c := New() + // No commands registered — should not panic + r := c.Cli().Run() + assert.False(t, r.OK) +} + +func TestCli_PrintHelp_Good(t *testing.T) { + c := New(Options{{K: "name", V: "myapp"}}) + c.Command("deploy", func(_ Options) Result[any] { return Result[any]{OK: true} }) + c.Command("serve", func(_ Options) Result[any] { return Result[any]{OK: true} }) + // Should not panic + c.Cli().PrintHelp() } diff --git a/tests/command_test.go b/tests/command_test.go new file mode 100644 index 0000000..8b2294b --- /dev/null +++ b/tests/command_test.go @@ -0,0 +1,137 @@ +package core_test + +import ( + "testing" + + . "forge.lthn.ai/core/go/pkg/core" + "github.com/stretchr/testify/assert" +) + +// --- Command DTO --- + +func TestCommand_Register_Good(t *testing.T) { + c := New() + result := c.Command("deploy", func(_ Options) Result[any] { + return Result[any]{Value: "deployed", OK: true} + }) + assert.Nil(t, result) // nil = success +} + +func TestCommand_Get_Good(t *testing.T) { + c := New() + c.Command("deploy", func(_ Options) Result[any] { + return Result[any]{OK: true} + }) + cmd := c.Command("deploy") + assert.NotNil(t, cmd) +} + +func TestCommand_Get_Bad(t *testing.T) { + c := New() + cmd := c.Command("nonexistent") + assert.Nil(t, cmd) +} + +func TestCommand_Run_Good(t *testing.T) { + c := New() + c.Command("greet", func(opts Options) Result[any] { + return Result[any]{Value: "hello " + opts.String("name"), OK: true} + }) + cmd := c.Command("greet").(*Command) + r := cmd.Run(Options{{K: "name", V: "world"}}) + assert.True(t, r.OK) + assert.Equal(t, "hello world", r.Value) +} + +func TestCommand_Run_NoAction_Good(t *testing.T) { + c := New() + c.Command("empty", Options{{K: "description", V: "no action"}}) + cmd := c.Command("empty").(*Command) + r := cmd.Run(Options{}) + assert.False(t, r.OK) +} + +// --- Nested Commands --- + +func TestCommand_Nested_Good(t *testing.T) { + c := New() + c.Command("deploy/to/homelab", func(_ Options) Result[any] { + return Result[any]{Value: "deployed to homelab", OK: true} + }) + + // Direct path lookup + cmd := c.Command("deploy/to/homelab") + assert.NotNil(t, cmd) + + // Parent auto-created + parent := c.Command("deploy") + assert.NotNil(t, parent) + + mid := c.Command("deploy/to") + assert.NotNil(t, mid) +} + +func TestCommand_Paths_Good(t *testing.T) { + c := New() + c.Command("deploy", func(_ Options) Result[any] { return Result[any]{OK: true} }) + c.Command("serve", func(_ Options) Result[any] { return Result[any]{OK: true} }) + c.Command("deploy/to/homelab", func(_ Options) Result[any] { return Result[any]{OK: true} }) + + paths := c.Commands() + assert.Contains(t, paths, "deploy") + assert.Contains(t, paths, "serve") + assert.Contains(t, paths, "deploy/to/homelab") + assert.Contains(t, paths, "deploy/to") // auto-created parent +} + +// --- I18n Key Derivation --- + +func TestCommand_I18nKey_Good(t *testing.T) { + c := New() + c.Command("deploy/to/homelab", func(_ Options) Result[any] { return Result[any]{OK: true} }) + cmd := c.Command("deploy/to/homelab").(*Command) + assert.Equal(t, "cmd.deploy.to.homelab.description", cmd.I18nKey()) +} + +func TestCommand_I18nKey_Custom_Good(t *testing.T) { + c := New() + c.Command("deploy", func(_ Options) Result[any] { return Result[any]{OK: true} }, Options{{K: "description", V: "custom.deploy.key"}}) + cmd := c.Command("deploy").(*Command) + assert.Equal(t, "custom.deploy.key", cmd.I18nKey()) +} + +func TestCommand_I18nKey_Simple_Good(t *testing.T) { + c := New() + c.Command("serve", func(_ Options) Result[any] { return Result[any]{OK: true} }) + cmd := c.Command("serve").(*Command) + assert.Equal(t, "cmd.serve.description", cmd.I18nKey()) +} + +// --- Lifecycle --- + +func TestCommand_Lifecycle_NoImpl_Good(t *testing.T) { + c := New() + c.Command("serve", func(_ Options) Result[any] { + return Result[any]{Value: "running", OK: true} + }) + cmd := c.Command("serve").(*Command) + + // Start falls back to Run when no lifecycle impl + r := cmd.Start(Options{}) + assert.True(t, r.OK) + assert.Equal(t, "running", r.Value) + + // Stop/Restart/Reload/Signal return empty Result without lifecycle + assert.False(t, cmd.Stop().OK) + assert.False(t, cmd.Restart().OK) + assert.False(t, cmd.Reload().OK) + assert.False(t, cmd.Signal("HUP").OK) +} + +// --- Empty path --- + +func TestCommand_EmptyPath_Bad(t *testing.T) { + c := New() + result := c.Command("", func(_ Options) Result[any] { return Result[any]{OK: true} }) + assert.NotNil(t, result) // error +} diff --git a/tests/testdata/cli_clir.go.bak b/tests/testdata/cli_clir.go.bak new file mode 100644 index 0000000..fd4c33b --- /dev/null +++ b/tests/testdata/cli_clir.go.bak @@ -0,0 +1,1339 @@ +package core + +import ( + "flag" + "fmt" + "io" + "os" + "reflect" + "strconv" + "strings" +) + +// Command represents a command that may be run by the user +type clirCommand struct { + name string + commandPath string + shortdescription string + longdescription string + subCommands []*clirCommand + subCommandsMap map[string]*clirCommand + longestSubcommand int + actionCallback CliAction + app *Cli + flags *flag.FlagSet + flagCount int + helpFlag bool + hidden bool + positionalArgsMap map[string]reflect.Value + sliceSeparator map[string]string +} + +// NewCommand creates a new Command. +// Description is optional — if omitted, i18n resolves it from the command path. +func newClirCommand(name string, description ...string) *clirCommand { + desc := "" + if len(description) > 0 { + desc = description[0] + } + result := &Command{ + name: name, + shortdescription: desc, + subCommandsMap: make(map[string]*clirCommand), + hidden: false, + positionalArgsMap: make(map[string]reflect.Value), + sliceSeparator: make(map[string]string), + } + + // Init flagset so flags can be added before Run + result.setParentCommandPath("") + + return result +} + +func (c *clirCommand) setParentCommandPath(parentCommandPath string) { + // Set up command path + if parentCommandPath != "" { + c.commandPath += parentCommandPath + " " + } + c.commandPath += c.name + + // Set up flag set + c.flags = flag.NewFlagSet(c.commandPath, flag.ContinueOnError) + c.BoolFlag("help", "Get help on the '"+strings.ToLower(c.commandPath)+"' command.", &c.helpFlag) + + // result.Flags.Usage = result.PrintHelp + +} + +func (c *clirCommand) inheritFlags(inheritFlags *flag.FlagSet) { + // inherit flags + inheritFlags.VisitAll(func(f *flag.Flag) { + if f.Name != "help" { + c.flags.Var(f.Value, f.Name, f.Usage) + } + }) +} + +func (c *clirCommand) setApp(app *Cli) { + c.app = app +} + +// parseFlags parses the given flags +func (c *clirCommand) parseFlags(args []string) error { + // Parse flags + // Suppress flag parse errors to stderr + + c.flags.SetOutput(io.Discard) + + // Credit: https://stackoverflow.com/a/74146375 + var positionalArgs []string + for { + if err := c.flags.Parse(args); err != nil { + return err + } + // Consume all the flags that were parsed as flags. + args = args[len(args)-c.flags.NArg():] + if len(args) == 0 { + break + } + // There's at least one flag remaining and it must be a positional arg since + // we consumed all args that were parsed as flags. Consume just the first + // one, and retry parsing, since subsequent args may be flags. + positionalArgs = append(positionalArgs, args[0]) + args = args[1:] + } + + // Parse just the positional args so that flagset.Args()/flagset.NArgs() + // return the expected value. + // Note: This should never return an error. + err := c.flags.Parse(positionalArgs) + if err != nil { + return err + } + + if len(positionalArgs) > 0 { + return c.parsePositionalArgs(positionalArgs) + } + return nil +} + +// Run - Runs the Command with the given arguments +func (c *clirCommand) run(args []string) error { + + // If we have arguments, process them + if len(args) > 0 { + // Check for subcommand + subcommand := c.subCommandsMap[args[0]] + if subcommand != nil { + return subcommand.run(args[1:]) + } + + // Parse flags + err := c.parseFlags(args) + if err != nil { + if c.app.errorHandler != nil { + return c.app.errorHandler(c.commandPath, err) + } + return E("cli.Run", fmt.Sprintf("see '%s --help' for usage", c.commandPath), err) + } + + // Help takes precedence + if c.helpFlag { + c.PrintHelp() + return nil + } + } + + // Do we have an action? + if c.actionCallback != nil { + return c.actionCallback() + } + + // If we haven't specified a subcommand + // check for an app level default command + if c.app.defaultCommand != nil { + // Prevent recursion! + if c.app.defaultCommand != c { + // only run default command if no args passed + if len(args) == 0 { + return c.app.defaultCommand.run(args) + } + } + } + + // Nothing left we can do + c.PrintHelp() + + return nil +} + +// Action - Define an action from this command +func (c *clirCommand) Action(callback CliAction) *clirCommand { + c.actionCallback = callback + return c +} + +// PrintHelp - Output the help text for this command +func (c *clirCommand) PrintHelp() { + c.app.PrintBanner() + + commandTitle := c.commandPath + if c.shortdescription != "" { + commandTitle += " - " + c.shortdescription + } + // Ignore root command + if c.commandPath != c.name { + fmt.Println(commandTitle) + } + if c.longdescription != "" { + fmt.Println(c.longdescription + "\n") + } + if len(c.subCommands) > 0 { + fmt.Println("Available commands:") + fmt.Println("") + for _, subcommand := range c.subCommands { + if subcommand.isHidden() { + continue + } + spacer := strings.Repeat(" ", 3+c.longestSubcommand-len(subcommand.name)) + isDefault := "" + if subcommand.isDefaultCommand() { + isDefault = "[default]" + } + fmt.Printf(" %s%s%s %s\n", subcommand.name, spacer, subcommand.shortdescription, isDefault) + } + fmt.Println("") + } + if c.flagCount > 0 { + fmt.Println("Flags:") + fmt.Println() + c.flags.SetOutput(os.Stdout) + c.flags.PrintDefaults() + c.flags.SetOutput(os.Stderr) + + } + fmt.Println() +} + +// isDefaultCommand returns true if called on the default command +func (c *clirCommand) isDefaultCommand() bool { + return c.app.defaultCommand == c +} + +// isHidden returns true if the command is a hidden command +func (c *clirCommand) isHidden() bool { + return c.hidden +} + +// Hidden hides the command from the Help system +func (c *clirCommand) Hidden() { + c.hidden = true +} + +// NewChildCommand - Creates a new subcommand +func (c *clirCommand) NewChildCommand(name string, description ...string) *clirCommand { + result := NewCommand(name, description...) + c.AddCommand(result) + return result +} + +// AddCommand - Adds a subcommand +func (c *clirCommand) AddCommand(command *clirCommand) { + command.setApp(c.app) + command.setParentCommandPath(c.commandPath) + name := command.name + c.subCommands = append(c.subCommands, command) + c.subCommandsMap[name] = command + if len(name) > c.longestSubcommand { + c.longestSubcommand = len(name) + } +} + +// NewChildCommandInheritFlags - Creates a new subcommand, inherits flags from command +func (c *clirCommand) NewChildCommandInheritFlags(name string, description ...string) *clirCommand { + result := c.NewChildCommand(name, description...) + result.inheritFlags(c.flags) + return result +} + +func (c *clirCommand) AddFlags(optionStruct any) *clirCommand { + // use reflection to determine if this is a pointer to a struct + // if not, panic + + t := reflect.TypeOf(optionStruct) + + // Check for a pointer to a struct + if t.Kind() != reflect.Ptr { + panic("AddFlags() requires a pointer to a struct") + } + if t.Elem().Kind() != reflect.Struct { + panic("AddFlags() requires a pointer to a struct") + } + + // Iterate through the fields of the struct reading the struct tags + // and adding the flags + v := reflect.ValueOf(optionStruct).Elem() + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + fieldType := t.Elem().Field(i) + if !fieldType.IsExported() { + continue + } + // If this is an embedded struct, recurse + if fieldType.Type.Kind() == reflect.Struct { + c.AddFlags(field.Addr().Interface()) + continue + } + + tag := t.Elem().Field(i).Tag + name := tag.Get("name") + description := tag.Get("description") + defaultValue := tag.Get("default") + pos := tag.Get("pos") + sep := tag.Get("sep") + c.positionalArgsMap[pos] = field + if sep != "" { + c.sliceSeparator[pos] = sep + } + if name == "" { + name = strings.ToLower(t.Elem().Field(i).Name) + } + switch field.Kind() { + case reflect.Bool: + var defaultValueBool bool + if defaultValue != "" { + var err error + defaultValueBool, err = strconv.ParseBool(defaultValue) + if err != nil { + panic("Invalid default value for bool flag") + } + } + field.SetBool(defaultValueBool) + c.BoolFlag(name, description, field.Addr().Interface().(*bool)) + case reflect.String: + if defaultValue != "" { + // set value of field to default value + field.SetString(defaultValue) + } + c.StringFlag(name, description, field.Addr().Interface().(*string)) + case reflect.Int: + if defaultValue != "" { + // set value of field to default value + value, err := strconv.Atoi(defaultValue) + if err != nil { + panic("Invalid default value for int flag") + } + field.SetInt(int64(value)) + } + c.IntFlag(name, description, field.Addr().Interface().(*int)) + case reflect.Int8: + if defaultValue != "" { + // set value of field to default value + value, err := strconv.Atoi(defaultValue) + if err != nil { + panic("Invalid default value for int8 flag") + } + field.SetInt(int64(value)) + } + c.Int8Flag(name, description, field.Addr().Interface().(*int8)) + case reflect.Int16: + if defaultValue != "" { + // set value of field to default value + value, err := strconv.Atoi(defaultValue) + if err != nil { + panic("Invalid default value for int16 flag") + } + field.SetInt(int64(value)) + } + c.Int16Flag(name, description, field.Addr().Interface().(*int16)) + case reflect.Int32: + if defaultValue != "" { + // set value of field to default value + value, err := strconv.Atoi(defaultValue) + if err != nil { + panic("Invalid default value for int32 flag") + } + field.SetInt(int64(value)) + } + c.Int32Flag(name, description, field.Addr().Interface().(*int32)) + case reflect.Int64: + if defaultValue != "" { + // set value of field to default value + value, err := strconv.Atoi(defaultValue) + if err != nil { + panic("Invalid default value for int64 flag") + } + field.SetInt(int64(value)) + } + c.Int64Flag(name, description, field.Addr().Interface().(*int64)) + case reflect.Uint: + if defaultValue != "" { + // set value of field to default value + value, err := strconv.Atoi(defaultValue) + if err != nil { + panic("Invalid default value for uint flag") + } + field.SetUint(uint64(value)) + } + c.UintFlag(name, description, field.Addr().Interface().(*uint)) + case reflect.Uint8: + if defaultValue != "" { + // set value of field to default value + value, err := strconv.Atoi(defaultValue) + if err != nil { + panic("Invalid default value for uint8 flag") + } + field.SetUint(uint64(value)) + } + c.Uint8Flag(name, description, field.Addr().Interface().(*uint8)) + case reflect.Uint16: + if defaultValue != "" { + // set value of field to default value + value, err := strconv.Atoi(defaultValue) + if err != nil { + panic("Invalid default value for uint16 flag") + } + field.SetUint(uint64(value)) + } + c.Uint16Flag(name, description, field.Addr().Interface().(*uint16)) + case reflect.Uint32: + if defaultValue != "" { + // set value of field to default value + value, err := strconv.Atoi(defaultValue) + if err != nil { + panic("Invalid default value for uint32 flag") + } + field.SetUint(uint64(value)) + } + c.Uint32Flag(name, description, field.Addr().Interface().(*uint32)) + case reflect.Uint64: + if defaultValue != "" { + // set value of field to default value + value, err := strconv.Atoi(defaultValue) + if err != nil { + panic("Invalid default value for uint64 flag") + } + field.SetUint(uint64(value)) + } + c.UInt64Flag(name, description, field.Addr().Interface().(*uint64)) + case reflect.Float32: + if defaultValue != "" { + // set value of field to default value + value, err := strconv.ParseFloat(defaultValue, 64) + if err != nil { + panic("Invalid default value for float32 flag") + } + field.SetFloat(value) + } + c.Float32Flag(name, description, field.Addr().Interface().(*float32)) + case reflect.Float64: + if defaultValue != "" { + // set value of field to default value + value, err := strconv.ParseFloat(defaultValue, 64) + if err != nil { + panic("Invalid default value for float64 flag") + } + field.SetFloat(value) + } + c.Float64Flag(name, description, field.Addr().Interface().(*float64)) + case reflect.Slice: + c.addSliceField(field, defaultValue, sep) + c.addSliceFlags(name, description, field) + default: + if pos != "" { + fmt.Fprintf(os.Stderr, "WARNING: unsupported type for flag: %s %s\n", fieldType.Type.Kind(), name) + } + } + } + + return c +} + +func (c *clirCommand) addSliceFlags(name, description string, field reflect.Value) *clirCommand { + if field.Kind() != reflect.Slice { + panic("addSliceFlags() requires a pointer to a slice") + } + t := reflect.TypeOf(field.Addr().Interface()) + if t.Kind() != reflect.Ptr { + panic("addSliceFlags() requires a pointer to a slice") + } + if t.Elem().Kind() != reflect.Slice { + panic("addSliceFlags() requires a pointer to a slice") + } + switch t.Elem().Elem().Kind() { + case reflect.Bool: + c.BoolsFlag(name, description, field.Addr().Interface().(*[]bool)) + case reflect.String: + c.StringsFlag(name, description, field.Addr().Interface().(*[]string)) + case reflect.Int: + c.IntsFlag(name, description, field.Addr().Interface().(*[]int)) + case reflect.Int8: + c.Int8sFlag(name, description, field.Addr().Interface().(*[]int8)) + case reflect.Int16: + c.Int16sFlag(name, description, field.Addr().Interface().(*[]int16)) + case reflect.Int32: + c.Int32sFlag(name, description, field.Addr().Interface().(*[]int32)) + case reflect.Int64: + c.Int64sFlag(name, description, field.Addr().Interface().(*[]int64)) + case reflect.Uint: + c.UintsFlag(name, description, field.Addr().Interface().(*[]uint)) + case reflect.Uint8: + c.Uint8sFlag(name, description, field.Addr().Interface().(*[]uint8)) + case reflect.Uint16: + c.Uint16sFlag(name, description, field.Addr().Interface().(*[]uint16)) + case reflect.Uint32: + c.Uint32sFlag(name, description, field.Addr().Interface().(*[]uint32)) + case reflect.Uint64: + c.Uint64sFlag(name, description, field.Addr().Interface().(*[]uint64)) + case reflect.Float32: + c.Float32sFlag(name, description, field.Addr().Interface().(*[]float32)) + case reflect.Float64: + c.Float64sFlag(name, description, field.Addr().Interface().(*[]float64)) + default: + panic(fmt.Sprintf("addSliceFlags() not supported slice type %s", t.Elem().Elem().Kind().String())) + } + return c +} + +func (c *clirCommand) addSliceField(field reflect.Value, defaultValue, separator string) *clirCommand { + if defaultValue == "" { + return c + } + if field.Kind() != reflect.Slice { + panic("addSliceField() requires a pointer to a slice") + } + t := reflect.TypeOf(field.Addr().Interface()) + if t.Kind() != reflect.Ptr { + panic("addSliceField() requires a pointer to a slice") + } + if t.Elem().Kind() != reflect.Slice { + panic("addSliceField() requires a pointer to a slice") + } + defaultSlice := []string{defaultValue} + if separator != "" { + defaultSlice = strings.Split(defaultValue, separator) + } + switch t.Elem().Elem().Kind() { + case reflect.Bool: + defaultValues := make([]bool, 0, len(defaultSlice)) + for _, value := range defaultSlice { + val, err := strconv.ParseBool(value) + if err != nil { + panic("Invalid default value for bool flag") + } + defaultValues = append(defaultValues, val) + } + field.Set(reflect.ValueOf(defaultValues)) + case reflect.String: + field.Set(reflect.ValueOf(defaultSlice)) + case reflect.Int: + defaultValues := make([]int, 0, len(defaultSlice)) + for _, value := range defaultSlice { + val, err := strconv.Atoi(value) + if err != nil { + panic("Invalid default value for int flag") + } + defaultValues = append(defaultValues, val) + } + field.Set(reflect.ValueOf(defaultValues)) + case reflect.Int8: + defaultValues := make([]int8, 0, len(defaultSlice)) + for _, value := range defaultSlice { + val, err := strconv.Atoi(value) + if err != nil { + panic("Invalid default value for int8 flag") + } + defaultValues = append(defaultValues, int8(val)) + } + field.Set(reflect.ValueOf(defaultValues)) + case reflect.Int16: + defaultValues := make([]int16, 0, len(defaultSlice)) + for _, value := range defaultSlice { + val, err := strconv.Atoi(value) + if err != nil { + panic("Invalid default value for int16 flag") + } + defaultValues = append(defaultValues, int16(val)) + } + field.Set(reflect.ValueOf(defaultValues)) + case reflect.Int32: + defaultValues := make([]int32, 0, len(defaultSlice)) + for _, value := range defaultSlice { + val, err := strconv.ParseInt(value, 10, 32) + if err != nil { + panic("Invalid default value for int32 flag") + } + defaultValues = append(defaultValues, int32(val)) + } + field.Set(reflect.ValueOf(defaultValues)) + case reflect.Int64: + defaultValues := make([]int64, 0, len(defaultSlice)) + for _, value := range defaultSlice { + val, err := strconv.ParseInt(value, 10, 64) + if err != nil { + panic("Invalid default value for int64 flag") + } + defaultValues = append(defaultValues, val) + } + field.Set(reflect.ValueOf(defaultValues)) + case reflect.Uint: + defaultValues := make([]uint, 0, len(defaultSlice)) + for _, value := range defaultSlice { + val, err := strconv.Atoi(value) + if err != nil { + panic("Invalid default value for uint flag") + } + defaultValues = append(defaultValues, uint(val)) + } + field.Set(reflect.ValueOf(defaultValues)) + case reflect.Uint8: + defaultValues := make([]uint8, 0, len(defaultSlice)) + for _, value := range defaultSlice { + val, err := strconv.Atoi(value) + if err != nil { + panic("Invalid default value for uint8 flag") + } + defaultValues = append(defaultValues, uint8(val)) + } + field.Set(reflect.ValueOf(defaultValues)) + case reflect.Uint16: + defaultValues := make([]uint16, 0, len(defaultSlice)) + for _, value := range defaultSlice { + val, err := strconv.Atoi(value) + if err != nil { + panic("Invalid default value for uint16 flag") + } + defaultValues = append(defaultValues, uint16(val)) + } + field.Set(reflect.ValueOf(defaultValues)) + case reflect.Uint32: + defaultValues := make([]uint32, 0, len(defaultSlice)) + for _, value := range defaultSlice { + val, err := strconv.Atoi(value) + if err != nil { + panic("Invalid default value for uint32 flag") + } + defaultValues = append(defaultValues, uint32(val)) + } + field.Set(reflect.ValueOf(defaultValues)) + case reflect.Uint64: + defaultValues := make([]uint64, 0, len(defaultSlice)) + for _, value := range defaultSlice { + val, err := strconv.Atoi(value) + if err != nil { + panic("Invalid default value for uint64 flag") + } + defaultValues = append(defaultValues, uint64(val)) + } + field.Set(reflect.ValueOf(defaultValues)) + case reflect.Float32: + defaultValues := make([]float32, 0, len(defaultSlice)) + for _, value := range defaultSlice { + val, err := strconv.ParseFloat(value, 32) + if err != nil { + panic("Invalid default value for float32 flag") + } + defaultValues = append(defaultValues, float32(val)) + } + field.Set(reflect.ValueOf(defaultValues)) + case reflect.Float64: + defaultValues := make([]float64, 0, len(defaultSlice)) + for _, value := range defaultSlice { + val, err := strconv.ParseFloat(value, 64) + if err != nil { + panic("Invalid default value for float64 flag") + } + defaultValues = append(defaultValues, float64(val)) + } + field.Set(reflect.ValueOf(defaultValues)) + default: + panic(fmt.Sprintf("addSliceField() not supported slice type %s", t.Elem().Elem().Kind().String())) + } + return c +} + +// BoolFlag - Adds a boolean flag to the command +func (c *clirCommand) BoolFlag(name, description string, variable *bool) *clirCommand { + c.flags.BoolVar(variable, name, *variable, description) + c.flagCount++ + return c +} + +// BoolsFlag - Adds a booleans flag to the command +func (c *clirCommand) BoolsFlag(name, description string, variable *[]bool) *clirCommand { + c.flags.Var(newBoolsValue(*variable, variable), name, description) + c.flagCount++ + return c +} + +// StringFlag - Adds a string flag to the command +func (c *clirCommand) StringFlag(name, description string, variable *string) *clirCommand { + c.flags.StringVar(variable, name, *variable, description) + c.flagCount++ + return c +} + +// StringsFlag - Adds a strings flag to the command +func (c *clirCommand) StringsFlag(name, description string, variable *[]string) *clirCommand { + c.flags.Var(newStringsValue(*variable, variable), name, description) + c.flagCount++ + return c +} + +// IntFlag - Adds an int flag to the command +func (c *clirCommand) IntFlag(name, description string, variable *int) *clirCommand { + c.flags.IntVar(variable, name, *variable, description) + c.flagCount++ + return c +} + +// IntsFlag - Adds an ints flag to the command +func (c *clirCommand) IntsFlag(name, description string, variable *[]int) *clirCommand { + c.flags.Var(newIntsValue(*variable, variable), name, description) + c.flagCount++ + return c +} + +// Int8Flag - Adds an int8 flag to the command +func (c *clirCommand) Int8Flag(name, description string, variable *int8) *clirCommand { + c.flags.Var(newInt8Value(*variable, variable), name, description) + c.flagCount++ + return c +} + +// Int8sFlag - Adds an int8 s flag to the command +func (c *clirCommand) Int8sFlag(name, description string, variable *[]int8) *clirCommand { + c.flags.Var(newInt8sValue(*variable, variable), name, description) + c.flagCount++ + return c +} + +// Int16Flag - Adds an int16 flag to the command +func (c *clirCommand) Int16Flag(name, description string, variable *int16) *clirCommand { + c.flags.Var(newInt16Value(*variable, variable), name, description) + c.flagCount++ + return c +} + +// Int16sFlag - Adds an int16s flag to the command +func (c *clirCommand) Int16sFlag(name, description string, variable *[]int16) *clirCommand { + c.flags.Var(newInt16sValue(*variable, variable), name, description) + c.flagCount++ + return c +} + +// Int32Flag - Adds an int32 flag to the command +func (c *clirCommand) Int32Flag(name, description string, variable *int32) *clirCommand { + c.flags.Var(newInt32Value(*variable, variable), name, description) + c.flagCount++ + return c +} + +// Int32sFlag - Adds an int32s flag to the command +func (c *clirCommand) Int32sFlag(name, description string, variable *[]int32) *clirCommand { + c.flags.Var(newInt32sValue(*variable, variable), name, description) + c.flagCount++ + return c +} + +// Int64Flag - Adds an int64 flag to the command +func (c *clirCommand) Int64Flag(name, description string, variable *int64) *clirCommand { + c.flags.Int64Var(variable, name, *variable, description) + c.flagCount++ + return c +} + +// Int64sFlag - Adds an int64s flag to the command +func (c *clirCommand) Int64sFlag(name, description string, variable *[]int64) *clirCommand { + c.flags.Var(newInt64sValue(*variable, variable), name, description) + c.flagCount++ + return c +} + +// UintFlag - Adds an uint flag to the command +func (c *clirCommand) UintFlag(name, description string, variable *uint) *clirCommand { + c.flags.UintVar(variable, name, *variable, description) + c.flagCount++ + return c +} + +// UintsFlag - Adds an uints flag to the command +func (c *clirCommand) UintsFlag(name, description string, variable *[]uint) *clirCommand { + c.flags.Var(newUintsValue(*variable, variable), name, description) + c.flagCount++ + return c +} + +// Uint8Flag - Adds an uint8 flag to the command +func (c *clirCommand) Uint8Flag(name, description string, variable *uint8) *clirCommand { + c.flags.Var(newUint8Value(*variable, variable), name, description) + c.flagCount++ + return c +} + +// Uint8sFlag - Adds an uint8 s flag to the command +func (c *clirCommand) Uint8sFlag(name, description string, variable *[]uint8) *clirCommand { + c.flags.Var(newUint8sValue(*variable, variable), name, description) + c.flagCount++ + return c +} + +// Uint16Flag - Adds an uint16 flag to the command +func (c *clirCommand) Uint16Flag(name, description string, variable *uint16) *clirCommand { + c.flags.Var(newUint16Value(*variable, variable), name, description) + c.flagCount++ + return c +} + +// Uint16sFlag - Adds an uint16s flag to the command +func (c *clirCommand) Uint16sFlag(name, description string, variable *[]uint16) *clirCommand { + c.flags.Var(newUint16sValue(*variable, variable), name, description) + c.flagCount++ + return c +} + +// Uint32Flag - Adds an uint32 flag to the command +func (c *clirCommand) Uint32Flag(name, description string, variable *uint32) *clirCommand { + c.flags.Var(newUint32Value(*variable, variable), name, description) + c.flagCount++ + return c +} + +// Uint32sFlag - Adds an uint32s flag to the command +func (c *clirCommand) Uint32sFlag(name, description string, variable *[]uint32) *clirCommand { + c.flags.Var(newUint32sValue(*variable, variable), name, description) + c.flagCount++ + return c +} + +// UInt64Flag - Adds an uint64 flag to the command +func (c *clirCommand) UInt64Flag(name, description string, variable *uint64) *clirCommand { + c.flags.Uint64Var(variable, name, *variable, description) + c.flagCount++ + return c +} + +// Uint64sFlag - Adds an uint64s flag to the command +func (c *clirCommand) Uint64sFlag(name, description string, variable *[]uint64) *clirCommand { + c.flags.Var(newUint64sValue(*variable, variable), name, description) + c.flagCount++ + return c +} + +// Float64Flag - Adds a float64 flag to the command +func (c *clirCommand) Float64Flag(name, description string, variable *float64) *clirCommand { + c.flags.Float64Var(variable, name, *variable, description) + c.flagCount++ + return c +} + +// Float32Flag - Adds a float32 flag to the command +func (c *clirCommand) Float32Flag(name, description string, variable *float32) *clirCommand { + c.flags.Var(newFloat32Value(*variable, variable), name, description) + c.flagCount++ + return c +} + +// Float32sFlag - Adds a float32s flag to the command +func (c *clirCommand) Float32sFlag(name, description string, variable *[]float32) *clirCommand { + c.flags.Var(newFloat32sValue(*variable, variable), name, description) + c.flagCount++ + return c +} + +// Float64sFlag - Adds a float64s flag to the command +func (c *clirCommand) Float64sFlag(name, description string, variable *[]float64) *clirCommand { + c.flags.Var(newFloat64sValue(*variable, variable), name, description) + c.flagCount++ + return c +} + +type boolsFlagVar []bool + +func (f *boolsFlagVar) String() string { return fmt.Sprint([]bool(*f)) } + +func (f *boolsFlagVar) Set(value string) error { + if value == "" { + *f = append(*f, false) + return nil + } + b, err := strconv.ParseBool(value) + if err != nil { + return err + } + *f = append(*f, b) + return nil +} + +func (f *boolsFlagVar) IsBoolFlag() bool { + return true +} + +func newBoolsValue(val []bool, p *[]bool) *boolsFlagVar { + *p = val + return (*boolsFlagVar)(p) +} + +type stringsFlagVar []string + +func (f *stringsFlagVar) String() string { return fmt.Sprint([]string(*f)) } + +func (f *stringsFlagVar) Set(value string) error { + *f = append(*f, value) + return nil +} + +func newStringsValue(val []string, p *[]string) *stringsFlagVar { + *p = val + return (*stringsFlagVar)(p) +} + +type intsFlagVar []int + +func (f *intsFlagVar) String() string { return fmt.Sprint([]int(*f)) } + +func (f *intsFlagVar) Set(value string) error { + i, err := strconv.Atoi(value) + if err != nil { + return err + } + *f = append(*f, i) + return nil +} + +func newIntsValue(val []int, p *[]int) *intsFlagVar { + *p = val + return (*intsFlagVar)(p) +} + +type int8Value int8 + +func newInt8Value(val int8, p *int8) *int8Value { + *p = val + return (*int8Value)(p) +} + +func (f *int8Value) Set(value string) error { + i, err := strconv.Atoi(value) + if err != nil { + return err + } + *f = int8Value(i) + return nil +} + +func (f *int8Value) String() string { return fmt.Sprint(int8(*f)) } + +type int8sFlagVar []int8 + +func (f *int8sFlagVar) String() string { return fmt.Sprint([]int8(*f)) } + +func (f *int8sFlagVar) Set(value string) error { + i, err := strconv.Atoi(value) + if err != nil { + return err + } + *f = append(*f, int8(i)) + return nil +} + +func newInt8sValue(val []int8, p *[]int8) *int8sFlagVar { + *p = val + return (*int8sFlagVar)(p) +} + +type int16Value int16 + +func newInt16Value(val int16, p *int16) *int16Value { + *p = val + return (*int16Value)(p) +} + +func (f *int16Value) Set(value string) error { + i, err := strconv.Atoi(value) + if err != nil { + return err + } + *f = int16Value(i) + return nil +} + +func (f *int16Value) String() string { return fmt.Sprint(int16(*f)) } + +type int16sFlagVar []int16 + +func (f *int16sFlagVar) String() string { return fmt.Sprint([]int16(*f)) } + +func (f *int16sFlagVar) Set(value string) error { + i, err := strconv.Atoi(value) + if err != nil { + return err + } + *f = append(*f, int16(i)) + return nil +} + +func newInt16sValue(val []int16, p *[]int16) *int16sFlagVar { + *p = val + return (*int16sFlagVar)(p) +} + +type int32Value int32 + +func newInt32Value(val int32, p *int32) *int32Value { + *p = val + return (*int32Value)(p) +} + +func (f *int32Value) Set(value string) error { + i, err := strconv.Atoi(value) + if err != nil { + return err + } + *f = int32Value(i) + return nil +} + +func (f *int32Value) String() string { return fmt.Sprint(int32(*f)) } + +type int32sFlagVar []int32 + +func (f *int32sFlagVar) String() string { return fmt.Sprint([]int32(*f)) } + +func (f *int32sFlagVar) Set(value string) error { + i, err := strconv.Atoi(value) + if err != nil { + return err + } + *f = append(*f, int32(i)) + return nil +} + +func newInt32sValue(val []int32, p *[]int32) *int32sFlagVar { + *p = val + return (*int32sFlagVar)(p) +} + +type int64sFlagVar []int64 + +func (f *int64sFlagVar) String() string { return fmt.Sprint([]int64(*f)) } + +func (f *int64sFlagVar) Set(value string) error { + i, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + *f = append(*f, i) + return nil +} + +func newInt64sValue(val []int64, p *[]int64) *int64sFlagVar { + *p = val + return (*int64sFlagVar)(p) +} + +type uintsFlagVar []uint + +func (f *uintsFlagVar) String() string { + return fmt.Sprint([]uint(*f)) +} + +func (f *uintsFlagVar) Set(value string) error { + i, err := strconv.Atoi(value) + if err != nil { + return err + } + *f = append(*f, uint(i)) + return nil +} + +func newUintsValue(val []uint, p *[]uint) *uintsFlagVar { + *p = val + return (*uintsFlagVar)(p) +} + +type uint8FlagVar uint8 + +func newUint8Value(val uint8, p *uint8) *uint8FlagVar { + *p = val + return (*uint8FlagVar)(p) +} + +func (f *uint8FlagVar) String() string { + return fmt.Sprint(uint8(*f)) +} + +func (f *uint8FlagVar) Set(value string) error { + i, err := strconv.Atoi(value) + if err != nil { + return err + } + *f = uint8FlagVar(i) + return nil +} + +type uint8sFlagVar []uint8 + +func (f *uint8sFlagVar) String() string { + return fmt.Sprint([]uint8(*f)) +} + +func (f *uint8sFlagVar) Set(value string) error { + i, err := strconv.Atoi(value) + if err != nil { + return err + } + *f = append(*f, uint8(i)) + return nil +} + +func newUint8sValue(val []uint8, p *[]uint8) *uint8sFlagVar { + *p = val + return (*uint8sFlagVar)(p) +} + +type uint16FlagVar uint16 + +func newUint16Value(val uint16, p *uint16) *uint16FlagVar { + *p = val + return (*uint16FlagVar)(p) +} + +func (f *uint16FlagVar) String() string { + return fmt.Sprint(uint16(*f)) +} + +func (f *uint16FlagVar) Set(value string) error { + i, err := strconv.Atoi(value) + if err != nil { + return err + } + *f = uint16FlagVar(i) + return nil +} + +type uint16sFlagVar []uint16 + +func (f *uint16sFlagVar) String() string { + return fmt.Sprint([]uint16(*f)) +} + +func (f *uint16sFlagVar) Set(value string) error { + i, err := strconv.Atoi(value) + if err != nil { + return err + } + *f = append(*f, uint16(i)) + return nil +} + +func newUint16sValue(val []uint16, p *[]uint16) *uint16sFlagVar { + *p = val + return (*uint16sFlagVar)(p) +} + +type uint32FlagVar uint32 + +func newUint32Value(val uint32, p *uint32) *uint32FlagVar { + *p = val + return (*uint32FlagVar)(p) +} + +func (f *uint32FlagVar) String() string { + return fmt.Sprint(uint32(*f)) +} + +func (f *uint32FlagVar) Set(value string) error { + i, err := strconv.Atoi(value) + if err != nil { + return err + } + *f = uint32FlagVar(i) + return nil +} + +type uint32sFlagVar []uint32 + +func (f *uint32sFlagVar) String() string { + return fmt.Sprint([]uint32(*f)) +} + +func (f *uint32sFlagVar) Set(value string) error { + i, err := strconv.Atoi(value) + if err != nil { + return err + } + *f = append(*f, uint32(i)) + return nil +} + +func newUint32sValue(val []uint32, p *[]uint32) *uint32sFlagVar { + *p = val + return (*uint32sFlagVar)(p) +} + +type uint64sFlagVar []uint64 + +func (f *uint64sFlagVar) String() string { return fmt.Sprint([]uint64(*f)) } + +func (f *uint64sFlagVar) Set(value string) error { + i, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return err + } + *f = append(*f, i) + return nil +} + +func newUint64sValue(val []uint64, p *[]uint64) *uint64sFlagVar { + *p = val + return (*uint64sFlagVar)(p) +} + +type float32sFlagVar []float32 + +func (f *float32sFlagVar) String() string { return fmt.Sprint([]float32(*f)) } + +func (f *float32sFlagVar) Set(value string) error { + i, err := strconv.ParseFloat(value, 64) + if err != nil { + return err + } + *f = append(*f, float32(i)) + return nil +} + +func newFloat32sValue(val []float32, p *[]float32) *float32sFlagVar { + *p = val + return (*float32sFlagVar)(p) +} + +type float32FlagVar float32 + +func (f *float32FlagVar) String() string { return fmt.Sprint(float32(*f)) } + +func (f *float32FlagVar) Set(value string) error { + i, err := strconv.ParseFloat(value, 64) + if err != nil { + return err + } + *f = float32FlagVar(i) + return nil +} + +func newFloat32Value(val float32, p *float32) *float32FlagVar { + *p = val + return (*float32FlagVar)(p) +} + +type float64sFlagVar []float64 + +func (f *float64sFlagVar) String() string { return fmt.Sprint([]float64(*f)) } + +func (f *float64sFlagVar) Set(value string) error { + i, err := strconv.ParseFloat(value, 64) + if err != nil { + return err + } + *f = append(*f, i) + return nil +} + +func newFloat64sValue(val []float64, p *[]float64) *float64sFlagVar { + *p = val + return (*float64sFlagVar)(p) +} + +// LongDescription - Sets the long description for the command +func (c *clirCommand) LongDescription(longdescription string) *clirCommand { + c.longdescription = longdescription + return c +} + +// OtherArgs - Returns the non-flag arguments passed to the subcommand. NOTE: This should only be called within the context of an action. +func (c *clirCommand) OtherArgs() []string { + return c.flags.Args() +} + +func (c *clirCommand) NewChildCommandFunction(name string, description string, fn any) *clirCommand { + result := c.NewChildCommand(name, description) + // use reflection to determine if this is a function + // if not, panic + t := reflect.TypeOf(fn) + if t.Kind() != reflect.Func { + panic("NewChildCommandFunction '" + name + "' requires a function with the signature 'func(*struct) error'") + } + + // Check the function has 1 input ant it's a struct pointer + fnValue := reflect.ValueOf(fn) + if t.NumIn() != 1 { + panic("NewChildCommandFunction '" + name + "' requires a function with the signature 'func(*struct) error'") + } + // Check the input is a struct pointer + if t.In(0).Kind() != reflect.Ptr { + panic("NewChildCommandFunction '" + name + "' requires a function with the signature 'func(*struct) error'") + } + if t.In(0).Elem().Kind() != reflect.Struct { + panic("NewChildCommandFunction '" + name + "' requires a function with the signature 'func(*struct) error'") + } + // Check only 1 output and it's an error + if t.NumOut() != 1 { + panic("NewChildCommandFunction '" + name + "' requires a function with the signature 'func(*struct) error'") + } + if t.Out(0) != reflect.TypeOf((*error)(nil)).Elem() { + panic("NewChildCommandFunction '" + name + "' requires a function with the signature 'func(*struct) error'") + } + flags := reflect.New(t.In(0).Elem()) + result.Action(func() error { + result := fnValue.Call([]reflect.Value{flags})[0].Interface() + if result != nil { + return result.(error) + } + return nil + }) + result.AddFlags(flags.Interface()) + return result +} + +func (c *clirCommand) parsePositionalArgs(args []string) error { + for index, posArg := range args { + // Check the map for a field for this arg + key := strconv.Itoa(index + 1) + field, ok := c.positionalArgsMap[key] + if !ok { + continue + } + fieldType := field.Type() + switch fieldType.Kind() { + case reflect.Bool: + // set value of field to true + field.SetBool(true) + case reflect.String: + field.SetString(posArg) + case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int: + value, err := strconv.ParseInt(posArg, 10, 64) + if err != nil { + return err + } + field.SetInt(value) + case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint: + value, err := strconv.ParseUint(posArg, 10, 64) + if err != nil { + return err + } + field.SetUint(value) + case reflect.Float64, reflect.Float32: + value, err := strconv.ParseFloat(posArg, 64) + if err != nil { + return err + } + field.SetFloat(value) + case reflect.Slice: + c.addSliceField(field, posArg, c.sliceSeparator[key]) + default: + return E("cli.parsePositionalArgs", "unsupported type for positional argument: "+fieldType.Name(), nil) + } + } + return nil +}