// Package cobra adapts CommandKit's neutral command declarations to Cobra and // pflag. Backend types and failures remain inside this package. package cobra import ( "context" "fmt" "io" "strconv" "strings" spfcobra "github.com/spf13/cobra" "github.com/spf13/pflag" ) // Command describes one node in a per-parse command tree. type Command struct { Name string Summary string Description string Runnable bool Flags []Flag Children []*Command } // Flag describes one local option without exposing a pflag value. type Flag struct { Name string Short string Usage string Default string Boolean bool Repeated bool Required bool } // Failure is a backend-owned parse failure translated to neutral message data. type Failure struct { Message string } // Result contains the selected command and raw input collected by pflag. type Result struct { Path []string HelpRequested bool FlagValues [][]string Operands []string Failure *Failure } // ParseOptions makes the parser's context and streams explicit. Nil writers // are replaced with io.Discard. type ParseOptions struct { Context context.Context In io.Reader Out io.Writer Err io.Writer } // Parse constructs a fresh Cobra tree and parses one argv. It routes only the // leading command words, stopping permanently at the first option or --. func Parse(root *Command, argv []string, options ParseOptions) Result { result := Result{} if root == nil { return failure(result, "command tree is nil") } options = normalizeOptions(options) backendRoot, nodes := buildTree(root, options) leading, tail := splitRoutingPrefix(argv) selected, unconsumed, err := backendRoot.Find(leading) if err != nil { return failure(result, err.Error()) } if selected == nil { return failure(result, "no command was selected") } selectedNode := declarationFor(nodes, selected) if selectedNode == nil { return failure(result, "selected command is not part of the declared tree") } result.Path = pathFor(root, selectedNode) remaining := append(append([]string(nil), unconsumed...), tail...) if err := selected.ParseFlags(remaining); err != nil { return failure(result, err.Error()) } operands := selected.Flags().Args() result.Operands = append([]string(nil), operands...) result.FlagValues = make([][]string, len(selectedNode.Flags)) for index, flag := range selectedNode.Flags { if value := selected.Flags().Lookup(flag.Name); value != nil { result.FlagValues[index] = value.Value.(*rawValue).valuesCopy() } } if helpFlag := selected.Flags().Lookup("help"); helpFlag != nil { helpRequested, err := helpFlag.Value.(*rawValue).lastBool() if err != nil { return failure(result, err.Error()) } result.HelpRequested = helpRequested } if !selectedNode.Runnable && len(result.Operands) > 0 { return failure(result, "unknown command or unexpected group operands: "+strings.Join(result.Operands, " ")) } return result } func normalizeOptions(options ParseOptions) ParseOptions { if options.Context == nil { options.Context = context.Background() } if options.In == nil { options.In = strings.NewReader("") } if options.Out == nil { options.Out = io.Discard } if options.Err == nil { options.Err = io.Discard } return options } func buildTree(root *Command, options ParseOptions) (*spfcobra.Command, map[*Command]*spfcobra.Command) { nodes := make(map[*Command]*spfcobra.Command) var build func(*Command) *spfcobra.Command build = func(declaration *Command) *spfcobra.Command { backend := &spfcobra.Command{ Use: declaration.Name, Short: declaration.Summary, Long: declaration.Description, Args: func(*spfcobra.Command, []string) error { return nil }, SilenceErrors: true, SilenceUsage: true, DisableSuggestions: true, } backend.SetContext(options.Context) backend.SetIn(options.In) backend.SetOut(options.Out) backend.SetErr(options.Err) backend.SetHelpFunc(func(*spfcobra.Command, []string) {}) backend.SetUsageFunc(func(*spfcobra.Command) error { return nil }) backend.SetFlagErrorFunc(func(_ *spfcobra.Command, err error) error { return err }) backend.Flags().SetInterspersed(true) backend.Flags().SortFlags = false backend.DisableAutoGenTag = true helpValue := &rawValue{boolean: true} backend.Flags().VarP(helpValue, "help", "h", "help for this command") backend.Flags().Lookup("help").NoOptDefVal = "true" for _, declarationFlag := range declaration.Flags { value := &rawValue{boolean: declarationFlag.Boolean} backend.Flags().VarP(value, declarationFlag.Name, declarationFlag.Short, declarationFlag.Usage) backendFlag := backend.Flags().Lookup(declarationFlag.Name) backendFlag.DefValue = declarationFlag.Default if declarationFlag.Boolean { backendFlag.NoOptDefVal = "true" } } // Cobra's automatic help flag is suppressed because CommandKit declares // and interprets the local help flag itself. backend.SetHelpCommand(nil) nodes[declaration] = backend for _, child := range declaration.Children { backend.AddCommand(build(child)) } return backend } return build(root), nodes } func declarationFor(nodes map[*Command]*spfcobra.Command, selected *spfcobra.Command) *Command { for declaration, backend := range nodes { if backend == selected { return declaration } } return nil } func splitRoutingPrefix(argv []string) ([]string, []string) { for index, argument := range argv { if argument == "--" || (strings.HasPrefix(argument, "-") && argument != "-") { return append([]string(nil), argv[:index]...), append([]string(nil), argv[index:]...) } } return append([]string(nil), argv...), nil } func pathFor(root, selected *Command) []string { var reverse []string for current := selected; current != nil && current != root; { reverse = append(reverse, current.Name) current = parentOf(root, current) } path := make([]string, len(reverse)) for index := range reverse { path[len(reverse)-index-1] = reverse[index] } return path } func parentOf(root, target *Command) *Command { for _, child := range root.Children { if child == target { return root } if parent := parentOf(child, target); parent != nil { return parent } } return nil } func failure(result Result, message string) Result { result.Failure = &Failure{Message: message} return result } type rawValue struct { values []string boolean bool } func (v *rawValue) Set(value string) error { v.values = append(v.values, value) return nil } func (v *rawValue) String() string { if len(v.values) > 0 { return v.values[len(v.values)-1] } return "" } func (v *rawValue) Type() string { if v.boolean { return "bool" } return "string" } func (v *rawValue) IsBoolFlag() bool { return v.boolean } func (v *rawValue) valuesCopy() []string { return append([]string(nil), v.values...) } func (v *rawValue) lastBool() (bool, error) { var value bool for _, raw := range v.values { parsed, err := strconv.ParseBool(raw) if err != nil { return false, fmt.Errorf("invalid boolean value %q for --help", raw) } value = parsed } return value, nil } var _ pflag.Value = (*rawValue)(nil)