Files
commandkit/execute.go

266 lines
8.6 KiB
Go

package commandkit
import (
"context"
"errors"
"fmt"
"io"
"strings"
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
)
// Execute parses and dispatches one invocation. args excludes the executable
// name. Supplied values are converted before command input construction; help
// does not construct caller inputs or run acquisition callbacks. CommandKit
// writes diagnostics to streams.Err and leaves process exit to the caller. A
// nil context returns an operational failure, and nil streams are replaced with
// io.Discard.
func (a *App) Execute(ctx context.Context, args []string, streams Streams) Outcome {
current := newExecution(a, ctx, streams)
if a == nil {
err := errors.New("cannot execute commands on a nil app")
return current.finalize(Failure(err.Error()).WithCause(err), nil)
}
if ctx == nil {
err := errors.New("invocation context must not be nil")
return current.finalize(Failure(err.Error()).WithCause(err), nil)
}
routedSnapshot := current.snapshot
current.route = resolveEnteredRoute(args, routedSnapshot)
parsed := current.parse(current.route.args)
current.reporter.applySelection(parsed.Reporting)
var outcome Outcome
path := parsed.Path
if parsed.Failure != nil {
outcome = Usage(parsed.Failure.Message)
} else if current.route.kind == routeDiscovery {
if parsed.HelpRequested {
outcome = current.renderHelp(parsed.Path)
path = clonePath(current.route.enteredPath)
} else {
entered := current.route.enteredPath.String()
outcome = Usage(fmt.Sprintf("incomplete command path %q; use %q --help", entered, entered))
path = clonePath(current.route.enteredPath)
}
} else {
payload, ok := routedSnapshot.runners[parsed.Key]
if !ok || payload == nil {
outcome = current.renderHelp(parsed.Path)
} else if parsed.HelpRequested || !payload.hasRun() {
if _, err := payload.prepare(parsed.FlagValues, parsed.FlagSpellings, parsed.Operands, false); err != nil {
outcome = classifyInputError(err)
} else {
outcome = current.renderHelp(parsed.Path)
}
} else {
run, err := payload.prepare(parsed.FlagValues, parsed.FlagSpellings, parsed.Operands, true)
if err != nil {
outcome = classifyInputError(err)
} else {
outcome = run(current)
}
}
}
return current.finalize(outcome, path)
}
type execution struct {
app *App
ctx context.Context
streams Streams
reporter *Reporter
options commandmeta.ParseOptions
snapshot *routingSnapshot
session *internalcobra.Session
route routeResult
}
type commandExecution struct {
invocation Invocation
renderHelpTargetsFunc func([]string) Outcome
}
func (current *execution) commandContext() commandExecution {
return commandExecution{
invocation: current.invocation(),
renderHelpTargetsFunc: current.renderHelpTargets,
}
}
func (current *execution) invocation() Invocation {
return Invocation{
Context: current.ctx,
reporter: current.reporter,
enteredPath: current.route.enteredPath.String(),
canonicalPath: current.route.canonicalPath.String(),
}
}
func (current commandExecution) renderHelpTargets(targets []string) Outcome {
return current.renderHelpTargetsFunc(targets)
}
func newExecution(app *App, ctx context.Context, streams Streams) *execution {
streams = normalizeStreams(streams)
current := &execution{
app: app,
ctx: ctx,
streams: streams,
reporter: newReporter(streams),
options: commandmeta.ParseOptions{Context: ctx, Out: streams.Out, Err: streams.Err},
}
if app != nil {
current.snapshot = app.routeSnapshot()
}
return current
}
func (current *execution) parse(args []string) commandmeta.Result {
if current.app == nil {
return commandmeta.Result{Failure: &commandmeta.Failure{Message: "cannot parse commands on a nil app"}}
}
if current.session == nil {
current.prepareSession()
}
return reduceReportingOccurrences(current.session.Parse(args))
}
func (current *execution) prepareSession() {
if current.app == nil {
return
}
if current.snapshot == nil {
current.snapshot = current.app.routeSnapshot()
}
options := current.options
if current.snapshot != nil {
switch current.route.kind {
case routeCanonical:
options.FlagAliases = current.snapshot.flagAliases[current.route.canonicalPath.Key()].neutralRecords()
case routeDiscovery:
options.FlagAliases = current.snapshot.discoveryFlags[current.route.discoveryPath.Key()].neutralRecords()
}
current.session = internalcobra.Prepare(current.snapshot.metadata, options)
}
}
func (current *execution) presentationSession() *internalcobra.Session {
if current.app == nil {
return nil
}
snapshot := current.app.routeSnapshot()
if current.session == nil || current.snapshot != snapshot {
current.snapshot = snapshot
current.session = nil
current.prepareSession()
}
return current.session
}
func (current *execution) renderHelp(path []string) Outcome {
if current.app == nil {
err := errors.New("cannot render help on a nil app")
return Failure(err.Error()).WithCause(err)
}
session := current.presentationSession()
view := current.app.helpView(current.route, current.snapshot)
return current.app.renderHelpViewOutcomeWithSession(view, current.reporter, session)
}
func (current *execution) renderHelpTargets(targets []string) Outcome {
if current.app == nil {
err := errors.New("cannot render help on a nil app")
return Failure(err.Error()).WithCause(err)
}
snapshot := current.app.routeSnapshot()
if snapshot == nil {
err := errors.New("cannot render help without command metadata")
return Failure(err.Error()).WithCause(err)
}
route := resolveEnteredRoute(targets, snapshot)
// Explicit help targets are paths, not command invocations with operands.
// Discovery routes have no canonical path and retain only trailing options.
if route.kind == routeUnknown || len(route.args) != len(route.canonicalPath) {
session := internalcobra.Prepare(snapshot.metadata, current.options)
view := commandmeta.HelpView{EnteredPath: targets, CanonicalPath: targets}
return current.app.renderHelpViewOutcomeWithSession(view, current.reporter, session)
}
options := current.options
switch route.kind {
case routeCanonical:
options.FlagAliases = snapshot.flagAliases[route.canonicalPath.Key()].neutralRecords()
case routeDiscovery:
options.FlagAliases = snapshot.discoveryFlags[route.discoveryPath.Key()].neutralRecords()
}
session := internalcobra.Prepare(snapshot.metadata, options)
view := current.app.helpView(route, snapshot)
return current.app.renderHelpViewOutcomeWithSession(view, current.reporter, session)
}
func (current *execution) usageLine(path []string) string {
if current.app == nil {
return "Usage: command [command]"
}
session := current.presentationSession()
if session != nil {
if usage, failure := session.UsageLine(path); failure == nil {
return "Usage: " + usage
}
}
return current.app.usageFallback()
}
func (current *execution) routeUsageLine() string {
line := current.usageLine(clonePath(current.route.canonicalPath))
canonical := strings.TrimSpace(current.app.identity.Name + " " + current.route.canonicalPath.String())
entered := strings.TrimSpace(current.app.identity.Name + " " + current.route.enteredPath.String())
line = strings.Replace(line, "Usage: "+canonical, "Usage: "+entered, 1)
return line + "\nAlias for: " + canonical
}
func (current *execution) finalize(outcome Outcome, path []string) Outcome {
if outcome.Kind == OutcomeUsage {
details := append([]Detail(nil), outcome.Explanation.Details...)
details = append(details, func(w io.Writer) error {
line := current.usageText(path)
if !strings.HasSuffix(line, "\n") {
line += "\n"
}
_, err := io.WriteString(w, line)
return err
})
outcome.Explanation.Details = details
}
return current.reporter.finalize(outcome, nil)
}
func (current *execution) usageText(path []string) string {
switch {
case current.route.kind == routeDiscovery && current.app != nil:
return fmt.Sprintf("Usage: %s %s [command]", current.app.identity.Name, current.route.enteredPath.String())
case current.route.kind == routeCanonical && !current.route.enteredPath.Equal(current.route.canonicalPath):
return current.routeUsageLine()
default:
return current.usageLine(path)
}
}
func (a *App) usageLine(path []string) string {
if a != nil {
if usage, failure := internalcobra.UsageLine(a.routeSnapshot().metadata, path); failure == nil {
return "Usage: " + usage
}
}
return a.usageFallback()
}
func (a *App) usageFallback() string {
name := "command"
if a != nil && a.identity.Name != "" {
name = a.identity.Name
}
return fmt.Sprintf("Usage: %s [command]", name)
}