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