github.com/thajeztah/cli@v0.0.0-20240223162942-dc6bfac81a8b/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 cliflags "github.com/docker/cli/cli/flags" 13 "github.com/docker/docker/pkg/homedir" 14 "github.com/docker/docker/registry" 15 "github.com/fvbommel/sortorder" 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/docker/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}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}} 474 {{- end}} 475 476 {{- end}} 477 {{- if hasSwarmSubCommands . }} 478 479 Swarm Commands: 480 481 {{- range orchestratorSubCommands . }} 482 {{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}} 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}}`