浏览代码

feat(goctl): Support gateway sample generation (#3049)

anqiansong 2 年之前
父节点
当前提交
1904af2323

+ 77 - 152
tools/goctl/api/cmd.go

@@ -13,166 +13,91 @@ import (
 	"github.com/zeromicro/go-zero/tools/goctl/api/tsgen"
 	"github.com/zeromicro/go-zero/tools/goctl/api/validate"
 	"github.com/zeromicro/go-zero/tools/goctl/config"
+	"github.com/zeromicro/go-zero/tools/goctl/internal/cobrax"
 	"github.com/zeromicro/go-zero/tools/goctl/plugin"
 )
 
 var (
 	// Cmd describes an api command.
-	Cmd = &cobra.Command{
-		Use:   "api",
-		Short: "Generate api related files",
-		RunE:  apigen.CreateApiTemplate,
-	}
-
-	dartCmd = &cobra.Command{
-		Use:   "dart",
-		Short: "Generate dart files for provided api in api file",
-		RunE:  dartgen.DartCommand,
-	}
-
-	docCmd = &cobra.Command{
-		Use:   "doc",
-		Short: "Generate doc files",
-		RunE:  docgen.DocCommand,
-	}
-
-	formatCmd = &cobra.Command{
-		Use:   "format",
-		Short: "Format api files",
-		RunE:  format.GoFormatApi,
-	}
-
-	goCmd = &cobra.Command{
-		Use:   "go",
-		Short: "Generate go files for provided api in api file",
-		RunE:  gogen.GoCommand,
-	}
-
-	newCmd = &cobra.Command{
-		Use:     "new",
-		Short:   "Fast create api service",
-		Example: "goctl api new [options] service-name",
-		Args:    cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return new.CreateServiceCommand(args)
-		},
-	}
-
-	validateCmd = &cobra.Command{
-		Use:   "validate",
-		Short: "Validate api file",
-		RunE:  validate.GoValidateApi,
-	}
-
-	javaCmd = &cobra.Command{
-		Use:    "java",
-		Short:  "Generate java files for provided api in api file",
-		Hidden: true,
-		RunE:   javagen.JavaCommand,
-	}
-
-	ktCmd = &cobra.Command{
-		Use:   "kt",
-		Short: "Generate kotlin code for provided api file",
-		RunE:  ktgen.KtCommand,
-	}
-
-	pluginCmd = &cobra.Command{
-		Use:   "plugin",
-		Short: "Custom file generator",
-		RunE:  plugin.PluginCommand,
-	}
-
-	tsCmd = &cobra.Command{
-		Use:   "ts",
-		Short: "Generate ts files for provided api in api file",
-		RunE:  tsgen.TsCommand,
-	}
+	Cmd       = cobrax.NewCommand("api", cobrax.WithRunE(apigen.CreateApiTemplate))
+	dartCmd   = cobrax.NewCommand("dart", cobrax.WithRunE(dartgen.DartCommand))
+	docCmd    = cobrax.NewCommand("doc", cobrax.WithRunE(docgen.DocCommand))
+	formatCmd = cobrax.NewCommand("format", cobrax.WithRunE(format.GoFormatApi))
+	goCmd     = cobrax.NewCommand("go", cobrax.WithRunE(gogen.GoCommand))
+	newCmd    = cobrax.NewCommand("new", cobrax.WithRunE(new.CreateServiceCommand),
+		cobrax.WithArgs(cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs)))
+	validateCmd = cobrax.NewCommand("validate", cobrax.WithRunE(validate.GoValidateApi))
+	javaCmd     = cobrax.NewCommand("java", cobrax.WithRunE(javagen.JavaCommand), cobrax.WithHidden())
+	ktCmd       = cobrax.NewCommand("kt", cobrax.WithRunE(ktgen.KtCommand))
+	pluginCmd   = cobrax.NewCommand("plugin", cobrax.WithRunE(plugin.PluginCommand))
+	tsCmd       = cobrax.NewCommand("ts", cobrax.WithRunE(tsgen.TsCommand))
 )
 
 func init() {
-	Cmd.Flags().StringVar(&apigen.VarStringOutput, "o", "", "Output a sample api file")
-	Cmd.Flags().StringVar(&apigen.VarStringHome, "home", "", "The goctl home path of the"+
-		" template, --home and --remote cannot be set at the same time, if they are, --remote has "+
-		"higher priority")
-	Cmd.Flags().StringVar(&apigen.VarStringRemote, "remote", "", "The remote git repo of the"+
-		" template, --home and --remote cannot be set at the same time, if they are, --remote has higher"+
-		" priority\nThe git repo directory must be consistent with the"+
-		" https://github.com/zeromicro/go-zero-template directory structure")
-	Cmd.Flags().StringVar(&apigen.VarStringBranch, "branch", "", "The branch of the "+
-		"remote repo, it does work with --remote")
-
-	dartCmd.Flags().StringVar(&dartgen.VarStringDir, "dir", "", "The target dir")
-	dartCmd.Flags().StringVar(&dartgen.VarStringAPI, "api", "", "The api file")
-	dartCmd.Flags().BoolVar(&dartgen.VarStringLegacy, "legacy", false, "Legacy generator for flutter v1")
-	dartCmd.Flags().StringVar(&dartgen.VarStringHostname, "hostname", "", "hostname of the server")
-	dartCmd.Flags().StringVar(&dartgen.VarStringScheme, "scheme", "", "scheme of the server")
-
-	docCmd.Flags().StringVar(&docgen.VarStringDir, "dir", "", "The target dir")
-	docCmd.Flags().StringVar(&docgen.VarStringOutput, "o", "", "The output markdown directory")
-
-	formatCmd.Flags().StringVar(&format.VarStringDir, "dir", "", "The format target dir")
-	formatCmd.Flags().BoolVar(&format.VarBoolIgnore, "iu", false, "Ignore update")
-	formatCmd.Flags().BoolVar(&format.VarBoolUseStdin, "stdin", false, "Use stdin to input api"+
-		" doc content, press \"ctrl + d\" to send EOF")
-	formatCmd.Flags().BoolVar(&format.VarBoolSkipCheckDeclare, "declare", false, "Use to skip check "+
-		"api types already declare")
-
-	goCmd.Flags().StringVar(&gogen.VarStringDir, "dir", "", "The target dir")
-	goCmd.Flags().StringVar(&gogen.VarStringAPI, "api", "", "The api file")
-	goCmd.Flags().StringVar(&gogen.VarStringHome, "home", "", "The goctl home path of "+
-		"the template, --home and --remote cannot be set at the same time, if they are, --remote "+
-		"has higher priority")
-	goCmd.Flags().StringVar(&gogen.VarStringRemote, "remote", "", "The remote git repo "+
-		"of the template, --home and --remote cannot be set at the same time, if they are, --remote"+
-		" has higher priority\nThe git repo directory must be consistent with the "+
-		"https://github.com/zeromicro/go-zero-template directory structure")
-	goCmd.Flags().StringVar(&gogen.VarStringBranch, "branch", "", "The branch of "+
-		"the remote repo, it does work with --remote")
-	goCmd.Flags().StringVar(&gogen.VarStringStyle, "style", config.DefaultFormat, "The file naming format,"+
-		" see [https://github.com/zeromicro/go-zero/blob/master/tools/goctl/config/readme.md]")
-
-	javaCmd.Flags().StringVar(&javagen.VarStringDir, "dir", "", "The target dir")
-	javaCmd.Flags().StringVar(&javagen.VarStringAPI, "api", "", "The api file")
-
-	ktCmd.Flags().StringVar(&ktgen.VarStringDir, "dir", "", "The target dir")
-	ktCmd.Flags().StringVar(&ktgen.VarStringAPI, "api", "", "The api file")
-	ktCmd.Flags().StringVar(&ktgen.VarStringPKG, "pkg", "", "Define package name for kotlin file")
-
-	newCmd.Flags().StringVar(&new.VarStringHome, "home", "", "The goctl home path of "+
-		"the template, --home and --remote cannot be set at the same time, if they are, --remote "+
-		"has higher priority")
-	newCmd.Flags().StringVar(&new.VarStringRemote, "remote", "", "The remote git repo "+
-		"of the template, --home and --remote cannot be set at the same time, if they are, --remote"+
-		" has higher priority\n\tThe git repo directory must be consistent with the "+
-		"https://github.com/zeromicro/go-zero-template directory structure")
-	newCmd.Flags().StringVar(&new.VarStringBranch, "branch", "", "The branch of "+
-		"the remote repo, it does work with --remote")
-	newCmd.Flags().StringVar(&new.VarStringStyle, "style", config.DefaultFormat, "The file naming format,"+
-		" see [https://github.com/zeromicro/go-zero/blob/master/tools/goctl/config/readme.md]")
-
-	pluginCmd.Flags().StringVarP(&plugin.VarStringPlugin, "plugin", "p", "", "The plugin file")
-	pluginCmd.Flags().StringVar(&plugin.VarStringDir, "dir", "", "The target dir")
-	pluginCmd.Flags().StringVar(&plugin.VarStringAPI, "api", "", "The api file")
-	pluginCmd.Flags().StringVar(&plugin.VarStringStyle, "style", "",
-		"The file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]")
-
-	tsCmd.Flags().StringVar(&tsgen.VarStringDir, "dir", "", "The target dir")
-	tsCmd.Flags().StringVar(&tsgen.VarStringAPI, "api", "", "The api file")
-	tsCmd.Flags().StringVar(&tsgen.VarStringCaller, "caller", "", "The web api caller")
-	tsCmd.Flags().BoolVar(&tsgen.VarBoolUnWrap, "unwrap", false, "Unwrap the webapi caller for import")
-
-	validateCmd.Flags().StringVar(&validate.VarStringAPI, "api", "", "Validate target api file")
+	var (
+		apiCmdFlags      = Cmd.Flags()
+		dartCmdFlags     = dartCmd.Flags()
+		docCmdFlags      = docCmd.Flags()
+		formatCmdFlags   = formatCmd.Flags()
+		goCmdFlags       = goCmd.Flags()
+		javaCmdFlags     = javaCmd.Flags()
+		ktCmdFlags       = ktCmd.Flags()
+		newCmdFlags      = newCmd.Flags()
+		pluginCmdFlags   = pluginCmd.Flags()
+		tsCmdFlags       = tsCmd.Flags()
+		validateCmdFlags = validateCmd.Flags()
+	)
+
+	apiCmdFlags.StringVar(&apigen.VarStringOutput, "o")
+	apiCmdFlags.StringVar(&apigen.VarStringHome, "home")
+	apiCmdFlags.StringVar(&apigen.VarStringRemote, "remote")
+	apiCmdFlags.StringVar(&apigen.VarStringBranch, "branch")
+
+	dartCmdFlags.StringVar(&dartgen.VarStringDir, "dir")
+	dartCmdFlags.StringVar(&dartgen.VarStringAPI, "api")
+	dartCmdFlags.BoolVar(&dartgen.VarStringLegacy, "legacy")
+	dartCmdFlags.StringVar(&dartgen.VarStringHostname, "hostname")
+	dartCmdFlags.StringVar(&dartgen.VarStringScheme, "scheme")
+
+	docCmdFlags.StringVar(&docgen.VarStringDir, "dir")
+	docCmdFlags.StringVar(&docgen.VarStringOutput, "o")
+
+	formatCmdFlags.StringVar(&format.VarStringDir, "dir")
+	formatCmdFlags.BoolVar(&format.VarBoolIgnore, "iu")
+	formatCmdFlags.BoolVar(&format.VarBoolUseStdin, "stdin")
+	formatCmdFlags.BoolVar(&format.VarBoolSkipCheckDeclare, "declare")
+
+	goCmdFlags.StringVar(&gogen.VarStringDir, "dir")
+	goCmdFlags.StringVar(&gogen.VarStringAPI, "api")
+	goCmdFlags.StringVar(&gogen.VarStringHome, "home")
+	goCmdFlags.StringVar(&gogen.VarStringRemote, "remote")
+	goCmdFlags.StringVar(&gogen.VarStringBranch, "branch")
+	goCmdFlags.StringVarWithDefaultValue(&gogen.VarStringStyle, "style", config.DefaultFormat)
+
+	javaCmdFlags.StringVar(&javagen.VarStringDir, "dir")
+	javaCmdFlags.StringVar(&javagen.VarStringAPI, "api")
+
+	ktCmdFlags.StringVar(&ktgen.VarStringDir, "dir")
+	ktCmdFlags.StringVar(&ktgen.VarStringAPI, "api")
+	ktCmdFlags.StringVar(&ktgen.VarStringPKG, "pkg")
+
+	newCmdFlags.StringVar(&new.VarStringHome, "home")
+	newCmdFlags.StringVar(&new.VarStringRemote, "remote")
+	newCmdFlags.StringVar(&new.VarStringBranch, "branch")
+	newCmdFlags.StringVarWithDefaultValue(&new.VarStringStyle, "style", config.DefaultFormat)
+
+	pluginCmdFlags.StringVarP(&plugin.VarStringPlugin, "plugin", "p")
+	pluginCmdFlags.StringVar(&plugin.VarStringDir, "dir")
+	pluginCmdFlags.StringVar(&plugin.VarStringAPI, "api")
+	pluginCmdFlags.StringVar(&plugin.VarStringStyle, "style")
+
+	tsCmdFlags.StringVar(&tsgen.VarStringDir, "dir")
+	tsCmdFlags.StringVar(&tsgen.VarStringAPI, "api")
+	tsCmdFlags.StringVar(&tsgen.VarStringCaller, "caller")
+	tsCmdFlags.BoolVar(&tsgen.VarBoolUnWrap, "unwrap")
+
+	validateCmdFlags.StringVar(&validate.VarStringAPI, "api")
 
 	// Add sub-commands
-	Cmd.AddCommand(dartCmd)
-	Cmd.AddCommand(docCmd)
-	Cmd.AddCommand(formatCmd)
-	Cmd.AddCommand(goCmd)
-	Cmd.AddCommand(javaCmd)
-	Cmd.AddCommand(ktCmd)
-	Cmd.AddCommand(newCmd)
-	Cmd.AddCommand(pluginCmd)
-	Cmd.AddCommand(tsCmd)
-	Cmd.AddCommand(validateCmd)
+	Cmd.AddCommand(dartCmd, docCmd, formatCmd, goCmd, javaCmd, ktCmd, newCmd, pluginCmd, tsCmd, validateCmd)
 }

