261 lines
7.1 KiB
Go
261 lines
7.1 KiB
Go
// Package cobra adapts CommandKit's neutral command declarations to Cobra and
|
|
// pflag. Backend types and failures remain inside this package.
|
|
package cobra
|
|
|
|
import (
|
|
"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
|
|
Children []*Command
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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 := declarationFor(nodes, selected)
|
|
if selectedNode == nil {
|
|
return failure(result, "selected command is not part of the declared tree")
|
|
}
|
|
result.Path = pathFor(root, selectedNode)
|
|
|
|
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[*Command]*spfcobra.Command) {
|
|
nodes := make(map[*Command]*spfcobra.Command)
|
|
var build func(*Command) *spfcobra.Command
|
|
build = func(declaration *Command) *spfcobra.Command {
|
|
backend := &spfcobra.Command{
|
|
Use: declaration.Name,
|
|
Short: declaration.Summary,
|
|
Long: declaration.Description,
|
|
Args: func(*spfcobra.Command, []string) error { return nil },
|
|
SilenceErrors: true,
|
|
SilenceUsage: true,
|
|
DisableSuggestions: true,
|
|
}
|
|
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.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"
|
|
for _, declarationFlag := range declaration.Flags {
|
|
value := &rawValue{boolean: declarationFlag.Boolean}
|
|
backend.Flags().VarP(value, declarationFlag.Name, declarationFlag.Short, declarationFlag.Usage)
|
|
backendFlag := backend.Flags().Lookup(declarationFlag.Name)
|
|
backendFlag.DefValue = declarationFlag.Default
|
|
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[declaration] = backend
|
|
for _, child := range declaration.Children {
|
|
backend.AddCommand(build(child))
|
|
}
|
|
return backend
|
|
}
|
|
return build(root), nodes
|
|
}
|
|
|
|
func declarationFor(nodes map[*Command]*spfcobra.Command, selected *spfcobra.Command) *Command {
|
|
for declaration, backend := range nodes {
|
|
if backend == selected {
|
|
return declaration
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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(root, selected *Command) []string {
|
|
var reverse []string
|
|
for current := selected; current != nil && current != root; {
|
|
reverse = append(reverse, current.Name)
|
|
current = parentOf(root, current)
|
|
}
|
|
path := make([]string, len(reverse))
|
|
for index := range reverse {
|
|
path[len(reverse)-index-1] = reverse[index]
|
|
}
|
|
return path
|
|
}
|
|
|
|
func parentOf(root, target *Command) *Command {
|
|
for _, child := range root.Children {
|
|
if child == target {
|
|
return root
|
|
}
|
|
if parent := parentOf(child, target); parent != nil {
|
|
return parent
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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)
|