package commandkit import ( "context" "errors" "fmt" "io" "strings" 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 { current := newExecution(a, ctx, streams) if a == nil { err := errors.New("cannot execute commands on a nil app") return current.finalize(Failure(err.Error()).WithCause(err), nil) } if ctx == nil { err := errors.New("invocation context must not be nil") return current.finalize(Failure(err.Error()).WithCause(err), nil) } routedSnapshot := current.snapshot current.route = resolveEnteredRoute(args, routedSnapshot) parsed := current.parse(current.route.args) current.reporter.applySelection(parsed.Reporting) var outcome Outcome path := parsed.Path if parsed.Failure != nil { outcome = Usage(parsed.Failure.Message) } else if current.route.kind == routeDiscovery { if parsed.HelpRequested { outcome = current.renderHelp(parsed.Path) path = clonePath(current.route.enteredPath) } else { entered := current.route.enteredPath.String() outcome = Usage(fmt.Sprintf("incomplete command path %q; use %q --help", entered, entered)) path = clonePath(current.route.enteredPath) } } else { payload, ok := routedSnapshot.runners[parsed.Key] if !ok || payload == nil { outcome = current.renderHelp(parsed.Path) } else if parsed.HelpRequested || !payload.hasRun() { if _, err := payload.prepare(parsed.FlagValues, parsed.FlagSpellings, parsed.Operands, false); err != nil { outcome = classifyInputError(err) } else { outcome = current.renderHelp(parsed.Path) } } else { run, err := payload.prepare(parsed.FlagValues, parsed.FlagSpellings, parsed.Operands, true) if err != nil { outcome = classifyInputError(err) } else { outcome = run(current) } } } return current.finalize(outcome, path) } type execution struct { app *App ctx context.Context streams Streams reporter *Reporter 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, reporter: current.reporter, 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 { streams = normalizeStreams(streams) current := &execution{ app: app, ctx: ctx, streams: streams, reporter: newReporter(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 reduceReportingOccurrences(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 { err := errors.New("cannot render help on a nil app") return Failure(err.Error()).WithCause(err) } session := current.presentationSession() view := current.app.helpView(current.route, current.snapshot) return current.app.renderHelpViewOutcomeWithSession(view, current.reporter, session) } func (current *execution) renderHelpTargets(targets []string) Outcome { if current.app == nil { err := errors.New("cannot render help on a nil app") return Failure(err.Error()).WithCause(err) } snapshot := current.app.routeSnapshot() if snapshot == nil { err := errors.New("cannot render help without command metadata") return Failure(err.Error()).WithCause(err) } route := resolveEnteredRoute(targets, snapshot) // Explicit help targets are paths, not command invocations with operands. // Discovery routes have no canonical path and retain only trailing options. if route.kind == routeUnknown || len(route.args) != len(route.canonicalPath) { session := internalcobra.Prepare(snapshot.metadata, current.options) view := commandmeta.HelpView{EnteredPath: targets, CanonicalPath: targets} return current.app.renderHelpViewOutcomeWithSession(view, current.reporter, 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.reporter, 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) routeUsageLine() string { line := current.usageLine(clonePath(current.route.canonicalPath)) canonical := strings.TrimSpace(current.app.identity.Name + " " + current.route.canonicalPath.String()) entered := strings.TrimSpace(current.app.identity.Name + " " + current.route.enteredPath.String()) line = strings.Replace(line, "Usage: "+canonical, "Usage: "+entered, 1) return line + "\nAlias for: " + canonical } func (current *execution) finalize(outcome Outcome, path []string) Outcome { if outcome.Kind == OutcomeUsage { details := append([]Detail(nil), outcome.Explanation.Details...) details = append(details, func(w io.Writer) error { line := current.usageText(path) if !strings.HasSuffix(line, "\n") { line += "\n" } _, err := io.WriteString(w, line) return err }) outcome.Explanation.Details = details } return current.reporter.finalize(outcome, nil) } func (current *execution) usageText(path []string) string { switch { case current.route.kind == routeDiscovery && current.app != nil: return fmt.Sprintf("Usage: %s %s [command]", current.app.identity.Name, current.route.enteredPath.String()) case current.route.kind == routeCanonical && !current.route.enteredPath.Equal(current.route.canonicalPath): return current.routeUsageLine() default: return current.usageLine(path) } } 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) }