// 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 } // 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 } // Session owns one mutable backend tree for a single command invocation. type Session struct { metadata *Command root *spfcobra.Command declarations map[*spfcobra.Command]*Command } // Prepare constructs a fresh backend tree for one invocation. func Prepare(root *Command, options ParseOptions) *Session { session := &Session{} if root == nil { return session } session.metadata = root options = normalizeOptions(options) session.root, session.declarations = buildTree(root, options) return session } // Metadata returns the neutral tree used to prepare this session. func (session *Session) Metadata() *Command { if session == nil { return nil } return session.metadata } // Parse routes leading command words and parses flags on this session's tree. func (session *Session) Parse(argv []string) Result { result := Result{} if session == nil || session.root == nil { return failure(result, "command tree is nil") } leading, tail := splitRoutingPrefix(argv) selected, unconsumed, err := session.root.Find(leading) if err != nil { return failure(result, err.Error()) } if selected == nil { return failure(result, "no command was selected") } selectedNode := session.declarations[selected] if selectedNode == nil { return failure(result, "selected command is not part of the declared tree") } var pathOK bool result.Path, pathOK = pathFor(selected, session.declarations) 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 } // RenderHelp renders registered metadata for an exact path on this session. func (session *Session) RenderHelp(path []string) (string, *Failure) { if session == nil || session.root == nil { return "", &Failure{Message: "command tree is nil"} } selected, remaining, err := session.root.Find(path) if err != nil { return "", &Failure{Message: err.Error()} } declaration := session.declarations[selected] selectedPath, pathOK := pathFor(selected, session.declarations) if declaration == nil || !pathOK || len(remaining) != 0 || !equalPath(path, selectedPath) { return "", &Failure{Message: "help path is not registered"} } buffer := new(bytes.Buffer) previousOut, previousErr := selected.OutOrStdout(), selected.ErrOrStderr() selected.SetOut(buffer) selected.SetErr(buffer) defer func() { selected.SetOut(previousOut) selected.SetErr(previousErr) }() if err := selected.Usage(); err != nil { return "", &Failure{Message: err.Error()} } return buffer.String(), nil } // UsageLine returns Cobra's usage line for an exact path on this session. func (session *Session) UsageLine(path []string) (string, *Failure) { if session == nil || session.root == nil { return "", &Failure{Message: "command tree is nil"} } selected, remaining, err := session.root.Find(path) declaration := session.declarations[selected] selectedPath, pathOK := pathFor(selected, session.declarations) 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 } // RenderHelp renders a fresh command tree for one help request. func RenderHelp(root *Command, path []string) (string, *Failure) { return Prepare(root, ParseOptions{}).RenderHelp(path) } // UsageLine resolves a path on a fresh command tree. func UsageLine(root *Command, path []string) (string, *Failure) { return Prepare(root, ParseOptions{}).UsageLine(path) } // Parse constructs a fresh command tree and parses one argv. func Parse(root *Command, argv []string, options ParseOptions) Result { return Prepare(root, options).Parse(argv) } 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)