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}}`