// Package cobra adapts CommandKit's neutral command declarations to Cobra and // pflag. Backend types and failures remain inside this package. package cobra import ( "bytes" "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 Arguments []Argument Children []*Command } // Argument describes one positional input for usage rendering. type Argument struct { Name string Usage string Min int Max int } // 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 } // RenderHelp renders registered metadata for the exact command path. It uses // Cobra's usage metadata and returns backend failures as neutral message data. func RenderHelp(root *Command, path []string) (string, *Failure) { if root == nil { return "", &Failure{Message: "command tree is nil"} } buffer := new(bytes.Buffer) options := normalizeOptions(ParseOptions{Context: context.Background(), In: strings.NewReader(""), Out: buffer, Err: buffer}) backendRoot, nodes := buildTree(root, options) selected, remaining, err := backendRoot.Find(path) if err != nil { return "", &Failure{Message: err.Error()} } declaration := nodes[selected] selectedPath, pathOK := pathFor(selected, nodes) if declaration == nil || !pathOK || len(remaining) != 0 || !equalPath(path, selectedPath) { return "", &Failure{Message: "help path is not registered"} } selected.SetOut(buffer) selected.SetErr(buffer) if err := selected.Usage(); err != nil { return "", &Failure{Message: err.Error()} } return buffer.String(), nil } // UsageLine returns Cobra's usage line for an exact registered command path. func UsageLine(root *Command, path []string) (string, *Failure) { if root == nil { return "", &Failure{Message: "command tree is nil"} } options := normalizeOptions(ParseOptions{Context: context.Background(), In: strings.NewReader(""), Out: io.Discard, Err: io.Discard}) backendRoot, nodes := buildTree(root, options) selected, remaining, err := backendRoot.Find(path) declaration := nodes[selected] selectedPath, pathOK := pathFor(selected, nodes) if err != nil || declaration == nil || !pathOK || len(remaining) != 0 || !equalPath(path, selectedPath) { return "", &Failure{Message: "usage path is not registered"} } line := selected.UseLine() if selected.HasSubCommands() { line += " [command]" } return line, nil } // 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 := nodes[selected] if selectedNode == nil { return failure(result, "selected command is not part of the declared tree") } var pathOK bool result.Path, pathOK = pathFor(selected, nodes) if !pathOK { return failure(result, "selected command is not part of the declared tree") } consumed := len(leading) - len(unconsumed) if consumed != len(result.Path) || !equalPath(leading[:consumed], result.Path) { return failure(result, "command path must match a registered command exactly") } 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[*spfcobra.Command]*Command) { nodes := make(map[*spfcobra.Command]*Command) var build func(*Command) *spfcobra.Command build = func(declaration *Command) *spfcobra.Command { backend := &spfcobra.Command{ Use: commandUse(declaration), Short: declaration.Summary, Long: helpDescription(declaration), Args: func(*spfcobra.Command, []string) error { return nil }, SilenceErrors: true, SilenceUsage: true, DisableSuggestions: true, } if declaration.Runnable { backend.Run = func(*spfcobra.Command, []string) {} } backend.SetContext(options.Context) backend.SetIn(options.In) backend.SetOut(options.Out) backend.SetErr(options.Err) backend.SetHelpFunc(func(*spfcobra.Command, []string) {}) backend.SetFlagErrorFunc(func(_ *spfcobra.Command, err error) error { return err }) backend.SetUsageTemplate(usageTemplate) 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") helpFlag := backend.Flags().Lookup("help") helpFlag.NoOptDefVal = "true" helpFlag.Usage = "help for this command (default: false)" for _, declarationFlag := range declaration.Flags { value := &rawValue{boolean: declarationFlag.Boolean} backend.Flags().VarP(value, declarationFlag.Name, declarationFlag.Short, flagUsage(declarationFlag)) backendFlag := backend.Flags().Lookup(declarationFlag.Name) backendFlag.DefValue = "" 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[backend] = declaration for _, child := range declaration.Children { backend.AddCommand(build(child)) } return backend } return build(root), nodes } const usageTemplate = `{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}} {{end}}Usage: {{.UseLine}}{{if .HasSubCommands}} {{.CommandPath}} [command]{{end}}{{if .HasSubCommands}} Available Commands:{{range .Commands}} {{rpad .Name .NamePadding}} {{.Short}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} Flags: {{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}} ` func commandUse(command *Command) string { var parts []string for _, argument := range command.Arguments { name := argument.Name if argument.Min == 1 && argument.Max == 1 { parts = append(parts, name) } else if argument.Max == -1 { if argument.Min > 0 { parts = append(parts, name+"...") } else { parts = append(parts, "["+name+"...]") } } else { parts = append(parts, fmt.Sprintf("%s{%d..%d}", name, argument.Min, argument.Max)) } } if len(parts) == 0 { return command.Name } return command.Name + " " + strings.Join(parts, " ") } func helpDescription(command *Command) string { var sections []string if description := strings.TrimSpace(command.Description); description != "" { sections = append(sections, description) } if len(command.Arguments) > 0 { lines := []string{"Arguments:"} for _, argument := range command.Arguments { maximum := fmt.Sprint(argument.Max) if argument.Max == -1 { maximum = "unbounded" } line := fmt.Sprintf(" %s (min %d, max %s)", argument.Name, argument.Min, maximum) if argument.Usage != "" { line += " — " + argument.Usage } lines = append(lines, line) } sections = append(sections, strings.Join(lines, "\n")) } return strings.Join(sections, "\n\n") } func flagUsage(flag Flag) string { var details []string if flag.Required { details = append(details, "required") } if flag.Repeated { details = append(details, "repeatable") } details = append(details, "default: "+flag.Default) usage := flag.Usage if usage != "" { usage += " (" + strings.Join(details, "; ") + ")" } else { usage = strings.Join(details, "; ") } return usage } func equalPath(left, right []string) bool { if len(left) != len(right) { return false } for index := range left { if left[index] != right[index] { return false } } return true } 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(selected *spfcobra.Command, declarations map[*spfcobra.Command]*Command) ([]string, bool) { var reverse []string for current := selected; current != nil; current = current.Parent() { declaration := declarations[current] if declaration == nil { return nil, false } if current.Parent() != nil { reverse = append(reverse, declaration.Name) } } for left, right := 0, len(reverse)-1; left < right; left, right = left+1, right-1 { reverse[left], reverse[right] = reverse[right], reverse[left] } return reverse, true } 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)