Merge pull request #8934 from babakks/8588-improve-run-list-doc
Improve `run list` doc with available `--json` fields
This commit is contained in:
commit
f11f096695
10 changed files with 284 additions and 0 deletions
|
|
@ -26,6 +26,8 @@ func init() {
|
|||
printCmd.Flags().IntP("intthree", "i", 345, "help message for flag intthree")
|
||||
printCmd.Flags().BoolP("boolthree", "b", true, "help message for flag boolthree")
|
||||
|
||||
jsonCmd.Flags().StringSlice("json", nil, "help message for flag json")
|
||||
|
||||
echoCmd.AddCommand(timesCmd, echoSubCmd, deprecatedCmd)
|
||||
rootCmd.AddCommand(printCmd, echoCmd, dummyCmd)
|
||||
}
|
||||
|
|
@ -73,6 +75,14 @@ var printCmd = &cobra.Command{
|
|||
Long: `an absolutely utterly useless command for testing.`,
|
||||
}
|
||||
|
||||
var jsonCmd = &cobra.Command{
|
||||
Use: "blah --json <fields>",
|
||||
Short: "View details in JSON",
|
||||
Annotations: map[string]string{
|
||||
"help:json-fields": "foo,bar,baz",
|
||||
},
|
||||
}
|
||||
|
||||
var dummyCmd = &cobra.Command{
|
||||
Use: "dummy [action]",
|
||||
Short: "Performs a dummy action",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmd/root"
|
||||
"github.com/cpuguy83/go-md2man/v2/md2man"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -178,6 +179,17 @@ func manPrintOptions(buf *bytes.Buffer, command *cobra.Command) {
|
|||
}
|
||||
}
|
||||
|
||||
func manPrintJSONFields(buf *bytes.Buffer, command *cobra.Command) {
|
||||
raw, ok := command.Annotations["help:json-fields"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
buf.WriteString("# JSON FIELDS\n")
|
||||
buf.WriteString(text.FormatSlice(strings.Split(raw, ","), 0, 0, "`", "`", true))
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
func genMan(cmd *cobra.Command, header *GenManHeader) []byte {
|
||||
cmd.InitDefaultHelpCmd()
|
||||
cmd.InitDefaultHelpFlag()
|
||||
|
|
@ -195,6 +207,7 @@ func genMan(cmd *cobra.Command, header *GenManHeader) []byte {
|
|||
}
|
||||
}
|
||||
manPrintOptions(buf, cmd)
|
||||
manPrintJSONFields(buf, cmd)
|
||||
if len(cmd.Example) > 0 {
|
||||
buf.WriteString("# EXAMPLE\n")
|
||||
buf.WriteString(fmt.Sprintf("```\n%s\n```\n", cmd.Example))
|
||||
|
|
|
|||
|
|
@ -98,6 +98,22 @@ func TestGenManSeeAlso(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGenManJSONFields(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
header := &GenManHeader{}
|
||||
if err := renderMan(jsonCmd, header, buf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
output := buf.String()
|
||||
|
||||
checkStringContains(t, output, translate(jsonCmd.Name()))
|
||||
checkStringContains(t, output, "JSON FIELDS")
|
||||
checkStringContains(t, output, "foo")
|
||||
checkStringContains(t, output, "bar")
|
||||
checkStringContains(t, output, "baz")
|
||||
}
|
||||
|
||||
func TestManPrintFlagsHidesShortDeprecated(t *testing.T) {
|
||||
c := &cobra.Command{}
|
||||
c.Flags().StringP("foo", "f", "default", "Foo flag")
|
||||
|
|
|
|||
|
|
@ -8,12 +8,24 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmd/root"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func printJSONFields(w io.Writer, cmd *cobra.Command) {
|
||||
raw, ok := cmd.Annotations["help:json-fields"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprint(w, "### JSON Fields\n\n")
|
||||
fmt.Fprint(w, text.FormatSlice(strings.Split(raw, ","), 0, 0, "`", "`", true))
|
||||
fmt.Fprint(w, "\n\n")
|
||||
}
|
||||
|
||||
func printOptions(w io.Writer, cmd *cobra.Command) error {
|
||||
flags := cmd.NonInheritedFlags()
|
||||
flags.SetOutput(w)
|
||||
|
|
@ -135,6 +147,7 @@ func genMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string)
|
|||
if err := printOptions(w, cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
printJSONFields(w, cmd)
|
||||
fmt.Fprint(w, "{% endraw %}\n")
|
||||
|
||||
if len(cmd.Example) > 0 {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,21 @@ func TestGenMdNoHiddenParents(t *testing.T) {
|
|||
checkStringOmits(t, output, "Options inherited from parent commands")
|
||||
}
|
||||
|
||||
func TestGenMdJSONFields(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := genMarkdownCustom(jsonCmd, buf, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := buf.String()
|
||||
|
||||
checkStringContains(t, output, jsonCmd.Long)
|
||||
checkStringContains(t, output, jsonCmd.Example)
|
||||
checkStringContains(t, output, "JSON Fields")
|
||||
checkStringContains(t, output, "`foo`")
|
||||
checkStringContains(t, output, "`bar`")
|
||||
checkStringContains(t, output, "`baz`")
|
||||
}
|
||||
|
||||
func TestGenMdTree(t *testing.T) {
|
||||
c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"}
|
||||
tmpdir, err := os.MkdirTemp("", "test-gen-md-tree")
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ package text
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -81,3 +83,64 @@ func RemoveDiacritics(value string) string {
|
|||
func PadRight(maxWidth int, s string) string {
|
||||
return text.PadRight(maxWidth, s)
|
||||
}
|
||||
|
||||
// FormatSlice concatenates elements of the given string slice into a
|
||||
// well-formatted, possibly multiline, string with specific line length limit.
|
||||
// Elements can be optionally surrounded by custom strings (e.g., quotes or
|
||||
// brackets). If the lineLength argument is non-positive, no line length limit
|
||||
// will be applied.
|
||||
func FormatSlice(values []string, lineLength uint, indent uint, prependWith string, appendWith string, sort bool) string {
|
||||
if lineLength <= 0 {
|
||||
lineLength = math.MaxInt
|
||||
}
|
||||
|
||||
sortedValues := values
|
||||
if sort {
|
||||
sortedValues = slices.Clone(values)
|
||||
slices.Sort(sortedValues)
|
||||
}
|
||||
|
||||
pre := strings.Repeat(" ", int(indent))
|
||||
if len(sortedValues) == 0 {
|
||||
return pre
|
||||
} else if len(sortedValues) == 1 {
|
||||
return pre + sortedValues[0]
|
||||
}
|
||||
|
||||
builder := strings.Builder{}
|
||||
currentLineLength := 0
|
||||
sep := ","
|
||||
ws := " "
|
||||
|
||||
for i := 0; i < len(sortedValues); i++ {
|
||||
v := prependWith + sortedValues[i] + appendWith
|
||||
isLast := i == -1+len(sortedValues)
|
||||
|
||||
if currentLineLength == 0 {
|
||||
builder.WriteString(pre)
|
||||
builder.WriteString(v)
|
||||
currentLineLength += len(v)
|
||||
if !isLast {
|
||||
builder.WriteString(sep)
|
||||
currentLineLength += len(sep)
|
||||
}
|
||||
} else {
|
||||
if !isLast && currentLineLength+len(ws)+len(v)+len(sep) > int(lineLength) ||
|
||||
isLast && currentLineLength+len(ws)+len(v) > int(lineLength) {
|
||||
currentLineLength = 0
|
||||
builder.WriteString("\n")
|
||||
i--
|
||||
continue
|
||||
}
|
||||
|
||||
builder.WriteString(ws)
|
||||
builder.WriteString(v)
|
||||
currentLineLength += len(ws) + len(v)
|
||||
if !isLast {
|
||||
builder.WriteString(sep)
|
||||
currentLineLength += len(sep)
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,3 +54,95 @@ func TestFuzzyAgoAbbr(t *testing.T) {
|
|||
assert.Equal(t, expected, fuzzy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSlice(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
values []string
|
||||
indent uint
|
||||
lineLength uint
|
||||
prependWith string
|
||||
appendWith string
|
||||
sort bool
|
||||
wants string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
lineLength: 10,
|
||||
values: []string{},
|
||||
wants: "",
|
||||
},
|
||||
{
|
||||
name: "empty with indent",
|
||||
lineLength: 10,
|
||||
indent: 2,
|
||||
values: []string{},
|
||||
wants: " ",
|
||||
},
|
||||
{
|
||||
name: "single",
|
||||
lineLength: 10,
|
||||
values: []string{"foo"},
|
||||
wants: "foo",
|
||||
},
|
||||
{
|
||||
name: "single with indent",
|
||||
lineLength: 10,
|
||||
indent: 2,
|
||||
values: []string{"foo"},
|
||||
wants: " foo",
|
||||
},
|
||||
{
|
||||
name: "long single with indent",
|
||||
lineLength: 10,
|
||||
indent: 2,
|
||||
values: []string{"some-long-value"},
|
||||
wants: " some-long-value",
|
||||
},
|
||||
{
|
||||
name: "exact line length",
|
||||
lineLength: 4,
|
||||
values: []string{"a", "b"},
|
||||
wants: "a, b",
|
||||
},
|
||||
{
|
||||
name: "values longer than line length",
|
||||
lineLength: 4,
|
||||
values: []string{"long-value", "long-value"},
|
||||
wants: "long-value,\nlong-value",
|
||||
},
|
||||
{
|
||||
name: "zero line length (no wrapping expected)",
|
||||
lineLength: 0,
|
||||
values: []string{"foo", "bar"},
|
||||
wants: "foo, bar",
|
||||
},
|
||||
{
|
||||
name: "simple",
|
||||
lineLength: 10,
|
||||
values: []string{"foo", "bar", "baz", "foo", "bar", "baz"},
|
||||
wants: "foo, bar,\nbaz, foo,\nbar, baz",
|
||||
},
|
||||
{
|
||||
name: "simple, surrounded",
|
||||
lineLength: 13,
|
||||
prependWith: "<",
|
||||
appendWith: ">",
|
||||
values: []string{"foo", "bar", "baz", "foo", "bar", "baz"},
|
||||
wants: "<foo>, <bar>,\n<baz>, <foo>,\n<bar>, <baz>",
|
||||
},
|
||||
{
|
||||
name: "sort",
|
||||
lineLength: 99,
|
||||
sort: true,
|
||||
values: []string{"c", "b", "a"},
|
||||
wants: "a, b, c",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.wants, FormatSlice(tt.values, tt.lineLength, tt.indent, tt.prependWith, tt.appendWith, tt.sort))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,6 +166,10 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) {
|
|||
if inheritedFlagUsages != "" {
|
||||
helpEntries = append(helpEntries, helpEntry{"INHERITED FLAGS", dedent(inheritedFlagUsages)})
|
||||
}
|
||||
if _, ok := command.Annotations["help:json-fields"]; ok {
|
||||
fields := strings.Split(command.Annotations["help:json-fields"], ",")
|
||||
helpEntries = append(helpEntries, helpEntry{"JSON FIELDS", text.FormatSlice(fields, 80, 0, "", "", true)})
|
||||
}
|
||||
if _, ok := command.Annotations["help:arguments"]; ok {
|
||||
helpEntries = append(helpEntries, helpEntry{"ARGUMENTS", command.Annotations["help:arguments"]})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,15 @@ func AddJSONFlags(cmd *cobra.Command, exportTarget *Exporter, fields []string) {
|
|||
}
|
||||
return e
|
||||
})
|
||||
|
||||
if len(fields) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if cmd.Annotations == nil {
|
||||
cmd.Annotations = map[string]string{}
|
||||
}
|
||||
cmd.Annotations["help:json-fields"] = strings.Join(fields, ",")
|
||||
}
|
||||
|
||||
func checkJSONFlags(cmd *cobra.Command) (*jsonExporter, error) {
|
||||
|
|
|
|||
|
|
@ -119,6 +119,55 @@ func TestAddJSONFlags(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestAddJSONFlagsSetsAnnotations asserts that `AddJSONFlags` function adds the
|
||||
// appropriate annotation to the command, which could later be used by doc
|
||||
// generator functions.
|
||||
func TestAddJSONFlagsSetsAnnotations(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cmd *cobra.Command
|
||||
jsonFields []string
|
||||
expectedAnnotations map[string]string
|
||||
}{
|
||||
{
|
||||
name: "empty set of fields",
|
||||
cmd: &cobra.Command{},
|
||||
jsonFields: []string{},
|
||||
expectedAnnotations: nil,
|
||||
},
|
||||
{
|
||||
name: "empty set of fields, with existing annotations",
|
||||
cmd: &cobra.Command{Annotations: map[string]string{"foo": "bar"}},
|
||||
jsonFields: []string{},
|
||||
expectedAnnotations: map[string]string{"foo": "bar"},
|
||||
},
|
||||
{
|
||||
name: "no other annotations",
|
||||
cmd: &cobra.Command{},
|
||||
jsonFields: []string{"few", "json", "fields"},
|
||||
expectedAnnotations: map[string]string{
|
||||
"help:json-fields": "few,json,fields",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with existing annotations (ensure no overwrite)",
|
||||
cmd: &cobra.Command{Annotations: map[string]string{"foo": "bar"}},
|
||||
jsonFields: []string{"few", "json", "fields"},
|
||||
expectedAnnotations: map[string]string{
|
||||
"foo": "bar",
|
||||
"help:json-fields": "few,json,fields",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
AddJSONFlags(tt.cmd, nil, tt.jsonFields)
|
||||
assert.Equal(t, tt.expectedAnnotations, tt.cmd.Annotations)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddFormatFlags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue