2026-10-07 13:30:24 +00:00
|
|
|
package commandkit
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2026-10-26 03:03:57 +00:00
|
|
|
"strconv"
|
2026-10-19 19:46:22 +00:00
|
|
|
"strings"
|
2026-10-07 13:30:24 +00:00
|
|
|
|
|
|
|
|
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
|
2026-10-11 01:25:45 +00:00
|
|
|
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
2026-10-26 14:33:31 +00:00
|
|
|
"gitea.maximumdirect.net/eric/CommandKit/internal/commandpath"
|
2026-10-07 13:30:24 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Execute parses and dispatches one invocation. args excludes the executable
|
2026-10-11 02:06:06 +00:00
|
|
|
// name. Supplied values are converted before command input construction; help
|
2026-10-26 03:57:11 +00:00
|
|
|
// does not construct caller inputs or run acquisition callbacks. It applies the
|
|
|
|
|
// parsed reporting selection, finalizes invocation presentation, and returns
|
|
|
|
|
// the resulting Outcome without exiting the process. Supplied streams remain
|
|
|
|
|
// caller-owned; nil destinations are replaced with io.Discard. A nil context
|
|
|
|
|
// returns an operational failure.
|
2026-10-07 13:30:24 +00:00
|
|
|
func (a *App) Execute(ctx context.Context, args []string, streams Streams) Outcome {
|
2026-10-26 02:59:28 +00:00
|
|
|
current := newExecution(a, ctx, streams)
|
|
|
|
|
if a == nil {
|
|
|
|
|
err := errors.New("cannot execute commands on a nil app")
|
2026-10-26 03:03:57 +00:00
|
|
|
return current.finalize(Failure(err.Error()).WithCause(err), nil, false)
|
2026-10-26 02:59:28 +00:00
|
|
|
}
|
2026-10-07 13:30:24 +00:00
|
|
|
if ctx == nil {
|
2026-10-26 02:59:28 +00:00
|
|
|
err := errors.New("invocation context must not be nil")
|
2026-10-26 03:03:57 +00:00
|
|
|
return current.finalize(Failure(err.Error()).WithCause(err), nil, false)
|
2026-10-07 13:30:24 +00:00
|
|
|
}
|
2026-10-11 01:25:45 +00:00
|
|
|
routedSnapshot := current.snapshot
|
2026-10-19 17:37:59 +00:00
|
|
|
current.route = resolveEnteredRoute(args, routedSnapshot)
|
2026-10-26 03:03:57 +00:00
|
|
|
current.recordRouteEvents()
|
2026-10-19 17:37:59 +00:00
|
|
|
parsed := current.parse(current.route.args)
|
2026-10-26 02:59:28 +00:00
|
|
|
current.reporter.applySelection(parsed.Reporting)
|
2026-10-26 03:03:57 +00:00
|
|
|
current.recordSelectionEvents(parsed)
|
|
|
|
|
current.submitDebugEvents()
|
2026-10-26 02:59:28 +00:00
|
|
|
var outcome Outcome
|
|
|
|
|
path := parsed.Path
|
2026-10-26 03:03:57 +00:00
|
|
|
handlerRan := false
|
2026-10-07 13:30:24 +00:00
|
|
|
if parsed.Failure != nil {
|
2026-10-26 02:59:28 +00:00
|
|
|
outcome = Usage(parsed.Failure.Message)
|
|
|
|
|
} else if current.route.kind == routeDiscovery {
|
2026-10-19 19:40:55 +00:00
|
|
|
if parsed.HelpRequested {
|
2026-10-26 02:59:28 +00:00
|
|
|
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)
|
2026-10-19 19:40:55 +00:00
|
|
|
}
|
2026-10-26 02:59:28 +00:00
|
|
|
} 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 {
|
2026-10-26 03:03:57 +00:00
|
|
|
handlerRan = true
|
2026-10-26 02:59:28 +00:00
|
|
|
outcome = run(current)
|
|
|
|
|
}
|
2026-10-07 13:30:24 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-26 03:03:57 +00:00
|
|
|
return current.finalize(outcome, path, handlerRan)
|
2026-10-09 01:42:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type execution struct {
|
2026-10-26 03:03:57 +00:00
|
|
|
app *App
|
|
|
|
|
ctx context.Context
|
|
|
|
|
streams Streams
|
|
|
|
|
reporter *Reporter
|
|
|
|
|
debugEvents []string
|
|
|
|
|
options commandmeta.ParseOptions
|
|
|
|
|
snapshot *routingSnapshot
|
|
|
|
|
session *internalcobra.Session
|
|
|
|
|
route routeResult
|
2026-10-09 01:42:03 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-11 01:21:52 +00:00
|
|
|
type commandExecution struct {
|
|
|
|
|
invocation Invocation
|
|
|
|
|
renderHelpTargetsFunc func([]string) Outcome
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (current *execution) commandContext() commandExecution {
|
|
|
|
|
return commandExecution{
|
2026-10-19 19:33:49 +00:00
|
|
|
invocation: current.invocation(),
|
2026-10-11 01:21:52 +00:00
|
|
|
renderHelpTargetsFunc: current.renderHelpTargets,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-19 19:33:49 +00:00
|
|
|
func (current *execution) invocation() Invocation {
|
|
|
|
|
return Invocation{
|
|
|
|
|
Context: current.ctx,
|
2026-10-26 02:59:28 +00:00
|
|
|
reporter: current.reporter,
|
2026-10-19 19:33:49 +00:00
|
|
|
enteredPath: current.route.enteredPath.String(),
|
|
|
|
|
canonicalPath: current.route.canonicalPath.String(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 01:21:52 +00:00
|
|
|
func (current commandExecution) renderHelpTargets(targets []string) Outcome {
|
|
|
|
|
return current.renderHelpTargetsFunc(targets)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-09 01:42:03 +00:00
|
|
|
func newExecution(app *App, ctx context.Context, streams Streams) *execution {
|
2026-10-26 02:59:28 +00:00
|
|
|
streams = normalizeStreams(streams)
|
2026-10-09 01:42:03 +00:00
|
|
|
current := &execution{
|
2026-10-26 02:59:28 +00:00
|
|
|
app: app,
|
|
|
|
|
ctx: ctx,
|
|
|
|
|
streams: streams,
|
|
|
|
|
reporter: newReporter(streams),
|
|
|
|
|
options: commandmeta.ParseOptions{Context: ctx, Out: streams.Out, Err: streams.Err},
|
2026-10-09 01:42:03 +00:00
|
|
|
}
|
|
|
|
|
if app != nil {
|
2026-10-11 01:25:45 +00:00
|
|
|
current.snapshot = app.routeSnapshot()
|
2026-10-09 01:42:03 +00:00
|
|
|
}
|
|
|
|
|
return current
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 01:25:45 +00:00
|
|
|
func (current *execution) parse(args []string) commandmeta.Result {
|
2026-10-09 01:42:03 +00:00
|
|
|
if current.app == nil {
|
2026-10-11 01:25:45 +00:00
|
|
|
return commandmeta.Result{Failure: &commandmeta.Failure{Message: "cannot parse commands on a nil app"}}
|
2026-10-09 01:42:03 +00:00
|
|
|
}
|
2026-10-19 19:31:23 +00:00
|
|
|
if current.session == nil {
|
|
|
|
|
current.prepareSession()
|
|
|
|
|
}
|
2026-10-26 02:59:28 +00:00
|
|
|
return reduceReportingOccurrences(current.session.Parse(args))
|
2026-10-09 01:42:03 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-19 19:31:23 +00:00
|
|
|
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()
|
2026-10-26 14:33:31 +00:00
|
|
|
default:
|
|
|
|
|
snapshot := current.snapshot
|
|
|
|
|
options.FlagAliasesForPath = func(path []string) []commandmeta.FlagAlias {
|
|
|
|
|
return snapshot.flagAliases[commandpath.FromTokens(path).Key()].neutralRecords()
|
|
|
|
|
}
|
2026-10-19 19:31:23 +00:00
|
|
|
}
|
|
|
|
|
current.session = internalcobra.Prepare(current.snapshot.metadata, options)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-09 01:42:03 +00:00
|
|
|
func (current *execution) presentationSession() *internalcobra.Session {
|
|
|
|
|
if current.app == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-10-11 01:25:45 +00:00
|
|
|
snapshot := current.app.routeSnapshot()
|
|
|
|
|
if current.session == nil || current.snapshot != snapshot {
|
|
|
|
|
current.snapshot = snapshot
|
2026-10-19 19:31:23 +00:00
|
|
|
current.session = nil
|
|
|
|
|
current.prepareSession()
|
2026-10-09 01:42:03 +00:00
|
|
|
}
|
|
|
|
|
return current.session
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (current *execution) renderHelp(path []string) Outcome {
|
|
|
|
|
if current.app == nil {
|
2026-10-26 02:59:28 +00:00
|
|
|
err := errors.New("cannot render help on a nil app")
|
|
|
|
|
return Failure(err.Error()).WithCause(err)
|
2026-10-09 01:42:03 +00:00
|
|
|
}
|
2026-10-19 19:40:55 +00:00
|
|
|
session := current.presentationSession()
|
|
|
|
|
view := current.app.helpView(current.route, current.snapshot)
|
2026-10-26 03:02:08 +00:00
|
|
|
return current.app.renderHelpViewOutcomeWithSession(view, current.reporter, session)
|
2026-10-09 01:42:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (current *execution) renderHelpTargets(targets []string) Outcome {
|
|
|
|
|
if current.app == nil {
|
2026-10-26 02:59:28 +00:00
|
|
|
err := errors.New("cannot render help on a nil app")
|
|
|
|
|
return Failure(err.Error()).WithCause(err)
|
2026-10-09 01:42:03 +00:00
|
|
|
}
|
2026-10-19 19:40:55 +00:00
|
|
|
snapshot := current.app.routeSnapshot()
|
|
|
|
|
if snapshot == nil {
|
2026-10-26 02:59:28 +00:00
|
|
|
err := errors.New("cannot render help without command metadata")
|
|
|
|
|
return Failure(err.Error()).WithCause(err)
|
2026-10-19 19:40:55 +00:00
|
|
|
}
|
|
|
|
|
route := resolveEnteredRoute(targets, snapshot)
|
2026-10-20 00:23:19 +00:00
|
|
|
// 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) {
|
2026-10-19 19:40:55 +00:00
|
|
|
session := internalcobra.Prepare(snapshot.metadata, current.options)
|
2026-10-26 03:02:08 +00:00
|
|
|
view := commandmeta.HelpView{EnteredPath: targets, CanonicalPath: targets}
|
|
|
|
|
return current.app.renderHelpViewOutcomeWithSession(view, current.reporter, session)
|
2026-10-19 19:40:55 +00:00
|
|
|
}
|
|
|
|
|
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)
|
2026-10-26 03:02:08 +00:00
|
|
|
return current.app.renderHelpViewOutcomeWithSession(view, current.reporter, session)
|
2026-10-09 01:42:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-19 19:46:22 +00:00
|
|
|
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
|
2026-10-07 13:30:24 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-26 03:03:57 +00:00
|
|
|
func (current *execution) recordRouteEvents() {
|
|
|
|
|
entered := current.route.enteredPath.String()
|
|
|
|
|
canonical := current.route.canonicalPath.String()
|
|
|
|
|
switch current.route.kind {
|
|
|
|
|
case routeCanonical:
|
|
|
|
|
if entered == canonical {
|
|
|
|
|
if canonical == "" {
|
|
|
|
|
current.debugEvents = append(current.debugEvents, "selected the application root command")
|
|
|
|
|
} else {
|
|
|
|
|
current.debugEvents = append(current.debugEvents, fmt.Sprintf("selected command path %q", canonical))
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
current.debugEvents = append(current.debugEvents, fmt.Sprintf("resolved command alias %q to %q", entered, canonical))
|
|
|
|
|
}
|
|
|
|
|
case routeDiscovery:
|
|
|
|
|
current.debugEvents = append(current.debugEvents, fmt.Sprintf("selected discovery route for prefix %q", entered))
|
|
|
|
|
default:
|
|
|
|
|
if entered != "" {
|
|
|
|
|
current.debugEvents = append(current.debugEvents, fmt.Sprintf("no canonical route matched command path %q", entered))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (current *execution) recordSelectionEvents(result commandmeta.Result) {
|
|
|
|
|
mode := "ordinary"
|
|
|
|
|
flagName := ""
|
|
|
|
|
switch result.Reporting.Mode {
|
|
|
|
|
case commandmeta.ReportingModeQuiet:
|
|
|
|
|
mode, flagName = "quiet", "quiet"
|
|
|
|
|
case commandmeta.ReportingModeVerbose:
|
|
|
|
|
mode, flagName = "verbose", "verbose"
|
|
|
|
|
case commandmeta.ReportingModeDebug:
|
|
|
|
|
mode, flagName = "debug", "debug"
|
|
|
|
|
}
|
|
|
|
|
event := "selected ordinary reporting mode"
|
|
|
|
|
if flagName != "" {
|
|
|
|
|
event = fmt.Sprintf("selected %s reporting mode", mode)
|
|
|
|
|
for index := len(result.FlagOccurrences) - 1; index >= 0; index-- {
|
|
|
|
|
occurrence := result.FlagOccurrences[index]
|
|
|
|
|
if occurrence.Name != flagName {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
enabled, err := strconv.ParseBool(occurrence.RawValue)
|
|
|
|
|
if err != nil || !enabled {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
canonical := "--" + flagName
|
|
|
|
|
if occurrence.Spelling != canonical {
|
|
|
|
|
event += fmt.Sprintf(" using %s (alias for %s)", occurrence.Spelling, canonical)
|
|
|
|
|
} else {
|
|
|
|
|
event += fmt.Sprintf(" using %s", canonical)
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
current.debugEvents = append(current.debugEvents, event)
|
|
|
|
|
if result.Reporting.ProgressSet {
|
|
|
|
|
state := "disabled"
|
|
|
|
|
if result.Reporting.Progress {
|
|
|
|
|
state = "enabled"
|
|
|
|
|
}
|
|
|
|
|
event := "explicitly " + state + " progress output"
|
|
|
|
|
for index := len(result.FlagOccurrences) - 1; index >= 0; index-- {
|
|
|
|
|
occurrence := result.FlagOccurrences[index]
|
|
|
|
|
if occurrence.Name == "progress" {
|
|
|
|
|
if occurrence.Spelling != "--progress" {
|
|
|
|
|
event += fmt.Sprintf(" using %s (alias for --progress)", occurrence.Spelling)
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
current.debugEvents = append(current.debugEvents, event)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (current *execution) submitDebugEvents() {
|
|
|
|
|
events := current.debugEvents
|
|
|
|
|
current.debugEvents = nil
|
|
|
|
|
for _, event := range events {
|
|
|
|
|
_ = current.reporter.Debug(event)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (current *execution) finalize(outcome Outcome, path []string, handlerRan bool) Outcome {
|
2026-10-07 13:30:24 +00:00
|
|
|
if outcome.Kind == OutcomeUsage {
|
2026-10-26 02:59:28 +00:00
|
|
|
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"
|
2026-10-07 13:30:24 +00:00
|
|
|
}
|
2026-10-26 02:59:28 +00:00
|
|
|
_, err := io.WriteString(w, line)
|
|
|
|
|
return err
|
|
|
|
|
})
|
|
|
|
|
outcome.Explanation.Details = details
|
2026-10-07 13:30:24 +00:00
|
|
|
}
|
2026-10-26 03:03:57 +00:00
|
|
|
var completion func(*Reporter)
|
|
|
|
|
if handlerRan {
|
|
|
|
|
completion = func(reporter *Reporter) {
|
|
|
|
|
kind := outcome.Kind
|
|
|
|
|
if kind == "" && outcome.Status == 0 && outcome.Domain == "" {
|
|
|
|
|
kind = OutcomeSuccess
|
|
|
|
|
}
|
|
|
|
|
path := current.route.canonicalPath.String()
|
|
|
|
|
_ = reporter.Debug(fmt.Sprintf("handler completed for %q with %s outcome and status %d", path, kind, outcome.Status))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return current.reporter.finalize(outcome, completion)
|
2026-10-26 02:59:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
2026-10-07 16:49:56 +00:00
|
|
|
}
|
2026-10-07 13:30:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) usageLine(path []string) string {
|
2026-10-07 13:37:57 +00:00
|
|
|
if a != nil {
|
2026-10-11 01:25:45 +00:00
|
|
|
if usage, failure := internalcobra.UsageLine(a.routeSnapshot().metadata, path); failure == nil {
|
2026-10-07 13:37:57 +00:00
|
|
|
return "Usage: " + usage
|
|
|
|
|
}
|
2026-10-07 13:30:24 +00:00
|
|
|
}
|
2026-10-09 01:42:03 +00:00
|
|
|
return a.usageFallback()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) usageFallback() string {
|
|
|
|
|
name := "command"
|
|
|
|
|
if a != nil && a.identity.Name != "" {
|
|
|
|
|
name = a.identity.Name
|
|
|
|
|
}
|
2026-10-07 13:37:57 +00:00
|
|
|
return fmt.Sprintf("Usage: %s [command]", name)
|
2026-10-07 13:30:24 +00:00
|
|
|
}
|