Files
commandkit/execute.go

278 lines
7.7 KiB
Go

package commandkit
import (
"context"
"errors"
"fmt"
"io"
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
)
// Execute parses and dispatches one invocation. args excludes the executable
// name. 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)
parsed := current.parse(args)
if parsed.Failure != nil {
return current.present(Usage(errors.New(parsed.Failure.Message)), parsed.Path)
}
command, ok := a.commandAt(parsed.Path)
if !ok || command.record == nil || command.record.payload == nil {
return current.present(current.renderHelp(parsed.Path), parsed.Path)
}
if parsed.HelpRequested || !command.record.payload.hasRun() {
if _, err := command.record.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 := command.record.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 internalcobra.ParseOptions
metadata *internalcobra.Command
session *internalcobra.Session
}
type commandExecution struct {
invocation Invocation
renderHelpTargetsFunc func([]string) Outcome
}
func (current *execution) commandContext() commandExecution {
return commandExecution{
invocation: Invocation{Context: current.ctx, Streams: current.streams},
renderHelpTargetsFunc: current.renderHelpTargets,
}
}
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: internalcobra.ParseOptions{Context: ctx, Out: streams.Out, Err: streams.Err},
}
if app != nil {
current.metadata = app.bridgeTree()
current.session = internalcobra.Prepare(current.metadata, current.options)
}
return current
}
func (current *execution) parse(args []string) internalcobra.Result {
if current.app == nil {
return internalcobra.Result{Failure: &internalcobra.Failure{Message: "cannot parse commands on a nil app"}}
}
return current.session.Parse(args)
}
func (current *execution) presentationSession() *internalcobra.Session {
if current.app == nil {
return nil
}
metadata := current.app.bridgeTree()
if current.session == nil || current.session.Metadata() != metadata {
current.metadata = metadata
current.session = internalcobra.Prepare(metadata, current.options)
}
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"))
}
return current.app.renderHelpOutcomeWithSession(path, current.streams, current.presentationSession())
}
func (current *execution) renderHelpTargets(targets []string) Outcome {
if current.app == nil {
return Failure(errors.New("cannot render help on a nil app"))
}
return current.app.renderHelpTargetsWithSession(targets, current.streams, current.presentationSession())
}
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) commandAt(path []string) (Command, bool) {
if a == nil {
return Command{}, false
}
if len(path) == 0 {
return Command{}, true
}
command, ok := a.commands[path[0]]
if !ok {
return Command{}, false
}
current := command.command
for _, name := range path[1:] {
found := false
for _, child := range current.record.children {
if child.record != nil && child.record.name == name {
current = child
found = true
break
}
}
if !found {
return Command{}, false
}
}
return current, true
}
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.bridgeTree(), 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
}