1package cli 2 3import ( 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. 21func 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. 60func 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. 69func 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 76func 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. 94type 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 103func 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 108func (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 114func (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. 124func (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. 159func (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. 167func 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. 176func 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 183var 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 199func 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 212func 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 220func hasAdditionalHelp(cmd *cobra.Command) bool { 221 return additionalHelp(cmd) != "" 222} 223 224func isPlugin(cmd *cobra.Command) bool { 225 return cmd.Annotations[pluginmanager.CommandAnnotationPlugin] == "true" 226} 227 228func hasSubCommands(cmd *cobra.Command) bool { 229 return len(operationSubCommands(cmd)) > 0 230} 231 232func hasManagementSubCommands(cmd *cobra.Command) bool { 233 return len(managementSubCommands(cmd)) > 0 234} 235 236func hasInvalidPlugins(cmd *cobra.Command) bool { 237 return len(invalidPlugins(cmd)) > 0 238} 239 240func 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 253func 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 261func decoratedName(cmd *cobra.Command) string { 262 decoration := " " 263 if isPlugin(cmd) { 264 decoration = "*" 265 } 266 return cmd.Name() + decoration 267} 268 269func 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 280func 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 296func 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 309func invalidPluginReason(cmd *cobra.Command) string { 310 return cmd.Annotations[pluginmanager.CommandAnnotationPluginInvalid] 311} 312 313var 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 321EXPERIMENTAL: 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 331Aliases: 332 {{.NameAndAliases}} 333 334{{- end}} 335{{- if .HasExample}} 336 337Examples: 338{{ .Example }} 339 340{{- end}} 341{{- if .HasAvailableFlags}} 342 343Options: 344{{ wrappedFlagUsages . | trimRightSpace}} 345 346{{- end}} 347{{- if hasManagementSubCommands . }} 348 349Management 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 358Commands: 359 360{{- range operationSubCommands . }} 361 {{rpad .Name .NamePadding }} {{.Short}} 362{{- end}} 363{{- end}} 364 365{{- if hasInvalidPlugins . }} 366 367Invalid Plugins: 368 369{{- range invalidPlugins . }} 370 {{rpad .Name .NamePadding }} {{invalidPluginReason .}} 371{{- end}} 372 373{{- end}} 374 375{{- if .HasSubCommands }} 376 377Run '{{.CommandPath}} COMMAND --help' for more information on a command. 378{{- end}} 379{{- if hasAdditionalHelp .}} 380 381{{ additionalHelp . }} 382{{- end}} 383` 384 385var helpTemplate = ` 386{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` 387