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