314 lines
9.4 KiB
Go
314 lines
9.4 KiB
Go
package commandkit
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
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 {
|
|
if ctx == nil {
|
|
return a.present(Failure(errors.New("invocation context must not be nil")), nil, streams)
|
|
}
|
|
streams = normalizeStreams(streams)
|
|
current := newExecution(a, ctx, streams)
|
|
routedSnapshot := current.snapshot
|
|
current.route = resolveEnteredRoute(args, routedSnapshot)
|
|
parsed := current.parse(current.route.args)
|
|
if parsed.Failure != nil {
|
|
return current.present(Usage(errors.New(parsed.Failure.Message)), parsed.Path)
|
|
}
|
|
if current.route.kind == routeDiscovery {
|
|
if parsed.HelpRequested {
|
|
return current.present(current.renderHelp(parsed.Path), clonePath(current.route.enteredPath))
|
|
}
|
|
entered := current.route.enteredPath.String()
|
|
return current.present(Usage(fmt.Errorf("incomplete command path %q; use %q --help", entered, entered)), clonePath(current.route.enteredPath))
|
|
}
|
|
payload, ok := routedSnapshot.runners[parsed.Key]
|
|
if !ok || payload == nil {
|
|
return current.present(current.renderHelp(parsed.Path), parsed.Path)
|
|
}
|
|
if parsed.HelpRequested || !payload.hasRun() {
|
|
if _, err := payload.prepare(parsed.FlagValues, parsed.Operands, false); err != nil {
|
|
return current.present(Usage(err), parsed.Path)
|
|
}
|
|
return current.present(current.renderHelp(parsed.Path), parsed.Path)
|
|
}
|
|
run, err := payload.prepare(parsed.FlagValues, parsed.Operands, true)
|
|
if err != nil {
|
|
return current.present(Usage(err), parsed.Path)
|
|
}
|
|
return current.present(run(current), parsed.Path)
|
|
}
|
|
|
|
type execution struct {
|
|
app *App
|
|
ctx context.Context
|
|
streams Streams
|
|
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,
|
|
Streams: current.streams,
|
|
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 {
|
|
current := &execution{
|
|
app: app,
|
|
ctx: ctx,
|
|
streams: 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 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 {
|
|
return Failure(errors.New("cannot render help on a nil app"))
|
|
}
|
|
session := current.presentationSession()
|
|
view := current.app.helpView(current.route, current.snapshot)
|
|
return current.app.renderHelpViewOutcomeWithSession(view, current.streams, session)
|
|
}
|
|
|
|
func (current *execution) renderHelpTargets(targets []string) Outcome {
|
|
if current.app == nil {
|
|
return Failure(errors.New("cannot render help on a nil app"))
|
|
}
|
|
snapshot := current.app.routeSnapshot()
|
|
if snapshot == nil {
|
|
return Failure(errors.New("cannot render help without command metadata"))
|
|
}
|
|
route := resolveEnteredRoute(targets, snapshot)
|
|
if route.kind == routeUnknown {
|
|
session := internalcobra.Prepare(snapshot.metadata, current.options)
|
|
return current.app.renderHelpOutcomeWithSession(targets, current.streams, 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.streams, 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) present(outcome Outcome, path []string) Outcome {
|
|
return current.app.presentWithUsageLine(outcome, path, current.streams, current.usageLine)
|
|
}
|
|
|
|
func normalizeStreams(streams Streams) Streams {
|
|
if streams.Out == nil {
|
|
streams.Out = io.Discard
|
|
}
|
|
if streams.Err == nil {
|
|
streams.Err = io.Discard
|
|
}
|
|
return streams
|
|
}
|
|
|
|
func (a *App) present(outcome Outcome, path []string, streams Streams) Outcome {
|
|
return a.presentWithUsageLine(outcome, path, streams, a.usageLine)
|
|
}
|
|
|
|
func (a *App) presentWithUsageLine(outcome Outcome, path []string, streams Streams, usageLine func([]string) string) Outcome {
|
|
if outcome.Kind == "" && outcome.Status == 0 && outcome.Domain == "" &&
|
|
outcome.Cause == nil && outcome.DeliveryError == nil &&
|
|
!outcome.ReportIncomplete && len(outcome.Diagnostics) == 0 {
|
|
outcome.Kind = OutcomeSuccess
|
|
}
|
|
if outcome.DiagnosticDelivery == "" {
|
|
outcome.DiagnosticDelivery = DeliveryNone
|
|
}
|
|
streams = normalizeStreams(streams)
|
|
if outcome.DiagnosticDelivery == DeliveryComplete {
|
|
return outcome
|
|
}
|
|
diagnostics := append([]Diagnostic(nil), outcome.Diagnostics...)
|
|
if outcome.Cause != nil && !hasErrorDiagnostic(diagnostics) {
|
|
diagnostics = append(diagnostics, MessageDiagnostic(SeverityError, outcome.Cause.Error()))
|
|
}
|
|
if outcome.Kind == OutcomeUsage {
|
|
diagnostics = append(diagnostics, MessageDiagnostic(SeverityError, usageLine(path)))
|
|
}
|
|
outcome.Diagnostics = diagnostics
|
|
if len(diagnostics) == 0 {
|
|
return outcome
|
|
}
|
|
delivered := false
|
|
for _, diagnostic := range diagnostics {
|
|
if diagnostic.Write == nil {
|
|
continue
|
|
}
|
|
delivered = true
|
|
writer := &checkedWriter{writer: streams.Err}
|
|
writeErr := diagnostic.Write(writer)
|
|
if writer.failure != nil {
|
|
if writeErr == nil {
|
|
writeErr = writer.failure
|
|
} else if !errors.Is(writeErr, writer.failure) {
|
|
writeErr = errors.Join(writeErr, writer.failure)
|
|
}
|
|
}
|
|
if writeErr != nil {
|
|
return deliveryFailure(outcome, writeErr)
|
|
}
|
|
}
|
|
if delivered {
|
|
outcome.DiagnosticDelivery = DeliveryComplete
|
|
}
|
|
return outcome
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func hasErrorDiagnostic(diagnostics []Diagnostic) bool {
|
|
for _, diagnostic := range diagnostics {
|
|
if diagnostic.Write != nil && diagnostic.Severity == SeverityError {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func deliveryFailure(outcome Outcome, err error) Outcome {
|
|
if outcome.DeliveryError == nil {
|
|
outcome.DeliveryError = err
|
|
} else {
|
|
outcome.DeliveryError = errors.Join(outcome.DeliveryError, err)
|
|
}
|
|
outcome.DiagnosticDelivery = DeliveryIncomplete
|
|
if outcome.Kind != OutcomeDomain {
|
|
outcome.Kind = OutcomeOperational
|
|
outcome.Status = 1
|
|
}
|
|
return outcome
|
|
}
|
|
|
|
type checkedWriter struct {
|
|
writer io.Writer
|
|
failure error
|
|
}
|
|
|
|
func (w *checkedWriter) Write(data []byte) (int, error) {
|
|
n, err := w.writer.Write(data)
|
|
if err == nil && n < len(data) {
|
|
err = io.ErrShortWrite
|
|
}
|
|
if err != nil && w.failure == nil {
|
|
w.failure = err
|
|
}
|
|
return n, err
|
|
}
|