github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/root/help.go (about) 1 package root 2 3 import ( 4 "bytes" 5 "fmt" 6 "strings" 7 8 "github.com/cli/cli/pkg/cmdutil" 9 "github.com/cli/cli/pkg/text" 10 "github.com/spf13/cobra" 11 "github.com/spf13/pflag" 12 ) 13 14 func 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 37 func rootFlagErrorFunc(cmd *cobra.Command, err error) error { 38 if err == pflag.ErrHelp { 39 return err 40 } 41 return &cmdutil.FlagError{Err: err} 42 } 43 44 var hasFailed bool 45 46 // HasFailed signals that the main process should exit with non-zero status 47 func 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. 54 func 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 78 func isRootCmd(command *cobra.Command) bool { 79 return command != nil && !command.HasParent() 80 } 81 82 func 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", ` 175 Use 'gh <command> <subcommand> --help' for more information about a command. 176 Read 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. 196 func rpad(s string, padding int) string { 197 template := fmt.Sprintf("%%-%ds ", padding) 198 return fmt.Sprintf(template, s) 199 } 200 201 func 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 }