Add generated help and version commands

This commit is contained in:
2026-10-07 13:37:57 +00:00
parent c3448a2ebe
commit ecd2b19783
13 changed files with 736 additions and 27 deletions

View File

@@ -3,6 +3,7 @@
package cobra
import (
"bytes"
"context"
"fmt"
"io"
@@ -20,9 +21,18 @@ type Command struct {
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
@@ -48,6 +58,50 @@ type Result struct {
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 := declarationFor(nodes, selected)
if declaration == nil || len(remaining) != 0 || !equalPath(path, pathFor(root, declaration)) {
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 := declarationFor(nodes, selected)
if err != nil || declaration == nil || len(remaining) != 0 || !equalPath(path, pathFor(root, declaration)) {
return "", &Failure{Message: "usage path is not registered"}
}
line := selected.UseLine()
if selected.HasAvailableSubCommands() {
line += " [command]"
}
return line, nil
}
// ParseOptions makes the parser's context and streams explicit. Nil writers
// are replaced with io.Discard.
type ParseOptions struct {
@@ -79,6 +133,10 @@ func Parse(root *Command, argv []string, options ParseOptions) Result {
return failure(result, "selected command is not part of the declared tree")
}
result.Path = pathFor(root, selectedNode)
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 {
@@ -127,32 +185,37 @@ func buildTree(root *Command, options ParseOptions) (*spfcobra.Command, map[*Com
var build func(*Command) *spfcobra.Command
build = func(declaration *Command) *spfcobra.Command {
backend := &spfcobra.Command{
Use: declaration.Name,
Use: commandUse(declaration),
Short: declaration.Summary,
Long: declaration.Description,
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.SetUsageFunc(func(*spfcobra.Command) error { return nil })
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")
backend.Flags().Lookup("help").NoOptDefVal = "true"
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, declarationFlag.Usage)
backend.Flags().VarP(value, declarationFlag.Name, declarationFlag.Short, flagUsage(declarationFlag))
backendFlag := backend.Flags().Lookup(declarationFlag.Name)
backendFlag.DefValue = declarationFlag.Default
backendFlag.DefValue = ""
if declarationFlag.Boolean {
backendFlag.NoOptDefVal = "true"
}
@@ -169,6 +232,94 @@ func buildTree(root *Command, options ParseOptions) (*spfcobra.Command, map[*Com
return build(root), nodes
}
const usageTemplate = `{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}}
{{end}}Usage:
{{.UseLine}}{{if .HasAvailableSubCommands}}
{{.CommandPath}} [command]{{end}}{{if .HasAvailableSubCommands}}
Available Commands:{{range .Commands}}{{if .IsAvailableCommand}}
{{rpad .Name .NamePadding}} {{.Short}}{{end}}{{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 declarationFor(nodes map[*Command]*spfcobra.Command, selected *spfcobra.Command) *Command {
for declaration, backend := range nodes {
if backend == selected {