github.com/ali-iotechsys/cli@v20.10.0+incompatible/cli/cobra.go (about)

     1  package cli
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"strings"
     7  
     8  	pluginmanager "github.com/docker/cli/cli-plugins/manager"
     9  	"github.com/docker/cli/cli/command"
    10  	cliconfig "github.com/docker/cli/cli/config"
    11  	cliflags "github.com/docker/cli/cli/flags"
    12  	"github.com/moby/term"
    13  	"github.com/pkg/errors"
    14  	"github.com/spf13/cobra"
    15  	"github.com/spf13/pflag"
    16  )
    17  
    18  // setupCommonRootCommand contains the setup common to
    19  // SetupRootCommand and SetupPluginRootCommand.
    20  func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet, *cobra.Command) {
    21  	opts := cliflags.NewClientOptions()
    22  	flags := rootCmd.Flags()
    23  
    24  	flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files")
    25  	opts.Common.InstallFlags(flags)
    26  
    27  	cobra.AddTemplateFunc("add", func(a, b int) int { return a + b })
    28  	cobra.AddTemplateFunc("hasSubCommands", hasSubCommands)
    29  	cobra.AddTemplateFunc("hasManagementSubCommands", hasManagementSubCommands)
    30  	cobra.AddTemplateFunc("hasInvalidPlugins", hasInvalidPlugins)
    31  	cobra.AddTemplateFunc("operationSubCommands", operationSubCommands)
    32  	cobra.AddTemplateFunc("managementSubCommands", managementSubCommands)
    33  	cobra.AddTemplateFunc("invalidPlugins", invalidPlugins)
    34  	cobra.AddTemplateFunc("wrappedFlagUsages", wrappedFlagUsages)
    35  	cobra.AddTemplateFunc("vendorAndVersion", vendorAndVersion)
    36  	cobra.AddTemplateFunc("invalidPluginReason", invalidPluginReason)
    37  	cobra.AddTemplateFunc("isPlugin", isPlugin)
    38  	cobra.AddTemplateFunc("isExperimental", isExperimental)
    39  	cobra.AddTemplateFunc("decoratedName", decoratedName)
    40  
    41  	rootCmd.SetUsageTemplate(usageTemplate)
    42  	rootCmd.SetHelpTemplate(helpTemplate)
    43  	rootCmd.SetFlagErrorFunc(FlagErrorFunc)
    44  	rootCmd.SetHelpCommand(helpCommand)
    45  
    46  	rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage")
    47  	rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help")
    48  	rootCmd.PersistentFlags().Lookup("help").Hidden = true
    49  
    50  	return opts, flags, helpCommand
    51  }
    52  
    53  // SetupRootCommand sets default usage, help, and error handling for the
    54  // root command.
    55  func SetupRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet, *cobra.Command) {
    56  	opts, flags, helpCmd := setupCommonRootCommand(rootCmd)
    57  
    58  	rootCmd.SetVersionTemplate("Docker version {{.Version}}\n")
    59  
    60  	return opts, flags, helpCmd
    61  }
    62  
    63  // SetupPluginRootCommand sets default usage, help and error handling for a plugin root command.
    64  func SetupPluginRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet) {
    65  	opts, flags, _ := setupCommonRootCommand(rootCmd)
    66  	return opts, flags
    67  }
    68  
    69  // FlagErrorFunc prints an error message which matches the format of the
    70  // docker/cli/cli error messages
    71  func FlagErrorFunc(cmd *cobra.Command, err error) error {
    72  	if err == nil {
    73  		return nil
    74  	}
    75  
    76  	usage := ""
    77  	if cmd.HasSubCommands() {
    78  		usage = "\n\n" + cmd.UsageString()
    79  	}
    80  	return StatusError{
    81  		Status:     fmt.Sprintf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage),
    82  		StatusCode: 125,
    83  	}
    84  }
    85  
    86  // TopLevelCommand encapsulates a top-level cobra command (either
    87  // docker CLI or a plugin) and global flag handling logic necessary
    88  // for plugins.
    89  type TopLevelCommand struct {
    90  	cmd       *cobra.Command
    91  	dockerCli *command.DockerCli
    92  	opts      *cliflags.ClientOptions
    93  	flags     *pflag.FlagSet
    94  	args      []string
    95  }
    96  
    97  // NewTopLevelCommand returns a new TopLevelCommand object
    98  func NewTopLevelCommand(cmd *cobra.Command, dockerCli *command.DockerCli, opts *cliflags.ClientOptions, flags *pflag.FlagSet) *TopLevelCommand {
    99  	return &TopLevelCommand{cmd, dockerCli, opts, flags, os.Args[1:]}
   100  }
   101  
   102  // SetArgs sets the args (default os.Args[:1] used to invoke the command
   103  func (tcmd *TopLevelCommand) SetArgs(args []string) {
   104  	tcmd.args = args
   105  	tcmd.cmd.SetArgs(args)
   106  }
   107  
   108  // SetFlag sets a flag in the local flag set of the top-level command
   109  func (tcmd *TopLevelCommand) SetFlag(name, value string) {
   110  	tcmd.cmd.Flags().Set(name, value)
   111  }
   112  
   113  // HandleGlobalFlags takes care of parsing global flags defined on the
   114  // command, it returns the underlying cobra command and the args it
   115  // will be called with (or an error).
   116  //
   117  // On success the caller is responsible for calling Initialize()
   118  // before calling `Execute` on the returned command.
   119  func (tcmd *TopLevelCommand) HandleGlobalFlags() (*cobra.Command, []string, error) {
   120  	cmd := tcmd.cmd
   121  
   122  	// We manually parse the global arguments and find the
   123  	// subcommand in order to properly deal with plugins. We rely
   124  	// on the root command never having any non-flag arguments. We
   125  	// create our own FlagSet so that we can configure it
   126  	// (e.g. `SetInterspersed` below) in an idempotent way.
   127  	flags := pflag.NewFlagSet(cmd.Name(), pflag.ContinueOnError)
   128  
   129  	// We need !interspersed to ensure we stop at the first
   130  	// potential command instead of accumulating it into
   131  	// flags.Args() and then continuing on and finding other
   132  	// arguments which we try and treat as globals (when they are
   133  	// actually arguments to the subcommand).
   134  	flags.SetInterspersed(false)
   135  
   136  	// We need the single parse to see both sets of flags.
   137  	flags.AddFlagSet(cmd.Flags())
   138  	flags.AddFlagSet(cmd.PersistentFlags())
   139  	// Now parse the global flags, up to (but not including) the
   140  	// first command. The result will be that all the remaining
   141  	// arguments are in `flags.Args()`.
   142  	if err := flags.Parse(tcmd.args); err != nil {
   143  		// Our FlagErrorFunc uses the cli, make sure it is initialized
   144  		if err := tcmd.Initialize(); err != nil {
   145  			return nil, nil, err
   146  		}
   147  		return nil, nil, cmd.FlagErrorFunc()(cmd, err)
   148  	}
   149  
   150  	return cmd, flags.Args(), nil
   151  }
   152  
   153  // Initialize finalises global option parsing and initializes the docker client.
   154  func (tcmd *TopLevelCommand) Initialize(ops ...command.InitializeOpt) error {
   155  	tcmd.opts.Common.SetDefaultOptions(tcmd.flags)
   156  	return tcmd.dockerCli.Initialize(tcmd.opts, ops...)
   157  }
   158  
   159  // VisitAll will traverse all commands from the root.
   160  // This is different from the VisitAll of cobra.Command where only parents
   161  // are checked.
   162  func VisitAll(root *cobra.Command, fn func(*cobra.Command)) {
   163  	for _, cmd := range root.Commands() {
   164  		VisitAll(cmd, fn)
   165  	}
   166  	fn(root)
   167  }
   168  
   169  // DisableFlagsInUseLine sets the DisableFlagsInUseLine flag on all
   170  // commands within the tree rooted at cmd.
   171  func DisableFlagsInUseLine(cmd *cobra.Command) {
   172  	VisitAll(cmd, func(ccmd *cobra.Command) {
   173  		// do not add a `[flags]` to the end of the usage line.
   174  		ccmd.DisableFlagsInUseLine = true
   175  	})
   176  }
   177  
   178  var helpCommand = &cobra.Command{
   179  	Use:               "help [command]",
   180  	Short:             "Help about the command",
   181  	PersistentPreRun:  func(cmd *cobra.Command, args []string) {},
   182  	PersistentPostRun: func(cmd *cobra.Command, args []string) {},
   183  	RunE: func(c *cobra.Command, args []string) error {
   184  		cmd, args, e := c.Root().Find(args)
   185  		if cmd == nil || e != nil || len(args) > 0 {
   186  			return errors.Errorf("unknown help topic: %v", strings.Join(args, " "))
   187  		}
   188  
   189  		helpFunc := cmd.HelpFunc()
   190  		helpFunc(cmd, args)
   191  		return nil
   192  	},
   193  }
   194  
   195  func isExperimental(cmd *cobra.Command) bool {
   196  	if _, ok := cmd.Annotations["experimentalCLI"]; ok {
   197  		return true
   198  	}
   199  	var experimental bool
   200  	cmd.VisitParents(func(cmd *cobra.Command) {
   201  		if _, ok := cmd.Annotations["experimentalCLI"]; ok {
   202  			experimental = true
   203  		}
   204  	})
   205  	return experimental
   206  }
   207  
   208  func isPlugin(cmd *cobra.Command) bool {
   209  	return cmd.Annotations[pluginmanager.CommandAnnotationPlugin] == "true"
   210  }
   211  
   212  func hasSubCommands(cmd *cobra.Command) bool {
   213  	return len(operationSubCommands(cmd)) > 0
   214  }
   215  
   216  func hasManagementSubCommands(cmd *cobra.Command) bool {
   217  	return len(managementSubCommands(cmd)) > 0
   218  }
   219  
   220  func hasInvalidPlugins(cmd *cobra.Command) bool {
   221  	return len(invalidPlugins(cmd)) > 0
   222  }
   223  
   224  func operationSubCommands(cmd *cobra.Command) []*cobra.Command {
   225  	cmds := []*cobra.Command{}
   226  	for _, sub := range cmd.Commands() {
   227  		if isPlugin(sub) {
   228  			continue
   229  		}
   230  		if sub.IsAvailableCommand() && !sub.HasSubCommands() {
   231  			cmds = append(cmds, sub)
   232  		}
   233  	}
   234  	return cmds
   235  }
   236  
   237  func wrappedFlagUsages(cmd *cobra.Command) string {
   238  	width := 80
   239  	if ws, err := term.GetWinsize(0); err == nil {
   240  		width = int(ws.Width)
   241  	}
   242  	return cmd.Flags().FlagUsagesWrapped(width - 1)
   243  }
   244  
   245  func decoratedName(cmd *cobra.Command) string {
   246  	decoration := " "
   247  	if isPlugin(cmd) {
   248  		decoration = "*"
   249  	}
   250  	return cmd.Name() + decoration
   251  }
   252  
   253  func vendorAndVersion(cmd *cobra.Command) string {
   254  	if vendor, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) {
   255  		version := ""
   256  		if v, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVersion]; ok && v != "" {
   257  			version = ", " + v
   258  		}
   259  		return fmt.Sprintf("(%s%s)", vendor, version)
   260  	}
   261  	return ""
   262  }
   263  
   264  func managementSubCommands(cmd *cobra.Command) []*cobra.Command {
   265  	cmds := []*cobra.Command{}
   266  	for _, sub := range cmd.Commands() {
   267  		if isPlugin(sub) {
   268  			if invalidPluginReason(sub) == "" {
   269  				cmds = append(cmds, sub)
   270  			}
   271  			continue
   272  		}
   273  		if sub.IsAvailableCommand() && sub.HasSubCommands() {
   274  			cmds = append(cmds, sub)
   275  		}
   276  	}
   277  	return cmds
   278  }
   279  
   280  func invalidPlugins(cmd *cobra.Command) []*cobra.Command {
   281  	cmds := []*cobra.Command{}
   282  	for _, sub := range cmd.Commands() {
   283  		if !isPlugin(sub) {
   284  			continue
   285  		}
   286  		if invalidPluginReason(sub) != "" {
   287  			cmds = append(cmds, sub)
   288  		}
   289  	}
   290  	return cmds
   291  }
   292  
   293  func invalidPluginReason(cmd *cobra.Command) string {
   294  	return cmd.Annotations[pluginmanager.CommandAnnotationPluginInvalid]
   295  }
   296  
   297  var usageTemplate = `Usage:
   298  
   299  {{- if not .HasSubCommands}}	{{.UseLine}}{{end}}
   300  {{- if .HasSubCommands}}	{{ .CommandPath}}{{- if .HasAvailableFlags}} [OPTIONS]{{end}} COMMAND{{end}}
   301  
   302  {{if ne .Long ""}}{{ .Long | trim }}{{ else }}{{ .Short | trim }}{{end}}
   303  {{- if isExperimental .}}
   304  
   305  EXPERIMENTAL:
   306    {{.CommandPath}} is an experimental feature.
   307    Experimental features provide early access to product functionality. These
   308    features may change between releases without warning, or can be removed from a
   309    future release. Learn more about experimental features in our documentation:
   310    https://docs.docker.com/go/experimental/
   311  
   312  {{- end}}
   313  {{- if gt .Aliases 0}}
   314  
   315  Aliases:
   316    {{.NameAndAliases}}
   317  
   318  {{- end}}
   319  {{- if .HasExample}}
   320  
   321  Examples:
   322  {{ .Example }}
   323  
   324  {{- end}}
   325  {{- if .HasAvailableFlags}}
   326  
   327  Options:
   328  {{ wrappedFlagUsages . | trimRightSpace}}
   329  
   330  {{- end}}
   331  {{- if hasManagementSubCommands . }}
   332  
   333  Management Commands:
   334  
   335  {{- range managementSubCommands . }}
   336    {{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}}
   337  {{- end}}
   338  
   339  {{- end}}
   340  {{- if hasSubCommands .}}
   341  
   342  Commands:
   343  
   344  {{- range operationSubCommands . }}
   345    {{rpad .Name .NamePadding }} {{.Short}}
   346  {{- end}}
   347  {{- end}}
   348  
   349  {{- if hasInvalidPlugins . }}
   350  
   351  Invalid Plugins:
   352  
   353  {{- range invalidPlugins . }}
   354    {{rpad .Name .NamePadding }} {{invalidPluginReason .}}
   355  {{- end}}
   356  
   357  {{- end}}
   358  
   359  {{- if .HasSubCommands }}
   360  
   361  Run '{{.CommandPath}} COMMAND --help' for more information on a command.
   362  {{- end}}
   363  `
   364  
   365  var helpTemplate = `
   366  {{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`