diff --git a/internal/repl/completion.go b/internal/repl/completion.go index 2b551f7..80083e2 100644 --- a/internal/repl/completion.go +++ b/internal/repl/completion.go @@ -16,30 +16,7 @@ type flagSpec struct { const CompletionEnvVar = "CLOUDCANAL_INTERNAL_COMPLETE" var ( - visibleTopLevelCommands = []string{ - "help", - "jobs", - "datasources", - "clusters", - "workers", - "consolejobs", - "job-config", - "schemas", - "config", - } visibleReplOnlyCommands = []string{"exit"} - visibleHelpTopics = []string{ - "jobs", - "datasources", - "clusters", - "workers", - "consolejobs", - "job-config", - "schemas", - "config", - } - boolValues = []string{"true", "false"} - outputValues = []string{"text", "json"} ) func RenderCompletionScript(args []string) (string, error) { @@ -79,12 +56,29 @@ func (s *Shell) completeLine(line string) []string { } func (s *Shell) handleCompletion(tokens []string) error { + return s.dispatchRegisteredCommand(tokens) +} + +func (s *Shell) runCompletionZsh(tokens []string) error { + return s.runCompletionShell(tokens, "zsh") +} + +func (s *Shell) runCompletionBash(tokens []string) error { + return s.runCompletionShell(tokens, "bash") +} + +func (s *Shell) runCompletionShell(tokens []string, shellName string) error { if len(tokens) < 2 || len(tokens) > 3 { s.io.Println(s.usageCompletion()) return nil } - script, err := RenderCompletionScript(tokens[1:]) + args := []string{shellName} + if len(tokens) == 3 { + args = append(args, tokens[2]) + } + + script, err := RenderCompletionScript(args) if err != nil { return err } @@ -103,7 +97,7 @@ func completeContext(context []string, prefix string, replMode bool) []string { if name, valuePrefix, ok := splitInlineFlag(prefix); ok && name == "--output" { return prependInlineFlag(name, matchCandidates(outputValues, valuePrefix)) } - candidates := append([]string{}, visibleTopLevelCommands...) + candidates := append([]string{}, visibleTopLevelCommands()...) if replMode { candidates = append(candidates, visibleReplOnlyCommands...) } @@ -113,162 +107,33 @@ func completeContext(context []string, prefix string, replMode bool) []string { return matchCandidates(candidates, prefix) } - root := strings.ToLower(context[0]) - switch root { - case "help": - return matchCandidates(visibleHelpTopics, prefix) - case "completion": - if len(context) == 1 { - return matchCandidates(append(append([]string{}, completionSubcommands...), "--help"), prefix) - } - return nil - case "lang", "language": - if len(context) == 1 { - return matchCandidates(append(append([]string{}, langSubcommands...), "--help"), prefix) - } - if strings.EqualFold(context[1], "set") { - return matchCandidates([]string{"en", "zh"}, prefix) - } - return nil - case "config": - if len(context) == 1 { - return matchCandidates(append(append([]string{}, configSubcommands...), "--help"), prefix) - } - if strings.EqualFold(context[1], "lang") { - if len(context) == 2 { - return matchCandidates(append(append([]string{}, langSubcommands...), "--help"), prefix) - } - if len(context) == 3 && strings.EqualFold(context[2], "set") { - return matchCandidates([]string{"en", "zh"}, prefix) - } - } - return nil - case "jobs": - if len(context) == 1 { - return matchCandidates(append(append([]string{}, jobsSubcommands...), "--help"), prefix) - } - switch strings.ToLower(context[1]) { - case "list": - return completeFlags(context[2:], prefix, withGlobalFlags([]flagSpec{ - {name: "--name"}, - {name: "--type"}, - {name: "--desc"}, - {name: "--source-id"}, - {name: "--target-id"}, - })) - case "create", "update-incre-pos": - return completeFlags(context[2:], prefix, withGlobalFlags([]flagSpec{ - {name: "--body"}, - {name: "--body-file"}, - })) - case "replay": - return completeFlags(context[2:], prefix, withGlobalFlags([]flagSpec{ - {name: "--auto-start", values: boolValues}, - {name: "--reset-to-created", values: boolValues}, - })) - } - return nil - case "datasources": - if len(context) == 1 { - return matchCandidates(append(append([]string{}, dataSourceSubcommands...), "--help"), prefix) - } - if strings.EqualFold(context[1], "list") { - return completeFlags(context[2:], prefix, withGlobalFlags([]flagSpec{ - {name: "--id"}, - {name: "--type"}, - {name: "--deploy-type"}, - {name: "--host-type"}, - {name: "--lifecycle"}, - })) - } - if strings.EqualFold(context[1], "add") { - return completeFlags(context[2:], prefix, withGlobalFlags([]flagSpec{ - {name: "--body"}, - {name: "--body-file"}, - {name: "--security-file"}, - {name: "--secret-file"}, - })) - } - return nil - case "clusters": - if len(context) == 1 { - return matchCandidates(append(append([]string{}, clusterSubcommands...), "--help"), prefix) - } - if strings.EqualFold(context[1], "list") { - return completeFlags(context[2:], prefix, withGlobalFlags([]flagSpec{ - {name: "--name"}, - {name: "--desc"}, - {name: "--cloud"}, - {name: "--region"}, - })) - } - return nil - case "workers": - if len(context) == 1 { - return matchCandidates(append(append([]string{}, workerSubcommands...), "--help"), prefix) - } - if strings.EqualFold(context[1], "list") { - return completeFlags(context[2:], prefix, withGlobalFlags([]flagSpec{ - {name: "--cluster-id"}, - {name: "--source-id"}, - {name: "--target-id"}, - })) - } - if strings.EqualFold(context[1], "modify-mem-oversold") { - return completeFlags(context[2:], prefix, withGlobalFlags([]flagSpec{ - {name: "--percent"}, - })) - } - if strings.EqualFold(context[1], "update-alert") { - return completeFlags(context[2:], prefix, withGlobalFlags([]flagSpec{ - {name: "--phone", values: boolValues}, - {name: "--email", values: boolValues}, - {name: "--im", values: boolValues}, - {name: "--sms", values: boolValues}, - })) - } - return nil - case "consolejobs": - if len(context) == 1 { - return matchCandidates(append(append([]string{}, consoleJobSubcommands...), "--help"), prefix) - } + if strings.EqualFold(context[0], "help") { + return matchCandidates(visibleHelpTopics(), prefix) + } + + spec, consumed := findCommandPath(context) + if spec == nil { return nil - case "job-config", "jobconfig": - if len(context) == 1 { - return matchCandidates(append(append([]string{}, jobConfigSubcommands...), "--help"), prefix) - } - if strings.EqualFold(context[1], "specs") { - return completeFlags(context[2:], prefix, withGlobalFlags([]flagSpec{ - {name: "--type"}, - {name: "--initial-sync", values: boolValues}, - {name: "--short-term-sync", values: boolValues}, - })) + } + + if consumed == len(context) { + if len(spec.children) > 0 { + candidates := append(append([]string{}, visibleCommandNames(spec.children)...), "--help") + return matchCandidates(candidates, prefix) } - if strings.EqualFold(context[1], "transform-job-type") { - return completeFlags(context[2:], prefix, withGlobalFlags([]flagSpec{ - {name: "--source-type"}, - {name: "--target-type"}, - })) + if len(spec.nextArgs) > 0 && !strings.HasPrefix(prefix, "--") { + return matchCandidates(spec.nextArgs, prefix) } - return nil - case "schemas", "schema": - if len(context) == 1 { - return matchCandidates(append(append([]string{}, schemaSubcommands...), "--help"), prefix) + if len(spec.flags) == 0 { + return nil } - if strings.EqualFold(context[1], "list-trans-objs-by-meta") { - return completeFlags(context[2:], prefix, withGlobalFlags([]flagSpec{ - {name: "--src-db"}, - {name: "--src-schema"}, - {name: "--src-trans-obj"}, - {name: "--dst-db"}, - {name: "--dst-schema"}, - {name: "--dst-tran-obj"}, - })) - } - return nil - default: + return completeFlags(nil, prefix, withGlobalFlags(spec.flags)) + } + + if len(spec.flags) == 0 { return nil } + return completeFlags(context[consumed:], prefix, withGlobalFlags(spec.flags)) } func completeFlags(args []string, prefix string, specs []flagSpec) []string { diff --git a/internal/repl/help.go b/internal/repl/help.go index 9a39414..9fe2273 100644 --- a/internal/repl/help.go +++ b/internal/repl/help.go @@ -19,35 +19,18 @@ func RenderHelp(args []string) string { func (s *Shell) renderHelp(args []string) string { topic := "" if len(args) > 0 { - topic = strings.ToLower(args[0]) + topic = strings.ToLower(strings.TrimSpace(args[0])) } - switch topic { - case "", "overview": + if topic == "" || topic == "overview" { return s.helpOverview() - case "jobs": - return s.helpJobs() - case "datasources": - return s.helpDataSources() - case "clusters": - return s.helpClusters() - case "workers": - return s.helpWorkers() - case "consolejobs": - return s.helpConsoleJobs() - case "job-config", "jobconfig": - return s.helpJobConfig() - case "schemas", "schema": - return s.helpSchemas() - case "config": - return s.helpConfig() - case "lang", "language": - return s.helpLanguage() - case "completion": - return s.helpCompletion() - default: - return s.unknownHelpText(topic) } + + if spec := findRootCommand(topic); canRenderHelp(spec) { + return commandHelpText(s, spec) + } + + return s.unknownHelpText(topic) } func (s *Shell) helpOverview() string { diff --git a/internal/repl/jobs.go b/internal/repl/jobs.go index 8874c47..aa5c73d 100644 --- a/internal/repl/jobs.go +++ b/internal/repl/jobs.go @@ -8,138 +8,171 @@ import ( ) func (s *Shell) handleJobs(tokens []string) error { - if len(tokens) < 2 { - s.io.Println(s.usageJobsGroup()) + return s.dispatchRegisteredCommand(tokens) +} + +func (s *Shell) runJobsList(tokens []string) error { + options, err := parseJobListOptions(tokens[2:]) + if err != nil { + return err + } + return s.printJobs(options) +} + +func (s *Shell) runJobsCreate(tokens []string) error { + options, err := parseFlagArgs(tokens[2:]) + if err != nil { + return err + } + var request datajob.CreateJobRequest + if err := decodeBodyOptions(options, &request); err != nil { + return err + } + if err := ensureNoUnknownOptions(options); err != nil { + return err + } + result, err := s.runtime.DataJobs().CreateJob(request) + if err != nil { + return err + } + return s.printJobCreateResult(result) +} + +func (s *Shell) runJobsShow(tokens []string) error { + if len(tokens) != 3 { + s.io.Println(s.usageJobAction("show")) return nil } + jobID, err := parsePositiveInt64(tokens[2], "jobId") + if err != nil { + return err + } + return s.printJob(jobID) +} - switch strings.ToLower(tokens[1]) { - case "list": - options, err := parseJobListOptions(tokens[2:]) - if err != nil { - return err - } - return s.printJobs(options) - case "create": - options, err := parseFlagArgs(tokens[2:]) - if err != nil { - return err - } - var request datajob.CreateJobRequest - if err := decodeBodyOptions(options, &request); err != nil { - return err - } - if err := ensureNoUnknownOptions(options); err != nil { - return err - } - result, err := s.runtime.DataJobs().CreateJob(request) - if err != nil { - return err - } - return s.printJobCreateResult(result) - case "show": - if len(tokens) != 3 { - s.io.Println(s.usageJobAction("show")) - return nil - } - jobID, err := parsePositiveInt64(tokens[2], "jobId") - if err != nil { - return err - } - return s.printJob(jobID) - case "schema": - if len(tokens) != 3 { - s.io.Println(s.usageJobAction("schema")) - return nil - } - jobID, err := parsePositiveInt64(tokens[2], "jobId") - if err != nil { - return err - } - return s.printJobSchema(jobID) - case "start", "stop", "delete": - if len(tokens) != 3 { - s.io.Println(s.usageJobAction(strings.ToLower(tokens[1]))) - return nil - } - jobID, err := parsePositiveInt64(tokens[2], "jobId") - if err != nil { - return err - } - switch strings.ToLower(tokens[1]) { - case "start": - if err := s.runtime.DataJobs().StartJob(jobID); err != nil { - return err - } - return s.printActionResult("job.started", "job", "started", jobID) - case "stop": - if err := s.runtime.DataJobs().StopJob(jobID); err != nil { - return err - } - return s.printActionResult("job.stopped", "job", "stopped", jobID) - default: - if err := s.runtime.DataJobs().DeleteJob(jobID); err != nil { - return err - } - return s.printActionResult("job.deleted", "job", "deleted", jobID) - } - case "replay": - if len(tokens) < 3 { - s.io.Println(s.usageJobReplay()) - return nil - } - jobID, err := parsePositiveInt64(tokens[2], "jobId") - if err != nil { - return err - } - options, err := parseReplayOptions(tokens[3:]) - if err != nil { - return err - } - if err := s.runtime.DataJobs().ReplayJob(jobID, options); err != nil { - return err - } - return s.printActionResult("job.replayed", "job", "replayed", jobID) - case "attach-incre-task", "detach-incre-task": - if len(tokens) != 3 { - s.io.Println(s.usageJobAction(strings.ToLower(tokens[1]))) - return nil - } - jobID, err := parsePositiveInt64(tokens[2], "jobId") - if err != nil { - return err - } - if strings.EqualFold(tokens[1], "attach-incre-task") { - if err := s.runtime.DataJobs().AttachIncreJob(jobID); err != nil { - return err - } - return s.printActionResult("job.increAttached", "job", "attach-incre-task", jobID) - } - if err := s.runtime.DataJobs().DetachIncreJob(jobID); err != nil { - return err - } - return s.printActionResult("job.increDetached", "job", "detach-incre-task", jobID) - case "update-incre-pos": - options, err := parseFlagArgs(tokens[2:]) - if err != nil { - return err - } - var request datajob.UpdateIncrePosRequest - if err := decodeBodyOptions(options, &request); err != nil { - return err - } - if err := ensureNoUnknownOptions(options); err != nil { - return err - } - result, err := s.runtime.DataJobs().UpdateIncrePos(request) - if err != nil { - return err - } - return s.printUpdateIncrePosResult(request.TaskID, result) - default: - s.printUnknownSubcommand("jobs", tokens[1], jobsSubcommands, s.usageJobsGroup()) +func (s *Shell) runJobsSchema(tokens []string) error { + if len(tokens) != 3 { + s.io.Println(s.usageJobAction("schema")) return nil } + jobID, err := parsePositiveInt64(tokens[2], "jobId") + if err != nil { + return err + } + return s.printJobSchema(jobID) +} + +func (s *Shell) runJobsStart(tokens []string) error { + if len(tokens) != 3 { + s.io.Println(s.usageJobAction("start")) + return nil + } + jobID, err := parsePositiveInt64(tokens[2], "jobId") + if err != nil { + return err + } + if err := s.runtime.DataJobs().StartJob(jobID); err != nil { + return err + } + return s.printActionResult("job.started", "job", "started", jobID) +} + +func (s *Shell) runJobsStop(tokens []string) error { + if len(tokens) != 3 { + s.io.Println(s.usageJobAction("stop")) + return nil + } + jobID, err := parsePositiveInt64(tokens[2], "jobId") + if err != nil { + return err + } + if err := s.runtime.DataJobs().StopJob(jobID); err != nil { + return err + } + return s.printActionResult("job.stopped", "job", "stopped", jobID) +} + +func (s *Shell) runJobsDelete(tokens []string) error { + if len(tokens) != 3 { + s.io.Println(s.usageJobAction("delete")) + return nil + } + jobID, err := parsePositiveInt64(tokens[2], "jobId") + if err != nil { + return err + } + if err := s.runtime.DataJobs().DeleteJob(jobID); err != nil { + return err + } + return s.printActionResult("job.deleted", "job", "deleted", jobID) +} + +func (s *Shell) runJobsReplay(tokens []string) error { + if len(tokens) < 3 { + s.io.Println(s.usageJobReplay()) + return nil + } + jobID, err := parsePositiveInt64(tokens[2], "jobId") + if err != nil { + return err + } + options, err := parseReplayOptions(tokens[3:]) + if err != nil { + return err + } + if err := s.runtime.DataJobs().ReplayJob(jobID, options); err != nil { + return err + } + return s.printActionResult("job.replayed", "job", "replayed", jobID) +} + +func (s *Shell) runJobsAttachIncreTask(tokens []string) error { + if len(tokens) != 3 { + s.io.Println(s.usageJobAction("attach-incre-task")) + return nil + } + jobID, err := parsePositiveInt64(tokens[2], "jobId") + if err != nil { + return err + } + if err := s.runtime.DataJobs().AttachIncreJob(jobID); err != nil { + return err + } + return s.printActionResult("job.increAttached", "job", "attach-incre-task", jobID) +} + +func (s *Shell) runJobsDetachIncreTask(tokens []string) error { + if len(tokens) != 3 { + s.io.Println(s.usageJobAction("detach-incre-task")) + return nil + } + jobID, err := parsePositiveInt64(tokens[2], "jobId") + if err != nil { + return err + } + if err := s.runtime.DataJobs().DetachIncreJob(jobID); err != nil { + return err + } + return s.printActionResult("job.increDetached", "job", "detach-incre-task", jobID) +} + +func (s *Shell) runJobsUpdateIncrePos(tokens []string) error { + options, err := parseFlagArgs(tokens[2:]) + if err != nil { + return err + } + var request datajob.UpdateIncrePosRequest + if err := decodeBodyOptions(options, &request); err != nil { + return err + } + if err := ensureNoUnknownOptions(options); err != nil { + return err + } + result, err := s.runtime.DataJobs().UpdateIncrePos(request) + if err != nil { + return err + } + return s.printUpdateIncrePosResult(request.TaskID, result) } func (s *Shell) printJobs(options datajob.ListOptions) error { diff --git a/internal/repl/registry.go b/internal/repl/registry.go new file mode 100644 index 0000000..e5f10d9 --- /dev/null +++ b/internal/repl/registry.go @@ -0,0 +1,439 @@ +package repl + +import "strings" + +type commandTextFunc func(*Shell) string +type commandRunFunc func(*Shell, []string) error + +type commandSpec struct { + name string + aliases []string + visible bool + visibleInHelp bool + help commandTextFunc + usage commandTextFunc + run commandRunFunc + flags []flagSpec + nextArgs []string + children []*commandSpec +} + +var ( + boolValues = []string{"true", "false"} + outputValues = []string{"text", "json"} + rootCommands = []*commandSpec{ + { + name: "jobs", + visible: true, + visibleInHelp: true, + help: (*Shell).helpJobs, + usage: (*Shell).usageJobsGroup, + children: []*commandSpec{ + {name: "list", visible: true, usage: (*Shell).usageJobsList, flags: []flagSpec{{name: "--name"}, {name: "--type"}, {name: "--desc"}, {name: "--source-id"}, {name: "--target-id"}}}, + {name: "create", visible: true, usage: (*Shell).usageJobCreate, flags: []flagSpec{{name: "--body"}, {name: "--body-file"}}}, + {name: "show", visible: true, usage: bindActionUsage("show", (*Shell).usageJobAction)}, + {name: "schema", visible: true, usage: bindActionUsage("schema", (*Shell).usageJobAction)}, + {name: "start", visible: true, usage: bindActionUsage("start", (*Shell).usageJobAction)}, + {name: "stop", visible: true, usage: bindActionUsage("stop", (*Shell).usageJobAction)}, + {name: "delete", visible: true, usage: bindActionUsage("delete", (*Shell).usageJobAction)}, + {name: "replay", visible: true, usage: (*Shell).usageJobReplay, flags: []flagSpec{{name: "--auto-start", values: boolValues}, {name: "--reset-to-created", values: boolValues}}}, + {name: "attach-incre-task", visible: true, usage: bindActionUsage("attach-incre-task", (*Shell).usageJobAction)}, + {name: "detach-incre-task", visible: true, usage: bindActionUsage("detach-incre-task", (*Shell).usageJobAction)}, + {name: "update-incre-pos", visible: true, usage: (*Shell).usageJobUpdateIncrePos, flags: []flagSpec{{name: "--body"}, {name: "--body-file"}}}, + }, + }, + { + name: "datasources", + visible: true, + visibleInHelp: true, + help: (*Shell).helpDataSources, + usage: (*Shell).usageDataSources, + children: []*commandSpec{ + {name: "list", visible: true, usage: (*Shell).usageDataSourcesList, flags: []flagSpec{{name: "--id"}, {name: "--type"}, {name: "--deploy-type"}, {name: "--host-type"}, {name: "--lifecycle"}}}, + {name: "add", visible: true, usage: (*Shell).usageDataSourceAdd, flags: []flagSpec{{name: "--body"}, {name: "--body-file"}, {name: "--security-file"}, {name: "--secret-file"}}}, + {name: "delete", visible: true, usage: bindActionUsage("delete", (*Shell).usageDataSourceAction)}, + {name: "show", visible: true, usage: (*Shell).usageDataSourceShow}, + }, + }, + { + name: "clusters", + visible: true, + visibleInHelp: true, + help: (*Shell).helpClusters, + usage: (*Shell).usageClusters, + children: []*commandSpec{ + {name: "list", visible: true, usage: (*Shell).usageClustersList, flags: []flagSpec{{name: "--name"}, {name: "--desc"}, {name: "--cloud"}, {name: "--region"}}}, + }, + }, + { + name: "workers", + visible: true, + visibleInHelp: true, + help: (*Shell).helpWorkers, + usage: (*Shell).usageWorkers, + children: []*commandSpec{ + {name: "list", visible: true, usage: (*Shell).usageWorkersList, flags: []flagSpec{{name: "--cluster-id"}, {name: "--source-id"}, {name: "--target-id"}}}, + {name: "start", visible: true, usage: bindActionUsage("start", (*Shell).usageWorkerAction)}, + {name: "stop", visible: true, usage: bindActionUsage("stop", (*Shell).usageWorkerAction)}, + {name: "delete", visible: true, usage: bindActionUsage("delete", (*Shell).usageWorkerAction)}, + {name: "modify-mem-oversold", visible: true, usage: (*Shell).usageWorkerModifyMemOverSold, flags: []flagSpec{{name: "--percent"}}}, + {name: "update-alert", visible: true, usage: (*Shell).usageWorkerUpdateAlert, flags: []flagSpec{{name: "--phone", values: boolValues}, {name: "--email", values: boolValues}, {name: "--im", values: boolValues}, {name: "--sms", values: boolValues}}}, + }, + }, + { + name: "consolejobs", + visible: true, + visibleInHelp: true, + help: (*Shell).helpConsoleJobs, + usage: (*Shell).usageConsoleJobs, + children: []*commandSpec{ + {name: "show", visible: true, usage: (*Shell).usageConsoleJobShow}, + }, + }, + { + name: "job-config", + aliases: []string{"jobconfig"}, + visible: true, + visibleInHelp: true, + help: (*Shell).helpJobConfig, + usage: (*Shell).usageJobConfig, + children: []*commandSpec{ + {name: "specs", visible: true, usage: (*Shell).usageJobConfigSpecs, flags: []flagSpec{{name: "--type"}, {name: "--initial-sync", values: boolValues}, {name: "--short-term-sync", values: boolValues}}}, + {name: "transform-job-type", visible: true, usage: (*Shell).usageJobConfigTransform, flags: []flagSpec{{name: "--source-type"}, {name: "--target-type"}}}, + }, + }, + { + name: "schemas", + aliases: []string{"schema"}, + visible: true, + visibleInHelp: true, + help: (*Shell).helpSchemas, + usage: (*Shell).usageSchemas, + children: []*commandSpec{ + {name: "list-trans-objs-by-meta", visible: true, usage: (*Shell).usageSchemas, flags: []flagSpec{{name: "--src-db"}, {name: "--src-schema"}, {name: "--src-trans-obj"}, {name: "--dst-db"}, {name: "--dst-schema"}, {name: "--dst-tran-obj"}}}, + }, + }, + { + name: "config", + visible: true, + visibleInHelp: true, + help: (*Shell).helpConfig, + usage: (*Shell).usageConfig, + children: []*commandSpec{ + {name: "show", visible: true, usage: (*Shell).usageConfigShow}, + {name: "init", visible: true, usage: (*Shell).usageConfigInit}, + newLanguageCommand("lang", true), + }, + }, + newLanguageCommand("lang", false, "language"), + { + name: "completion", + help: (*Shell).helpCompletion, + usage: (*Shell).usageCompletion, + children: []*commandSpec{ + {name: "zsh", visible: true, usage: (*Shell).usageCompletion}, + {name: "bash", visible: true, usage: (*Shell).usageCompletion}, + }, + }, + { + name: "clear", + aliases: []string{"cls"}, + }, + { + name: "__complete", + }, + } +) + +func bindActionUsage(action string, fn func(*Shell, string) string) commandTextFunc { + return func(s *Shell) string { + return fn(s, action) + } +} + +func newLanguageCommand(name string, visible bool, aliases ...string) *commandSpec { + return &commandSpec{ + name: name, + aliases: aliases, + visible: visible, + help: (*Shell).helpLanguage, + usage: (*Shell).usageConfigLang, + children: []*commandSpec{ + {name: "show", visible: true}, + {name: "set", visible: true, nextArgs: []string{"en", "zh"}}, + }, + } +} + +func init() { + mustSetCommandRun("jobs", (*Shell).handleJobs) + mustSetCommandRun("datasources", (*Shell).handleDataSources) + mustSetCommandRun("clusters", (*Shell).handleClusters) + mustSetCommandRun("workers", (*Shell).handleWorkers) + mustSetCommandRun("consolejobs", (*Shell).handleConsoleJobs) + mustSetCommandRun("job-config", (*Shell).handleJobConfig) + mustSetCommandRun("schemas", (*Shell).handleSchemas) + mustSetCommandRun("config", (*Shell).handleConfig) + mustSetCommandRun("lang", (*Shell).handleLang) + mustSetCommandRun("completion", (*Shell).handleCompletion) + mustSetCommandRun("clear", runClearScreen) + mustSetCommandRun("__complete", runHiddenCompletion) + + mustSetSubcommandRun("jobs", "list", (*Shell).runJobsList) + mustSetSubcommandRun("jobs", "create", (*Shell).runJobsCreate) + mustSetSubcommandRun("jobs", "show", (*Shell).runJobsShow) + mustSetSubcommandRun("jobs", "schema", (*Shell).runJobsSchema) + mustSetSubcommandRun("jobs", "start", (*Shell).runJobsStart) + mustSetSubcommandRun("jobs", "stop", (*Shell).runJobsStop) + mustSetSubcommandRun("jobs", "delete", (*Shell).runJobsDelete) + mustSetSubcommandRun("jobs", "replay", (*Shell).runJobsReplay) + mustSetSubcommandRun("jobs", "attach-incre-task", (*Shell).runJobsAttachIncreTask) + mustSetSubcommandRun("jobs", "detach-incre-task", (*Shell).runJobsDetachIncreTask) + mustSetSubcommandRun("jobs", "update-incre-pos", (*Shell).runJobsUpdateIncrePos) + + mustSetSubcommandRun("datasources", "list", (*Shell).runDataSourcesList) + mustSetSubcommandRun("datasources", "add", (*Shell).runDataSourcesAdd) + mustSetSubcommandRun("datasources", "delete", (*Shell).runDataSourcesDelete) + mustSetSubcommandRun("datasources", "show", (*Shell).runDataSourcesShow) + + mustSetSubcommandRun("clusters", "list", (*Shell).runClustersList) + + mustSetSubcommandRun("workers", "list", (*Shell).runWorkersList) + mustSetSubcommandRun("workers", "start", (*Shell).runWorkersStart) + mustSetSubcommandRun("workers", "stop", (*Shell).runWorkersStop) + mustSetSubcommandRun("workers", "delete", (*Shell).runWorkersDelete) + mustSetSubcommandRun("workers", "modify-mem-oversold", (*Shell).runWorkersModifyMemOversold) + mustSetSubcommandRun("workers", "update-alert", (*Shell).runWorkersUpdateAlert) + + mustSetSubcommandRun("consolejobs", "show", (*Shell).runConsoleJobsShow) + + mustSetSubcommandRun("job-config", "specs", (*Shell).runJobConfigSpecs) + mustSetSubcommandRun("job-config", "transform-job-type", (*Shell).runJobConfigTransformJobType) + + mustSetSubcommandRun("schemas", "list-trans-objs-by-meta", (*Shell).runSchemasListTransferObjects) + + mustSetCommandRunPath([]string{"config", "show"}, (*Shell).runConfigShow) + mustSetCommandRunPath([]string{"config", "init"}, (*Shell).runConfigInit) + mustSetCommandRunPath([]string{"config", "lang", "show"}, (*Shell).runLanguageShow) + mustSetCommandRunPath([]string{"config", "lang", "set"}, (*Shell).runLanguageSet) + + mustSetCommandRunPath([]string{"lang", "show"}, (*Shell).runLanguageShow) + mustSetCommandRunPath([]string{"lang", "set"}, (*Shell).runLanguageSet) + + mustSetCommandRunPath([]string{"completion", "zsh"}, (*Shell).runCompletionZsh) + mustSetCommandRunPath([]string{"completion", "bash"}, (*Shell).runCompletionBash) +} + +func mustSetCommandRun(name string, run commandRunFunc) { + mustSetCommandRunPath([]string{name}, run) +} + +func mustSetSubcommandRun(root string, child string, run commandRunFunc) { + mustSetCommandRunPath([]string{root, child}, run) +} + +func mustSetCommandRunPath(path []string, run commandRunFunc) { + spec, consumed := findCommandPath(path) + if spec == nil || consumed != len(path) { + panic("command not found: " + strings.Join(path, " ")) + } + spec.run = run +} + +func runClearScreen(shell *Shell, _ []string) error { + shell.io.ClearScreen() + return nil +} + +func runHiddenCompletion(shell *Shell, tokens []string) error { + shell.printHiddenCompletions(tokens[1:]) + return nil +} + +func (s *Shell) dispatchRegisteredCommand(tokens []string) error { + if len(tokens) == 0 { + return nil + } + spec, consumed := findCommandPath(tokens) + if spec == nil { + return nil + } + parent := findCommandParent(tokens, consumed) + + if len(tokens) == consumed { + if len(spec.children) > 0 { + s.io.Println(commandUsageText(s, spec, parent)) + return nil + } + if spec.run != nil { + return spec.run(s, tokens) + } + s.io.Println(commandUsageText(s, spec, parent)) + return nil + } + + if len(spec.children) > 0 { + s.printUnknownSubcommand(commandPathName(tokens, consumed), tokens[consumed], visibleCommandNames(spec.children), commandUsageText(s, spec, parent)) + return nil + } + if spec.run != nil { + return spec.run(s, tokens) + } + s.io.Println(commandUsageText(s, spec, parent)) + return nil +} + +func visibleTopLevelCommands() []string { + return append([]string{"help"}, visibleCommandNames(rootCommands)...) +} + +func visibleHelpTopics() []string { + topics := make([]string, 0, len(rootCommands)) + for _, spec := range rootCommands { + if spec.visibleInHelp { + topics = append(topics, spec.name) + } + } + return topics +} + +func visibleCommandNames(specs []*commandSpec) []string { + names := make([]string, 0, len(specs)) + for _, spec := range specs { + if spec.visible { + names = append(names, spec.name) + } + } + return names +} + +func findRootCommand(token string) *commandSpec { + return findCommand(rootCommands, token) +} + +func findChildCommand(parent *commandSpec, token string) *commandSpec { + if parent == nil { + return nil + } + return findCommand(parent.children, token) +} + +func subcommandCandidates(path ...string) []string { + spec, consumed := findCommandPath(path) + if spec == nil || consumed != len(path) { + return nil + } + return visibleCommandNames(spec.children) +} + +func findCommandPath(tokens []string) (*commandSpec, int) { + if len(tokens) == 0 { + return nil, 0 + } + + spec := findRootCommand(tokens[0]) + if spec == nil { + return nil, 0 + } + + consumed := 1 + for consumed < len(tokens) { + child := findChildCommand(spec, tokens[consumed]) + if child == nil { + break + } + spec = child + consumed++ + } + return spec, consumed +} + +func findCommandParent(tokens []string, consumed int) *commandSpec { + if consumed <= 1 { + return nil + } + parent, _ := findCommandPath(tokens[:consumed-1]) + return parent +} + +func commandPathName(tokens []string, consumed int) string { + names := make([]string, 0, consumed) + spec := findRootCommand(tokens[0]) + if spec == nil { + return "" + } + names = append(names, spec.name) + for i := 1; i < consumed; i++ { + spec = findChildCommand(spec, tokens[i]) + if spec == nil { + break + } + names = append(names, spec.name) + } + return strings.Join(names, " ") +} + +func findCommand(specs []*commandSpec, token string) *commandSpec { + lower := strings.ToLower(strings.TrimSpace(token)) + if lower == "" { + return nil + } + + for _, spec := range specs { + if strings.EqualFold(spec.name, lower) { + return spec + } + for _, alias := range spec.aliases { + if strings.EqualFold(alias, lower) { + return spec + } + } + } + return nil +} + +func commandHelpText(shell *Shell, spec *commandSpec) string { + if spec == nil { + return shell.helpOverview() + } + if spec.help != nil { + return spec.help(shell) + } + if spec.usage != nil { + return spec.usage(shell) + } + return shell.helpOverview() +} + +func commandUsageText(shell *Shell, spec *commandSpec, parent *commandSpec) string { + if spec == nil { + return shell.helpOverview() + } + if spec.usage != nil { + return spec.usage(shell) + } + return commandUsageOrHelpText(shell, spec, parent) +} + +func canRenderHelp(spec *commandSpec) bool { + if spec == nil { + return false + } + return spec.visibleInHelp || spec.help != nil || spec.usage != nil +} + +func commandUsageOrHelpText(shell *Shell, spec *commandSpec, parent *commandSpec) string { + if spec == nil { + return commandHelpText(shell, parent) + } + if len(spec.children) > 0 && spec.help != nil { + return spec.help(shell) + } + if spec.usage != nil { + return spec.usage(shell) + } + if spec.help != nil { + return spec.help(shell) + } + return commandHelpText(shell, parent) +} diff --git a/internal/repl/resources.go b/internal/repl/resources.go index d1be086..96f42d6 100644 --- a/internal/repl/resources.go +++ b/internal/repl/resources.go @@ -14,67 +14,77 @@ import ( ) func (s *Shell) handleDataSources(tokens []string) error { - if len(tokens) < 2 { - s.io.Println(s.usageDataSources()) - return nil + return s.dispatchRegisteredCommand(tokens) +} + +func (s *Shell) handleClusters(tokens []string) error { + return s.dispatchRegisteredCommand(tokens) +} + +func (s *Shell) handleWorkers(tokens []string) error { + return s.dispatchRegisteredCommand(tokens) +} + +func (s *Shell) handleConsoleJobs(tokens []string) error { + return s.dispatchRegisteredCommand(tokens) +} + +func (s *Shell) handleJobConfig(tokens []string) error { + return s.dispatchRegisteredCommand(tokens) +} + +func (s *Shell) handleSchemas(tokens []string) error { + return s.dispatchRegisteredCommand(tokens) +} + +func (s *Shell) runDataSourcesList(tokens []string) error { + options, err := parseDataSourceListOptions(tokens[2:]) + if err != nil { + return err } + return s.printDataSources(options) +} - switch strings.ToLower(tokens[1]) { - case "list": - options, err := parseDataSourceListOptions(tokens[2:]) - if err != nil { - return err - } - return s.printDataSources(options) - case "add": - options, err := parseDataSourceAddOptions(tokens[2:]) - if err != nil { - return err - } - result, err := s.runtime.DataSources().Add(options) - if err != nil { - return err - } - return s.printDataSourceAddResult(result) - case "delete": - if len(tokens) != 3 { - s.io.Println(s.usageDataSourceAction("delete")) - return nil - } - dataSourceID, err := parsePositiveInt64(tokens[2], "dataSourceId") - if err != nil { - return err - } - if err := s.runtime.DataSources().Delete(dataSourceID); err != nil { - return err - } - return s.printActionResult("datasource.deleted", "datasource", "deleted", dataSourceID) - case "show": - if len(tokens) != 3 { - s.io.Println(s.usageDataSourceShow()) - return nil - } - dataSourceID, err := parsePositiveInt64(tokens[2], "dataSourceId") - if err != nil { - return err - } - return s.printDataSource(dataSourceID) - default: - s.printUnknownSubcommand("datasources", tokens[1], dataSourceSubcommands, s.usageDataSources()) - return nil +func (s *Shell) runDataSourcesAdd(tokens []string) error { + options, err := parseDataSourceAddOptions(tokens[2:]) + if err != nil { + return err + } + result, err := s.runtime.DataSources().Add(options) + if err != nil { + return err } + return s.printDataSourceAddResult(result) } -func (s *Shell) handleClusters(tokens []string) error { - if len(tokens) < 2 { - s.io.Println(s.usageClusters()) +func (s *Shell) runDataSourcesDelete(tokens []string) error { + if len(tokens) != 3 { + s.io.Println(s.usageDataSourceAction("delete")) return nil } - if !strings.EqualFold(tokens[1], "list") { - s.printUnknownSubcommand("clusters", tokens[1], clusterSubcommands, s.usageClusters()) + dataSourceID, err := parsePositiveInt64(tokens[2], "dataSourceId") + if err != nil { + return err + } + if err := s.runtime.DataSources().Delete(dataSourceID); err != nil { + return err + } + return s.printActionResult("datasource.deleted", "datasource", "deleted", dataSourceID) +} + +func (s *Shell) runDataSourcesShow(tokens []string) error { + if len(tokens) != 3 { + s.io.Println(s.usageDataSourceShow()) return nil } + dataSourceID, err := parsePositiveInt64(tokens[2], "dataSourceId") + if err != nil { + return err + } + return s.printDataSource(dataSourceID) +} +func (s *Shell) runClustersList(tokens []string) error { options, err := parseClusterListOptions(tokens[2:]) if err != nil { return err @@ -82,144 +92,136 @@ func (s *Shell) handleClusters(tokens []string) error { return s.printClusters(options) } -func (s *Shell) handleWorkers(tokens []string) error { - if len(tokens) < 2 { - s.io.Println(s.usageWorkers()) +func (s *Shell) runWorkersList(tokens []string) error { + options, err := parseWorkerListOptions(tokens[2:]) + if err != nil { + return err + } + return s.printWorkers(options) +} + +func (s *Shell) runWorkersStart(tokens []string) error { + if len(tokens) != 3 { + s.io.Println(s.usageWorkerAction("start")) return nil } + workerID, err := parsePositiveInt64(tokens[2], "workerId") + if err != nil { + return err + } + if err := s.runtime.Workers().Start(workerID); err != nil { + return err + } + return s.printActionResult("worker.started", "worker", "started", workerID) +} - switch strings.ToLower(tokens[1]) { - case "list": - options, err := parseWorkerListOptions(tokens[2:]) - if err != nil { - return err - } - return s.printWorkers(options) - case "start", "stop", "delete": - if len(tokens) != 3 { - s.io.Println(s.usageWorkerAction(strings.ToLower(tokens[1]))) - return nil - } - workerID, err := parsePositiveInt64(tokens[2], "workerId") - if err != nil { - return err - } - if strings.EqualFold(tokens[1], "start") { - if err := s.runtime.Workers().Start(workerID); err != nil { - return err - } - return s.printActionResult("worker.started", "worker", "started", workerID) - } - if strings.EqualFold(tokens[1], "delete") { - if err := s.runtime.Workers().Delete(workerID); err != nil { - return err - } - return s.printActionResult("worker.deleted", "worker", "deleted", workerID) - } - if err := s.runtime.Workers().Stop(workerID); err != nil { - return err - } - return s.printActionResult("worker.stopped", "worker", "stopped", workerID) - case "modify-mem-oversold": - if len(tokens) < 3 { - s.io.Println(s.usageWorkerModifyMemOverSold()) - return nil - } - workerID, err := parsePositiveInt64(tokens[2], "workerId") - if err != nil { - return err - } - options, err := parseFlagArgs(tokens[3:]) - if err != nil { - return err - } - percentValue, err := parseRequiredPositiveInt64Option(options, "memOverSoldPercent", "percent", "mem-over-sold-percent") - if err != nil { - return err - } - if err := ensureNoUnknownOptions(options); err != nil { - return err - } - if err := s.runtime.Workers().ModifyMemOverSold(workerID, int(percentValue)); err != nil { - return err - } - return s.printActionResult("worker.memOverSoldUpdated", "worker", "modify-mem-oversold", workerID) - case "update-alert": - if len(tokens) < 3 { - s.io.Println(s.usageWorkerUpdateAlert()) - return nil - } - workerID, err := parsePositiveInt64(tokens[2], "workerId") - if err != nil { - return err - } - options, err := parseFlagArgs(tokens[3:]) - if err != nil { - return err - } - phone, err := parseRequiredBoolOption(options, "phone", "phone") - if err != nil { - return err - } - email, err := parseRequiredBoolOption(options, "email", "email") - if err != nil { - return err - } - im, err := parseRequiredBoolOption(options, "im", "im") - if err != nil { - return err - } - sms, err := parseRequiredBoolOption(options, "sms", "sms") - if err != nil { - return err - } - if err := ensureNoUnknownOptions(options); err != nil { - return err - } - if err := s.runtime.Workers().UpdateWorkerAlert(workerID, phone, email, im, sms); err != nil { - return err - } - return s.printActionResult("worker.alertUpdated", "worker", "update-alert", workerID) - default: - s.printUnknownSubcommand("workers", tokens[1], workerSubcommands, s.usageWorkers()) +func (s *Shell) runWorkersStop(tokens []string) error { + if len(tokens) != 3 { + s.io.Println(s.usageWorkerAction("stop")) return nil } + workerID, err := parsePositiveInt64(tokens[2], "workerId") + if err != nil { + return err + } + if err := s.runtime.Workers().Stop(workerID); err != nil { + return err + } + return s.printActionResult("worker.stopped", "worker", "stopped", workerID) } -func (s *Shell) handleConsoleJobs(tokens []string) error { +func (s *Shell) runWorkersDelete(tokens []string) error { if len(tokens) != 3 { - s.io.Println(s.usageConsoleJobs()) + s.io.Println(s.usageWorkerAction("delete")) return nil } - if !strings.EqualFold(tokens[1], "show") { - s.printUnknownSubcommand("consolejobs", tokens[1], consoleJobSubcommands, s.usageConsoleJobs()) + workerID, err := parsePositiveInt64(tokens[2], "workerId") + if err != nil { + return err + } + if err := s.runtime.Workers().Delete(workerID); err != nil { + return err + } + return s.printActionResult("worker.deleted", "worker", "deleted", workerID) +} + +func (s *Shell) runWorkersModifyMemOversold(tokens []string) error { + if len(tokens) < 3 { + s.io.Println(s.usageWorkerModifyMemOverSold()) return nil } + workerID, err := parsePositiveInt64(tokens[2], "workerId") + if err != nil { + return err + } + options, err := parseFlagArgs(tokens[3:]) + if err != nil { + return err + } + percentValue, err := parseRequiredPositiveInt64Option(options, "memOverSoldPercent", "percent", "mem-over-sold-percent") + if err != nil { + return err + } + if err := ensureNoUnknownOptions(options); err != nil { + return err + } + if err := s.runtime.Workers().ModifyMemOverSold(workerID, int(percentValue)); err != nil { + return err + } + return s.printActionResult("worker.memOverSoldUpdated", "worker", "modify-mem-oversold", workerID) +} - consoleJobID, err := parsePositiveInt64(tokens[2], "consoleJobId") +func (s *Shell) runWorkersUpdateAlert(tokens []string) error { + if len(tokens) < 3 { + s.io.Println(s.usageWorkerUpdateAlert()) + return nil + } + workerID, err := parsePositiveInt64(tokens[2], "workerId") if err != nil { return err } - return s.printConsoleJob(consoleJobID) + options, err := parseFlagArgs(tokens[3:]) + if err != nil { + return err + } + phone, err := parseRequiredBoolOption(options, "phone", "phone") + if err != nil { + return err + } + email, err := parseRequiredBoolOption(options, "email", "email") + if err != nil { + return err + } + im, err := parseRequiredBoolOption(options, "im", "im") + if err != nil { + return err + } + sms, err := parseRequiredBoolOption(options, "sms", "sms") + if err != nil { + return err + } + if err := ensureNoUnknownOptions(options); err != nil { + return err + } + if err := s.runtime.Workers().UpdateWorkerAlert(workerID, phone, email, im, sms); err != nil { + return err + } + return s.printActionResult("worker.alertUpdated", "worker", "update-alert", workerID) } -func (s *Shell) handleJobConfig(tokens []string) error { - if len(tokens) < 2 { - s.io.Println(s.usageJobConfig()) +func (s *Shell) runConsoleJobsShow(tokens []string) error { + if len(tokens) != 3 { + s.io.Println(s.usageConsoleJobs()) return nil } - if !strings.EqualFold(tokens[1], "specs") { - if !strings.EqualFold(tokens[1], "transform-job-type") { - s.printUnknownSubcommand("job-config", tokens[1], jobConfigSubcommands, s.usageJobConfig()) - return nil - } - options, err := parseTransformJobTypeOptions(tokens[2:]) - if err != nil { - return err - } - return s.printTransformJobType(options) + consoleJobID, err := parsePositiveInt64(tokens[2], "consoleJobId") + if err != nil { + return err } + return s.printConsoleJob(consoleJobID) +} +func (s *Shell) runJobConfigSpecs(tokens []string) error { options, err := parseListSpecsOptions(tokens[2:]) if err != nil { return err @@ -227,15 +229,15 @@ func (s *Shell) handleJobConfig(tokens []string) error { return s.printSpecs(options) } -func (s *Shell) handleSchemas(tokens []string) error { - if len(tokens) < 2 { - s.io.Println(s.usageSchemas()) - return nil - } - if !strings.EqualFold(tokens[1], "list-trans-objs-by-meta") { - s.printUnknownSubcommand("schemas", tokens[1], schemaSubcommands, s.usageSchemas()) - return nil +func (s *Shell) runJobConfigTransformJobType(tokens []string) error { + options, err := parseTransformJobTypeOptions(tokens[2:]) + if err != nil { + return err } + return s.printTransformJobType(options) +} + +func (s *Shell) runSchemasListTransferObjects(tokens []string) error { options, err := parseSchemaListOptions(tokens[2:]) if err != nil { return err diff --git a/internal/repl/shell.go b/internal/repl/shell.go index b3c77d0..34e8ec0 100644 --- a/internal/repl/shell.go +++ b/internal/repl/shell.go @@ -92,126 +92,102 @@ func (s *Shell) handleTokens(tokens []string) error { return nil } - switch strings.ToLower(tokens[0]) { - case "clear", "cls": - s.io.ClearScreen() - return nil - case "completion": - return wrapCommandError(s.handleCompletion(tokens), format) - case "__complete": - s.printHiddenCompletions(tokens[1:]) - return nil - case "jobs": - return wrapCommandError(s.handleJobs(tokens), format) - case "datasources": - return wrapCommandError(s.handleDataSources(tokens), format) - case "clusters": - return wrapCommandError(s.handleClusters(tokens), format) - case "workers": - return wrapCommandError(s.handleWorkers(tokens), format) - case "consolejobs": - return wrapCommandError(s.handleConsoleJobs(tokens), format) - case "job-config", "jobconfig": - return wrapCommandError(s.handleJobConfig(tokens), format) - case "schemas", "schema": - return wrapCommandError(s.handleSchemas(tokens), format) - case "config": - return wrapCommandError(s.handleConfig(tokens), format) - case "lang", "language": - return wrapCommandError(s.handleLang(tokens), format) - default: - s.printUnknownCommand(tokens[0]) - return nil + if spec := findRootCommand(tokens[0]); spec != nil && spec.run != nil { + return wrapCommandError(spec.run(s, tokens), format) } + + s.printUnknownCommand(tokens[0]) + return nil } func (s *Shell) handleConfig(tokens []string) error { - if len(tokens) < 2 { - s.io.Println(s.usageConfig()) + return s.dispatchRegisteredCommand(tokens) +} + +func (s *Shell) handleLang(tokens []string) error { + return s.dispatchRegisteredCommand(tokens) +} + +func (s *Shell) runConfigShow(tokens []string) error { + if len(tokens) != 2 { + s.io.Println(s.usageConfigShow()) return nil } - switch strings.ToLower(tokens[1]) { - case "show": - if len(tokens) != 2 { - s.io.Println(s.usageConfigShow()) - return nil - } - cfg := s.runtime.Config() - if s.isJSONOutput() { - return s.printJSON(map[string]any{ - "apiBaseUrl": cfg.APIBaseURL, - "accessKeyMasked": util.MaskSecret(cfg.AccessKey), - "language": cfg.NormalizedLanguage(), - "httpTimeoutSeconds": cfg.HTTPTimeoutSecondsValue(), - "httpReadMaxRetries": cfg.HTTPReadMaxRetriesValue(), - "httpReadRetryBackoffMillis": cfg.HTTPReadRetryBackoffMillisValue(), - }) - } - s.io.Println(i18n.T("config.apiBaseUrlLabel") + ": " + cfg.APIBaseURL) - s.io.Println(i18n.T("config.accessKeyLabel") + ": " + util.MaskSecret(cfg.AccessKey)) - s.io.Println(i18n.T("config.languageLabel") + ": " + cfg.NormalizedLanguage()) - s.io.Println(i18n.T("config.httpTimeoutLabel") + ": " + strconv.Itoa(cfg.HTTPTimeoutSecondsValue())) - s.io.Println(i18n.T("config.httpReadMaxRetriesLabel") + ": " + strconv.Itoa(cfg.HTTPReadMaxRetriesValue())) - s.io.Println(i18n.T("config.httpReadRetryBackoffMillisLabel") + ": " + strconv.Itoa(cfg.HTTPReadRetryBackoffMillisValue())) - return nil - case "init": - if len(tokens) != 2 { - s.io.Println(s.usageConfigInit()) - return nil - } - updated, err := s.runtime.Reinitialize(s.io) - if err != nil { - return err - } - if updated { - s.io.Println(i18n.T("runtime.configUpdated")) - } - return nil - case "lang": - return s.handleLanguageTokens(tokens[2:], "config lang", s.usageConfigLang()) - default: - s.printUnknownSubcommand("config", tokens[1], configSubcommands, s.usageConfig()) - return nil + + cfg := s.runtime.Config() + if s.isJSONOutput() { + return s.printJSON(map[string]any{ + "apiBaseUrl": cfg.APIBaseURL, + "accessKeyMasked": util.MaskSecret(cfg.AccessKey), + "language": cfg.NormalizedLanguage(), + "httpTimeoutSeconds": cfg.HTTPTimeoutSecondsValue(), + "httpReadMaxRetries": cfg.HTTPReadMaxRetriesValue(), + "httpReadRetryBackoffMillis": cfg.HTTPReadRetryBackoffMillisValue(), + }) } + s.io.Println(i18n.T("config.apiBaseUrlLabel") + ": " + cfg.APIBaseURL) + s.io.Println(i18n.T("config.accessKeyLabel") + ": " + util.MaskSecret(cfg.AccessKey)) + s.io.Println(i18n.T("config.languageLabel") + ": " + cfg.NormalizedLanguage()) + s.io.Println(i18n.T("config.httpTimeoutLabel") + ": " + strconv.Itoa(cfg.HTTPTimeoutSecondsValue())) + s.io.Println(i18n.T("config.httpReadMaxRetriesLabel") + ": " + strconv.Itoa(cfg.HTTPReadMaxRetriesValue())) + s.io.Println(i18n.T("config.httpReadRetryBackoffMillisLabel") + ": " + strconv.Itoa(cfg.HTTPReadRetryBackoffMillisValue())) + return nil } -func (s *Shell) handleLang(tokens []string) error { - return s.handleLanguageTokens(tokens[1:], "config lang", s.usageConfigLang()) +func (s *Shell) runConfigInit(tokens []string) error { + if len(tokens) != 2 { + s.io.Println(s.usageConfigInit()) + return nil + } + updated, err := s.runtime.Reinitialize(s.io) + if err != nil { + return err + } + if updated { + s.io.Println(i18n.T("runtime.configUpdated")) + } + return nil } -func (s *Shell) handleLanguageTokens(tokens []string, group string, usage string) error { - if len(tokens) == 0 || strings.EqualFold(tokens[0], "show") { - if len(tokens) > 1 { - s.io.Println(usage) - return nil - } - if s.isJSONOutput() { - return s.printJSON(map[string]any{ - "language": s.runtime.Config().NormalizedLanguage(), - "supported": []string{"en", "zh"}, - }) - } - s.io.Println(i18n.T("lang.current", s.runtime.Config().NormalizedLanguage())) - s.io.Println(i18n.T("common.supportedLanguages")) +func (s *Shell) runLanguageShow(tokens []string) error { + expectedLen := languageValueIndex(tokens) + if len(tokens) != expectedLen { + s.io.Println(s.usageConfigLang()) return nil } - if strings.EqualFold(tokens[0], "set") { - if len(tokens) != 2 { - s.io.Println(usage) - return nil - } - if err := s.runtime.SetLanguage(tokens[1]); err != nil { - return err - } - if s.isJSONOutput() { - return s.printJSON(map[string]any{ - "language": s.runtime.Config().NormalizedLanguage(), - "message": i18n.T("lang.updated", i18n.DisplayName(s.runtime.Config().NormalizedLanguage())), - }) - } - s.io.Println(i18n.T("lang.updated", i18n.DisplayName(s.runtime.Config().NormalizedLanguage()))) + if s.isJSONOutput() { + return s.printJSON(map[string]any{ + "language": s.runtime.Config().NormalizedLanguage(), + "supported": []string{"en", "zh"}, + }) + } + s.io.Println(i18n.T("lang.current", s.runtime.Config().NormalizedLanguage())) + s.io.Println(i18n.T("common.supportedLanguages")) + return nil +} + +func (s *Shell) runLanguageSet(tokens []string) error { + valueIndex := languageValueIndex(tokens) + if len(tokens) != valueIndex+1 { + s.io.Println(s.usageConfigLang()) return nil } - s.printUnknownSubcommand(group, tokens[0], langSubcommands, usage) + if err := s.runtime.SetLanguage(tokens[valueIndex]); err != nil { + return err + } + if s.isJSONOutput() { + return s.printJSON(map[string]any{ + "language": s.runtime.Config().NormalizedLanguage(), + "message": i18n.T("lang.updated", i18n.DisplayName(s.runtime.Config().NormalizedLanguage())), + }) + } + s.io.Println(i18n.T("lang.updated", i18n.DisplayName(s.runtime.Config().NormalizedLanguage()))) return nil } + +func languageValueIndex(tokens []string) int { + if len(tokens) > 0 && strings.EqualFold(tokens[0], "config") { + return 3 + } + return 2 +} diff --git a/internal/repl/ux.go b/internal/repl/ux.go index cb29820..aede0ca 100644 --- a/internal/repl/ux.go +++ b/internal/repl/ux.go @@ -5,19 +5,6 @@ import ( "strings" ) -var ( - jobsSubcommands = []string{"list", "create", "show", "schema", "start", "stop", "delete", "replay", "attach-incre-task", "detach-incre-task", "update-incre-pos"} - dataSourceSubcommands = []string{"list", "add", "delete", "show"} - clusterSubcommands = []string{"list"} - workerSubcommands = []string{"list", "start", "stop", "delete", "modify-mem-oversold", "update-alert"} - consoleJobSubcommands = []string{"show"} - jobConfigSubcommands = []string{"specs", "transform-job-type"} - schemaSubcommands = []string{"list-trans-objs-by-meta"} - configSubcommands = []string{"show", "init", "lang"} - langSubcommands = []string{"show", "set"} - completionSubcommands = []string{"zsh", "bash"} -) - func isHelpToken(token string) bool { switch strings.ToLower(strings.TrimSpace(token)) { case "help", "-h", "--help": @@ -103,7 +90,7 @@ func (s *Shell) unknownHelpText(topic string) string { lines := []string{ i18n.T("common.errorPrefix", i18n.T("common.unknownHelpTopic", topic)), } - if suggestion := suggestCandidate(topic, visibleHelpTopics); suggestion != "" { + if suggestion := suggestCandidate(topic, visibleHelpTopics()); suggestion != "" { lines = append(lines, i18n.T("common.didYouMean", "help "+suggestion)) } lines = append(lines, "", s.helpOverview()) @@ -112,7 +99,7 @@ func (s *Shell) unknownHelpText(topic string) string { func (s *Shell) printUnknownCommand(command string) { s.io.Println(i18n.T("common.unknownCommand", command)) - if suggestion := suggestCandidate(command, visibleTopLevelCommands); suggestion != "" { + if suggestion := suggestCandidate(command, visibleTopLevelCommands()); suggestion != "" { s.io.Println(i18n.T("common.didYouMean", suggestion)) } s.io.Println(i18n.T("common.useHelp")) @@ -139,122 +126,20 @@ func RenderCommandHelp(tokens []string) (string, bool) { return shell.renderHelp(nil), true } - root := strings.ToLower(tokens[0]) if len(tokens) >= 2 && isHelpToken(tokens[1]) { - switch root { - case "jobs": - return shell.helpJobs(), true - case "datasources": - return shell.helpDataSources(), true - case "clusters": - return shell.helpClusters(), true - case "workers": - return shell.helpWorkers(), true - case "consolejobs": - return shell.helpConsoleJobs(), true - case "job-config", "jobconfig": - return shell.helpJobConfig(), true - case "schemas", "schema": - return shell.helpSchemas(), true - case "config": - return shell.helpConfig(), true - case "lang", "language": - return shell.helpLanguage(), true - case "completion": - return shell.helpCompletion(), true + if spec := findRootCommand(tokens[0]); canRenderHelp(spec) { + return commandHelpText(shell, spec), true } + return "", false } if len(tokens) < 3 || !isHelpToken(tokens[2]) { return "", false } - switch root { - case "jobs": - switch strings.ToLower(tokens[1]) { - case "list": - return shell.usageJobsList(), true - case "create": - return shell.usageJobCreate(), true - case "show", "schema", "start", "stop", "delete": - return shell.usageJobAction(strings.ToLower(tokens[1])), true - case "replay": - return shell.usageJobReplay(), true - case "attach-incre-task", "detach-incre-task": - return shell.usageJobAction(strings.ToLower(tokens[1])), true - case "update-incre-pos": - return shell.usageJobUpdateIncrePos(), true - default: - return shell.helpJobs(), true - } - case "datasources": - switch strings.ToLower(tokens[1]) { - case "list": - return shell.usageDataSourcesList(), true - case "add": - return shell.usageDataSourceAdd(), true - case "delete": - return shell.usageDataSourceAction("delete"), true - case "show": - return shell.usageDataSourceShow(), true - default: - return shell.helpDataSources(), true - } - case "clusters": - if strings.EqualFold(tokens[1], "list") { - return shell.usageClustersList(), true - } - return shell.helpClusters(), true - case "workers": - switch strings.ToLower(tokens[1]) { - case "list": - return shell.usageWorkersList(), true - case "start", "stop", "delete": - return shell.usageWorkerAction(strings.ToLower(tokens[1])), true - case "modify-mem-oversold": - return shell.usageWorkerModifyMemOverSold(), true - case "update-alert": - return shell.usageWorkerUpdateAlert(), true - default: - return shell.helpWorkers(), true - } - case "consolejobs": - if strings.EqualFold(tokens[1], "show") { - return shell.usageConsoleJobShow(), true - } - return shell.helpConsoleJobs(), true - case "job-config", "jobconfig": - if strings.EqualFold(tokens[1], "specs") { - return shell.usageJobConfigSpecs(), true - } - if strings.EqualFold(tokens[1], "transform-job-type") { - return shell.usageJobConfigTransform(), true - } - return shell.helpJobConfig(), true - case "schemas", "schema": - if strings.EqualFold(tokens[1], "list-trans-objs-by-meta") { - return shell.usageSchemas(), true - } - return shell.helpSchemas(), true - case "config": - switch strings.ToLower(tokens[1]) { - case "show": - return shell.usageConfigShow(), true - case "init": - return shell.usageConfigInit(), true - case "lang": - return shell.helpLanguage(), true - default: - return shell.helpConfig(), true - } - case "lang", "language": - return shell.helpLanguage(), true - case "completion": - if strings.EqualFold(tokens[1], "zsh") || strings.EqualFold(tokens[1], "bash") { - return shell.usageCompletion(), true - } - return shell.helpCompletion(), true - default: + parent := findRootCommand(tokens[0]) + if parent == nil { return "", false } + return commandUsageOrHelpText(shell, findChildCommand(parent, tokens[1]), parent), true } diff --git a/test/repl/completion_test.go b/test/repl/completion_test.go index 2a21d59..4d276cb 100644 --- a/test/repl/completion_test.go +++ b/test/repl/completion_test.go @@ -40,9 +40,11 @@ func TestCompletionCandidatesSuggestCommandsFlagsAndValues(t *testing.T) { {name: "config subcommand", args: []string{"config", "la"}, want: []string{"lang"}}, {name: "config lang value", args: []string{"config", "lang", "set", ""}, want: []string{"en", "zh"}}, {name: "jobs subcommand", args: []string{"jobs", "re"}, want: []string{"replay"}}, + {name: "job-config alias path", args: []string{"jobconfig", "sp"}, want: []string{"specs"}}, {name: "jobs create flag", args: []string{"jobs", "create", "--bo"}, want: []string{"--body", "--body-file"}}, {name: "list flag", args: []string{"jobs", "list", "--so"}, want: []string{"--source-id"}}, {name: "global flag value", args: []string{"jobs", "list", "--output", ""}, want: []string{"text", "json"}}, + {name: "language alias value", args: []string{"language", "set", ""}, want: []string{"en", "zh"}}, {name: "bool value", args: []string{"job-config", "specs", "--initial-sync", ""}, want: []string{"true", "false"}}, {name: "inline bool value", args: []string{"job-config", "specs", "--initial-sync=t"}, want: []string{"--initial-sync=true"}}, {name: "datasource add file flag", args: []string{"datasources", "add", "--sec"}, want: []string{"--security-file"}}, diff --git a/test/repl/shell_test.go b/test/repl/shell_test.go index 501b0d3..7b1f369 100644 --- a/test/repl/shell_test.go +++ b/test/repl/shell_test.go @@ -387,6 +387,36 @@ func TestShellShowsGroupedUsageOnSeparateLines(t *testing.T) { } } +func TestShellShowsNestedCommandUsage(t *testing.T) { + runtime := &fakeRuntime{ + cfg: config.AppConfig{APIBaseURL: "https://cc.example.com", AccessKey: "abcdefghijkl", SecretKey: "qrstuvwxyz1234"}, + dataJobs: &fakeDataJobs{}, + dataSources: &fakeDataSources{}, + clusters: &fakeClusters{}, + workers: &fakeWorkers{}, + consoleJobs: &fakeConsoleJobs{}, + jobConfigs: &fakeJobConfigs{}, + } + io := testsupport.NewTestConsole() + + shell := repl.NewShell(io, runtime) + if err := shell.ExecuteArgs([]string{"config", "lang"}); err != nil { + t.Fatalf("ExecuteArgs(config lang) error = %v", err) + } + if !strings.Contains(io.Output(), "config lang set ") { + t.Fatalf("output missing nested usage in %q", io.Output()) + } + + io = testsupport.NewTestConsole() + shell = repl.NewShell(io, runtime) + if err := shell.ExecuteArgs([]string{"language"}); err != nil { + t.Fatalf("ExecuteArgs(language) error = %v", err) + } + if !strings.Contains(io.Output(), "config lang show") { + t.Fatalf("output missing language alias usage in %q", io.Output()) + } +} + func TestShellClearsScreenWithAliases(t *testing.T) { runtime := &fakeRuntime{ cfg: config.AppConfig{APIBaseURL: "https://cc.example.com", AccessKey: "abcdefghijkl", SecretKey: "qrstuvwxyz1234"}, @@ -590,6 +620,44 @@ func TestShellUnknownHelpTopicShowsSuggestion(t *testing.T) { } } +func TestShellSupportsAliasDispatch(t *testing.T) { + jobConfigs := &fakeJobConfigs{} + schemas := &fakeSchemas{} + runtime := &fakeRuntime{ + cfg: config.AppConfig{APIBaseURL: "https://cc.example.com", AccessKey: "abcdefghijkl", SecretKey: "qrstuvwxyz1234", Language: "en"}, + dataJobs: &fakeDataJobs{}, + dataSources: &fakeDataSources{}, + clusters: &fakeClusters{}, + workers: &fakeWorkers{}, + consoleJobs: &fakeConsoleJobs{}, + jobConfigs: jobConfigs, + schemas: schemas, + } + io := testsupport.NewTestConsole() + shell := repl.NewShell(io, runtime) + + if err := shell.ExecuteArgs([]string{"jobconfig", "specs", "--type", "SYNC"}); err != nil { + t.Fatalf("ExecuteArgs(jobconfig specs) error = %v", err) + } + if jobConfigs.lastOptions.DataJobType != "SYNC" { + t.Fatalf("jobconfig alias did not dispatch to specs handler: %+v", jobConfigs.lastOptions) + } + + if err := shell.ExecuteArgs([]string{"schema", "list-trans-objs-by-meta", "--src-db", "demo"}); err != nil { + t.Fatalf("ExecuteArgs(schema list-trans-objs-by-meta) error = %v", err) + } + if schemas.lastOptions.SrcDb != "demo" { + t.Fatalf("schema alias did not dispatch to schema handler: %+v", schemas.lastOptions) + } + + if err := shell.ExecuteArgs([]string{"language", "set", "zh"}); err != nil { + t.Fatalf("ExecuteArgs(language set zh) error = %v", err) + } + if runtime.cfg.Language != "zh" { + t.Fatalf("language alias did not update runtime language: %q", runtime.cfg.Language) + } +} + func TestShellSwitchesLanguageForFollowUpOutput(t *testing.T) { runtime := &fakeRuntime{ cfg: config.AppConfig{APIBaseURL: "https://cc.example.com", AccessKey: "abcdefghijkl", SecretKey: "qrstuvwxyz1234"},