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