package commandkit import ( "context" "errors" "fmt" "io" 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 { return a.present(a.renderHelpOutcome(parsed.Path, streams), parsed.Path, streams) } 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 a.present(a.renderHelpOutcome(parsed.Path, streams), parsed.Path, streams) } 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 && command.command.payload == nil) { 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 { 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(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 } 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 { name := "command" if a != nil && a.identity.Name != "" { name = a.identity.Name } if a != nil { if usage, failure := internalcobra.UsageLine(a.bridgeTree(), path); failure == nil { return "Usage: " + usage } } return fmt.Sprintf("Usage: %s [command]", name) } func hasErrorDiagnostic(diagnostics []Diagnostic) bool { for _, diagnostic := range diagnostics { if diagnostic.Write != nil && 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 }