github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cli/cobra.go (about)

     1  package cli
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"sort"
     8  	"strings"
     9  
    10  	pluginmanager "github.com/docker/cli/cli-plugins/manager"
    11  	"github.com/docker/cli/cli/command"
    12  	"github.com/docker/cli/cli/config"
    13  	cliflags "github.com/docker/cli/cli/flags"
    14  	"github.com/docker/docker/pkg/homedir"
    15  	"github.com/docker/docker/registry"
    16  	"github.com/fvbommel/sortorder"
    17  	"github.com/moby/term"
    18  	"github.com/morikuni/aec"
    19  	"github.com/pkg/errors"
    20  	"github.com/spf13/cobra"
    21  	"github.com/spf13/pflag"
    22  )
    23  
    24  // setupCommonRootCommand contains the setup common to
    25  // SetupRootCommand and SetupPluginRootCommand.
    26  func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet, *cobra.Command) {
    27  	opts := cliflags.NewClientOptions()
    28  	flags := rootCmd.Flags()
    29  
    30  	flags.StringVar(&opts.ConfigDir, "config", config.Dir(), "Location of client config files")
    31  	opts.InstallFlags(flags)
    32  
    33  	cobra.AddTemplateFunc("add", func(a, b int) int { return a + b })
    34  	cobra.AddTemplateFunc("hasAliases", hasAliases)
    35  	cobra.AddTemplateFunc("hasSubCommands", hasSubCommands)
    36  	cobra.AddTemplateFunc("hasTopCommands", hasTopCommands)
    37  	cobra.AddTemplateFunc("hasManagementSubCommands", hasManagementSubCommands)
    38  	cobra.AddTemplateFunc("hasSwarmSubCommands", hasSwarmSubCommands)
    39  	cobra.AddTemplateFunc("hasInvalidPlugins", hasInvalidPlugins)
    40  	cobra.AddTemplateFunc("topCommands", topCommands)
    41  	cobra.AddTemplateFunc("commandAliases", commandAliases)
    42  	cobra.AddTemplateFunc("operationSubCommands", operationSubCommands)
    43  	cobra.AddTemplateFunc("managementSubCommands", managementSubCommands)
    44  	cobra.AddTemplateFunc("orchestratorSubCommands", orchestratorSubCommands)
    45  	cobra.AddTemplateFunc("invalidPlugins", invalidPlugins)
    46  	cobra.AddTemplateFunc("wrappedFlagUsages", wrappedFlagUsages)
    47  	cobra.AddTemplateFunc("vendorAndVersion", vendorAndVersion)
    48  	cobra.AddTemplateFunc("invalidPluginReason", invalidPluginReason)
    49  	cobra.AddTemplateFunc("isPlugin", isPlugin)
    50  	cobra.AddTemplateFunc("isExperimental", isExperimental)
    51  	cobra.AddTemplateFunc("hasAdditionalHelp", hasAdditionalHelp)
    52  	cobra.AddTemplateFunc("additionalHelp", additionalHelp)
    53  	cobra.AddTemplateFunc("decoratedName", decoratedName)
    54  
    55  	rootCmd.SetUsageTemplate(usageTemplate)
    56  	rootCmd.SetHelpTemplate(helpTemplate)
    57  	rootCmd.SetFlagErrorFunc(FlagErrorFunc)
    58  	rootCmd.SetHelpCommand(helpCommand)
    59  
    60  	rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage")
    61  	rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help")
    62  	rootCmd.PersistentFlags().Lookup("help").Hidden = true
    63  
    64  	rootCmd.Annotations = map[string]string{
    65  		"additionalHelp":      "For more help on how to use Docker, head to https://docs.docker.com/go/guides/",
    66  		"docs.code-delimiter": `"`, // https://github.com/docker/cli-docs-tool/blob/77abede22166eaea4af7335096bdcedd043f5b19/annotation/annotation.go#L20-L22
    67  	}
    68  
    69  	// Configure registry.CertsDir() when running in rootless-mode
    70  	if os.Getenv("ROOTLESSKIT_STATE_DIR") != "" {
    71  		if configHome, err := homedir.GetConfigHome(); err == nil {
    72  			registry.SetCertsDir(filepath.Join(configHome, "docker/certs.d"))
    73  		}
    74  	}
    75  
    76  	return opts, flags, helpCommand
    77  }
    78  
    79  // SetupRootCommand sets default usage, help, and error handling for the
    80  // root command.
    81  func SetupRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet, *cobra.Command) {
    82  	rootCmd.SetVersionTemplate("Docker version {{.Version}}\n")
    83  	return setupCommonRootCommand(rootCmd)
    84  }
    85  
    86  // SetupPluginRootCommand sets default usage, help and error handling for a plugin root command.
    87  func SetupPluginRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet) {
    88  	opts, flags, _ := setupCommonRootCommand(rootCmd)
    89  	return opts, flags
    90  }
    91  
    92  // FlagErrorFunc prints an error message which matches the format of the
    93  // docker/cli/cli error messages
    94  func FlagErrorFunc(cmd *cobra.Command, err error) error {
    95  	if err == nil {
    96  		return nil
    97  	}
    98  
    99  	usage := ""
   100  	if cmd.HasSubCommands() {
   101  		usage = "\n\n" + cmd.UsageString()
   102  	}
   103  	return StatusError{
   104  		Status:     fmt.Sprintf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage),
   105  		StatusCode: 125,
   106  	}
   107  }
   108  
   109  // TopLevelCommand encapsulates a top-level cobra command (either
   110  // docker CLI or a plugin) and global flag handling logic necessary
   111  // for plugins.
   112  type TopLevelCommand struct {
   113  	cmd       *cobra.Command
   114  	dockerCli *command.DockerCli
   115  	opts      *cliflags.ClientOptions
   116  	flags     *pflag.FlagSet
   117  	args      []string
   118  }
   119  
   120  // NewTopLevelCommand returns a new TopLevelCommand object
   121  func NewTopLevelCommand(cmd *cobra.Command, dockerCli *command.DockerCli, opts *cliflags.ClientOptions, flags *pflag.FlagSet) *TopLevelCommand {
   122  	return &TopLevelCommand{
   123  		cmd:       cmd,
   124  		dockerCli: dockerCli,
   125  		opts:      opts,
   126  		flags:     flags,
   127  		args:      os.Args[1:],
   128  	}
   129  }
   130  
   131  // SetArgs sets the args (default os.Args[:1] used to invoke the command
   132  func (tcmd *TopLevelCommand) SetArgs(args []string) {
   133  	tcmd.args = args
   134  	tcmd.cmd.SetArgs(args)
   135  }
   136  
   137  // SetFlag sets a flag in the local flag set of the top-level command
   138  func (tcmd *TopLevelCommand) SetFlag(name, value string) {
   139  	tcmd.cmd.Flags().Set(name, value)
   140  }
   141  
   142  // HandleGlobalFlags takes care of parsing global flags defined on the
   143  // command, it returns the underlying cobra command and the args it
   144  // will be called with (or an error).
   145  //
   146  // On success the caller is responsible for calling Initialize()
   147  // before calling `Execute` on the returned command.
   148  func (tcmd *TopLevelCommand) HandleGlobalFlags() (*cobra.Command, []string, error) {
   149  	cmd := tcmd.cmd
   150  
   151  	// We manually parse the global arguments and find the
   152  	// subcommand in order to properly deal with plugins. We rely
   153  	// on the root command never having any non-flag arguments. We
   154  	// create our own FlagSet so that we can configure it
   155  	// (e.g. `SetInterspersed` below) in an idempotent way.
   156  	flags := pflag.NewFlagSet(cmd.Name(), pflag.ContinueOnError)
   157  
   158  	// We need !interspersed to ensure we stop at the first
   159  	// potential command instead of accumulating it into
   160  	// flags.Args() and then continuing on and finding other
   161  	// arguments which we try and treat as globals (when they are
   162  	// actually arguments to the subcommand).
   163  	flags.SetInterspersed(false)
   164  
   165  	// We need the single parse to see both sets of flags.
   166  	flags.AddFlagSet(cmd.Flags())
   167  	flags.AddFlagSet(cmd.PersistentFlags())
   168  	// Now parse the global flags, up to (but not including) the
   169  	// first command. The result will be that all the remaining
   170  	// arguments are in `flags.Args()`.
   171  	if err := flags.Parse(tcmd.args); err != nil {
   172  		// Our FlagErrorFunc uses the cli, make sure it is initialized
   173  		if err := tcmd.Initialize(); err != nil {
   174  			return nil, nil, err
   175  		}
   176  		return nil, nil, cmd.FlagErrorFunc()(cmd, err)
   177  	}
   178  
   179  	return cmd, flags.Args(), nil
   180  }
   181  
   182  // Initialize finalises global option parsing and initializes the docker client.
   183  func (tcmd *TopLevelCommand) Initialize(ops ...command.InitializeOpt) error {
   184  	tcmd.opts.SetDefaultOptions(tcmd.flags)
   185  	return tcmd.dockerCli.Initialize(tcmd.opts, ops...)
   186  }
   187  
   188  // VisitAll will traverse all commands from the root.
   189  // This is different from the VisitAll of cobra.Command where only parents
   190  // are checked.
   191  func VisitAll(root *cobra.Command, fn func(*cobra.Command)) {
   192  	for _, cmd := range root.Commands() {
   193  		VisitAll(cmd, fn)
   194  	}
   195  	fn(root)
   196  }
   197  
   198  // DisableFlagsInUseLine sets the DisableFlagsInUseLine flag on all
   199  // commands within the tree rooted at cmd.
   200  func DisableFlagsInUseLine(cmd *cobra.Command) {
   201  	VisitAll(cmd, func(ccmd *cobra.Command) {
   202  		// do not add a `[flags]` to the end of the usage line.
   203  		ccmd.DisableFlagsInUseLine = true
   204  	})
   205  }
   206  
   207  var helpCommand = &cobra.Command{
   208  	Use:               "help [command]",
   209  	Short:             "Help about the command",
   210  	PersistentPreRun:  func(cmd *cobra.Command, args []string) {},
   211  	PersistentPostRun: func(cmd *cobra.Command, args []string) {},
   212  	RunE: func(c *cobra.Command, args []string) error {
   213  		cmd, args, e := c.Root().Find(args)
   214  		if cmd == nil || e != nil || len(args) > 0 {
   215  			return errors.Errorf("unknown help topic: %v", strings.Join(args, " "))
   216  		}
   217  		helpFunc := cmd.HelpFunc()
   218  		helpFunc(cmd, args)
   219  		return nil
   220  	},
   221  }
   222  
   223  func isExperimental(cmd *cobra.Command) bool {
   224  	if _, ok := cmd.Annotations["experimentalCLI"]; ok {
   225  		return true
   226  	}
   227  	var experimental bool
   228  	cmd.VisitParents(func(cmd *cobra.Command) {
   229  		if _, ok := cmd.Annotations["experimentalCLI"]; ok {
   230  			experimental = true
   231  		}
   232  	})
   233  	return experimental
   234  }
   235  
   236  func additionalHelp(cmd *cobra.Command) string {
   237  	if msg, ok := cmd.Annotations["additionalHelp"]; ok {
   238  		out := cmd.OutOrStderr()
   239  		if _, isTerminal := term.GetFdInfo(out); !isTerminal {
   240  			return msg
   241  		}
   242  		style := aec.EmptyBuilder.Bold().ANSI
   243  		return style.Apply(msg)
   244  	}
   245  	return ""
   246  }
   247  
   248  func hasAdditionalHelp(cmd *cobra.Command) bool {
   249  	return additionalHelp(cmd) != ""
   250  }
   251  
   252  func isPlugin(cmd *cobra.Command) bool {
   253  	return pluginmanager.IsPluginCommand(cmd)
   254  }
   255  
   256  func hasAliases(cmd *cobra.Command) bool {
   257  	return len(cmd.Aliases) > 0 || cmd.Annotations["aliases"] != ""
   258  }
   259  
   260  func hasSubCommands(cmd *cobra.Command) bool {
   261  	return len(operationSubCommands(cmd)) > 0
   262  }
   263  
   264  func hasManagementSubCommands(cmd *cobra.Command) bool {
   265  	return len(managementSubCommands(cmd)) > 0
   266  }
   267  
   268  func hasSwarmSubCommands(cmd *cobra.Command) bool {
   269  	return len(orchestratorSubCommands(cmd)) > 0
   270  }
   271  
   272  func hasInvalidPlugins(cmd *cobra.Command) bool {
   273  	return len(invalidPlugins(cmd)) > 0
   274  }
   275  
   276  func hasTopCommands(cmd *cobra.Command) bool {
   277  	return len(topCommands(cmd)) > 0
   278  }
   279  
   280  // commandAliases is a templating function to return aliases for the command,
   281  // formatted as the full command as they're called (contrary to the default
   282  // Aliases function, which only returns the subcommand).
   283  func commandAliases(cmd *cobra.Command) string {
   284  	if cmd.Annotations["aliases"] != "" {
   285  		return cmd.Annotations["aliases"]
   286  	}
   287  	var parentPath string
   288  	if cmd.HasParent() {
   289  		parentPath = cmd.Parent().CommandPath() + " "
   290  	}
   291  	aliases := cmd.CommandPath()
   292  	for _, alias := range cmd.Aliases {
   293  		aliases += ", " + parentPath + alias
   294  	}
   295  	return aliases
   296  }
   297  
   298  func topCommands(cmd *cobra.Command) []*cobra.Command {
   299  	cmds := []*cobra.Command{}
   300  	if cmd.Parent() != nil {
   301  		// for now, only use top-commands for the root-command, and skip
   302  		// for sub-commands
   303  		return cmds
   304  	}
   305  	for _, sub := range cmd.Commands() {
   306  		if isPlugin(sub) || !sub.IsAvailableCommand() {
   307  			continue
   308  		}
   309  		if _, ok := sub.Annotations["category-top"]; ok {
   310  			cmds = append(cmds, sub)
   311  		}
   312  	}
   313  	sort.SliceStable(cmds, func(i, j int) bool {
   314  		return sortorder.NaturalLess(cmds[i].Annotations["category-top"], cmds[j].Annotations["category-top"])
   315  	})
   316  	return cmds
   317  }
   318  
   319  func operationSubCommands(cmd *cobra.Command) []*cobra.Command {
   320  	cmds := []*cobra.Command{}
   321  	for _, sub := range cmd.Commands() {
   322  		if isPlugin(sub) {
   323  			continue
   324  		}
   325  		if _, ok := sub.Annotations["category-top"]; ok {
   326  			if cmd.Parent() == nil {
   327  				// for now, only use top-commands for the root-command
   328  				continue
   329  			}
   330  		}
   331  		if sub.IsAvailableCommand() && !sub.HasSubCommands() {
   332  			cmds = append(cmds, sub)
   333  		}
   334  	}
   335  	return cmds
   336  }
   337  
   338  func wrappedFlagUsages(cmd *cobra.Command) string {
   339  	width := 80
   340  	if ws, err := term.GetWinsize(0); err == nil {
   341  		width = int(ws.Width)
   342  	}
   343  	return cmd.Flags().FlagUsagesWrapped(width - 1)
   344  }
   345  
   346  func decoratedName(cmd *cobra.Command) string {
   347  	decoration := " "
   348  	if isPlugin(cmd) {
   349  		decoration = "*"
   350  	}
   351  	return cmd.Name() + decoration
   352  }
   353  
   354  func vendorAndVersion(cmd *cobra.Command) string {
   355  	if vendor, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) {
   356  		version := ""
   357  		if v, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVersion]; ok && v != "" {
   358  			version = ", " + v
   359  		}
   360  		return fmt.Sprintf("(%s%s)", vendor, version)
   361  	}
   362  	return ""
   363  }
   364  
   365  func managementSubCommands(cmd *cobra.Command) []*cobra.Command {
   366  	cmds := []*cobra.Command{}
   367  	for _, sub := range allManagementSubCommands(cmd) {
   368  		if _, ok := sub.Annotations["swarm"]; ok {
   369  			continue
   370  		}
   371  		cmds = append(cmds, sub)
   372  	}
   373  	return cmds
   374  }
   375  
   376  func orchestratorSubCommands(cmd *cobra.Command) []*cobra.Command {
   377  	cmds := []*cobra.Command{}
   378  	for _, sub := range allManagementSubCommands(cmd) {
   379  		if _, ok := sub.Annotations["swarm"]; ok {
   380  			cmds = append(cmds, sub)
   381  		}
   382  	}
   383  	return cmds
   384  }
   385  
   386  func allManagementSubCommands(cmd *cobra.Command) []*cobra.Command {
   387  	cmds := []*cobra.Command{}
   388  	for _, sub := range cmd.Commands() {
   389  		if isPlugin(sub) {
   390  			if invalidPluginReason(sub) == "" {
   391  				cmds = append(cmds, sub)
   392  			}
   393  			continue
   394  		}
   395  		if sub.IsAvailableCommand() && sub.HasSubCommands() {
   396  			cmds = append(cmds, sub)
   397  		}
   398  	}
   399  	return cmds
   400  }
   401  
   402  func invalidPlugins(cmd *cobra.Command) []*cobra.Command {
   403  	cmds := []*cobra.Command{}
   404  	for _, sub := range cmd.Commands() {
   405  		if !isPlugin(sub) {
   406  			continue
   407  		}
   408  		if invalidPluginReason(sub) != "" {
   409  			cmds = append(cmds, sub)
   410  		}
   411  	}
   412  	return cmds
   413  }
   414  
   415  func invalidPluginReason(cmd *cobra.Command) string {
   416  	return cmd.Annotations[pluginmanager.CommandAnnotationPluginInvalid]
   417  }
   418  
   419  var usageTemplate = `Usage:
   420  
   421  {{- if not .HasSubCommands}}  {{.UseLine}}{{end}}
   422  {{- if .HasSubCommands}}  {{ .CommandPath}}{{- if .HasAvailableFlags}} [OPTIONS]{{end}} COMMAND{{end}}
   423  
   424  {{if ne .Long ""}}{{ .Long | trim }}{{ else }}{{ .Short | trim }}{{end}}
   425  {{- if isExperimental .}}
   426  
   427  EXPERIMENTAL:
   428    {{.CommandPath}} is an experimental feature.
   429    Experimental features provide early access to product functionality. These
   430    features may change between releases without warning, or can be removed from a
   431    future release. Learn more about experimental features in our documentation:
   432    https://docs.docker.com/go/experimental/
   433  
   434  {{- end}}
   435  {{- if hasAliases . }}
   436  
   437  Aliases:
   438    {{ commandAliases . }}
   439  
   440  {{- end}}
   441  {{- if .HasExample}}
   442  
   443  Examples:
   444  {{ .Example }}
   445  
   446  {{- end}}
   447  {{- if .HasParent}}
   448  {{- if .HasAvailableFlags}}
   449  
   450  Options:
   451  {{ wrappedFlagUsages . | trimRightSpace}}
   452  
   453  {{- end}}
   454  {{- end}}
   455  {{- if hasTopCommands .}}
   456  
   457  Common Commands:
   458  {{- range topCommands .}}
   459    {{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
   460  {{- end}}
   461  {{- end}}
   462  {{- if hasManagementSubCommands . }}
   463  
   464  Management Commands:
   465  
   466  {{- range managementSubCommands . }}
   467    {{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}}
   468  {{- end}}
   469  
   470  {{- end}}
   471  {{- if hasSwarmSubCommands . }}
   472  
   473  Swarm Commands:
   474  
   475  {{- range orchestratorSubCommands . }}
   476    {{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}}
   477  {{- end}}
   478  
   479  {{- end}}
   480  {{- if hasSubCommands .}}
   481  
   482  Commands:
   483  
   484  {{- range operationSubCommands . }}
   485    {{rpad .Name .NamePadding }} {{.Short}}
   486  {{- end}}
   487  {{- end}}
   488  
   489  {{- if hasInvalidPlugins . }}
   490  
   491  Invalid Plugins:
   492  
   493  {{- range invalidPlugins . }}
   494    {{rpad .Name .NamePadding }} {{invalidPluginReason .}}
   495  {{- end}}
   496  
   497  {{- end}}
   498  {{- if not .HasParent}}
   499  {{- if .HasAvailableFlags}}
   500  
   501  Global Options:
   502  {{ wrappedFlagUsages . | trimRightSpace}}
   503  
   504  {{- end}}
   505  {{- end}}
   506  
   507  {{- if .HasSubCommands }}
   508  
   509  Run '{{.CommandPath}} COMMAND --help' for more information on a command.
   510  {{- end}}
   511  {{- if hasAdditionalHelp .}}
   512  
   513  {{ additionalHelp . }}
   514  
   515  {{- end}}
   516  `
   517  
   518  var helpTemplate = `
   519  {{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`