Add generated help and version commands
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user