From 9e6919550be5689628d0020ec14e90ea6f527716 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Fri, 24 Feb 2023 18:09:12 -0500 Subject: cmd: Expand cobra support, add short flags (#5379) * cmd: Expand cobra support * Convert commands to cobra, add short flags * Fix version command typo Co-authored-by: Emily Lange * Apply suggestions from code review Co-authored-by: Matt Holt --------- Co-authored-by: Emily Lange Co-authored-by: Matt Holt --- cmd/cobra.go | 21 +++-- cmd/commands.go | 233 ++++++++++++++++++++++++++------------------------------ 2 files changed, 124 insertions(+), 130 deletions(-) (limited to 'cmd') diff --git a/cmd/cobra.go b/cmd/cobra.go index 203b7bd..4339cdb 100644 --- a/cmd/cobra.go +++ b/cmd/cobra.go @@ -109,12 +109,21 @@ func caddyCmdToCobra(caddyCmd Command) *cobra.Command { Use: caddyCmd.Name, Short: caddyCmd.Short, Long: caddyCmd.Long, - RunE: func(cmd *cobra.Command, _ []string) error { - fls := cmd.Flags() - _, err := caddyCmd.Func(Flags{fls}) - return err - }, } - cmd.Flags().AddGoFlagSet(caddyCmd.Flags) + if caddyCmd.CobraFunc != nil { + caddyCmd.CobraFunc(cmd) + } else { + cmd.RunE = WrapCommandFuncForCobra(caddyCmd.Func) + cmd.Flags().AddGoFlagSet(caddyCmd.Flags) + } return cmd } + +// WrapCommandFuncForCobra wraps a Caddy CommandFunc for use +// in a cobra command's RunE field. +func WrapCommandFuncForCobra(f CommandFunc) func(cmd *cobra.Command, _ []string) error { + return func(cmd *cobra.Command, _ []string) error { + _, err := f(Flags{cmd.Flags()}) + return err + } +} diff --git a/cmd/commands.go b/cmd/commands.go index 9daabd1..a06336d 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -34,12 +34,6 @@ type Command struct { // Required. Name string - // Func is a function that executes a subcommand using - // the parsed flags. It returns an exit code and any - // associated error. - // Required. - Func CommandFunc - // Usage is a brief message describing the syntax of // the subcommand's flags and args. Use [] to indicate // optional parameters and <> to enclose literal values @@ -60,7 +54,21 @@ type Command struct { Long string // Flags is the flagset for command. + // This is ignored if CobraFunc is set. Flags *flag.FlagSet + + // Func is a function that executes a subcommand using + // the parsed flags. It returns an exit code and any + // associated error. + // Required if CobraFunc is not set. + Func CommandFunc + + // CobraFunc allows further configuration of the command + // via cobra's APIs. If this is set, then Func and Flags + // are ignored, with the assumption that they are set in + // this function. A caddycmd.WrapCommandFuncForCobra helper + // exists to simplify porting CommandFunc to Cobra's RunE. + CobraFunc func(*cobra.Command) } // CommandFunc is a command's function. It runs the @@ -79,7 +87,6 @@ var commands = make(map[string]Command) func init() { RegisterCommand(Command{ Name: "start", - Func: cmdStart, Usage: "[--config [--adapter ]] [--envfile ] [--watch] [--pidfile ]", Short: "Starts the Caddy process in the background and then returns", Long: ` @@ -93,21 +100,19 @@ On Windows, the spawned child process will remain attached to the terminal, so closing the window will forcefully stop Caddy; to avoid forgetting this, try using 'caddy run' instead to keep it in the foreground. `, - Flags: func() *flag.FlagSet { - fs := flag.NewFlagSet("start", flag.ExitOnError) - fs.String("config", "", "Configuration file") - fs.String("adapter", "", "Name of config adapter to apply") - fs.String("envfile", "", "Environment file to load") - fs.Bool("watch", false, "Reload changed config file automatically") - fs.String("pidfile", "", "Path of file to which to write process ID") - return fs - }(), + CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().StringP("config", "c", "", "Configuration file") + cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply") + cmd.Flags().StringP("envfile", "", "", "Environment file to load") + cmd.Flags().BoolP("watch", "w", false, "Reload changed config file automatically") + cmd.Flags().StringP("pidfile", "", "", "Path of file to which to write process ID") + cmd.RunE = WrapCommandFuncForCobra(cmdStart) + }, }) RegisterCommand(Command{ Name: "run", - Func: cmdRun, - Usage: "[--config [--adapter ]] [--envfile ] [--environ] [--resume] [--watch] [--pidfile ]", + Usage: "[--config [--adapter ]] [--envfile ] [--environ] [--resume] [--watch] [--pidfile ]", Short: `Starts the Caddy process and blocks indefinitely`, Long: ` Starts the Caddy process, optionally bootstrapped with an initial config file, @@ -141,24 +146,22 @@ If --watch is specified, the config file will be loaded automatically after changes. ⚠️ This can make unintentional config changes easier; only use this option in a local development environment. `, - Flags: func() *flag.FlagSet { - fs := flag.NewFlagSet("run", flag.ExitOnError) - fs.String("config", "", "Configuration file") - fs.String("adapter", "", "Name of config adapter to apply") - fs.String("envfile", "", "Environment file to load") - fs.Bool("environ", false, "Print environment") - fs.Bool("resume", false, "Use saved config, if any (and prefer over --config file)") - fs.Bool("watch", false, "Watch config file for changes and reload it automatically") - fs.String("pidfile", "", "Path of file to which to write process ID") - fs.String("pingback", "", "Echo confirmation bytes to this address on success") - return fs - }(), + CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().StringP("config", "c", "", "Configuration file") + cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply") + cmd.Flags().StringP("envfile", "", "", "Environment file to load") + cmd.Flags().BoolP("environ", "e", false, "Print environment") + cmd.Flags().BoolP("resume", "r", false, "Use saved config, if any (and prefer over --config file)") + cmd.Flags().BoolP("watch", "w", false, "Watch config file for changes and reload it automatically") + cmd.Flags().StringP("pidfile", "", "", "Path of file to which to write process ID") + cmd.Flags().StringP("pingback", "", "", "Echo confirmation bytes to this address on success") + cmd.RunE = WrapCommandFuncForCobra(cmdRun) + }, }) RegisterCommand(Command{ Name: "stop", - Func: cmdStop, - Usage: "[--address ] [--config [--adapter ]]", + Usage: "[--config [--adapter ]] [--address ]", Short: "Gracefully stops a started Caddy process", Long: ` Stops the background Caddy process as gracefully as possible. @@ -167,18 +170,16 @@ It requires that the admin API is enabled and accessible, since it will use the API's /stop endpoint. The address of this request can be customized using the --address flag, or from the given --config, if not the default. `, - Flags: func() *flag.FlagSet { - fs := flag.NewFlagSet("stop", flag.ExitOnError) - fs.String("address", "", "The address to use to reach the admin API endpoint, if not the default") - fs.String("config", "", "Configuration file to use to parse the admin address, if --address is not used") - fs.String("adapter", "", "Name of config adapter to apply (when --config is used)") - return fs - }(), + CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().StringP("config", "c", "", "Configuration file to use to parse the admin address, if --address is not used") + cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply (when --config is used)") + cmd.Flags().StringP("address", "", "", "The address to use to reach the admin API endpoint, if not the default") + cmd.RunE = WrapCommandFuncForCobra(cmdStop) + }, }) RegisterCommand(Command{ Name: "reload", - Func: cmdReload, Usage: "--config [--adapter ] [--address ]", Short: "Changes the config of the running Caddy instance", Long: ` @@ -190,19 +191,17 @@ Since the admin endpoint is configurable, the endpoint configuration is loaded from the --address flag if specified; otherwise it is loaded from the given config file; otherwise the default is assumed. `, - Flags: func() *flag.FlagSet { - fs := flag.NewFlagSet("reload", flag.ExitOnError) - fs.String("config", "", "Configuration file (required)") - fs.String("adapter", "", "Name of config adapter to apply") - fs.String("address", "", "Address of the administration listener, if different from config") - fs.Bool("force", false, "Force config reload, even if it is the same") - return fs - }(), + CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().StringP("config", "c", "", "Configuration file (required)") + cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply") + cmd.Flags().StringP("address", "", "", "Address of the administration listener, if different from config") + cmd.Flags().BoolP("force", "f", false, "Force config reload, even if it is the same") + cmd.RunE = WrapCommandFuncForCobra(cmdReload) + }, }) RegisterCommand(Command{ Name: "version", - Func: cmdVersion, Short: "Prints the version", Long: ` Prints the version of this Caddy binary. @@ -217,31 +216,29 @@ detailed version information is printed as given by Go modules. For more details about the full version string, see the Go module documentation: https://go.dev/doc/modules/version-numbers `, + Func: cmdVersion, }) RegisterCommand(Command{ Name: "list-modules", - Func: cmdListModules, - Usage: "[--packages] [--versions]", + Usage: "[--packages] [--versions] [--skip-standard]", Short: "Lists the installed Caddy modules", - Flags: func() *flag.FlagSet { - fs := flag.NewFlagSet("list-modules", flag.ExitOnError) - fs.Bool("packages", false, "Print package paths") - fs.Bool("versions", false, "Print version information") - fs.Bool("skip-standard", false, "Skip printing standard modules") - return fs - }(), + CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().BoolP("packages", "", false, "Print package paths") + cmd.Flags().BoolP("versions", "", false, "Print version information") + cmd.Flags().BoolP("skip-standard", "s", false, "Skip printing standard modules") + cmd.RunE = WrapCommandFuncForCobra(cmdListModules) + }, }) RegisterCommand(Command{ Name: "build-info", - Func: cmdBuildInfo, Short: "Prints information about this build", + Func: cmdBuildInfo, }) RegisterCommand(Command{ Name: "environ", - Func: cmdEnviron, Short: "Prints the environment", Long: ` Prints the environment as seen by this Caddy process. @@ -261,11 +258,11 @@ by adding the "--environ" flag. Environments may contain sensitive data. `, + Func: cmdEnviron, }) RegisterCommand(Command{ Name: "adapt", - Func: cmdAdaptConfig, Usage: "--config [--adapter ] [--pretty] [--validate]", Short: "Adapts a configuration to Caddy's native JSON", Long: ` @@ -279,19 +276,17 @@ If --validate is used, the adapted config will be checked for validity. If the config is invalid, an error will be printed to stderr and a non- zero exit status will be returned. `, - Flags: func() *flag.FlagSet { - fs := flag.NewFlagSet("adapt", flag.ExitOnError) - fs.String("config", "", "Configuration file to adapt (required)") - fs.String("adapter", "caddyfile", "Name of config adapter") - fs.Bool("pretty", false, "Format the output for human readability") - fs.Bool("validate", false, "Validate the output") - return fs - }(), + CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().StringP("config", "c", "", "Configuration file to adapt (required)") + cmd.Flags().StringP("adapter", "a", "caddyfile", "Name of config adapter") + cmd.Flags().BoolP("pretty", "p", false, "Format the output for human readability") + cmd.Flags().BoolP("validate", "", false, "Validate the output") + cmd.RunE = WrapCommandFuncForCobra(cmdAdaptConfig) + }, }) RegisterCommand(Command{ Name: "validate", - Func: cmdValidateConfig, Usage: "--config [--adapter ] [--envfile ]", Short: "Tests whether a configuration file is valid", Long: ` @@ -302,18 +297,16 @@ provisioning stages. If --envfile is specified, an environment file with environment variables in the KEY=VALUE format will be loaded into the Caddy process. `, - Flags: func() *flag.FlagSet { - fs := flag.NewFlagSet("validate", flag.ExitOnError) - fs.String("config", "", "Input configuration file") - fs.String("adapter", "", "Name of config adapter") - fs.String("envfile", "", "Environment file to load") - return fs - }(), + CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().StringP("config", "c", "", "Input configuration file") + cmd.Flags().StringP("adapter", "a", "", "Name of config adapter") + cmd.Flags().StringP("envfile", "", "", "Environment file to load") + cmd.RunE = WrapCommandFuncForCobra(cmdValidateConfig) + }, }) RegisterCommand(Command{ Name: "fmt", - Func: cmdFmt, Usage: "[--overwrite] [--diff] []", Short: "Formats a Caddyfile", Long: ` @@ -332,32 +325,28 @@ If you wish you use stdin instead of a regular file, use - as the path. When reading from stdin, the --overwrite flag has no effect: the result is always printed to stdout. `, - Flags: func() *flag.FlagSet { - fs := flag.NewFlagSet("fmt", flag.ExitOnError) - fs.Bool("overwrite", false, "Overwrite the input file with the results") - fs.Bool("diff", false, "Print the differences between the input file and the formatted output") - return fs - }(), + CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().BoolP("overwrite", "w", false, "Overwrite the input file with the results") + cmd.Flags().BoolP("diff", "d", false, "Print the differences between the input file and the formatted output") + cmd.RunE = WrapCommandFuncForCobra(cmdFmt) + }, }) RegisterCommand(Command{ Name: "upgrade", - Func: cmdUpgrade, Short: "Upgrade Caddy (EXPERIMENTAL)", Long: ` Downloads an updated Caddy binary with the same modules/plugins at the latest versions. EXPERIMENTAL: May be changed or removed. `, - Flags: func() *flag.FlagSet { - fs := flag.NewFlagSet("upgrade", flag.ExitOnError) - fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it") - return fs - }(), + CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it") + cmd.RunE = WrapCommandFuncForCobra(cmdUpgrade) + }, }) RegisterCommand(Command{ Name: "add-package", - Func: cmdAddPackage, Usage: "", Short: "Adds Caddy packages (EXPERIMENTAL)", Long: ` @@ -365,11 +354,10 @@ Downloads an updated Caddy binary with the specified packages (module/plugin) added. Retains existing packages. Returns an error if the any of packages are already included. EXPERIMENTAL: May be changed or removed. `, - Flags: func() *flag.FlagSet { - fs := flag.NewFlagSet("add-package", flag.ExitOnError) - fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it") - return fs - }(), + CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it") + cmd.RunE = WrapCommandFuncForCobra(cmdAddPackage) + }, }) RegisterCommand(Command{ @@ -382,31 +370,14 @@ Downloads an updated Caddy binaries without the specified packages (module/plugi Returns an error if any of the packages are not included. EXPERIMENTAL: May be changed or removed. `, - Flags: func() *flag.FlagSet { - fs := flag.NewFlagSet("remove-package", flag.ExitOnError) - fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it") - return fs - }(), + CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it") + cmd.RunE = WrapCommandFuncForCobra(cmdRemovePackage) + }, }) RegisterCommand(Command{ - Name: "manpage", - Func: func(fl Flags) (int, error) { - dir := strings.TrimSpace(fl.String("directory")) - if dir == "" { - return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required") - } - if err := os.MkdirAll(dir, 0755); err != nil { - return caddy.ExitCodeFailedQuit, err - } - if err := doc.GenManTree(rootCmd, &doc.GenManHeader{ - Title: "Caddy", - Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections - }, dir); err != nil { - return caddy.ExitCodeFailedQuit, err - } - return caddy.ExitCodeSuccess, nil - }, + Name: "manpage", Usage: "--directory ", Short: "Generates the manual pages for Caddy commands", Long: ` @@ -416,11 +387,25 @@ tagged into section 8 (System Administration). The manual page files are generated into the directory specified by the argument of --directory. If the directory does not exist, it will be created. `, - Flags: func() *flag.FlagSet { - fs := flag.NewFlagSet("manpage", flag.ExitOnError) - fs.String("directory", "", "The output directory where the manpages are generated") - return fs - }(), + CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().StringP("directory", "o", "", "The output directory where the manpages are generated") + cmd.RunE = WrapCommandFuncForCobra(func(fl Flags) (int, error) { + dir := strings.TrimSpace(fl.String("directory")) + if dir == "" { + return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required") + } + if err := os.MkdirAll(dir, 0755); err != nil { + return caddy.ExitCodeFailedQuit, err + } + if err := doc.GenManTree(rootCmd, &doc.GenManHeader{ + Title: "Caddy", + Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections + }, dir); err != nil { + return caddy.ExitCodeFailedQuit, err + } + return caddy.ExitCodeSuccess, nil + }) + }, }) // source: https://github.com/spf13/cobra/blob/main/shell_completions.md @@ -504,7 +489,7 @@ func RegisterCommand(cmd Command) { if cmd.Name == "" { panic("command name is required") } - if cmd.Func == nil { + if cmd.Func == nil && cmd.CobraFunc == nil { panic("command function missing") } if cmd.Short == "" { -- cgit v1.2.3