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 }