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  }