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