+ 2 - 1
tools/goctl/api/new/newservice.go

@@ -8,6 +8,7 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/spf13/cobra"
 	"github.com/zeromicro/go-zero/tools/goctl/api/gogen"
 	conf "github.com/zeromicro/go-zero/tools/goctl/config"
 	"github.com/zeromicro/go-zero/tools/goctl/util"
@@ -29,7 +30,7 @@ var (
 )
 
 // CreateServiceCommand fast create service
-func CreateServiceCommand(args []string) error {
+func CreateServiceCommand(_ *cobra.Command, args []string) error {
 	dirName := args[0]
 	if len(VarStringStyle) == 0 {
 		VarStringStyle = conf.DefaultFormat

+ 5 - 7
tools/goctl/bug/cmd.go

@@ -1,11 +1,9 @@
 package bug
 
-import "github.com/spf13/cobra"
+import (
+	"github.com/spf13/cobra"
+	"github.com/zeromicro/go-zero/tools/goctl/internal/cobrax"
+)
 
 // Cmd describes a bug command.
-var Cmd = &cobra.Command{
-	Use:   "bug",
-	Short: "Report a bug",
-	Args:  cobra.NoArgs,
-	RunE:  runE,
-}
+var Cmd = cobrax.NewCommand("bug", cobrax.WithRunE(cobra.NoArgs), cobrax.WithArgs(cobra.NoArgs))

+ 7 - 20
tools/goctl/cmd/root.go

@@ -15,6 +15,8 @@ import (
 	"github.com/zeromicro/go-zero/tools/goctl/bug"
 	"github.com/zeromicro/go-zero/tools/goctl/docker"
 	"github.com/zeromicro/go-zero/tools/goctl/env"
+	"github.com/zeromicro/go-zero/tools/goctl/gateway"
+	"github.com/zeromicro/go-zero/tools/goctl/internal/cobrax"
 	"github.com/zeromicro/go-zero/tools/goctl/internal/version"
 	"github.com/zeromicro/go-zero/tools/goctl/kube"
 	"github.com/zeromicro/go-zero/tools/goctl/migrate"
@@ -35,14 +37,7 @@ const (
 var (
 	//go:embed usage.tpl
 	usageTpl string
-
-	rootCmd = &cobra.Command{
-		Use:   "goctl",
-		Short: "A cli tool to generate go-zero code",
-		Long: "A cli tool to generate api, zrpc, model code\n\n" +
-			"GitHub: https://github.com/zeromicro/go-zero\n" +
-			"Site:   https://go-zero.dev",
-	}
+	rootCmd  = cobrax.NewCommand("goctl")
 )
 
 // Execute executes the given command
@@ -117,16 +112,8 @@ func init() {
 		runtime.GOOS, runtime.GOARCH)
 
 	rootCmd.SetUsageTemplate(usageTpl)
-	rootCmd.AddCommand(api.Cmd)
-	rootCmd.AddCommand(bug.Cmd)
-	rootCmd.AddCommand(docker.Cmd)
-	rootCmd.AddCommand(kube.Cmd)
-	rootCmd.AddCommand(env.Cmd)
-	rootCmd.AddCommand(model.Cmd)
-	rootCmd.AddCommand(migrate.Cmd)
-	rootCmd.AddCommand(quickstart.Cmd)
-	rootCmd.AddCommand(rpc.Cmd)
-	rootCmd.AddCommand(tpl.Cmd)
-	rootCmd.AddCommand(upgrade.Cmd)
-	rootCmd.AddCommand(cobracompletefig.CreateCompletionSpecCommand())
+	rootCmd.AddCommand(api.Cmd, bug.Cmd, docker.Cmd, kube.Cmd, env.Cmd, gateway.Cmd, model.Cmd)
+	rootCmd.AddCommand(migrate.Cmd, quickstart.Cmd, rpc.Cmd, tpl.Cmd, upgrade.Cmd)
+	rootCmd.Command.AddCommand(cobracompletefig.CreateCompletionSpecCommand())
+	rootCmd.MustInit()
 }

+ 12 - 15
tools/goctl/docker/cmd.go

@@ -1,6 +1,6 @@
 package docker
 
-import "github.com/spf13/cobra"
+import "github.com/zeromicro/go-zero/tools/goctl/internal/cobrax"
 
 var (
 	varExeName       string
@@ -14,21 +14,18 @@ var (
 	varStringTZ      string
 
 	// Cmd describes a docker command.
-	Cmd = &cobra.Command{
-		Use:   "docker",
-		Short: "Generate Dockerfile",
-		RunE:  dockerCommand,
-	}
+	Cmd = cobrax.NewCommand("docker", cobrax.WithRunE(dockerCommand))
 )
 
 func init() {
-	Cmd.Flags().StringVar(&varExeName, "exe", "", "The executable name in the built image")
-	Cmd.Flags().StringVar(&varStringGo, "go", "", "The file that contains main function")
-	Cmd.Flags().StringVar(&varStringBase, "base", "scratch", "The base image to build the docker image, default scratch")
-	Cmd.Flags().IntVar(&varIntPort, "port", 0, "The port to expose, default none")
-	Cmd.Flags().StringVar(&varStringHome, "home", "", "The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority")
-	Cmd.Flags().StringVar(&varStringRemote, "remote", "", "The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority\nThe git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure")
-	Cmd.Flags().StringVar(&varStringBranch, "branch", "", "The branch of the remote repo, it does work with --remote")
-	Cmd.Flags().StringVar(&varStringVersion, "version", "", "The goctl builder golang image version")
-	Cmd.Flags().StringVar(&varStringTZ, "tz", "Asia/Shanghai", "The timezone of the container")
+	dockerCmdFlags := Cmd.Flags()
+	dockerCmdFlags.StringVar(&varExeName, "exe")
+	dockerCmdFlags.StringVar(&varStringGo, "go")
+	dockerCmdFlags.StringVarWithDefaultValue(&varStringBase, "base", "scratch")
+	dockerCmdFlags.IntVar(&varIntPort, "port")
+	dockerCmdFlags.StringVar(&varStringHome, "home")
+	dockerCmdFlags.StringVar(&varStringRemote, "remote")
+	dockerCmdFlags.StringVar(&varStringBranch, "branch")
+	dockerCmdFlags.StringVar(&varStringVersion, "version")
+	dockerCmdFlags.StringVarWithDefaultValue(&varStringTZ, "tz", "Asia/Shanghai")
 }

+ 9 - 27
tools/goctl/env/cmd.go

@@ -1,6 +1,6 @@
 package env
 
-import "github.com/spf13/cobra"
+import "github.com/zeromicro/go-zero/tools/goctl/internal/cobrax"
 
 var (
 	sliceVarWriteValue []string
@@ -9,38 +9,20 @@ var (
 	boolVarInstall     bool
 
 	// Cmd describes an env command.
-	Cmd = &cobra.Command{
-		Use:   "env",
-		Short: "Check or edit goctl environment",
-		RunE:  write,
-	}
-	installCmd = &cobra.Command{
-		Use:   "install",
-		Short: "Goctl env installation",
-		RunE:  install,
-	}
-	checkCmd = &cobra.Command{
-		Use:   "check",
-		Short: "Detect goctl env and dependency tools",
-		RunE:  check,
-	}
+	Cmd        = cobrax.NewCommand("env", cobrax.WithRunE(write))
+	installCmd = cobrax.NewCommand("install", cobrax.WithRunE(install))
+	checkCmd   = cobrax.NewCommand("check", cobrax.WithRunE(check))
 )
 
 func init() {
 	// The root command flags
-	Cmd.Flags().StringSliceVarP(&sliceVarWriteValue,
-		"write", "w", nil, "Edit goctl environment")
-	Cmd.PersistentFlags().BoolVarP(&boolVarForce,
-		"force", "f", false,
-		"Silent installation of non-existent dependencies")
-	Cmd.PersistentFlags().BoolVarP(&boolVarVerbose,
-		"verbose", "v", false, "Enable log output")
+	Cmd.Flags().StringSliceVarP(&sliceVarWriteValue, "write", "w")
+	Cmd.PersistentFlags().BoolVarP(&boolVarForce, "force", "f")
+	Cmd.PersistentFlags().BoolVarP(&boolVarVerbose, "verbose", "v")
 
 	// The sub-command flags
-	checkCmd.Flags().BoolVarP(&boolVarInstall, "install", "i",
-		false, "Install dependencies if not found")
+	checkCmd.Flags().BoolVarP(&boolVarInstall, "install", "i")
 
 	// Add sub-command
-	Cmd.AddCommand(installCmd)
-	Cmd.AddCommand(checkCmd)
+	Cmd.AddCommand(checkCmd, installCmd)
 }

+ 2 - 2
tools/goctl/env/env.go

@@ -7,10 +7,10 @@ import (
 	"github.com/zeromicro/go-zero/tools/goctl/pkg/env"
 )
 
-func write(_ *cobra.Command, _ []string) error {
+func write(_ *cobra.Command, args []string) error {
 	if len(sliceVarWriteValue) > 0 {
 		return env.WriteEnv(sliceVarWriteValue)
 	}
-	fmt.Println(env.Print())
+	fmt.Println(env.Print(args...))
 	return nil
 }

+ 1 - 3
tools/goctl/env/install.go

@@ -1,8 +1,6 @@
 package env
 
-import (
-	"github.com/spf13/cobra"
-)
+import "github.com/spf13/cobra"
 
 func install(_ *cobra.Command, _ []string) error {
 	return Prepare(true, boolVarForce, boolVarVerbose)

+ 60 - 0
tools/goctl/gateway/cmd.go

@@ -0,0 +1,60 @@
+package gateway
+
+import (
+	_ "embed"
+	"os"
+	"path/filepath"
+
+	"github.com/spf13/cobra"
+	"github.com/zeromicro/go-zero/tools/goctl/internal/cobrax"
+	"github.com/zeromicro/go-zero/tools/goctl/util/ctx"
+	"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
+)
+
+var (
+	varStringHome   string
+	varStringRemote string
+	varStringBranch string
+	varStringDir    string
+
+	Cmd = cobrax.NewCommand("gateway", cobrax.WithRunE(generateGateway))
+)
+
+func init() {
+	Cmd.PersistentFlags().StringVar(&varStringHome, "home")
+	Cmd.PersistentFlags().StringVar(&varStringRemote, "remote")
+	Cmd.PersistentFlags().StringVar(&varStringBranch, "branch")
+	Cmd.PersistentFlags().StringVar(&varStringDir, "dir")
+}
+
+func generateGateway(*cobra.Command, []string) error {
+	if err:=pathx.MkdirIfNotExist(varStringDir);err!=nil{
+		return err
+	}
+
+	if _,err:=ctx.Prepare(varStringDir);err!=nil{
+		return err
+	}
+
+	etcContent, err := pathx.LoadTemplate(category, etcTemplateFileFile, etcTemplate)
+	if err != nil {
+		return err
+	}
+
+	mainContent, err := pathx.LoadTemplate(category, mainTemplateFile, mainTemplate)
+	if err != nil {
+		return err
+	}
+
+	etcDir := filepath.Join(varStringDir, "etc")
+	if err := pathx.MkdirIfNotExist(etcDir); err != nil {
+		return err
+	}
+	etcFile := filepath.Join(etcDir, "gateway.yaml")
+	if err := os.WriteFile(etcFile, []byte(etcContent), 0644); err != nil {
+		return err
+	}
+
+	mainFile := filepath.Join(varStringDir, "main.go")
+	return os.WriteFile(mainFile, []byte(mainContent), 0644)
+}

+ 18 - 0
tools/goctl/gateway/conf.yml

@@ -0,0 +1,18 @@
+Name: gateway-example # gateway name
+Host: localhost # gateway host
+Port: 8888 # gateway port
+Upstreams: # upstreams
+  - Grpc: # grpc upstream
+      Target: 0.0.0.0:8080 # grpc target,the direct grpc server address,for only one node
+#      Endpoints: [0.0.0.0:8080,192.168.120.1:8080] # grpc endpoints, the grpc server address list, for multiple nodes
+#      Etcd: # etcd config, if you want to use etcd to discover the grpc server address
+#        Hosts: [127.0.0.1:2378,127.0.0.1:2379] # etcd hosts
+#        Key: greet.grpc # the discovery key
+    # protoset mode
+    ProtoSets:
+      - hello.pb
+    # Mappings can also be written in proto options
+#    Mappings: # routes mapping
+#      - Method: get
+#        Path: /ping
+#        RpcPath: hello.Hello/Ping

+ 20 - 0
tools/goctl/gateway/gateway.tpl

@@ -0,0 +1,20 @@
+package main
+
+import (
+	"flag"
+
+	"github.com/zeromicro/go-zero/core/conf"
+	"github.com/zeromicro/go-zero/gateway"
+)
+
+var configFile = flag.String("f", "etc/gateway.yaml", "config file")
+
+func main() {
+	flag.Parse()
+
+	var c gateway.GatewayConf
+	conf.MustLoad(*configFile, &c)
+	gw := gateway.MustNewServer(c)
+	defer gw.Stop()
+	gw.Start()
+}

+ 62 - 0
tools/goctl/gateway/template.go

@@ -0,0 +1,62 @@
+package gateway
+
+import (
+	_ "embed"
+	"fmt"
+
+	"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
+)
+
+const (
+	category                          = "gateway"
+	etcTemplateFileFile               = "etc.tpl"
+	mainTemplateFile                  = "main.tpl"
+)
+
+//go:embed conf.yml
+var etcTemplate string
+
+//go:embed gateway.tpl
+var mainTemplate string
+
+
+var templates = map[string]string{
+	etcTemplateFileFile:       etcTemplate,
+	mainTemplateFile:          mainTemplate,
+}
+
+// GenTemplates is the entry for command goctl template,
+// it will create the specified category
+func GenTemplates() error {
+	return pathx.InitTemplates(category, templates)
+}
+
+// RevertTemplate restores the deleted template files
+func RevertTemplate(name string) error {
+	content, ok := templates[name]
+	if !ok {
+		return fmt.Errorf("%s: no such file name", name)
+	}
+	return pathx.CreateTemplate(category, name, content)
+}
+
+// Clean deletes all template files
+func Clean() error {
+	return pathx.Clean(category)
+}
+
+// Update is used to update the template files, it will delete the existing old templates at first,
+// and then create the latest template files
+func Update() error {
+	err := Clean()
+	if err != nil {
+		return err
+	}
+
+	return pathx.InitTemplates(category, templates)
+}
+
+// Category returns a const string value for rpc template category
+func Category() string {
+	return category
+}

+ 2 - 1
tools/goctl/go.mod

@@ -10,6 +10,7 @@ require (
 	github.com/iancoleman/strcase v0.2.0
 	github.com/logrusorgru/aurora v2.0.3+incompatible
 	github.com/spf13/cobra v1.6.1
+	github.com/spf13/pflag v1.0.5
 	github.com/stretchr/testify v1.8.2
 	github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1
 	github.com/zeromicro/antlr v0.0.1
@@ -34,6 +35,7 @@ require (
 	github.com/emicklei/go-restful/v3 v3.9.0 // indirect
 	github.com/fatih/color v1.14.1 // indirect
 	github.com/felixge/fgprof v0.9.3 // indirect
+	github.com/fsnotify/fsnotify v1.6.0 // indirect
 	github.com/go-logr/logr v1.2.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-openapi/jsonpointer v0.19.5 // indirect
@@ -69,7 +71,6 @@ require (
 	github.com/prometheus/common v0.37.0 // indirect
 	github.com/prometheus/procfs v0.8.0 // indirect
 	github.com/spaolacci/murmur3 v1.1.0 // indirect
-	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 // indirect
 	go.etcd.io/etcd/api/v3 v3.5.7 // indirect
 	go.etcd.io/etcd/client/pkg/v3 v3.5.7 // indirect

+ 3 - 1
tools/goctl/go.sum

@@ -101,7 +101,8 @@ github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4
 github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
 github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
 github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
-github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
+github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -541,6 +542,7 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

+ 171 - 0
tools/goctl/internal/cobrax/cobrax.go

@@ -0,0 +1,171 @@
+package cobrax
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
+	"github.com/zeromicro/go-zero/tools/goctl/internal/flags"
+)
+
+type Option func(*cobra.Command)
+
+func WithRunE(runE func(*cobra.Command, []string) error) Option {
+	return func(cmd *cobra.Command) {
+		cmd.RunE = runE
+	}
+}
+
+func WithRun(run func(*cobra.Command, []string)) Option {
+	return func(cmd *cobra.Command) {
+		cmd.Run = run
+	}
+}
+
+func WithArgs(arg cobra.PositionalArgs) Option {
+	return func(command *cobra.Command) {
+		command.Args = arg
+	}
+}
+
+func WithHidden() Option {
+	return func(command *cobra.Command) {
+		command.Hidden = true
+	}
+}
+
+type Command struct {
+	*cobra.Command
+}
+
+type FlagSet struct {
+	*pflag.FlagSet
+}
+
+func (f *FlagSet) StringVar(p *string, name string) {
+	f.StringVarWithDefaultValue(p, name, "")
+}
+
+func (f *FlagSet) StringVarWithDefaultValue(p *string, name string, value string) {
+	f.FlagSet.StringVar(p, name, value, "")
+}
+
+func (f *FlagSet) StringVarP(p *string, name, shorthand string) {
+	f.StringVarPWithDefaultValue(p, name, shorthand, "")
+}
+
+func (f *FlagSet) StringVarPWithDefaultValue(p *string, name, shorthand string, value string) {
+	f.FlagSet.StringVarP(p, name, shorthand, value, "")
+}
+
+func (f *FlagSet) BoolVar(p *bool, name string) {
+	f.BoolVarWithDefaultValue(p, name, false)
+}
+
+func (f *FlagSet) BoolVarWithDefaultValue(p *bool, name string, value bool) {
+	f.FlagSet.BoolVar(p, name, value, "")
+}
+
+func (f *FlagSet) BoolVarP(p *bool, name, shorthand string) {
+	f.BoolVarPWithDefaultValue(p, name, shorthand, false)
+}
+
+func (f *FlagSet) BoolVarPWithDefaultValue(p *bool, name, shorthand string, value bool) {
+	f.FlagSet.BoolVarP(p, name, shorthand, value, "")
+}
+
+func (f *FlagSet) IntVar(p *int, name string) {
+	f.IntVarWithDefaultValue(p, name, 0)
+}
+
+func (f *FlagSet) IntVarWithDefaultValue(p *int, name string, value int) {
+	f.FlagSet.IntVar(p, name, value, "")
+}
+
+func (f *FlagSet) StringSliceVarP(p *[]string, name, shorthand string) {
+	f.FlagSet.StringSliceVarP(p, name, shorthand, []string{}, "")
+}
+
+func (f *FlagSet) StringSliceVarPWithDefaultValue(p *[]string, name, shorthand string, value []string) {
+	f.FlagSet.StringSliceVarP(p, name, shorthand, value, "")
+}
+
+func (f *FlagSet) StringSliceVar(p *[]string, name string) {
+	f.StringSliceVarWithDefaultValue(p, name, []string{})
+}
+
+func (f *FlagSet) StringSliceVarWithDefaultValue(p *[]string, name string, value []string) {
+	f.FlagSet.StringSliceVar(p, name, value, "")
+}
+
+func NewCommand(use string, opts ...Option) *Command {
+	c := &Command{
+		Command: &cobra.Command{
+			Use: use,
+		},
+	}
+
+	for _, opt := range opts {
+		opt(c.Command)
+	}
+
+	return c
+}
+
+func (c *Command) AddCommand(cmds ...*Command) {
+	for _, cmd := range cmds {
+		c.Command.AddCommand(cmd.Command)
+	}
+}
+
+func (c *Command) Flags() *FlagSet {
+	set := c.Command.Flags()
+	return &FlagSet{
+		FlagSet: set,
+	}
+}
+
+func (c *Command) PersistentFlags() *FlagSet {
+	set := c.Command.PersistentFlags()
+	return &FlagSet{
+		FlagSet: set,
+	}
+}
+
+func (c *Command) MustInit() {
+	commands := append([]*cobra.Command{c.Command}, getCommandsRecursively(c.Command)...)
+	for _, command := range commands {
+		commandKey := getCommandName(command)
+		if len(command.Short) == 0 {
+			command.Short = flags.Get(commandKey + ".short")
+		}
+		if len(command.Long) == 0 {
+			command.Long = flags.Get(commandKey + ".long")
+		}
+		if len(command.Example) == 0 {
+			command.Example = flags.Get(commandKey + ".example")
+		}
+		command.Flags().VisitAll(func(flag *pflag.Flag) {
+			flag.Usage = flags.Get(fmt.Sprintf("%s.%s", commandKey, flag.Name))
+		})
+		command.PersistentFlags().VisitAll(func(flag *pflag.Flag) {
+			flag.Usage = flags.Get(fmt.Sprintf("%s.%s", commandKey, flag.Name))
+		})
+	}
+}
+
+func getCommandName(cmd *cobra.Command) string {
+	if cmd.HasParent() {
+		return getCommandName(cmd.Parent()) + "." + cmd.Name()
+	}
+	return cmd.Name()
+}
+
+func getCommandsRecursively(parent *cobra.Command) []*cobra.Command {
+	var commands []*cobra.Command
+	for _, cmd := range parent.Commands() {
+		commands = append(commands, cmd)
+		commands = append(commands, getCommandsRecursively(cmd)...)
+	}
+	return commands
+}

+ 283 - 0
tools/goctl/internal/flags/default_en.json

@@ -0,0 +1,283 @@
+{
+  "goctl": {
+    "short": "A cli tool to generate go-zero code",
+    "long": "A cli tool to generate api, zrpc, model code\n\nGitHub: https://github.com/zeromicro/go-zero\nSite:   https://go-zero.dev",
+    "api": {
+      "short": "Generate api related files",
+      "o": "Output a sample api file",
+      "home": "{{.global.home}}",
+      "remote": "{{.global.remote}}",
+      "branch": "{{.global.branch}}",
+      "api": "The api file",
+      "dir": "The target dir",
+      "dart": {
+        "short": "Generate dart files for provided api in api file",
+        "dir": "{{.goctl.api.dir}}",
+        "api": "{{.goctl.api.api}}",
+        "legacy": "Legacy generator for flutter v1",
+        "hostname": "hostname of the server",
+        "scheme": "scheme of the server"
+      },
+      "doc": {
+        "short": "Generate doc files",
+        "dir": "{{.goctl.api.dir}}",
+        "o": "The output markdown directory"
+      },
+      "format": {
+        "short": "Format api files",
+        "dir": "{{.goctl.api.dir}}",
+        "iu": "Ignore update",
+        "stdin": "Use stdin to input api doc content, press \"ctrl + d\" to send EOF",
+        "declare": "Use to skip check api types already declare"
+      },
+      "go": {
+        "short": "Generate go files for provided api in api file",
+        "dir": "{{.goctl.api.dir}}",
+        "api": "{{.goctl.api.api}}",
+        "home": "{{.global.home}}",
+        "remote": "{{.global.remote}}",
+        "branch": "{{.global.branch}}",
+        "style": "{{.global.style}}"
+      },
+      "new": {
+        "short": "Fast create api service",
+        "Example": "goctl api new [options] service-name",
+        "home": "{{.global.home}}",
+        "remote": "{{.global.remote}}",
+        "branch": "{{.global.branch}}",
+        "style": "{{.global.style}}"
+      },
+      "validate": {
+        "short": "Validate api file",
+        "api": "{{.goctl.api.api}}"
+      },
+      "kt": {
+        "short": "Generate kotlin code for provided api file",
+        "dir": "{{.goctl.api.dir}}",
+        "api": "{{.goctl.api.api}}",
+        "pkg": "Define package name for kotlin file"
+      },
+      "plugin": {
+        "short": "Custom file generator",
+        "plugin": "The plugin file",
+        "dir": "{{.goctl.api.dir}}",
+        "api": "{{.goctl.api.api}}",
+        "style": "{{.global.style}}"
+      },
+      "ts": {
+        "short": "Generate ts files for provided api in api file",
+        "dir": "{{.goctl.api.dir}}",
+        "api": "{{.goctl.api.api}}",
+        "caller": "The web api caller",
+        "unwrap": "Unwrap the webapi caller for import"
+      }
+    },
+    "bug": {
+      "short": "Report a bug"
+    },
+    "docker": {
+      "short": "Generate Dockerfile",
+      "exe": "The executable name in the built image",
+      "go": "The file that contains main function",
+      "base": "The base image to build the docker image, default scratch",
+      "port": "The port to expose, default none",
+      "home": "{{.global.home}}",
+      "remote": "{{.global.remote}}",
+      "branch": "{{.global.branch}}",
+      "version": "The goctl builder golang image version",
+      "tz": "The timezone of the container"
+    },
+    "kube": {
+      "short": "Generate kubernetes files",
+      "deploy": {
+        "short": "Generate deployment yaml file",
+        "name": "The name of deployment (required)",
+        "namespace": "The namespace of deployment (required)",
+        "image": "The docker image of deployment (required)",
+        "secret": "The secret to image pull from registry",
+        "requestCpu": "The request cpu to deploy",
+        "requestMem": "The request memory to deploy",
+        "limitCpu": "The limit cpu to deploy",
+        "limitMem": "The limit memory to deploy",
+        "o": "The output yaml file (required)",
+        "replicas": "The number of replicas to deploy",
+        "revisions": "The number of revision history to limit",
+        "port": "The port of the deployment to listen on pod (required)",
+        "nodePort": "The nodePort of the deployment to expose",
+        "targetPort": "The targetPort of the deployment, default to port",
+        "minReplicas": "The min replicas to deploy",
+        "maxReplicas": "The max replicas to deploy",
+        "imagePullPolicy": "The image pull policy of the deployment, default to IfNotPresent",
+        "home": "{{.global.home}}",
+        "remote": "{{.global.remote}}",
+        "branch": "{{.global.branch}}",
+        "serviceAccount": "TheServiceAccount for the deployment"
+      }
+    },
+    "env": {
+      "short": "Check or edit goctl environment",
+      "write": "Edit goctl environment",
+      "force": "Silent installation of non-existent dependencies",
+      "verbose": "Enable log output",
+      "install": {
+        "short": "Goctl env installation"
+      },
+      "check": {
+        "short": "Detect goctl env and dependency tools",
+        "install": "Install dependencies if not found"
+      }
+    },
+    "gateway": {
+      "short": "gateway is a tool to generate gateway code",
+      "home": "{{.global.home}}",
+      "remote": "{{.global.remote}}",
+      "branch": "{{.global.branch}}",
+      "dir": "The output dir",
+      "protoc": {
+        "short": "generate gateway code from proto file"
+      },
+      "protoset": {
+        "short": "generate gateway code from protoset file"
+      },
+      "server": {
+        "short": "generate gateway code from grpc server"
+      }
+    },
+    "model": {
+      "short": "Generate model code",
+      "dir": "The target dir",
+      "mysql": {
+        "short": "Generate mysql model",
+        "strict": "Generate model in strict mode",
+        "ignore-columns": "Ignore columns while creating or updating rows",
+        "datasource": {
+          "short": "Generate model from datasource",
+          "url": "The data source of database,like \"root:password@tcp(127.0.0.1:3306)/database",
+          "table": "The table or table globbing patterns in the database",
+          "cache": "Generate code with cache [optional]",
+          "dir": "{{.goctl.model.dir}}",
+          "style": "{{.global.style}}",
+          "idea": "For idea plugin [optional]",
+          "home": "{{.global.home}}",
+          "remote": "{{.global.remote}}",
+          "branch": "{{.global.branch}}"
+        },
+        "ddl": {
+          "short": "Generate mysql model from ddl",
+          "src": "The path or path globbing patterns of the ddl",
+          "dir": "{{.goctl.model.dir}}",
+          "style": "{{.global.style}}",
+          "cache": "Generate code with cache [optional]",
+          "idea": "For idea plugin [optional]",
+          "home": "{{.global.home}}",
+          "remote": "{{.global.remote}}",
+          "branch": "{{.global.branch}}"
+        }
+      },
+      "pg": {
+        "short": "Generate postgresql model",
+        "datasource": {
+          "short": "Generate model from datasource",
+          "url": "The data source of database,like \"root:password@tcp(127.0.0.1:3306)/database",
+          "table": "The table or table globbing patterns in the database",
+          "schema": "The schema or schema globbing patterns in the database",
+          "cache": "Generate code with cache [optional]",
+          "dir": "{{.goctl.model.dir}}",
+          "style": "{{.global.style}}",
+          "idea": "For idea plugin [optional]",
+          "strict": "Generate model in strict mode",
+          "home": "{{.global.home}}",
+          "remote": "{{.global.remote}}",
+          "branch": "{{.global.branch}}"
+        }
+      },
+      "mongo": {
+        "short": "Generate mongo model",
+        "type": "Specified model type name",
+        "cache": "Generate code with cache [optional]",
+        "easy": "Generate code with auto generated CollectionName for easy declare [optional]",
+        "dir": "{{.goctl.model.dir}}",
+        "style": "{{.global.style}}",
+        "home": "{{.global.home}}",
+        "remote": "{{.global.remote}}",
+        "branch": "{{.global.branch}}"
+      }
+    },
+    "migrate": {
+      "short": "Migrate from tal-tech to zeromicro",
+      "long": "Migrate is a transition command to help users migrate their projects from tal-tech to zeromicro version",
+      "verbose": "Verbose enables extra logging",
+      "version": "The target release version of github.com/zeromicro/go-zero to migrate"
+    },
+    "quickstart": {
+      "short": "quickly start a project",
+      "service-type": "specify the service type, supported values: [mono, micro]"
+    },
+    "rpc": {
+      "short": "Generate rpc code",
+      "o": "Output a sample proto file",
+      "home": "{{.global.home}}",
+      "remote": "{{.global.remote}}",
+      "branch": "{{.global.branch}}",
+      "new": {
+        "short": "Generate rpc demo service",
+        "style": "{{.global.style}}",
+        "idea": "For idea plugin [optional]",
+        "home": "{{.global.home}}",
+        "remote": "{{.global.remote}}",
+        "branch": "{{.global.branch}}",
+        "verbose": "Enable log output"
+      },
+      "template": {
+        "short": "Generate proto template",
+        "home": "{{.global.home}}",
+        "remote": "{{.global.remote}}",
+        "branch": "{{.global.branch}}"
+      },
+      "protoc": {
+        "short": "Generate grpc code",
+        "example": "goctl rpc protoc xx.proto --go_out=./pb --go-grpc_out=./pb --zrpc_out=.",
+        "multiple": "Generated in multiple rpc service mode",
+        "zrpc_out": "The zrpc output directory",
+        "style": "{{.global.style}}",
+        "home": "{{.global.home}}",
+        "remote": "{{.global.remote}}",
+        "branch": "{{.global.branch}}",
+        "verbose": "Enable log output"
+      }
+    },
+    "template": {
+      "short": "Template operation",
+      "home": "The goctl home path of the template",
+      "init": {
+        "short": "Initialize the all templates(force update)",
+        "home": "{{.goctl.template.home}}",
+        "category": "The category of template, enum [api,rpc,model,docker,kube]"
+      },
+      "clean": {
+        "short": "Clean the all cache templates",
+        "home": "{{.goctl.template.home}}"
+      },
+      "update": {
+        "short": "Update template of the target category to the latest",
+        "home": "{{.goctl.template.home}}",
+        "category": "{{.goctl.template.category}}"
+      },
+      "revert": {
+        "short": "Revert the target template to the latest",
+        "home": "{{.goctl.template.home}}",
+        "category": "{{.goctl.template.category}}",
+        "name": "The target file name of template"
+      }
+    },
+    "upgrade": {
+      "short": "Upgrade goctl to latest version"
+    }
+  },
+  "global": {
+    "home": "The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority",
+    "remote": "The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority\nThe git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure",
+    "branch": "The branch of the remote repo, it does work with --remote",
+    "style": "The file naming format, see [https://github.com/zeromicro/go-zero/blob/master/tools/goctl/config/readme.md]"
+  }
+}

+ 101 - 0
tools/goctl/internal/flags/flags.go

@@ -0,0 +1,101 @@
+package flags
+
+import (
+	"bytes"
+	_ "embed"
+	"encoding/json"
+	"fmt"
+	"io"
+	"log"
+	"strings"
+	"testing"
+
+	"github.com/zeromicro/go-zero/tools/goctl/util"
+)
+
+//go:embed default_en.json
+var defaultEnFlagConfig []byte
+
+type ConfigLoader struct {
+	conf map[string]any
+}
+
+func (cl *ConfigLoader) ReadConfig(in io.Reader) error {
+	return json.NewDecoder(in).Decode(&cl.conf)
+}
+
+func (cl *ConfigLoader) GetString(key string) string {
+	keyList := strings.FieldsFunc(key, func(r rune) bool {
+		return r == '.'
+	})
+	var conf = cl.conf
+	for idx, k := range keyList {
+		val, ok := conf[k]
+		if !ok {
+			return ""
+		}
+		if idx < len(keyList)-1 {
+			conf, ok = val.(map[string]any)
+			if !ok {
+				return ""
+			}
+			continue
+		}
+
+		return fmt.Sprint(val)
+	}
+	return ""
+}
+
+type Flags struct {
+	loader *ConfigLoader
+}
+
+func MustLoad() *Flags {
+	loader := &ConfigLoader{
+		conf: map[string]any{},
+	}
+	if err := loader.ReadConfig(bytes.NewBuffer(defaultEnFlagConfig)); err != nil {
+		log.Fatal(err)
+	}
+
+	return &Flags{
+		loader: loader,
+	}
+}
+
+func setTestData(t *testing.T, data []byte) {
+	origin := defaultEnFlagConfig
+	defaultEnFlagConfig = data
+	t.Cleanup(func() {
+		defaultEnFlagConfig = origin
+	})
+}
+
+func (f *Flags) Get(key string) (string, error) {
+	value := f.loader.GetString(key)
+	for util.IsTemplateVariable(value) {
+		value = util.TemplateVariable(value)
+		if value == key {
+			return "", fmt.Errorf("the variable can not be self: %q", key)
+		}
+		return f.Get(value)
+	}
+	return value, nil
+}
+
+var flags *Flags
+
+func Get(key string) string {
+	if flags == nil {
+		flags = MustLoad()
+	}
+
+	v, err := flags.Get(key)
+	if err != nil {
+		log.Fatal(err)
+		return ""
+	}
+
+	return v
+}

+ 78 - 0
tools/goctl/internal/flags/flags_test.go

@@ -0,0 +1,78 @@
+package flags
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/zeromicro/go-zero/tools/goctl/test"
+)
+
+func TestFlags_Get(t *testing.T) {
+	setTestData(t, []byte(`{"host":"0.0.0.0","port":8888,"service":{"host":"{{.host}}","port":"{{.port}}","invalid":"{{.service.invalid}}"}}`))
+	f := MustLoad()
+	executor := test.NewExecutor[string, string]()
+	executor.Add([]test.Data[string, string]{
+		{
+			Name:  "key_host",
+			Input: "host",
+			Want:  "0.0.0.0",
+		},
+		{
+			Name:  "key_port",
+			Input: "port",
+			Want:  "8888",
+		},
+		{
+			Name:  "key_service.host",
+			Input: "service.host",
+			Want:  "0.0.0.0",
+		},
+		{
+			Name:  "key_service.port",
+			Input: "service.port",
+			Want:  "8888",
+		},
+		{
+			Name:  "key_not_exists",
+			Input: "service.port.invalid",
+		},
+		{
+			Name:  "key_service.invalid",
+			Input: "service.invalid",
+			E:     fmt.Errorf("the variable can not be self: %q", "service.invalid"),
+		},
+	}...)
+	executor.RunE(t, f.Get)
+}
+
+func Test_Get(t *testing.T) {
+	setTestData(t, []byte(`{"host":"0.0.0.0","port":8888,"service":{"host":"{{.host}}","port":"{{.port}}","invalid":"{{.service.invalid}}"}}`))
+	executor := test.NewExecutor[string, string]()
+	executor.Add([]test.Data[string, string]{
+		{
+			Name:  "key_host",
+			Input: "host",
+			Want:  "0.0.0.0",
+		},
+		{
+			Name:  "key_port",
+			Input: "port",
+			Want:  "8888",
+		},
+		{
+			Name:  "key_service.host",
+			Input: "service.host",
+			Want:  "0.0.0.0",
+		},
+		{
+			Name:  "key_service.port",
+			Input: "service.port",
+			Want:  "8888",
+		},
+		{
+			Name:  "key_not_exists",
+			Input: "service.port.invalid",
+		},
+	}...)
+	executor.Run(t, Get)
+}

+ 29 - 41
tools/goctl/kube/cmd.go

@@ -1,6 +1,6 @@
 package kube
 
-import "github.com/spf13/cobra"
+import "github.com/zeromicro/go-zero/tools/goctl/internal/cobrax"
 
 var (
 	varStringName            string
@@ -26,50 +26,38 @@ var (
 	varStringImagePullPolicy string
 
 	// Cmd describes a kube command.
-	Cmd = &cobra.Command{
-		Use:   "kube",
-		Short: "Generate kubernetes files",
-	}
-
-	deployCmd = &cobra.Command{
-		Use:   "deploy",
-		Short: "Generate deployment yaml file",
-		RunE:  deploymentCommand,
-	}
+	Cmd       = cobrax.NewCommand("kube")
+	deployCmd = cobrax.NewCommand("deploy", cobrax.WithRunE(deploymentCommand))
 )
 
 func init() {
-	deployCmd.Flags().StringVar(&varStringName, "name", "", "The name of deployment (required)")
-	deployCmd.Flags().StringVar(&varStringNamespace, "namespace", "", "The namespace of deployment (required)")
-	deployCmd.Flags().StringVar(&varStringImage, "image", "", "The docker image of deployment (required)")
-	deployCmd.Flags().StringVar(&varStringSecret, "secret", "", "The secret to image pull from registry")
-	deployCmd.Flags().IntVar(&varIntRequestCpu, "requestCpu", 500, "The request cpu to deploy")
-	deployCmd.Flags().IntVar(&varIntRequestMem, "requestMem", 512, "The request memory to deploy")
-	deployCmd.Flags().IntVar(&varIntLimitCpu, "limitCpu", 1000, "The limit cpu to deploy")
-	deployCmd.Flags().IntVar(&varIntLimitMem, "limitMem", 1024, "The limit memory to deploy")
-	deployCmd.Flags().StringVar(&varStringO, "o", "", "The output yaml file (required)")
-	deployCmd.Flags().IntVar(&varIntReplicas, "replicas", 3, "The number of replicas to deploy")
-	deployCmd.Flags().IntVar(&varIntRevisions, "revisions", 5, "The number of revision history to limit")
-	deployCmd.Flags().IntVar(&varIntPort, "port", 0, "The port of the deployment to listen on pod (required)")
-	deployCmd.Flags().IntVar(&varIntNodePort, "nodePort", 0, "The nodePort of the deployment to expose")
-	deployCmd.Flags().IntVar(&varIntTargetPort, "targetPort", 0, "The targetPort of the deployment, default to port")
-	deployCmd.Flags().IntVar(&varIntMinReplicas, "minReplicas", 3, "The min replicas to deploy")
-	deployCmd.Flags().IntVar(&varIntMaxReplicas, "maxReplicas", 10, "The max replicas to deploy")
-	deployCmd.Flags().StringVar(&varStringImagePullPolicy, "imagePullPolicy", "", "Image pull policy. One of Always, Never, IfNotPresent")
+	deployCmdFlags := deployCmd.Flags()
+	deployCmdFlags.StringVar(&varStringName, "name")
+	deployCmdFlags.StringVar(&varStringNamespace, "namespace")
+	deployCmdFlags.StringVar(&varStringImage, "image")
+	deployCmdFlags.StringVar(&varStringSecret, "secret")
+	deployCmdFlags.IntVarWithDefaultValue(&varIntRequestCpu, "requestCpu", 500)
+	deployCmdFlags.IntVarWithDefaultValue(&varIntRequestMem, "requestMem", 512)
+	deployCmdFlags.IntVarWithDefaultValue(&varIntLimitCpu, "limitCpu", 1000)
+	deployCmdFlags.IntVarWithDefaultValue(&varIntLimitMem, "limitMem", 1024)
+	deployCmdFlags.StringVar(&varStringO, "o")
+	deployCmdFlags.IntVarWithDefaultValue(&varIntReplicas, "replicas", 3)
+	deployCmdFlags.IntVarWithDefaultValue(&varIntRevisions, "revisions", 5)
+	deployCmdFlags.IntVar(&varIntPort, "port")
+	deployCmdFlags.IntVar(&varIntNodePort, "nodePort")
+	deployCmdFlags.IntVar(&varIntTargetPort, "targetPort")
+	deployCmdFlags.IntVarWithDefaultValue(&varIntMinReplicas, "minReplicas", 3)
+	deployCmdFlags.IntVarWithDefaultValue(&varIntMaxReplicas, "maxReplicas", 10)
+	deployCmdFlags.StringVar(&varStringImagePullPolicy, "imagePullPolicy")
+	deployCmdFlags.StringVar(&varStringHome, "home")
+	deployCmdFlags.StringVar(&varStringRemote, "remote")
+	deployCmdFlags.StringVar(&varStringBranch, "branch")
+	deployCmdFlags.StringVar(&varStringServiceAccount, "serviceAccount")
 
-	deployCmd.Flags().StringVar(&varStringHome, "home", "", "The goctl home path of the template, "+
-		"--home and --remote cannot be set at the same time, if they are, --remote has higher priority")
-	deployCmd.Flags().StringVar(&varStringRemote, "remote", "", "The remote git repo of the template, "+
-		"--home and --remote cannot be set at the same time, if they are, --remote has higher priority\nThe git repo "+
-		"directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure")
-	deployCmd.Flags().StringVar(&varStringBranch, "branch", "", "The branch of the remote repo, it "+
-		"does work with --remote")
-	deployCmd.Flags().StringVar(&varStringServiceAccount, "serviceAccount", "", "The ServiceAccount "+
-		"for the deployment")
-	deployCmd.MarkFlagRequired("name")
-	deployCmd.MarkFlagRequired("namespace")
-	deployCmd.MarkFlagRequired("o")
-	deployCmd.MarkFlagRequired("port")
+	_ = deployCmd.MarkFlagRequired("name")
+	_ = deployCmd.MarkFlagRequired("namespace")
+	_ = deployCmd.MarkFlagRequired("o")
+	_ = deployCmd.MarkFlagRequired("port")
 
 	Cmd.AddCommand(deployCmd)
 }

+ 5 - 12
tools/goctl/migrate/cmd.go

@@ -1,23 +1,16 @@
 package migrate
 
-import "github.com/spf13/cobra"
+import "github.com/zeromicro/go-zero/tools/goctl/internal/cobrax"
 
 var (
 	boolVarVerbose   bool
 	stringVarVersion string
 	// Cmd describes a migrate command.
-	Cmd = &cobra.Command{
-		Use:   "migrate",
-		Short: "Migrate from tal-tech to zeromicro",
-		Long: "Migrate is a transition command to help users migrate their " +
-			"projects from tal-tech to zeromicro version",
-		RunE: migrate,
-	}
+	Cmd = cobrax.NewCommand("migrate", cobrax.WithRunE(migrate))
 )
 
 func init() {
-	Cmd.Flags().BoolVarP(&boolVarVerbose, "verbose", "v",
-		false, "Verbose enables extra logging")
-	Cmd.Flags().StringVar(&stringVarVersion, "version", defaultMigrateVersion,
-		"The target release version of github.com/zeromicro/go-zero to migrate")
+	migrateCmdFlags := Cmd.Flags()
+	migrateCmdFlags.BoolVarP(&boolVarVerbose, "verbose", "v")
+	migrateCmdFlags.StringVarWithDefaultValue(&stringVarVersion, "version", defaultMigrateVersion)
 }

+ 56 - 85
tools/goctl/model/cmd.go

@@ -1,104 +1,75 @@
 package model
 
 import (
-	"github.com/spf13/cobra"
-
+	"github.com/zeromicro/go-zero/tools/goctl/internal/cobrax"
 	"github.com/zeromicro/go-zero/tools/goctl/model/mongo"
 	"github.com/zeromicro/go-zero/tools/goctl/model/sql/command"
 )
 
 var (
 	// Cmd describes a model command.
-	Cmd = &cobra.Command{
-		Use:   "model",
-		Short: "Generate model code",
-	}
-
-	mysqlCmd = &cobra.Command{
-		Use:   "mysql",
-		Short: "Generate mysql model",
-	}
-
-	ddlCmd = &cobra.Command{
-		Use:   "ddl",
-		Short: "Generate mysql model from ddl",
-		RunE:  command.MysqlDDL,
-	}
-
-	datasourceCmd = &cobra.Command{
-		Use:   "datasource",
-		Short: "Generate model from datasource",
-		RunE:  command.MySqlDataSource,
-	}
-
-	pgCmd = &cobra.Command{
-		Use:   "pg",
-		Short: "Generate postgresql model",
-		RunE:  command.PostgreSqlDataSource,
-	}
-
-	pgDatasourceCmd = &cobra.Command{
-		Use:   "datasource",
-		Short: "Generate model from datasource",
-		RunE:  command.PostgreSqlDataSource,
-	}
-
-	mongoCmd = &cobra.Command{
-		Use:   "mongo",
-		Short: "Generate mongo model",
-		RunE:  mongo.Action,
-	}
+	Cmd             = cobrax.NewCommand("model")
+	mysqlCmd        = cobrax.NewCommand("mysql")
+	ddlCmd          = cobrax.NewCommand("ddl", cobrax.WithRunE(command.MysqlDDL))
+	datasourceCmd   = cobrax.NewCommand("datasource", cobrax.WithRunE(command.MySqlDataSource))
+	pgCmd           = cobrax.NewCommand("pg", cobrax.WithRunE(command.PostgreSqlDataSource))
+	pgDatasourceCmd = cobrax.NewCommand("datasource", cobrax.WithRunE(command.PostgreSqlDataSource))
+	mongoCmd        = cobrax.NewCommand("mongo", cobrax.WithRunE(mongo.Action))
 )
 
 func init() {
-	ddlCmd.Flags().StringVarP(&command.VarStringSrc, "src", "s", "", "The path or path globbing patterns of the ddl")
-	ddlCmd.Flags().StringVarP(&command.VarStringDir, "dir", "d", "", "The target dir")
-	ddlCmd.Flags().StringVar(&command.VarStringStyle, "style", "", "The file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]")
-	ddlCmd.Flags().BoolVarP(&command.VarBoolCache, "cache", "c", false, "Generate code with cache [optional]")
-	ddlCmd.Flags().BoolVar(&command.VarBoolIdea, "idea", false, "For idea plugin [optional]")
-	ddlCmd.Flags().StringVar(&command.VarStringDatabase, "database", "", "The name of database [optional]")
-	ddlCmd.Flags().StringVar(&command.VarStringHome, "home", "", "The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority")
-	ddlCmd.Flags().StringVar(&command.VarStringRemote, "remote", "", "The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority\nThe git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure")
-	ddlCmd.Flags().StringVar(&command.VarStringBranch, "branch", "", "The branch of the remote repo, it does work with --remote")
+	var (
+		ddlCmdFlags          = ddlCmd.Flags()
+		datasourceCmdFlags   = datasourceCmd.Flags()
+		pgDatasourceCmdFlags = pgDatasourceCmd.Flags()
+		mongoCmdFlags        = mongoCmd.Flags()
+	)
+
+	ddlCmdFlags.StringVarP(&command.VarStringSrc, "src", "s")
+	ddlCmdFlags.StringVarP(&command.VarStringDir, "dir", "d")
+	ddlCmdFlags.StringVar(&command.VarStringStyle, "style")
+	ddlCmdFlags.BoolVarP(&command.VarBoolCache, "cache", "c")
+	ddlCmdFlags.BoolVar(&command.VarBoolIdea, "idea")
+	ddlCmdFlags.StringVar(&command.VarStringDatabase, "database")
+	ddlCmdFlags.StringVar(&command.VarStringHome, "home")
+	ddlCmdFlags.StringVar(&command.VarStringRemote, "remote")
+	ddlCmdFlags.StringVar(&command.VarStringBranch, "branch")
 
-	datasourceCmd.Flags().StringVar(&command.VarStringURL, "url", "", `The data source of database,like "root:password@tcp(127.0.0.1:3306)/database"`)
-	datasourceCmd.Flags().StringSliceVarP(&command.VarStringSliceTable, "table", "t", nil, "The table or table globbing patterns in the database")
-	datasourceCmd.Flags().BoolVarP(&command.VarBoolCache, "cache", "c", false, "Generate code with cache [optional]")
-	datasourceCmd.Flags().StringVarP(&command.VarStringDir, "dir", "d", "", "The target dir")
-	datasourceCmd.Flags().StringVar(&command.VarStringStyle, "style", "", "The file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]")
-	datasourceCmd.Flags().BoolVar(&command.VarBoolIdea, "idea", false, "For idea plugin [optional]")
-	datasourceCmd.Flags().StringVar(&command.VarStringHome, "home", "", "The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority")
-	datasourceCmd.Flags().StringVar(&command.VarStringRemote, "remote", "", "The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority\nThe git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure")
-	datasourceCmd.Flags().StringVar(&command.VarStringBranch, "branch", "", "The branch of the remote repo, it does work with --remote")
+	datasourceCmdFlags.StringVar(&command.VarStringURL, "url")
+	datasourceCmdFlags.StringSliceVarP(&command.VarStringSliceTable, "table", "t")
+	datasourceCmdFlags.BoolVarP(&command.VarBoolCache, "cache", "c")
+	datasourceCmdFlags.StringVarP(&command.VarStringDir, "dir", "d")
+	datasourceCmdFlags.StringVar(&command.VarStringStyle, "style")
+	datasourceCmdFlags.BoolVar(&command.VarBoolIdea, "idea")
+	datasourceCmdFlags.StringVar(&command.VarStringHome, "home")
+	datasourceCmdFlags.StringVar(&command.VarStringRemote, "remote")
+	datasourceCmdFlags.StringVar(&command.VarStringBranch, "branch")
 
-	pgDatasourceCmd.Flags().StringVar(&command.VarStringURL, "url", "", `The data source of database,like "postgres://root:password@127.0.0.1:5432/database?sslmode=disable"`)
-	pgDatasourceCmd.Flags().StringVarP(&command.VarStringTable, "table", "t", "", "The table or table globbing patterns in the database")
-	pgDatasourceCmd.Flags().StringVarP(&command.VarStringSchema, "schema", "s", "public", "The table schema")
-	pgDatasourceCmd.Flags().BoolVarP(&command.VarBoolCache, "cache", "c", false, "Generate code with cache [optional]")
-	pgDatasourceCmd.Flags().StringVarP(&command.VarStringDir, "dir", "d", "", "The target dir")
-	pgDatasourceCmd.Flags().StringVar(&command.VarStringStyle, "style", "", "The file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]")
-	pgDatasourceCmd.Flags().BoolVar(&command.VarBoolIdea, "idea", false, "For idea plugin [optional]")
-	pgDatasourceCmd.Flags().BoolVar(&command.VarBoolStrict, "strict", false, "Generate model in strict mode")
-	pgDatasourceCmd.Flags().StringVar(&command.VarStringHome, "home", "", "The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority")
-	pgDatasourceCmd.Flags().StringVar(&command.VarStringRemote, "remote", "", "The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority\n\tThe git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure")
-	pgDatasourceCmd.Flags().StringVar(&command.VarStringBranch, "branch", "", "The branch of the remote repo, it does work with --remote")
+	pgDatasourceCmdFlags.StringVar(&command.VarStringURL, "url")
+	pgDatasourceCmdFlags.StringVarP(&command.VarStringTable, "table", "t")
+	pgDatasourceCmdFlags.StringVarPWithDefaultValue(&command.VarStringSchema, "schema", "s", "public")
+	pgDatasourceCmdFlags.BoolVarP(&command.VarBoolCache, "cache", "c")
+	pgDatasourceCmdFlags.StringVarP(&command.VarStringDir, "dir", "d")
+	pgDatasourceCmdFlags.StringVar(&command.VarStringStyle, "style")
+	pgDatasourceCmdFlags.BoolVar(&command.VarBoolIdea, "idea")
+	pgDatasourceCmdFlags.BoolVar(&command.VarBoolStrict, "strict")
+	pgDatasourceCmdFlags.StringVar(&command.VarStringHome, "home")
+	pgDatasourceCmdFlags.StringVar(&command.VarStringRemote, "remote")
+	pgDatasourceCmdFlags.StringVar(&command.VarStringBranch, "branch")
 
-	mongoCmd.Flags().StringSliceVarP(&mongo.VarStringSliceType, "type", "t", nil, "Specified model type name")
-	mongoCmd.Flags().BoolVarP(&mongo.VarBoolCache, "cache", "c", false, "Generate code with cache [optional]")
-	mongoCmd.Flags().BoolVarP(&mongo.VarBoolEasy, "easy", "e", false, "Generate code with auto generated CollectionName for easy declare [optional]")
-	mongoCmd.Flags().StringVarP(&mongo.VarStringDir, "dir", "d", "", "The target dir")
-	mongoCmd.Flags().StringVar(&mongo.VarStringStyle, "style", "", "The file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]")
-	mongoCmd.Flags().StringVar(&mongo.VarStringHome, "home", "", "The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority")
-	mongoCmd.Flags().StringVar(&mongo.VarStringRemote, "remote", "", "The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority\nThe git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure")
-	mongoCmd.Flags().StringVar(&mongo.VarStringBranch, "branch", "", "The branch of the remote repo, it does work with --remote")
+	mongoCmdFlags.StringSliceVarP(&mongo.VarStringSliceType, "type", "t")
+	mongoCmdFlags.BoolVarP(&mongo.VarBoolCache, "cache", "c")
+	mongoCmdFlags.BoolVarP(&mongo.VarBoolEasy, "easy", "e")
+	mongoCmdFlags.StringVarP(&mongo.VarStringDir, "dir", "d")
+	mongoCmdFlags.StringVar(&mongo.VarStringStyle, "style")
+	mongoCmdFlags.StringVar(&mongo.VarStringHome, "home")
+	mongoCmdFlags.StringVar(&mongo.VarStringRemote, "remote")
+	mongoCmdFlags.StringVar(&mongo.VarStringBranch, "branch")
 
-	mysqlCmd.PersistentFlags().BoolVar(&command.VarBoolStrict, "strict", false, "Generate model in strict mode")
-	mysqlCmd.PersistentFlags().StringSliceVarP(&command.VarStringSliceIgnoreColumns, "ignore-columns", "i", []string{"create_at", "created_at", "create_time", "update_at", "updated_at", "update_time"}, "Ignore columns while creating or updating rows")
+	mysqlCmd.PersistentFlags().BoolVar(&command.VarBoolStrict, "strict")
+	mysqlCmd.PersistentFlags().StringSliceVarPWithDefaultValue(&command.VarStringSliceIgnoreColumns, "ignore-columns", "i", []string{"create_at", "created_at", "create_time", "update_at", "updated_at", "update_time"})
 
-	mysqlCmd.AddCommand(datasourceCmd)
-	mysqlCmd.AddCommand(ddlCmd)
+	mysqlCmd.AddCommand(datasourceCmd, ddlCmd)
 	pgCmd.AddCommand(pgDatasourceCmd)
-	Cmd.AddCommand(mysqlCmd)
-	Cmd.AddCommand(mongoCmd)
-	Cmd.AddCommand(pgCmd)
+	Cmd.AddCommand(mysqlCmd, mongoCmd, pgCmd)
 }

+ 15 - 2
tools/goctl/pkg/env/env.go

@@ -62,6 +62,7 @@ func init() {
 		experimental := existsEnv.GetOr(GoctlExperimental, ExperimentalOff)
 		goctlEnv.SetKV(GoctlExperimental, experimental)
 	}
+
 	if !goctlEnv.HasKey(GoctlHome) {
 		goctlEnv.SetKV(GoctlHome, defaultGoctlHome)
 	}
@@ -90,8 +91,20 @@ func init() {
 	goctlEnv.SetKV(ProtocGenGoGRPCVersion, protocGenGoGrpcVer)
 }
 
-func Print() string {
-	return strings.Join(goctlEnv.Format(), "\n")
+func Print(args ...string) string {
+	if len(args) == 0 {
+		return strings.Join(goctlEnv.Format(), "\n")
+	}
+
+	var values []string
+	for _, key := range args {
+		value, ok := goctlEnv.GetString(key)
+		if !ok {
+			value = fmt.Sprintf("%s=%%not found%%", key)
+		}
+		values = append(values, fmt.Sprintf("%s=%s", key, value))
+	}
+	return strings.Join(values, "\n")
 }
 
 func Get(key string) string {

+ 3 - 9
tools/goctl/quickstart/cmd.go

@@ -1,6 +1,6 @@
 package quickstart
 
-import "github.com/spf13/cobra"
+import "github.com/zeromicro/go-zero/tools/goctl/internal/cobrax"
 
 const (
 	serviceTypeMono  = "mono"
@@ -11,15 +11,9 @@ var (
 	varStringServiceType string
 
 	// Cmd describes the command to run.
-	Cmd = &cobra.Command{
-		Use:   "quickstart",
-		Short: "quickly start a project",
-		RunE:  run,
-	}
+	Cmd = cobrax.NewCommand("quickstart", cobrax.WithRunE(run))
 )
 
 func init() {
-	Cmd.Flags().StringVarP(&varStringServiceType,
-		"service-type", "t", "mono",
-		"specify the service type, supported values: [mono, micro]")
+	Cmd.Flags().StringVarPWithDefaultValue(&varStringServiceType, "service-type", "t", "mono")
 }

+ 53 - 97
tools/goctl/rpc/cmd.go

@@ -3,114 +3,70 @@ package rpc
 import (
 	"github.com/spf13/cobra"
 	"github.com/zeromicro/go-zero/tools/goctl/config"
+	"github.com/zeromicro/go-zero/tools/goctl/internal/cobrax"
 	"github.com/zeromicro/go-zero/tools/goctl/rpc/cli"
 )
 
 var (
 	// Cmd describes a rpc command.
-	Cmd = &cobra.Command{
-		Use:   "rpc",
-		Short: "Generate rpc code",
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return cli.RPCTemplate(true)
-		},
-	}
+	Cmd = cobrax.NewCommand("rpc", cobrax.WithRunE(func(command *cobra.Command, strings []string) error {
+		return cli.RPCTemplate(true)
+	}))
+	templateCmd = cobrax.NewCommand("template", cobrax.WithRunE(func(command *cobra.Command, strings []string) error {
+		return cli.RPCTemplate(false)
+	}))
 
-	newCmd = &cobra.Command{
-		Use:   "new",
-		Short: "Generate rpc demo service",
-		Args:  cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
-		RunE:  cli.RPCNew,
-	}
-
-	templateCmd = &cobra.Command{
-		Use:   "template",
-		Short: "Generate proto template",
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return cli.RPCTemplate(false)
-		},
-	}
-
-	protocCmd = &cobra.Command{
-		Use:     "protoc",
-		Short:   "Generate grpc code",
-		Example: "goctl rpc protoc xx.proto --go_out=./pb --go-grpc_out=./pb --zrpc_out=.",
-		Args:    cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
-		RunE:    cli.ZRPC,
-	}
+	newCmd    = cobrax.NewCommand("new", cobrax.WithRunE(cli.RPCNew), cobrax.WithArgs(cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs)))
+	protocCmd = cobrax.NewCommand("protoc", cobrax.WithRunE(cli.ZRPC), cobrax.WithArgs(cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs)))
 )
 
 func init() {
-	Cmd.Flags().StringVar(&cli.VarStringOutput, "o", "", "Output a sample proto file")
-	Cmd.Flags().StringVar(&cli.VarStringHome, "home", "", "The goctl home path of "+
-		"the template, --home and --remote cannot be set at the same time, if they are, --remote has"+
-		" higher priority")
-	Cmd.Flags().StringVar(&cli.VarStringRemote, "remote", "", "The remote git repo"+
-		" of the template, --home and --remote cannot be set at the same time, if they are, --remote"+
-		" has higher priority\n\tThe git repo directory must be consistent with the "+
-		"https://github.com/zeromicro/go-zero-template directory structure")
-	Cmd.Flags().StringVar(&cli.VarStringBranch, "branch", "", "The branch of the "+
-		"remote repo, it does work with --remote")
+	var (
+		rpcCmdFlags      = Cmd.Flags()
+		newCmdFlags      = newCmd.Flags()
+		protocCmdFlags   = protocCmd.Flags()
+		templateCmdFlags = templateCmd.Flags()
+	)
+
+	rpcCmdFlags.StringVar(&cli.VarStringOutput, "o")
+	rpcCmdFlags.StringVar(&cli.VarStringHome, "home")
+	rpcCmdFlags.StringVar(&cli.VarStringRemote, "remote")
+	rpcCmdFlags.StringVar(&cli.VarStringBranch, "branch")
 
-	newCmd.Flags().StringSliceVar(&cli.VarStringSliceGoOpt, "go_opt", nil, "")
-	newCmd.Flags().StringSliceVar(&cli.VarStringSliceGoGRPCOpt, "go-grpc_opt", nil, "")
-	newCmd.Flags().StringVar(&cli.VarStringStyle, "style", config.DefaultFormat, "The file "+
-		"naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]")
-	newCmd.Flags().BoolVar(&cli.VarBoolIdea, "idea", false, "Whether the command "+
-		"execution environment is from idea plugin.")
-	newCmd.Flags().StringVar(&cli.VarStringHome, "home", "", "The goctl home path "+
-		"of the template, --home and --remote cannot be set at the same time, if they are, --remote "+
-		"has higher priority")
-	newCmd.Flags().StringVar(&cli.VarStringRemote, "remote", "", "The remote git "+
-		"repo of the template, --home and --remote cannot be set at the same time, if they are, "+
-		"--remote has higher priority\n\tThe git repo directory must be consistent with the "+
-		"https://github.com/zeromicro/go-zero-template directory structure")
-	newCmd.Flags().StringVar(&cli.VarStringBranch, "branch", "",
-		"The branch of the remote repo, it does work with --remote")
-	newCmd.Flags().BoolVarP(&cli.VarBoolVerbose, "verbose", "v", false, "Enable log output")
-	newCmd.Flags().MarkHidden("go_opt")
-	newCmd.Flags().MarkHidden("go-grpc_opt")
+	newCmdFlags.StringSliceVar(&cli.VarStringSliceGoOpt, "go_opt")
+	newCmdFlags.StringSliceVar(&cli.VarStringSliceGoGRPCOpt, "go-grpc_opt")
+	newCmdFlags.StringVarWithDefaultValue(&cli.VarStringStyle, "style", config.DefaultFormat)
+	newCmdFlags.BoolVar(&cli.VarBoolIdea, "idea")
+	newCmdFlags.StringVar(&cli.VarStringHome, "home")
+	newCmdFlags.StringVar(&cli.VarStringRemote, "remote")
+	newCmdFlags.StringVar(&cli.VarStringBranch, "branch")
+	newCmdFlags.BoolVarP(&cli.VarBoolVerbose, "verbose", "v")
+	newCmdFlags.MarkHidden("go_opt")
+	newCmdFlags.MarkHidden("go-grpc_opt")
 
-	protocCmd.Flags().BoolVarP(&cli.VarBoolMultiple, "multiple", "m", false,
-		"Generated in multiple rpc service mode")
-	protocCmd.Flags().StringSliceVar(&cli.VarStringSliceGoOut, "go_out", nil, "")
-	protocCmd.Flags().StringSliceVar(&cli.VarStringSliceGoGRPCOut, "go-grpc_out", nil, "")
-	protocCmd.Flags().StringSliceVar(&cli.VarStringSliceGoOpt, "go_opt", nil, "")
-	protocCmd.Flags().StringSliceVar(&cli.VarStringSliceGoGRPCOpt, "go-grpc_opt", nil, "")
-	protocCmd.Flags().StringSliceVar(&cli.VarStringSlicePlugin, "plugin", nil, "")
-	protocCmd.Flags().StringSliceVarP(&cli.VarStringSliceProtoPath, "proto_path", "I", nil, "")
-	protocCmd.Flags().StringVar(&cli.VarStringZRPCOut, "zrpc_out", "", "The zrpc output directory")
-	protocCmd.Flags().StringVar(&cli.VarStringStyle, "style", config.DefaultFormat, "The file "+
-		"naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]")
-	protocCmd.Flags().StringVar(&cli.VarStringHome, "home", "", "The goctl home "+
-		"path of the template, --home and --remote cannot be set at the same time, if they are, "+
-		"--remote has higher priority")
-	protocCmd.Flags().StringVar(&cli.VarStringRemote, "remote", "", "The remote "+
-		"git repo of the template, --home and --remote cannot be set at the same time, if they are, "+
-		"--remote has higher priority\n\tThe git repo directory must be consistent with the "+
-		"https://github.com/zeromicro/go-zero-template directory structure")
-	protocCmd.Flags().StringVar(&cli.VarStringBranch, "branch", "",
-		"The branch of the remote repo, it does work with --remote")
-	protocCmd.Flags().BoolVarP(&cli.VarBoolVerbose, "verbose", "v", false, "Enable log output")
-	protocCmd.Flags().MarkHidden("go_out")
-	protocCmd.Flags().MarkHidden("go-grpc_out")
-	protocCmd.Flags().MarkHidden("go_opt")
-	protocCmd.Flags().MarkHidden("go-grpc_opt")
-	protocCmd.Flags().MarkHidden("plugin")
-	protocCmd.Flags().MarkHidden("proto_path")
+	protocCmdFlags.BoolVarP(&cli.VarBoolMultiple, "multiple", "m")
+	protocCmdFlags.StringSliceVar(&cli.VarStringSliceGoOut, "go_out")
+	protocCmdFlags.StringSliceVar(&cli.VarStringSliceGoGRPCOut, "go-grpc_out")
+	protocCmdFlags.StringSliceVar(&cli.VarStringSliceGoOpt, "go_opt")
+	protocCmdFlags.StringSliceVar(&cli.VarStringSliceGoGRPCOpt, "go-grpc_opt")
+	protocCmdFlags.StringSliceVar(&cli.VarStringSlicePlugin, "plugin")
+	protocCmdFlags.StringSliceVarP(&cli.VarStringSliceProtoPath, "proto_path", "I")
+	protocCmdFlags.StringVar(&cli.VarStringZRPCOut, "zrpc_out")
+	protocCmdFlags.StringVar(&cli.VarStringHome, "home")
+	protocCmdFlags.StringVar(&cli.VarStringRemote, "remote")
+	protocCmdFlags.StringVar(&cli.VarStringBranch, "branch")
+	protocCmdFlags.BoolVarP(&cli.VarBoolVerbose, "verbose", "v")
+	protocCmdFlags.MarkHidden("go_out")
+	protocCmdFlags.MarkHidden("go-grpc_out")
+	protocCmdFlags.MarkHidden("go_opt")
+	protocCmdFlags.MarkHidden("go-grpc_opt")
+	protocCmdFlags.MarkHidden("plugin")
+	protocCmdFlags.MarkHidden("proto_path")
 
-	templateCmd.Flags().StringVar(&cli.VarStringOutput, "o", "", "Output a sample proto file")
-	templateCmd.Flags().StringVar(&cli.VarStringHome, "home", "", "The goctl home"+
-		" path of the template, --home and --remote cannot be set at the same time, if they are, "+
-		"--remote has higher priority")
-	templateCmd.Flags().StringVar(&cli.VarStringRemote, "remote", "", "The remote "+
-		"git repo of the template, --home and --remote cannot be set at the same time, if they are, "+
-		"--remote has higher priority\n\tThe git repo directory must be consistent with the "+
-		"https://github.com/zeromicro/go-zero-template directory structure")
-	templateCmd.Flags().StringVar(&cli.VarStringBranch, "branch", "", "The branch"+
-		" of the remote repo, it does work with --remote")
+	templateCmdFlags.StringVar(&cli.VarStringOutput, "o")
+	templateCmdFlags.StringVar(&cli.VarStringHome, "home")
+	templateCmdFlags.StringVar(&cli.VarStringRemote, "remote")
+	templateCmdFlags.StringVar(&cli.VarStringBranch, "branch")
 
-	Cmd.AddCommand(newCmd)
-	Cmd.AddCommand(protocCmd)
-	Cmd.AddCommand(templateCmd)
+	Cmd.AddCommand(newCmd, protocCmd, templateCmd)
 }

+ 86 - 0
tools/goctl/test/test.go

@@ -0,0 +1,86 @@
+package test
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type Data[T, Y any] struct {
+	Name  string
+	Input T
+	Want  Y
+	E     error
+}
+
+type Option[T, Y any] func(*Executor[T, Y])
+type assertFn[Y any] func(t *testing.T, expected, actual Y) bool
+
+func WithComparison[T, Y any](comparisonFn assertFn[Y]) Option[T, Y] {
+	return func(e *Executor[T, Y]) {
+		e.equalFn = comparisonFn
+	}
+}
+
+type Executor[T, Y any] struct {
+	list    []Data[T, Y]
+	equalFn assertFn[Y]
+}
+
+func NewExecutor[T, Y any](opt ...Option[T, Y]) *Executor[T, Y] {
+	e := &Executor[T, Y]{}
+	opt = append(opt, WithComparison[T, Y](func(t *testing.T, expected, actual Y) bool {
+		gotBytes, err := json.Marshal(actual)
+		if err != nil {
+			t.Fatal(err)
+			return false
+		}
+		wantBytes, err := json.Marshal(expected)
+		if err != nil {
+			t.Fatal(err)
+			return false
+		}
+		return assert.JSONEq(t, string(wantBytes), string(gotBytes))
+	}))
+
+	for _, o := range opt {
+		o(e)
+	}
+	return e
+}
+
+func (e *Executor[T, Y]) Add(data ...Data[T, Y]) {
+	e.list = append(e.list, data...)
+}
+
+func (e *Executor[T, Y]) Run(t *testing.T, do func(T) Y) {
+	if do == nil {
+		panic("execution body is nil")
+		return
+	}
+	for _, v := range e.list {
+		t.Run(v.Name, func(t *testing.T) {
+			inner := do
+			e.equalFn(t, v.Want, inner(v.Input))
+		})
+	}
+}
+
+func (e *Executor[T, Y]) RunE(t *testing.T, do func(T) (Y, error)) {
+	if do == nil {
+		panic("execution body is nil")
+		return
+	}
+	for _, v := range e.list {
+		t.Run(v.Name, func(t *testing.T) {
+			inner := do
+			got, err := inner(v.Input)
+			if v.E != nil {
+				assert.Equal(t, v.E, err)
+				return
+			}
+			e.equalFn(t, v.Want, got)
+		})
+	}
+}

+ 101 - 0
tools/goctl/test/test_test.go

@@ -0,0 +1,101 @@
+package test
+
+import (
+	"errors"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestExecutor_Run(t *testing.T) {
+	executor := NewExecutor[string, string]()
+	executor.Add([]Data[string, string]{
+		{
+			Name: "empty",
+		},
+		{
+			Name:  "snake_case",
+			input: "A_B_C",
+			want:  "a_b_c",
+		},
+		{
+			Name:  "camel_case",
+			input: "AaBbCc",
+			want:  "aabbcc",
+		},
+	}...)
+	executor.Run(t, func(s string) string {
+		return strings.ToLower(s)
+	})
+}
+
+func TestExecutor_RunE(t *testing.T) {
+	var dummyError = errors.New("dummy error")
+	executor := NewExecutor[string, string]()
+	executor.Add([]Data[string, string]{
+		{
+			Name: "empty",
+		},
+		{
+			Name:  "snake_case",
+			input: "A_B_C",
+			want:  "a_b_c",
+		},
+		{
+			Name:  "camel_case",
+			input: "AaBbCc",
+			want:  "aabbcc",
+		},
+		{
+			Name:  "invalid_input",
+			input: "😄",
+			E:     dummyError,
+		},
+	}...)
+	executor.RunE(t, func(s string) (string, error) {
+		for _, r := range s {
+			if r == '_' || r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' {
+				continue
+			}
+			return "", dummyError
+		}
+		return strings.ToLower(s), nil
+	})
+}
+
+func TestWithComparison(t *testing.T) {
+	var dummyError = errors.New("dummy error")
+	executor := NewExecutor[string, string](WithComparison[string, string](func(t *testing.T, expected, actual string) bool {
+		return assert.Equal(t, expected, actual)
+	}))
+	executor.Add([]Data[string, string]{
+		{
+			Name: "empty",
+		},
+		{
+			Name:  "snake_case",
+			input: "A_B_C",
+			want:  "a_b_c",
+		},
+		{
+			Name:  "camel_case",
+			input: "AaBbCc",
+			want:  "aabbcc",
+		},
+		{
+			Name:  "invalid_input",
+			input: "😄",
+			E:     dummyError,
+		},
+	}...)
+	executor.RunE(t, func(s string) (string, error) {
+		for _, r := range s {
+			if r == '_' || r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' {
+				continue
+			}
+			return "", dummyError
+		}
+		return strings.ToLower(s), nil
+	})
+}

+ 15 - 43
tools/goctl/tpl/cmd.go

@@ -1,55 +1,27 @@
 package tpl
 
-import (
-	"github.com/spf13/cobra"
-)
+import "github.com/zeromicro/go-zero/tools/goctl/internal/cobrax"
 
 var (
 	varStringHome     string
 	varStringCategory string
 	varStringName     string
 	// Cmd describes a template command.
-	Cmd = &cobra.Command{
-		Use:   "template",
-		Short: "Template operation",
-	}
-
-	initCmd = &cobra.Command{
-		Use:   "init",
-		Short: "Initialize the all templates(force update)",
-		RunE:  genTemplates,
-	}
-
-	cleanCmd = &cobra.Command{
-		Use:   "clean",
-		Short: "Clean the all cache templates",
-		RunE:  cleanTemplates,
-	}
-
-	updateCmd = &cobra.Command{
-		Use:   "update",
-		Short: "Update template of the target category to the latest",
-		RunE:  updateTemplates,
-	}
-
-	revertCmd = &cobra.Command{
-		Use:   "revert",
-		Short: "Revert the target template to the latest",
-		RunE:  revertTemplates,
-	}
+	Cmd       = cobrax.NewCommand("template")
+	initCmd   = cobrax.NewCommand("init", cobrax.WithRunE(genTemplates))
+	cleanCmd  = cobrax.NewCommand("clean", cobrax.WithRunE(cleanTemplates))
+	updateCmd = cobrax.NewCommand("update", cobrax.WithRunE(updateTemplates))
+	revertCmd = cobrax.NewCommand("revert", cobrax.WithRunE(revertTemplates))
 )
 
 func init() {
-	initCmd.Flags().StringVar(&varStringHome, "home", "", "The goctl home path of the template")
-	cleanCmd.Flags().StringVar(&varStringHome, "home", "", "The goctl home path of the template")
-	updateCmd.Flags().StringVar(&varStringHome, "home", "", "The goctl home path of the template")
-	updateCmd.Flags().StringVarP(&varStringCategory, "category", "c", "", "The category of template, enum [api,rpc,model,docker,kube]")
-	revertCmd.Flags().StringVar(&varStringHome, "home", "", "The goctl home path of the template")
-	revertCmd.Flags().StringVarP(&varStringCategory, "category", "c", "", "The category of template, enum [api,rpc,model,docker,kube]")
-	revertCmd.Flags().StringVarP(&varStringName, "name", "n", "", "The target file name of template")
-
-	Cmd.AddCommand(cleanCmd)
-	Cmd.AddCommand(initCmd)
-	Cmd.AddCommand(revertCmd)
-	Cmd.AddCommand(updateCmd)
+	initCmd.Flags().StringVar(&varStringHome, "home")
+	cleanCmd.Flags().StringVar(&varStringHome, "home")
+	updateCmd.Flags().StringVar(&varStringHome, "home")
+	updateCmd.Flags().StringVarP(&varStringCategory, "category", "c")
+	revertCmd.Flags().StringVar(&varStringHome, "home")
+	revertCmd.Flags().StringVarP(&varStringCategory, "category", "c")
+	revertCmd.Flags().StringVarP(&varStringName, "name", "n")
+
+	Cmd.AddCommand(cleanCmd, initCmd, revertCmd, updateCmd)
 }

+ 11 - 0
tools/goctl/tpl/templates.go

@@ -11,6 +11,7 @@ import (
 	"github.com/zeromicro/go-zero/tools/goctl/api/gogen"
 	apinew "github.com/zeromicro/go-zero/tools/goctl/api/new"
 	"github.com/zeromicro/go-zero/tools/goctl/docker"
+	"github.com/zeromicro/go-zero/tools/goctl/gateway"
 	"github.com/zeromicro/go-zero/tools/goctl/kube"
 	mongogen "github.com/zeromicro/go-zero/tools/goctl/model/mongo/generate"
 	modelgen "github.com/zeromicro/go-zero/tools/goctl/model/sql/gen"
@@ -52,6 +53,9 @@ func genTemplates(_ *cobra.Command, _ []string) error {
 		func() error {
 			return apinew.GenTemplates()
 		},
+		func() error {
+			return gateway.GenTemplates()
+		},
 	); err != nil {
 		return err
 	}
@@ -104,6 +108,9 @@ func cleanTemplates(_ *cobra.Command, _ []string) error {
 		func() error {
 			return apinew.Clean()
 		},
+		func() error {
+			return gateway.Clean()
+		},
 	)
 	if err != nil {
 		return err
@@ -144,6 +151,8 @@ func updateTemplates(_ *cobra.Command, _ []string) (err error) {
 		return apigen.Update()
 	case apinew.Category():
 		return apinew.Update()
+	case gateway.Category():
+		return gateway.Update()
 	default:
 		err = fmt.Errorf("unexpected category: %s", category)
 		return
@@ -181,6 +190,8 @@ func revertTemplates(_ *cobra.Command, _ []string) (err error) {
 		return apigen.RevertTemplate(filename)
 	case apinew.Category():
 		return apinew.RevertTemplate(filename)
+	case gateway.Category():
+		return gateway.RevertTemplate(filename)
 	default:
 		err = fmt.Errorf("unexpected category: %s", category)
 		return

+ 2 - 6
tools/goctl/upgrade/cmd.go

@@ -1,10 +1,6 @@
 package upgrade
 
-import "github.com/spf13/cobra"
+import "github.com/zeromicro/go-zero/tools/goctl/internal/cobrax"
 
 // Cmd describes an upgrade command.
-var Cmd = &cobra.Command{
-	Use:   "upgrade",
-	Short: "Upgrade goctl to latest version",
-	RunE:  upgrade,
-}
+var Cmd = cobrax.NewCommand("upgrade", cobrax.WithRunE(upgrade))

+ 12 - 0
tools/goctl/util/stringx/string.go

@@ -140,3 +140,15 @@ func ContainsAny(s string, runes ...rune) bool {
 func ContainsWhiteSpace(s string) bool {
 	return ContainsAny(s, WhiteSpace...)
 }
+
+func IsWhiteSpace(text string) bool {
+	if len(text) == 0 {
+		return true
+	}
+	for _, r := range text {
+		if !unicode.IsSpace(r) {
+			return false
+		}
+	}
+	return true
+}

+ 16 - 0
tools/goctl/util/templatex.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	goformat "go/format"
 	"io/ioutil"
+	"regexp"
 	"text/template"
 
 	"github.com/zeromicro/go-zero/tools/goctl/internal/errorx"
@@ -77,3 +78,18 @@ func (t *DefaultTemplate) Execute(data any) (*bytes.Buffer, error) {
 	buf.Write(formatOutput)
 	return buf, nil
 }
+
+// IsTemplateVariable returns true if the text is a template variable.
+// The text must start with a dot and be a valid template.
+func IsTemplateVariable(text string) bool {
+	match, _ := regexp.MatchString(`(?m)^{{(\.\w+)+}}$`, text)
+	return match
+}
+
+// TemplateVariable returns the variable name of the template.
+func TemplateVariable(text string) string {
+	if IsTemplateVariable(text) {
+		return text[3 : len(text)-2]
+	}
+	return ""
+}

+ 93 - 0
tools/goctl/util/templatex_test.go

@@ -0,0 +1,93 @@
+package util
+
+import (
+	"testing"
+
+	"github.com/zeromicro/go-zero/tools/goctl/test"
+)
+
+func TestIsTemplate(t *testing.T) {
+	executor := test.NewExecutor[string, bool]()
+	executor.Add([]test.Data[string, bool]{
+		{
+			Name: "empty",
+			Want: false,
+		},
+		{
+			Name:  "invalid",
+			Input: "{foo}",
+			Want:  false,
+		},
+		{
+			Name:  "invalid",
+			Input: "{.foo}",
+			Want:  false,
+		},
+		{
+			Name:  "invalid",
+			Input: "$foo",
+			Want:  false,
+		},
+		{
+			Name:  "invalid",
+			Input: "{{foo}}",
+			Want:  false,
+		},
+		{
+			Name:  "invalid",
+			Input: "{{.}}",
+			Want:  false,
+		},
+		{
+			Name:  "valid",
+			Input: "{{.foo}}",
+			Want:  true,
+		},
+		{
+			Name:  "valid",
+			Input: "{{.foo.bar}}",
+			Want:  true,
+		},
+	}...)
+	executor.Run(t, IsTemplateVariable)
+}
+
+func TestTemplateVariable(t *testing.T) {
+	executor := test.NewExecutor[string, string]()
+	executor.Add([]test.Data[string, string]{
+		{
+			Name: "empty",
+		},
+		{
+			Name:  "invalid",
+			Input: "{foo}",
+		},
+		{
+			Name:  "invalid",
+			Input: "{.foo}",
+		},
+		{
+			Name:  "invalid",
+			Input: "$foo",
+		},
+		{
+			Name:  "invalid",
+			Input: "{{foo}}",
+		},
+		{
+			Name:  "invalid",
+			Input: "{{.}}",
+		},
+		{
+			Name:  "valid",
+			Input: "{{.foo}}",
+			Want:  "foo",
+		},
+		{
+			Name:  "valid",
+			Input: "{{.foo.bar}}",
+			Want:  "foo.bar",
+		},
+	}...)
+	executor.Run(t, TemplateVariable)
+}