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