github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/cobra.go (about)

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