Add invocation coordination and diagnostics
This commit is contained in:
180
execute.go
Normal file
180
execute.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package commandkit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
)
|
||||
|
||||
// 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)
|
||||
parsed := a.parse(args, internalcobra.ParseOptions{
|
||||
Context: ctx,
|
||||
Out: streams.Out,
|
||||
Err: streams.Err,
|
||||
})
|
||||
if parsed.Failure != nil {
|
||||
return a.present(Usage(errors.New(parsed.Failure.Message)), parsed.Path, streams)
|
||||
}
|
||||
command, ok := a.commandAt(parsed.Path)
|
||||
if !ok || command.payload == nil {
|
||||
// Help rendering is completed in Stage 6. The dispatch boundary already
|
||||
// resolves metadata requests without constructing handler input.
|
||||
return Success()
|
||||
}
|
||||
if parsed.HelpRequested || !command.payload.hasRun() {
|
||||
if _, err := command.payload.prepare(parsed.FlagValues, parsed.Operands, false); err != nil {
|
||||
return a.present(Usage(err), parsed.Path, streams)
|
||||
}
|
||||
return Success()
|
||||
}
|
||||
buildInput, err := command.payload.prepare(parsed.FlagValues, parsed.Operands, true)
|
||||
if err != nil {
|
||||
return a.present(Usage(err), parsed.Path, streams)
|
||||
}
|
||||
input := buildInput()
|
||||
if err := command.payload.checkInput(input); err != nil {
|
||||
return a.present(Usage(err), parsed.Path, streams)
|
||||
}
|
||||
outcome := command.payload.run(Invocation{Context: ctx, Streams: streams}, input)
|
||||
return a.present(outcome, parsed.Path, streams)
|
||||
}
|
||||
|
||||
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 || command.reserved {
|
||||
return Command{}, false
|
||||
}
|
||||
current := command.command
|
||||
for _, name := range path[1:] {
|
||||
found := false
|
||||
for _, child := range current.children {
|
||||
if child.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 {
|
||||
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(configkit.SeverityError, outcome.Cause.Error()))
|
||||
}
|
||||
if outcome.Kind == OutcomeUsage {
|
||||
diagnostics = append(diagnostics, MessageDiagnostic(configkit.SeverityError, a.usageLine(path)))
|
||||
}
|
||||
outcome.Diagnostics = diagnostics
|
||||
if len(diagnostics) == 0 {
|
||||
return outcome
|
||||
}
|
||||
for _, diagnostic := range diagnostics {
|
||||
if diagnostic.Write == nil {
|
||||
outcome = deliveryFailure(outcome, errors.New("diagnostic has no writer"))
|
||||
return outcome
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
outcome.DiagnosticDelivery = DeliveryComplete
|
||||
return outcome
|
||||
}
|
||||
|
||||
func (a *App) usageLine(path []string) string {
|
||||
name := "command"
|
||||
if a != nil && a.identity.Name != "" {
|
||||
name = a.identity.Name
|
||||
}
|
||||
parts := append([]string{name}, path...)
|
||||
if len(path) == 0 {
|
||||
return fmt.Sprintf("Usage: %s [command]", name)
|
||||
}
|
||||
return fmt.Sprintf("Usage: %s [flags]", strings.Join(parts, " "))
|
||||
}
|
||||
|
||||
func hasErrorDiagnostic(diagnostics []Diagnostic) bool {
|
||||
for _, diagnostic := range diagnostics {
|
||||
if diagnostic.Severity == configkit.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
|
||||
}
|
||||
Reference in New Issue
Block a user