// 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" commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta" "gitea.maximumdirect.net/eric/CommandKit/internal/commandpath" spfcobra "github.com/spf13/cobra" "github.com/spf13/pflag" ) // Session owns one mutable backend tree for a single command invocation. type Session struct { metadata *commandmeta.Command root *spfcobra.Command declarations map[*spfcobra.Command]*commandmeta.Command } // Prepare constructs a fresh backend tree for one invocation. func Prepare(root *commandmeta.Command, options commandmeta.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() *commandmeta.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) commandmeta.Result { result := commandmeta.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") } result.Key = selectedNode.Key var pathOK bool selectedPath, pathOK := pathFor(selected, session.declarations) if !pathOK { return failure(result, "selected command is not part of the declared tree") } result.Path = []string(selectedPath) consumed := len(leading) - len(unconsumed) if consumed != len(selectedPath) || !commandpath.FromTokens(leading[:consumed]).Equal(selectedPath) { return failure(result, "command path must match a registered command exactly") } remaining := append(commandpath.CloneTokens(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, *commandmeta.Failure) { if session == nil || session.root == nil { return "", &commandmeta.Failure{Message: "command tree is nil"} } selected, remaining, err := session.root.Find(path) if err != nil { return "", &commandmeta.Failure{Message: fmt.Sprintf("unknown help path %q", commandpath.FromTokens(path).String()), Usage: true} } declaration := session.declarations[selected] selectedPath, pathOK := pathFor(selected, session.declarations) if declaration == nil || !pathOK || len(remaining) != 0 || !commandpath.FromTokens(path).Equal(selectedPath) { return "", &commandmeta.Failure{Message: fmt.Sprintf("unknown help path %q", commandpath.FromTokens(path).String()), Usage: true} } 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 "", &commandmeta.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, *commandmeta.Failure) { if session == nil || session.root == nil { return "", &commandmeta.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 || !commandpath.FromTokens(path).Equal(selectedPath) { return "", &commandmeta.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 *commandmeta.Command, path []string) (string, *commandmeta.Failure) { return Prepare(root, commandmeta.ParseOptions{}).RenderHelp(path) } // UsageLine resolves a path on a fresh command tree. func UsageLine(root *commandmeta.Command, path []string) (string, *commandmeta.Failure) { return Prepare(root, commandmeta.ParseOptions{}).UsageLine(path) } // Parse constructs a fresh command tree and parses one argv. func Parse(root *commandmeta.Command, argv []string, options commandmeta.ParseOptions) commandmeta.Result { return Prepare(root, options).Parse(argv) } func normalizeOptions(options commandmeta.ParseOptions) commandmeta.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 *commandmeta.Command, options commandmeta.ParseOptions) (*spfcobra.Command, map[*spfcobra.Command]*commandmeta.Command) { nodes := make(map[*spfcobra.Command]*commandmeta.Command) var build func(*commandmeta.Command) *spfcobra.Command build = func(declaration *commandmeta.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 *commandmeta.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 *commandmeta.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 commandmeta.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 splitRoutingPrefix(argv []string) ([]string, []string) { for index, argument := range argv { if argument == "--" || (strings.HasPrefix(argument, "-") && argument != "-") { return commandpath.CloneTokens(argv[:index]), commandpath.CloneTokens(argv[index:]) } } return commandpath.CloneTokens(argv), nil } func pathFor(selected *spfcobra.Command, declarations map[*spfcobra.Command]*commandmeta.Command) (commandpath.Path, 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 commandpath.FromTokens(reverse), true } func failure(result commandmeta.Result, message string) commandmeta.Result { result.Failure = &commandmeta.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)