1package root
2
3import (
4	"bytes"
5	"fmt"
6	"strings"
7
8	"github.com/cli/cli/v2/pkg/cmdutil"
9	"github.com/cli/cli/v2/pkg/text"
10	"github.com/spf13/cobra"
11	"github.com/spf13/pflag"
12)
13
14func rootUsageFunc(command *cobra.Command) error {
15	command.Printf("Usage:  %s", command.UseLine())
16
17	subcommands := command.Commands()
18	if len(subcommands) > 0 {
19		command.Print("\n\nAvailable commands:\n")
20		for _, c := range subcommands {
21			if c.Hidden {
22				continue
23			}
24			command.Printf("  %s\n", c.Name())
25		}
26		return nil
27	}
28
29	flagUsages := command.LocalFlags().FlagUsages()
30	if flagUsages != "" {
31		command.Println("\n\nFlags:")
32		command.Print(text.Indent(dedent(flagUsages), "  "))
33	}
34	return nil
35}
36
37func rootFlagErrorFunc(cmd *cobra.Command, err error) error {
38	if err == pflag.ErrHelp {
39		return err
40	}
41	return cmdutil.FlagErrorWrap(err)
42}
43
44var hasFailed bool
45
46// HasFailed signals that the main process should exit with non-zero status
47func HasFailed() bool {
48	return hasFailed
49}
50
51// Display helpful error message in case subcommand name was mistyped.
52// This matches Cobra's behavior for root command, which Cobra
53// confusingly doesn't apply to nested commands.
54func nestedSuggestFunc(command *cobra.Command, arg string) {
55	command.Printf("unknown command %q for %q\n", arg, command.CommandPath())
56
57	var candidates []string
58	if arg == "help" {
59		candidates = []string{"--help"}
60	} else {
61		if command.SuggestionsMinimumDistance <= 0 {
62			command.SuggestionsMinimumDistance = 2
63		}
64		candidates = command.SuggestionsFor(arg)
65	}
66
67	if len(candidates) > 0 {
68		command.Print("\nDid you mean this?\n")
69		for _, c := range candidates {
70			command.Printf("\t%s\n", c)
71		}
72	}
73
74	command.Print("\n")
75	_ = rootUsageFunc(command)
76}
77
78func isRootCmd(command *cobra.Command) bool {
79	return command != nil && !command.HasParent()
80}
81
82func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) {
83	cs := f.IOStreams.ColorScheme()
84
85	if isRootCmd(command.Parent()) && len(args) >= 2 && args[1] != "--help" && args[1] != "-h" {
86		nestedSuggestFunc(command, args[1])
87		hasFailed = true
88		return
89	}
90
91	coreCommands := []string{}
92	actionsCommands := []string{}
93	additionalCommands := []string{}
94	for _, c := range command.Commands() {
95		if c.Short == "" {
96			continue
97		}
98		if c.Hidden {
99			continue
100		}
101
102		s := rpad(c.Name()+":", c.NamePadding()) + c.Short
103		if _, ok := c.Annotations["IsCore"]; ok {
104			coreCommands = append(coreCommands, s)
105		} else if _, ok := c.Annotations["IsActions"]; ok {
106			actionsCommands = append(actionsCommands, s)
107		} else {
108			additionalCommands = append(additionalCommands, s)
109		}
110	}
111
112	// If there are no core commands, assume everything is a core command
113	if len(coreCommands) == 0 {
114		coreCommands = additionalCommands
115		additionalCommands = []string{}
116	}
117
118	type helpEntry struct {
119		Title string
120		Body  string
121	}
122
123	longText := command.Long
124	if longText == "" {
125		longText = command.Short
126	}
127	if longText != "" && command.LocalFlags().Lookup("jq") != nil {
128		longText = strings.TrimRight(longText, "\n") +
129			"\n\nFor more information about output formatting flags, see `gh help formatting`."
130	}
131
132	helpEntries := []helpEntry{}
133	if longText != "" {
134		helpEntries = append(helpEntries, helpEntry{"", longText})
135	}
136	helpEntries = append(helpEntries, helpEntry{"USAGE", command.UseLine()})
137	if len(coreCommands) > 0 {
138		helpEntries = append(helpEntries, helpEntry{"CORE COMMANDS", strings.Join(coreCommands, "\n")})
139	}
140	if len(actionsCommands) > 0 {
141		helpEntries = append(helpEntries, helpEntry{"ACTIONS COMMANDS", strings.Join(actionsCommands, "\n")})
142	}
143	if len(additionalCommands) > 0 {
144		helpEntries = append(helpEntries, helpEntry{"ADDITIONAL COMMANDS", strings.Join(additionalCommands, "\n")})
145	}
146
147	if isRootCmd(command) {
148		if exts := f.ExtensionManager.List(false); len(exts) > 0 {
149			var names []string
150			for _, ext := range exts {
151				names = append(names, ext.Name())
152			}
153			helpEntries = append(helpEntries, helpEntry{"EXTENSION COMMANDS", strings.Join(names, "\n")})
154		}
155	}
156
157	flagUsages := command.LocalFlags().FlagUsages()
158	if flagUsages != "" {
159		helpEntries = append(helpEntries, helpEntry{"FLAGS", dedent(flagUsages)})
160	}
161	inheritedFlagUsages := command.InheritedFlags().FlagUsages()
162	if inheritedFlagUsages != "" {
163		helpEntries = append(helpEntries, helpEntry{"INHERITED FLAGS", dedent(inheritedFlagUsages)})
164	}
165	if _, ok := command.Annotations["help:arguments"]; ok {
166		helpEntries = append(helpEntries, helpEntry{"ARGUMENTS", command.Annotations["help:arguments"]})
167	}
168	if command.Example != "" {
169		helpEntries = append(helpEntries, helpEntry{"EXAMPLES", command.Example})
170	}
171	if _, ok := command.Annotations["help:environment"]; ok {
172		helpEntries = append(helpEntries, helpEntry{"ENVIRONMENT VARIABLES", command.Annotations["help:environment"]})
173	}
174	helpEntries = append(helpEntries, helpEntry{"LEARN MORE", `
175Use 'gh <command> <subcommand> --help' for more information about a command.
176Read the manual at https://cli.github.com/manual`})
177	if _, ok := command.Annotations["help:feedback"]; ok {
178		helpEntries = append(helpEntries, helpEntry{"FEEDBACK", command.Annotations["help:feedback"]})
179	}
180
181	out := command.OutOrStdout()
182	for _, e := range helpEntries {
183		if e.Title != "" {
184			// If there is a title, add indentation to each line in the body
185			fmt.Fprintln(out, cs.Bold(e.Title))
186			fmt.Fprintln(out, text.Indent(strings.Trim(e.Body, "\r\n"), "  "))
187		} else {
188			// If there is no title print the body as is
189			fmt.Fprintln(out, e.Body)
190		}
191		fmt.Fprintln(out)
192	}
193}
194
195// rpad adds padding to the right of a string.
196func rpad(s string, padding int) string {
197	template := fmt.Sprintf("%%-%ds ", padding)
198	return fmt.Sprintf(template, s)
199}
200
201func dedent(s string) string {
202	lines := strings.Split(s, "\n")
203	minIndent := -1
204
205	for _, l := range lines {
206		if len(l) == 0 {
207			continue
208		}
209
210		indent := len(l) - len(strings.TrimLeft(l, " "))
211		if minIndent == -1 || indent < minIndent {
212			minIndent = indent
213		}
214	}
215
216	if minIndent <= 0 {
217		return s
218	}
219
220	var buf bytes.Buffer
221	for _, l := range lines {
222		fmt.Fprintln(&buf, strings.TrimPrefix(l, strings.Repeat(" ", minIndent)))
223	}
224	return strings.TrimSuffix(buf.String(), "\n")
225}
226