Route executions through neutral snapshots
This commit is contained in:
@@ -10,72 +10,21 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||
|
||||
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
|
||||
metadata *commandmeta.Command
|
||||
root *spfcobra.Command
|
||||
declarations map[*spfcobra.Command]*Command
|
||||
declarations map[*spfcobra.Command]*commandmeta.Command
|
||||
}
|
||||
|
||||
// Prepare constructs a fresh backend tree for one invocation.
|
||||
func Prepare(root *Command, options ParseOptions) *Session {
|
||||
func Prepare(root *commandmeta.Command, options commandmeta.ParseOptions) *Session {
|
||||
session := &Session{}
|
||||
if root == nil {
|
||||
return session
|
||||
@@ -87,7 +36,7 @@ func Prepare(root *Command, options ParseOptions) *Session {
|
||||
}
|
||||
|
||||
// Metadata returns the neutral tree used to prepare this session.
|
||||
func (session *Session) Metadata() *Command {
|
||||
func (session *Session) Metadata() *commandmeta.Command {
|
||||
if session == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -95,8 +44,8 @@ func (session *Session) Metadata() *Command {
|
||||
}
|
||||
|
||||
// Parse routes leading command words and parses flags on this session's tree.
|
||||
func (session *Session) Parse(argv []string) Result {
|
||||
result := Result{}
|
||||
func (session *Session) Parse(argv []string) commandmeta.Result {
|
||||
result := commandmeta.Result{}
|
||||
if session == nil || session.root == nil {
|
||||
return failure(result, "command tree is nil")
|
||||
}
|
||||
@@ -112,6 +61,7 @@ func (session *Session) Parse(argv []string) Result {
|
||||
if selectedNode == nil {
|
||||
return failure(result, "selected command is not part of the declared tree")
|
||||
}
|
||||
result.Key = selectedNode.Key
|
||||
var pathOK bool
|
||||
result.Path, pathOK = pathFor(selected, session.declarations)
|
||||
if !pathOK {
|
||||
@@ -149,18 +99,18 @@ func (session *Session) Parse(argv []string) Result {
|
||||
}
|
||||
|
||||
// RenderHelp renders registered metadata for an exact path on this session.
|
||||
func (session *Session) RenderHelp(path []string) (string, *Failure) {
|
||||
func (session *Session) RenderHelp(path []string) (string, *commandmeta.Failure) {
|
||||
if session == nil || session.root == nil {
|
||||
return "", &Failure{Message: "command tree is nil"}
|
||||
return "", &commandmeta.Failure{Message: "command tree is nil"}
|
||||
}
|
||||
selected, remaining, err := session.root.Find(path)
|
||||
if err != nil {
|
||||
return "", &Failure{Message: err.Error()}
|
||||
return "", &commandmeta.Failure{Message: fmt.Sprintf("unknown help path %q", strings.Join(path, " ")), Usage: true}
|
||||
}
|
||||
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"}
|
||||
return "", &commandmeta.Failure{Message: fmt.Sprintf("unknown help path %q", strings.Join(path, " ")), Usage: true}
|
||||
}
|
||||
buffer := new(bytes.Buffer)
|
||||
previousOut, previousErr := selected.OutOrStdout(), selected.ErrOrStderr()
|
||||
@@ -171,21 +121,21 @@ func (session *Session) RenderHelp(path []string) (string, *Failure) {
|
||||
selected.SetErr(previousErr)
|
||||
}()
|
||||
if err := selected.Usage(); err != nil {
|
||||
return "", &Failure{Message: err.Error()}
|
||||
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, *Failure) {
|
||||
func (session *Session) UsageLine(path []string) (string, *commandmeta.Failure) {
|
||||
if session == nil || session.root == nil {
|
||||
return "", &Failure{Message: "command tree is 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 || !equalPath(path, selectedPath) {
|
||||
return "", &Failure{Message: "usage path is not registered"}
|
||||
return "", &commandmeta.Failure{Message: "usage path is not registered"}
|
||||
}
|
||||
line := selected.UseLine()
|
||||
if selected.HasSubCommands() {
|
||||
@@ -195,21 +145,21 @@ func (session *Session) UsageLine(path []string) (string, *Failure) {
|
||||
}
|
||||
|
||||
// RenderHelp renders a fresh command tree for one help request.
|
||||
func RenderHelp(root *Command, path []string) (string, *Failure) {
|
||||
return Prepare(root, ParseOptions{}).RenderHelp(path)
|
||||
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 *Command, path []string) (string, *Failure) {
|
||||
return Prepare(root, ParseOptions{}).UsageLine(path)
|
||||
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 *Command, argv []string, options ParseOptions) Result {
|
||||
func Parse(root *commandmeta.Command, argv []string, options commandmeta.ParseOptions) commandmeta.Result {
|
||||
return Prepare(root, options).Parse(argv)
|
||||
}
|
||||
|
||||
func normalizeOptions(options ParseOptions) ParseOptions {
|
||||
func normalizeOptions(options commandmeta.ParseOptions) commandmeta.ParseOptions {
|
||||
if options.Context == nil {
|
||||
options.Context = context.Background()
|
||||
}
|
||||
@@ -225,10 +175,10 @@ func normalizeOptions(options ParseOptions) ParseOptions {
|
||||
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 {
|
||||
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,
|
||||
@@ -290,7 +240,7 @@ Flags:
|
||||
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}
|
||||
`
|
||||
|
||||
func commandUse(command *Command) string {
|
||||
func commandUse(command *commandmeta.Command) string {
|
||||
var parts []string
|
||||
for _, argument := range command.Arguments {
|
||||
name := argument.Name
|
||||
@@ -312,7 +262,7 @@ func commandUse(command *Command) string {
|
||||
return command.Name + " " + strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func helpDescription(command *Command) string {
|
||||
func helpDescription(command *commandmeta.Command) string {
|
||||
var sections []string
|
||||
if description := strings.TrimSpace(command.Description); description != "" {
|
||||
sections = append(sections, description)
|
||||
@@ -335,7 +285,7 @@ func helpDescription(command *Command) string {
|
||||
return strings.Join(sections, "\n\n")
|
||||
}
|
||||
|
||||
func flagUsage(flag Flag) string {
|
||||
func flagUsage(flag commandmeta.Flag) string {
|
||||
var details []string
|
||||
if flag.Required {
|
||||
details = append(details, "required")
|
||||
@@ -374,7 +324,7 @@ func splitRoutingPrefix(argv []string) ([]string, []string) {
|
||||
return append([]string(nil), argv...), nil
|
||||
}
|
||||
|
||||
func pathFor(selected *spfcobra.Command, declarations map[*spfcobra.Command]*Command) ([]string, bool) {
|
||||
func pathFor(selected *spfcobra.Command, declarations map[*spfcobra.Command]*commandmeta.Command) ([]string, bool) {
|
||||
var reverse []string
|
||||
for current := selected; current != nil; current = current.Parent() {
|
||||
declaration := declarations[current]
|
||||
@@ -391,8 +341,8 @@ func pathFor(selected *spfcobra.Command, declarations map[*spfcobra.Command]*Com
|
||||
return reverse, true
|
||||
}
|
||||
|
||||
func failure(result Result, message string) Result {
|
||||
result.Failure = &Failure{Message: message}
|
||||
func failure(result commandmeta.Result, message string) commandmeta.Result {
|
||||
result.Failure = &commandmeta.Failure{Message: message}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,17 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||
spfcobra "github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type Command = commandmeta.Command
|
||||
type Argument = commandmeta.Argument
|
||||
type Flag = commandmeta.Flag
|
||||
type Failure = commandmeta.Failure
|
||||
type Result = commandmeta.Result
|
||||
type ParseOptions = commandmeta.ParseOptions
|
||||
|
||||
func TestParseRoutesBeforeOptionsAndKeepsFlagsLocal(t *testing.T) {
|
||||
tree := &Command{Name: "app", Children: []*Command{
|
||||
{Name: "config", Children: []*Command{
|
||||
|
||||
66
internal/commandmeta/metadata.go
Normal file
66
internal/commandmeta/metadata.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Package commandmeta owns the neutral metadata exchanged by CommandKit and
|
||||
// its command-line backend.
|
||||
package commandmeta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Key identifies one command inside one routing snapshot.
|
||||
type Key uint64
|
||||
|
||||
// Command describes one node in a neutral command tree.
|
||||
type Command struct {
|
||||
Key Key
|
||||
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 backend value.
|
||||
type Flag struct {
|
||||
Name string
|
||||
Short string
|
||||
Usage string
|
||||
Default string
|
||||
Boolean bool
|
||||
Repeated bool
|
||||
Required bool
|
||||
}
|
||||
|
||||
// Failure is a neutral parsing or presentation failure.
|
||||
type Failure struct {
|
||||
Message string
|
||||
Usage bool
|
||||
}
|
||||
|
||||
// Result contains the selected command and raw input collected by the adapter.
|
||||
type Result struct {
|
||||
Key Key
|
||||
Path []string
|
||||
HelpRequested bool
|
||||
FlagValues [][]string
|
||||
Operands []string
|
||||
Failure *Failure
|
||||
}
|
||||
|
||||
// ParseOptions makes parser context and streams explicit.
|
||||
type ParseOptions struct {
|
||||
Context context.Context
|
||||
In io.Reader
|
||||
Out io.Writer
|
||||
Err io.Writer
|
||||
}
|
||||
Reference in New Issue
Block a user