diff --git a/internal/docs/markdown.go b/internal/docs/markdown.go index 3432e9784..fc98b2810 100644 --- a/internal/docs/markdown.go +++ b/internal/docs/markdown.go @@ -1,35 +1,83 @@ package docs import ( - "bytes" "fmt" + "html/template" "io" "os" "path/filepath" "strings" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) -func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) error { +func printOptions(w io.Writer, cmd *cobra.Command) error { flags := cmd.NonInheritedFlags() - flags.SetOutput(buf) + flags.SetOutput(w) if flags.HasAvailableFlags() { - buf.WriteString("### Options\n\n```\n") - flags.PrintDefaults() - buf.WriteString("```\n\n") + fmt.Fprint(w, "### Options\n\n") + if err := printFlagsHTML(w, flags); err != nil { + return err + } + fmt.Fprint(w, "\n\n") } parentFlags := cmd.InheritedFlags() - parentFlags.SetOutput(buf) - if parentFlags.HasAvailableFlags() { - buf.WriteString("### Options inherited from parent commands\n\n```\n") - parentFlags.PrintDefaults() - buf.WriteString("```\n\n") + parentFlags.SetOutput(w) + if hasNonHelpFlags(parentFlags) { + fmt.Fprint(w, "### Options inherited from parent commands\n\n") + if err := printFlagsHTML(w, parentFlags); err != nil { + return err + } + fmt.Fprint(w, "\n\n") } return nil } +func hasNonHelpFlags(fs *pflag.FlagSet) (found bool) { + fs.VisitAll(func(f *pflag.Flag) { + if !f.Hidden && f.Name != "help" { + found = true + } + }) + return +} + +type flagView struct { + Name string + Varname string + Shorthand string + Usage string +} + +var flagsTemplate = ` +
{{ range . }} +
{{ if .Shorthand }}-{{.Shorthand}}, {{ end -}} + --{{.Name}}{{ if .Varname }} <{{.Varname}}>{{ end }}
+
{{.Usage}}
+{{ end }}
+` + +var tpl = template.Must(template.New("flags").Parse(flagsTemplate)) + +func printFlagsHTML(w io.Writer, fs *pflag.FlagSet) error { + var flags []flagView + fs.VisitAll(func(f *pflag.Flag) { + if f.Hidden || f.Name == "help" { + return + } + varname, usage := pflag.UnquoteUsage(f) + flags = append(flags, flagView{ + Name: f.Name, + Varname: varname, + Shorthand: f.Shorthand, + Usage: usage, + }) + }) + return tpl.Execute(w, flags) +} + // GenMarkdown creates markdown output. func GenMarkdown(cmd *cobra.Command, w io.Writer) error { return GenMarkdownCustom(cmd, w, func(s string) string { return s }) @@ -37,33 +85,97 @@ func GenMarkdown(cmd *cobra.Command, w io.Writer) error { // GenMarkdownCustom creates custom markdown output. func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error { - cmd.InitDefaultHelpCmd() - cmd.InitDefaultHelpFlag() + fmt.Fprintf(w, "## %s\n\n", cmd.CommandPath()) - buf := new(bytes.Buffer) - name := cmd.CommandPath() - - buf.WriteString("## " + name + "\n\n") - buf.WriteString(cmd.Short + "\n\n") - if len(cmd.Long) > 0 { - buf.WriteString("### Synopsis\n\n") - buf.WriteString(cmd.Long + "\n\n") + hasLong := cmd.Long != "" + if !hasLong { + fmt.Fprintf(w, "%s\n\n", cmd.Short) + } + if cmd.Runnable() { + fmt.Fprintf(w, "```\n%s\n```\n\n", cmd.UseLine()) + } + if hasLong { + fmt.Fprintf(w, "%s\n\n", cmd.Long) } - if cmd.Runnable() { - buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.UseLine())) + for _, g := range subcommandGroups(cmd) { + if len(g.Commands) == 0 { + continue + } + fmt.Fprintf(w, "### %s\n\n", g.Name) + for _, subcmd := range g.Commands { + fmt.Fprintf(w, "* [%s](%s)\n", subcmd.CommandPath(), linkHandler(cmdManualPath(subcmd))) + } + fmt.Fprint(w, "\n\n") + } + + if err := printOptions(w, cmd); err != nil { + return err } if len(cmd.Example) > 0 { - buf.WriteString("### Examples\n\n") - buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.Example)) + fmt.Fprint(w, "### Examples\n\n{% highlight bash %}{% raw %}\n") + fmt.Fprint(w, cmd.Example) + fmt.Fprint(w, "{% endraw %}{% endhighlight %}\n\n") } - if err := printOptions(buf, cmd, name); err != nil { - return err + if cmd.HasParent() { + p := cmd.Parent() + fmt.Fprint(w, "### See also\n\n") + fmt.Fprintf(w, "* [%s](%s)\n", p.CommandPath(), linkHandler(cmdManualPath(p))) + } + + return nil +} + +type commandGroup struct { + Name string + Commands []*cobra.Command +} + +// subcommandGroups lists child commands of a Cobra command split into groups. +// TODO: have rootHelpFunc use this instead of repeating the same logic. +func subcommandGroups(c *cobra.Command) []commandGroup { + var rest []*cobra.Command + var core []*cobra.Command + var actions []*cobra.Command + + for _, subcmd := range c.Commands() { + if !subcmd.IsAvailableCommand() { + continue + } + if _, ok := subcmd.Annotations["IsCore"]; ok { + core = append(core, subcmd) + } else if _, ok := subcmd.Annotations["IsActions"]; ok { + actions = append(actions, subcmd) + } else { + rest = append(rest, subcmd) + } + } + + if len(core) > 0 { + return []commandGroup{ + { + Name: "Core commands", + Commands: core, + }, + { + Name: "Actions commands", + Commands: actions, + }, + { + Name: "Additional commands", + Commands: rest, + }, + } + } + + return []commandGroup{ + { + Name: "Commands", + Commands: rest, + }, } - _, err := buf.WriteTo(w) - return err } // GenMarkdownTree will generate a markdown page for this command and all @@ -92,12 +204,7 @@ func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHa } } - basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + ".md" - if basenameOverride, found := cmd.Annotations["markdown:basename"]; found { - basename = basenameOverride + ".md" - } - - filename := filepath.Join(dir, basename) + filename := filepath.Join(dir, cmdManualPath(cmd)) f, err := os.Create(filename) if err != nil { return err @@ -112,3 +219,10 @@ func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHa } return nil } + +func cmdManualPath(c *cobra.Command) string { + if basenameOverride, found := c.Annotations["markdown:basename"]; found { + return basenameOverride + ".md" + } + return strings.ReplaceAll(c.CommandPath(), " ", "_") + ".md" +} diff --git a/pkg/cmd/actions/actions.go b/pkg/cmd/actions/actions.go index 1ce0dc233..e5620c42e 100644 --- a/pkg/cmd/actions/actions.go +++ b/pkg/cmd/actions/actions.go @@ -1,8 +1,6 @@ package actions import ( - "fmt" - "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -14,11 +12,8 @@ func NewCmdActions(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "actions", - Short: "Learn about working with GitHub actions", + Short: "Learn about working with GitHub Actions", Long: actionsExplainer(cs), - Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintln(f.IOStreams.Out, actionsExplainer(cs)) - }, Annotations: map[string]string{ "IsActions": "true", }, diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index 6b813a8f1..74bb9c7a5 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -12,6 +12,7 @@ import ( "path/filepath" "strings" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/codespaces" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/liveshare" @@ -156,36 +157,36 @@ func newCpCmd(app *App) *cobra.Command { var opts cpOptions cpCmd := &cobra.Command{ - Use: "cp [-e] [-r] srcs... dest", + Use: "cp [-e] [-r] ... ", Short: "Copy files between local and remote file systems", - Long: ` -The cp command copies files between the local and remote file systems. + Long: heredoc.Docf(` + The cp command copies files between the local and remote file systems. -As with the UNIX cp command, the first argument specifies the source and the last -specifies the destination; additional sources may be specified after the first, -if the destination is a directory. + As with the UNIX %[1]scp%[1]s command, the first argument specifies the source and the last + specifies the destination; additional sources may be specified after the first, + if the destination is a directory. -The -r (recursive) flag is required if any source is a directory. + The %[1]s--recursive%[1]s flag is required if any source is a directory. -A 'remote:' prefix on any file name argument indicates that it refers to -the file system of the remote (Codespace) machine. It is resolved relative -to the home directory of the remote user. + A "remote:" prefix on any file name argument indicates that it refers to + the file system of the remote (Codespace) machine. It is resolved relative + to the home directory of the remote user. -By default, remote file names are interpreted literally. With the -e flag, -each such argument is treated in the manner of scp, as a Bash expression to -be evaluated on the remote machine, subject to expansion of tildes, braces, -globs, environment variables, and backticks, as in these examples: - - $ gh codespace cp -e README.md 'remote:/workspace/$RepositoryName/' - $ gh codespace cp -e 'remote:~/*.go' ./gofiles/ - $ gh codespace cp -e 'remote:/workspace/myproj/go.{mod,sum}' ./gofiles/ - -For security, do not use the -e flag with arguments provided by untrusted -users; see https://lwn.net/Articles/835962/ for discussion. -`, + By default, remote file names are interpreted literally. With the %[1]s--expand%[1]s flag, + each such argument is treated in the manner of %[1]sscp%[1]s, as a Bash expression to + be evaluated on the remote machine, subject to expansion of tildes, braces, globs, + environment variables, and backticks. For security, do not use this flag with arguments + provided by untrusted users; see for discussion. + `, "`"), + Example: heredoc.Doc(` + $ gh codespace cp -e README.md 'remote:/workspace/$RepositoryName/' + $ gh codespace cp -e 'remote:~/*.go' ./gofiles/ + $ gh codespace cp -e 'remote:/workspace/myproj/go.{mod,sum}' ./gofiles/ + `), RunE: func(cmd *cobra.Command, args []string) error { return app.Copy(cmd.Context(), args, opts) }, + DisableFlagsInUseLine: true, } // We don't expose all sshOptions. diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index f94f577da..c7d9bffae 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -54,7 +54,7 @@ var HelpTopics = map[string]map[string]string{ to, e.g. "less". GLAMOUR_STYLE: the style to use for rendering Markdown. See - https://github.com/charmbracelet/glamour#styles + NO_COLOR: set to any value to avoid printing ANSI escape sequences for color output. @@ -90,14 +90,14 @@ var HelpTopics = map[string]map[string]string{ The %[1]s--jq%[1]s option accepts a query in jq syntax and will print only the resulting values that match the query. This is equivalent to piping the output to %[1]sjq -r%[1]s, but does not require the jq utility to be installed on the system. To learn more - about the query syntax, see: https://stedolan.github.io/jq/manual/v1.6/ + about the query syntax, see: With %[1]s--template%[1]s, the provided Go template is rendered using the JSON data as input. - For the syntax of Go templates, see: https://golang.org/pkg/text/template/ + For the syntax of Go templates, see: The following functions are available in templates: - %[1]sautocolor%[1]s: like %[1]scolor%[1]s, but only emits color to terminals - - %[1]scolor