diff --git a/command/help.go b/command/help.go new file mode 100644 index 000000000..ad3561af2 --- /dev/null +++ b/command/help.go @@ -0,0 +1,119 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/cli/cli/utils" + "github.com/spf13/cobra" +) + +func rootHelpFunc(command *cobra.Command, args []string) { + // Display helpful error message in case subcommand name was mistyped. + // This matches Cobra's behavior for root command, which Cobra + // confusingly doesn't apply to nested commands. + if command != RootCmd { + if command.Parent() == RootCmd && len(args) >= 2 { + if command.SuggestionsMinimumDistance <= 0 { + command.SuggestionsMinimumDistance = 2 + } + candidates := command.SuggestionsFor(args[1]) + + errOut := command.OutOrStderr() + fmt.Fprintf(errOut, "unknown command %q for %q\n", args[1], "gh "+args[0]) + + if len(candidates) > 0 { + fmt.Fprint(errOut, "\nDid you mean this?\n") + for _, c := range candidates { + fmt.Fprintf(errOut, "\t%s\n", c) + } + fmt.Fprint(errOut, "\n") + } + + oldOut := command.OutOrStdout() + command.SetOut(errOut) + defer command.SetOut(oldOut) + } + } + + coreCommands := []string{} + additionalCommands := []string{} + for _, c := range command.Commands() { + if c.Short == "" { + continue + } + if c.Hidden { + continue + } + + s := rpad(c.Name()+":", c.NamePadding()) + c.Short + if _, ok := c.Annotations["IsCore"]; ok { + coreCommands = append(coreCommands, s) + } else { + additionalCommands = append(additionalCommands, s) + } + } + + // If there are no core commands, assume everything is a core command + if len(coreCommands) == 0 { + coreCommands = additionalCommands + additionalCommands = []string{} + } + + type helpEntry struct { + Title string + Body string + } + + helpEntries := []helpEntry{} + if command.Long != "" { + helpEntries = append(helpEntries, helpEntry{"", command.Long}) + } else if command.Short != "" { + helpEntries = append(helpEntries, helpEntry{"", command.Short}) + } + helpEntries = append(helpEntries, helpEntry{"USAGE", command.UseLine()}) + if len(coreCommands) > 0 { + helpEntries = append(helpEntries, helpEntry{"CORE COMMANDS", strings.Join(coreCommands, "\n")}) + } + if len(additionalCommands) > 0 { + helpEntries = append(helpEntries, helpEntry{"ADDITIONAL COMMANDS", strings.Join(additionalCommands, "\n")}) + } + if command.HasLocalFlags() { + helpEntries = append(helpEntries, helpEntry{"FLAGS", strings.TrimRight(command.LocalFlags().FlagUsages(), "\n")}) + } + if _, ok := command.Annotations["help:arguments"]; ok { + helpEntries = append(helpEntries, helpEntry{"ARGUMENTS", command.Annotations["help:arguments"]}) + } + if command.Example != "" { + helpEntries = append(helpEntries, helpEntry{"EXAMPLES", command.Example}) + } + helpEntries = append(helpEntries, helpEntry{"LEARN MORE", ` +Use "gh --help" for more information about a command. +Read the manual at http://cli.github.com/manual`}) + if _, ok := command.Annotations["help:feedback"]; ok { + helpEntries = append(helpEntries, helpEntry{"FEEDBACK", command.Annotations["help:feedback"]}) + } + + out := colorableOut(command) + for _, e := range helpEntries { + if e.Title != "" { + // If there is a title, add indentation to each line in the body + fmt.Fprintln(out, utils.Bold(e.Title)) + + for _, l := range strings.Split(strings.Trim(e.Body, "\n\r"), "\n") { + l = strings.Trim(l, " \n\r") + fmt.Fprintln(out, " "+l) + } + } else { + // If there is no title print the body as is + fmt.Fprintln(out, e.Body) + } + fmt.Fprintln(out) + } +} + +// rpad adds padding to the right of a string. +func rpad(s string, padding int) string { + template := fmt.Sprintf("%%-%ds ", padding) + return fmt.Sprintf(template, s) +} diff --git a/command/issue.go b/command/issue.go index 9a76fcf98..d630b340c 100644 --- a/command/issue.go +++ b/command/issue.go @@ -49,13 +49,17 @@ func init() { } var issueCmd = &cobra.Command{ - Use: "issue", + Use: "issue ", Short: "Create and view issues", - Long: `Work with GitHub issues. - -An issue can be supplied as argument in any of the following formats: + Long: `Work with GitHub issues`, + Example: `$ gh issue list +$ gh issue create --fill +$ gh issue view --web`, + Annotations: map[string]string{ + "IsCore": "true", + "help:arguments": `An issue can be supplied as argument in any of the following formats: - by number, e.g. "123"; or -- by URL, e.g. "https://github.com/OWNER/REPO/issues/123".`, +- by URL, e.g. "https://github.com/OWNER/REPO/issues/123".`}, } var issueCreateCmd = &cobra.Command{ Use: "create", diff --git a/command/pr.go b/command/pr.go index e92fa7cf0..dce006bf0 100644 --- a/command/pr.go +++ b/command/pr.go @@ -46,19 +46,27 @@ func init() { } var prCmd = &cobra.Command{ - Use: "pr", + Use: "pr ", Short: "Create, view, and checkout pull requests", - Long: `Work with GitHub pull requests. - -A pull request can be supplied as argument in any of the following formats: + Long: `Work with GitHub pull requests`, + Example: `$ gh pr checkout 353 +$ gh pr checkout bug-fix-branch +$ gh pr create --fill +$ gh pr view --web`, + Annotations: map[string]string{ + "IsCore": "true", + "help:arguments": `A pull request can be supplied as argument in any of the following formats: - by number, e.g. "123"; - by URL, e.g. "https://github.com/OWNER/REPO/pull/123"; or -- by the name of its head branch, e.g. "patch-1" or "OWNER:patch-1".`, +- by the name of its head branch, e.g. "patch-1" or "OWNER:patch-1".`}, } var prListCmd = &cobra.Command{ Use: "list", Short: "List and filter pull requests in this repository", - RunE: prList, + Example: `$ gh pr list --limit all +$ gh pr list --state closed +$ gh pr list --label “priority 1” “bug”`, + RunE: prList, } var prStatusCmd = &cobra.Command{ Use: "status", diff --git a/command/repo.go b/command/repo.go index b1a57d3a7..d62f0c715 100644 --- a/command/repo.go +++ b/command/repo.go @@ -44,13 +44,19 @@ func init() { } var repoCmd = &cobra.Command{ - Use: "repo", + Use: "repo ", Short: "Create, clone, fork, and view repositories", - Long: `Work with GitHub repositories. - + Long: `Work with GitHub repositories`, + Example: `$ gh repo create +$ gh repo clone cli/cli +$ gh repo view --web +`, + Annotations: map[string]string{ + "IsCore": "true", + "help:arguments": ` A repository can be supplied as an argument in any of the following formats: - "OWNER/REPO" -- by URL, e.g. "https://github.com/OWNER/REPO"`, +- by URL, e.g. "https://github.com/OWNER/REPO"`}, } var repoCloneCmd = &cobra.Command{ @@ -69,9 +75,18 @@ To pass 'git clone' flags, separate them with '--'.`, var repoCreateCmd = &cobra.Command{ Use: "create []", Short: "Create a new repository", - Long: `Create a new GitHub repository. + Long: `Create a new GitHub repository`, + Example: utils.Bold("$ gh repo create") + ` +Will create a repository on your account using the name of your current directory -Use the "ORG/NAME" syntax to create a repository within your organization.`, +` + utils.Bold("$ gh repo create my-project") + ` +Will create a repository on your account using the name 'my-project' + +` + utils.Bold("$ gh repo create cli/my-project") + ` +Will create a repository in the organization 'cli' using the name 'my-project'`, + Annotations: map[string]string{"help:arguments": `A repository can be supplied as an argument in any of the following formats: +- +- by URL, e.g. "https://github.com/OWNER/REPO"`}, RunE: repoCreate, } diff --git a/command/root.go b/command/root.go index 05e864eb7..12979ad5f 100644 --- a/command/root.go +++ b/command/root.go @@ -34,7 +34,6 @@ var Version = "DEV" var BuildDate = "" // YYYY-MM-DD var versionOutput = "" -var cobraDefaultHelpFunc func(*cobra.Command, []string) func init() { if Version == "DEV" { @@ -58,9 +57,11 @@ func init() { // TODO: // RootCmd.PersistentFlags().BoolP("verbose", "V", false, "enable verbose output") - cobraDefaultHelpFunc = RootCmd.HelpFunc() RootCmd.SetHelpFunc(rootHelpFunc) + // This will silence the usage func on error + RootCmd.SetUsageFunc(func(_ *cobra.Command) error { return nil }) + RootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { if err == pflag.ErrHelp { return err @@ -95,6 +96,13 @@ var RootCmd = &cobra.Command{ SilenceErrors: true, SilenceUsage: true, + Example: `$ gh issue create +$ gh repo clone +$ gh pr checkout 321`, + Annotations: map[string]string{ + "help:feedback": ` +Fill out our feedback form https://forms.gle/umxd3h31c7aMQFKG7 +Open an issue using “gh issue create -R cli/cli”`}, } var versionCmd = &cobra.Command{ @@ -320,100 +328,6 @@ func determineBaseRepo(apiClient *api.Client, cmd *cobra.Command, ctx context.Co return baseRepo, nil } -func rootHelpFunc(command *cobra.Command, args []string) { - if command != RootCmd { - // Display helpful error message in case subcommand name was mistyped. - // This matches Cobra's behavior for root command, which Cobra - // confusingly doesn't apply to nested commands. - if command.Parent() == RootCmd && len(args) >= 2 { - if command.SuggestionsMinimumDistance <= 0 { - command.SuggestionsMinimumDistance = 2 - } - candidates := command.SuggestionsFor(args[1]) - - errOut := command.OutOrStderr() - fmt.Fprintf(errOut, "unknown command %q for %q\n", args[1], "gh "+args[0]) - - if len(candidates) > 0 { - fmt.Fprint(errOut, "\nDid you mean this?\n") - for _, c := range candidates { - fmt.Fprintf(errOut, "\t%s\n", c) - } - fmt.Fprint(errOut, "\n") - } - - oldOut := command.OutOrStdout() - command.SetOut(errOut) - defer command.SetOut(oldOut) - } - cobraDefaultHelpFunc(command, args) - return - } - - type helpEntry struct { - Title string - Body string - } - - coreCommandNames := []string{"issue", "pr", "repo"} - var coreCommands []string - var additionalCommands []string - for _, c := range command.Commands() { - if c.Short == "" { - continue - } - s := " " + rpad(c.Name()+":", c.NamePadding()) + c.Short - if includes(coreCommandNames, c.Name()) { - coreCommands = append(coreCommands, s) - } else if !c.Hidden { - additionalCommands = append(additionalCommands, s) - } - } - - helpEntries := []helpEntry{ - { - "", - command.Long}, - {"USAGE", command.Use}, - {"CORE COMMANDS", strings.Join(coreCommands, "\n")}, - {"ADDITIONAL COMMANDS", strings.Join(additionalCommands, "\n")}, - {"FLAGS", strings.TrimRight(command.LocalFlags().FlagUsages(), "\n")}, - {"EXAMPLES", ` - $ gh issue create - $ gh repo clone - $ gh pr checkout 321`}, - {"LEARN MORE", ` - Use "gh --help" for more information about a command. - Read the manual at http://cli.github.com/manual`}, - {"FEEDBACK", ` - Fill out our feedback form https://forms.gle/umxd3h31c7aMQFKG7 - Open an issue using “gh issue create -R cli/cli”`}, - } - - out := colorableOut(command) - for _, e := range helpEntries { - if e.Title != "" { - fmt.Fprintln(out, utils.Bold(e.Title)) - } - fmt.Fprintln(out, strings.TrimLeft(e.Body, "\n")+"\n") - } -} - -// rpad adds padding to the right of a string. -func rpad(s string, padding int) string { - template := fmt.Sprintf("%%-%ds ", padding) - return fmt.Sprintf(template, s) -} - -func includes(a []string, s string) bool { - for _, x := range a { - if x == s { - return true - } - } - return false -} - func formatRemoteURL(cmd *cobra.Command, fullRepoName string) string { ctx := contextForCommand(cmd)