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  }