Add invocation-owned Cobra sessions

This commit is contained in:
2026-10-09 01:35:44 +00:00
parent 3ad355927c
commit a852301801
4 changed files with 179 additions and 58 deletions

View File

@@ -58,52 +58,6 @@ 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 := nodes[selected]
selectedPath, pathOK := pathFor(selected, nodes)
if declaration == nil || !pathOK || len(remaining) != 0 || !equalPath(path, selectedPath) {
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 := nodes[selected]
selectedPath, pathOK := pathFor(selected, nodes)
if err != nil || declaration == nil || !pathOK || len(remaining) != 0 || !equalPath(path, selectedPath) {
return "", &Failure{Message: "usage path is not registered"}
}
line := selected.UseLine()
if selected.HasSubCommands() {
line += " [command]"
}
return line, nil
}
// ParseOptions makes the parser's context and streams explicit. Nil writers
// are replaced with io.Discard.
type ParseOptions struct {
@@ -113,29 +67,43 @@ type ParseOptions struct {
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{}
// Session owns one mutable backend tree for a single command invocation.
type Session struct {
root *spfcobra.Command
declarations map[*spfcobra.Command]*Command
}
// Prepare constructs a fresh backend tree for one invocation.
func Prepare(root *Command, options ParseOptions) *Session {
session := &Session{}
if root == nil {
return failure(result, "command tree is nil")
return session
}
options = normalizeOptions(options)
backendRoot, nodes := buildTree(root, options)
session.root, session.declarations = buildTree(root, options)
return session
}
// Parse routes leading command words and parses flags on this session's tree.
func (session *Session) Parse(argv []string) Result {
result := Result{}
if session == nil || session.root == nil {
return failure(result, "command tree is nil")
}
leading, tail := splitRoutingPrefix(argv)
selected, unconsumed, err := backendRoot.Find(leading)
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 := nodes[selected]
selectedNode := session.declarations[selected]
if selectedNode == nil {
return failure(result, "selected command is not part of the declared tree")
}
var pathOK bool
result.Path, pathOK = pathFor(selected, nodes)
result.Path, pathOK = pathFor(selected, session.declarations)
if !pathOK {
return failure(result, "selected command is not part of the declared tree")
}
@@ -170,6 +138,67 @@ func Parse(root *Command, argv []string, options ParseOptions) Result {
return result
}
// RenderHelp renders registered metadata for an exact path on this session.
func (session *Session) RenderHelp(path []string) (string, *Failure) {
if session == nil || session.root == nil {
return "", &Failure{Message: "command tree is nil"}
}
selected, remaining, err := session.root.Find(path)
if err != nil {
return "", &Failure{Message: err.Error()}
}
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"}
}
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 "", &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) {
if session == nil || session.root == nil {
return "", &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"}
}
line := selected.UseLine()
if selected.HasSubCommands() {
line += " [command]"
}
return line, nil
}
// RenderHelp renders a fresh command tree for one help request.
func RenderHelp(root *Command, path []string) (string, *Failure) {
return Prepare(root, 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)
}
// Parse constructs a fresh command tree and parses one argv.
func Parse(root *Command, argv []string, options ParseOptions) Result {
return Prepare(root, options).Parse(argv)
}
func normalizeOptions(options ParseOptions) ParseOptions {
if options.Context == nil {
options.Context = context.Background()

View File

@@ -1,6 +1,7 @@
package cobra
import (
"bytes"
"context"
"fmt"
"io"
@@ -179,3 +180,47 @@ func TestSelectedPathsResolveThroughBackendParents(t *testing.T) {
t.Fatalf("root usage failed: %v", failure)
}
}
func TestSessionReusesTreeForParseHelpAndUsageWithFreshState(t *testing.T) {
tree := &Command{Name: "app", Children: []*Command{{Name: "run", Runnable: true, Flags: []Flag{{Name: "value"}}}}}
var output, errors bytes.Buffer
session := Prepare(tree, ParseOptions{Context: context.Background(), Out: &output, Err: &errors})
first := session.Parse([]string{"run", "--value=old"})
if first.Failure != nil || !reflect.DeepEqual(first.FlagValues, [][]string{{"old"}}) {
t.Fatalf("first session parse = %#v", first)
}
help, failure := session.RenderHelp([]string{"run"})
if failure != nil || !strings.Contains(help, "run") {
t.Fatalf("session help = %q, %v", help, failure)
}
line, failure := session.UsageLine([]string{"run"})
if failure != nil || !strings.Contains(line, "run") {
t.Fatalf("session usage = %q, %v", line, failure)
}
if output.Len() != 0 || errors.Len() != 0 {
t.Fatalf("help escaped session delivery control: out=%q err=%q", output.String(), errors.String())
}
var secondOutput, secondErrors bytes.Buffer
second := Prepare(tree, ParseOptions{Context: context.Background(), Out: &secondOutput, Err: &secondErrors})
secondResult := second.Parse([]string{"run"})
if secondResult.Failure != nil || !reflect.DeepEqual(secondResult.FlagValues, [][]string{nil}) {
t.Fatalf("fresh session state = %#v", secondResult)
}
if output.Len() != 0 || errors.Len() != 0 || secondOutput.Len() != 0 || secondErrors.Len() != 0 {
t.Fatal("prepared session streams were unexpectedly used")
}
}
func TestMissingSessionTreeReturnsNeutralFailures(t *testing.T) {
session := Prepare(nil, ParseOptions{})
if result := session.Parse(nil); result.Failure == nil || result.Failure.Message != "command tree is nil" {
t.Fatalf("missing-tree parse = %#v", result)
}
if _, failure := session.RenderHelp(nil); failure == nil || failure.Message != "command tree is nil" {
t.Fatalf("missing-tree help failure = %#v", failure)
}
if _, failure := session.UsageLine(nil); failure == nil || failure.Message != "command tree is nil" {
t.Fatalf("missing-tree usage failure = %#v", failure)
}
}

View File

@@ -38,6 +38,30 @@ func BenchmarkPerformanceParse(b *testing.B) {
}
}
func BenchmarkPerformanceSessionParseAndHelp(b *testing.B) {
root := performanceFlatTree(8)
args := []string{"wide007"}
session := Prepare(root, ParseOptions{Context: context.Background(), Out: io.Discard, Err: io.Discard})
result := session.Parse(args)
if result.Failure != nil {
b.Fatalf("fixture parse failed: %s", result.Failure.Message)
}
if help, failure := session.RenderHelp(result.Path); failure != nil || help == "" {
b.Fatalf("fixture help failed: %q %v", help, failure)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := session.Parse(args)
if result.Failure != nil {
b.Fatalf("parse failed: %s", result.Failure.Message)
}
if help, failure := session.RenderHelp(result.Path); failure != nil || help == "" {
b.Fatalf("help failed: %q %v", help, failure)
}
}
}
func performanceFlatTree(count int) *Command {
root := &Command{Name: "app", Children: make([]*Command, count)}
for index := range root.Children {