github.com/justincormack/cli@v0.0.0-20201215022714-831ebeae9675/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}}`