diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 17:15:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 17:15:09 +0000 |
commit | ea9f089f6ad153f432b8265a99bce7d261b9c820 (patch) | |
tree | 8590b318bb54874bf1f0c597ff503f8c3b9ace75 /doc | |
parent | Initial commit. (diff) | |
download | golang-github-spf13-cobra-ea9f089f6ad153f432b8265a99bce7d261b9c820.tar.xz golang-github-spf13-cobra-ea9f089f6ad153f432b8265a99bce7d261b9c820.zip |
Adding upstream version 1.8.0.upstream/1.8.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'doc')
-rw-r--r-- | doc/cmd_test.go | 105 | ||||
-rw-r--r-- | doc/man_docs.go | 246 | ||||
-rw-r--r-- | doc/man_docs_test.go | 235 | ||||
-rw-r--r-- | doc/man_examples_test.go | 49 | ||||
-rw-r--r-- | doc/md_docs.go | 158 | ||||
-rw-r--r-- | doc/md_docs_test.go | 126 | ||||
-rw-r--r-- | doc/rest_docs.go | 186 | ||||
-rw-r--r-- | doc/rest_docs_test.go | 113 | ||||
-rw-r--r-- | doc/util.go | 52 | ||||
-rw-r--r-- | doc/yaml_docs.go | 175 | ||||
-rw-r--r-- | doc/yaml_docs_test.go | 101 |
11 files changed, 1546 insertions, 0 deletions
diff --git a/doc/cmd_test.go b/doc/cmd_test.go new file mode 100644 index 0000000..0d022c7 --- /dev/null +++ b/doc/cmd_test.go @@ -0,0 +1,105 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package doc + +import ( + "strings" + "testing" + + "github.com/spf13/cobra" +) + +func emptyRun(*cobra.Command, []string) {} + +func init() { + rootCmd.PersistentFlags().StringP("rootflag", "r", "two", "") + rootCmd.PersistentFlags().StringP("strtwo", "t", "two", "help message for parent flag strtwo") + + echoCmd.PersistentFlags().StringP("strone", "s", "one", "help message for flag strone") + echoCmd.PersistentFlags().BoolP("persistentbool", "p", false, "help message for flag persistentbool") + echoCmd.Flags().IntP("intone", "i", 123, "help message for flag intone") + echoCmd.Flags().BoolP("boolone", "b", true, "help message for flag boolone") + + timesCmd.PersistentFlags().StringP("strtwo", "t", "2", "help message for child flag strtwo") + timesCmd.Flags().IntP("inttwo", "j", 234, "help message for flag inttwo") + timesCmd.Flags().BoolP("booltwo", "c", false, "help message for flag booltwo") + + printCmd.PersistentFlags().StringP("strthree", "s", "three", "help message for flag strthree") + printCmd.Flags().IntP("intthree", "i", 345, "help message for flag intthree") + printCmd.Flags().BoolP("boolthree", "b", true, "help message for flag boolthree") + + echoCmd.AddCommand(timesCmd, echoSubCmd, deprecatedCmd) + rootCmd.AddCommand(printCmd, echoCmd, dummyCmd) +} + +var rootCmd = &cobra.Command{ + Use: "root", + Short: "Root short description", + Long: "Root long description", + Run: emptyRun, +} + +var echoCmd = &cobra.Command{ + Use: "echo [string to echo]", + Aliases: []string{"say"}, + Short: "Echo anything to the screen", + Long: "an utterly useless command for testing", + Example: "Just run cobra-test echo", +} + +var echoSubCmd = &cobra.Command{ + Use: "echosub [string to print]", + Short: "second sub command for echo", + Long: "an absolutely utterly useless command for testing gendocs!.", + Run: emptyRun, +} + +var timesCmd = &cobra.Command{ + Use: "times [# times] [string to echo]", + SuggestFor: []string{"counts"}, + Short: "Echo anything to the screen more times", + Long: `a slightly useless command for testing.`, + Run: emptyRun, +} + +var deprecatedCmd = &cobra.Command{ + Use: "deprecated [can't do anything here]", + Short: "A command which is deprecated", + Long: `an absolutely utterly useless command for testing deprecation!.`, + Deprecated: "Please use echo instead", +} + +var printCmd = &cobra.Command{ + Use: "print [string to print]", + Short: "Print anything to the screen", + Long: `an absolutely utterly useless command for testing.`, +} + +var dummyCmd = &cobra.Command{ + Use: "dummy [action]", + Short: "Performs a dummy action", +} + +func checkStringContains(t *testing.T, got, expected string) { + if !strings.Contains(got, expected) { + t.Errorf("Expected to contain: \n %v\nGot:\n %v\n", expected, got) + } +} + +func checkStringOmits(t *testing.T, got, expected string) { + if strings.Contains(got, expected) { + t.Errorf("Expected to not contain: \n %v\nGot: %v", expected, got) + } +} diff --git a/doc/man_docs.go b/doc/man_docs.go new file mode 100644 index 0000000..b8c15ce --- /dev/null +++ b/doc/man_docs.go @@ -0,0 +1,246 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package doc + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "time" + + "github.com/cpuguy83/go-md2man/v2/md2man" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// GenManTree will generate a man page for this command and all descendants +// in the directory given. The header may be nil. This function may not work +// correctly if your command names have `-` in them. If you have `cmd` with two +// subcmds, `sub` and `sub-third`, and `sub` has a subcommand called `third` +// it is undefined which help output will be in the file `cmd-sub-third.1`. +func GenManTree(cmd *cobra.Command, header *GenManHeader, dir string) error { + return GenManTreeFromOpts(cmd, GenManTreeOptions{ + Header: header, + Path: dir, + CommandSeparator: "-", + }) +} + +// GenManTreeFromOpts generates a man page for the command and all descendants. +// The pages are written to the opts.Path directory. +func GenManTreeFromOpts(cmd *cobra.Command, opts GenManTreeOptions) error { + header := opts.Header + if header == nil { + header = &GenManHeader{} + } + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { + continue + } + if err := GenManTreeFromOpts(c, opts); err != nil { + return err + } + } + section := "1" + if header.Section != "" { + section = header.Section + } + + separator := "_" + if opts.CommandSeparator != "" { + separator = opts.CommandSeparator + } + basename := strings.ReplaceAll(cmd.CommandPath(), " ", separator) + filename := filepath.Join(opts.Path, basename+"."+section) + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + headerCopy := *header + return GenMan(cmd, &headerCopy, f) +} + +// GenManTreeOptions is the options for generating the man pages. +// Used only in GenManTreeFromOpts. +type GenManTreeOptions struct { + Header *GenManHeader + Path string + CommandSeparator string +} + +// GenManHeader is a lot like the .TH header at the start of man pages. These +// include the title, section, date, source, and manual. We will use the +// current time if Date is unset and will use "Auto generated by spf13/cobra" +// if the Source is unset. +type GenManHeader struct { + Title string + Section string + Date *time.Time + date string + Source string + Manual string +} + +// GenMan will generate a man page for the given command and write it to +// w. The header argument may be nil, however obviously w may not. +func GenMan(cmd *cobra.Command, header *GenManHeader, w io.Writer) error { + if header == nil { + header = &GenManHeader{} + } + if err := fillHeader(header, cmd.CommandPath(), cmd.DisableAutoGenTag); err != nil { + return err + } + + b := genMan(cmd, header) + _, err := w.Write(md2man.Render(b)) + return err +} + +func fillHeader(header *GenManHeader, name string, disableAutoGen bool) error { + if header.Title == "" { + header.Title = strings.ToUpper(strings.ReplaceAll(name, " ", "\\-")) + } + if header.Section == "" { + header.Section = "1" + } + if header.Date == nil { + now := time.Now() + if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" { + unixEpoch, err := strconv.ParseInt(epoch, 10, 64) + if err != nil { + return fmt.Errorf("invalid SOURCE_DATE_EPOCH: %v", err) + } + now = time.Unix(unixEpoch, 0) + } + header.Date = &now + } + header.date = (*header.Date).Format("Jan 2006") + if header.Source == "" && !disableAutoGen { + header.Source = "Auto generated by spf13/cobra" + } + return nil +} + +func manPreamble(buf io.StringWriter, header *GenManHeader, cmd *cobra.Command, dashedName string) { + description := cmd.Long + if len(description) == 0 { + description = cmd.Short + } + + cobra.WriteStringAndCheck(buf, fmt.Sprintf(`%% "%s" "%s" "%s" "%s" "%s" +# NAME +`, header.Title, header.Section, header.date, header.Source, header.Manual)) + cobra.WriteStringAndCheck(buf, fmt.Sprintf("%s \\- %s\n\n", dashedName, cmd.Short)) + cobra.WriteStringAndCheck(buf, "# SYNOPSIS\n") + cobra.WriteStringAndCheck(buf, fmt.Sprintf("**%s**\n\n", cmd.UseLine())) + cobra.WriteStringAndCheck(buf, "# DESCRIPTION\n") + cobra.WriteStringAndCheck(buf, description+"\n\n") +} + +func manPrintFlags(buf io.StringWriter, flags *pflag.FlagSet) { + flags.VisitAll(func(flag *pflag.Flag) { + if len(flag.Deprecated) > 0 || flag.Hidden { + return + } + format := "" + if len(flag.Shorthand) > 0 && len(flag.ShorthandDeprecated) == 0 { + format = fmt.Sprintf("**-%s**, **--%s**", flag.Shorthand, flag.Name) + } else { + format = fmt.Sprintf("**--%s**", flag.Name) + } + if len(flag.NoOptDefVal) > 0 { + format += "[" + } + if flag.Value.Type() == "string" { + // put quotes on the value + format += "=%q" + } else { + format += "=%s" + } + if len(flag.NoOptDefVal) > 0 { + format += "]" + } + format += "\n\t%s\n\n" + cobra.WriteStringAndCheck(buf, fmt.Sprintf(format, flag.DefValue, flag.Usage)) + }) +} + +func manPrintOptions(buf io.StringWriter, command *cobra.Command) { + flags := command.NonInheritedFlags() + if flags.HasAvailableFlags() { + cobra.WriteStringAndCheck(buf, "# OPTIONS\n") + manPrintFlags(buf, flags) + cobra.WriteStringAndCheck(buf, "\n") + } + flags = command.InheritedFlags() + if flags.HasAvailableFlags() { + cobra.WriteStringAndCheck(buf, "# OPTIONS INHERITED FROM PARENT COMMANDS\n") + manPrintFlags(buf, flags) + cobra.WriteStringAndCheck(buf, "\n") + } +} + +func genMan(cmd *cobra.Command, header *GenManHeader) []byte { + cmd.InitDefaultHelpCmd() + cmd.InitDefaultHelpFlag() + + // something like `rootcmd-subcmd1-subcmd2` + dashCommandName := strings.ReplaceAll(cmd.CommandPath(), " ", "-") + + buf := new(bytes.Buffer) + + manPreamble(buf, header, cmd, dashCommandName) + manPrintOptions(buf, cmd) + if len(cmd.Example) > 0 { + buf.WriteString("# EXAMPLE\n") + buf.WriteString(fmt.Sprintf("```\n%s\n```\n", cmd.Example)) + } + if hasSeeAlso(cmd) { + buf.WriteString("# SEE ALSO\n") + seealsos := make([]string, 0) + if cmd.HasParent() { + parentPath := cmd.Parent().CommandPath() + dashParentPath := strings.ReplaceAll(parentPath, " ", "-") + seealso := fmt.Sprintf("**%s(%s)**", dashParentPath, header.Section) + seealsos = append(seealsos, seealso) + cmd.VisitParents(func(c *cobra.Command) { + if c.DisableAutoGenTag { + cmd.DisableAutoGenTag = c.DisableAutoGenTag + } + }) + } + children := cmd.Commands() + sort.Sort(byName(children)) + for _, c := range children { + if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { + continue + } + seealso := fmt.Sprintf("**%s-%s(%s)**", dashCommandName, c.Name(), header.Section) + seealsos = append(seealsos, seealso) + } + buf.WriteString(strings.Join(seealsos, ", ") + "\n") + } + if !cmd.DisableAutoGenTag { + buf.WriteString(fmt.Sprintf("# HISTORY\n%s Auto generated by spf13/cobra\n", header.Date.Format("2-Jan-2006"))) + } + return buf.Bytes() +} diff --git a/doc/man_docs_test.go b/doc/man_docs_test.go new file mode 100644 index 0000000..a4435e6 --- /dev/null +++ b/doc/man_docs_test.go @@ -0,0 +1,235 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package doc + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/spf13/cobra" +) + +func assertNoErr(t *testing.T, e error) { + if e != nil { + t.Error(e) + } +} + +func translate(in string) string { + return strings.ReplaceAll(in, "-", "\\-") +} + +func TestGenManDoc(t *testing.T) { + header := &GenManHeader{ + Title: "Project", + Section: "2", + } + + // We generate on a subcommand so we have both subcommands and parents + buf := new(bytes.Buffer) + if err := GenMan(echoCmd, header, buf); err != nil { + t.Fatal(err) + } + output := buf.String() + + // Make sure parent has - in CommandPath() in SEE ALSO: + parentPath := echoCmd.Parent().CommandPath() + dashParentPath := strings.ReplaceAll(parentPath, " ", "-") + expected := translate(dashParentPath) + expected = expected + "(" + header.Section + ")" + checkStringContains(t, output, expected) + + checkStringContains(t, output, translate(echoCmd.Name())) + checkStringContains(t, output, translate(echoCmd.Name())) + checkStringContains(t, output, "boolone") + checkStringContains(t, output, "rootflag") + checkStringContains(t, output, translate(rootCmd.Name())) + checkStringContains(t, output, translate(echoSubCmd.Name())) + checkStringOmits(t, output, translate(deprecatedCmd.Name())) + checkStringContains(t, output, translate("Auto generated")) +} + +func TestGenManNoHiddenParents(t *testing.T) { + header := &GenManHeader{ + Title: "Project", + Section: "2", + } + + // We generate on a subcommand so we have both subcommands and parents + for _, name := range []string{"rootflag", "strtwo"} { + f := rootCmd.PersistentFlags().Lookup(name) + f.Hidden = true + defer func() { f.Hidden = false }() + } + buf := new(bytes.Buffer) + if err := GenMan(echoCmd, header, buf); err != nil { + t.Fatal(err) + } + output := buf.String() + + // Make sure parent has - in CommandPath() in SEE ALSO: + parentPath := echoCmd.Parent().CommandPath() + dashParentPath := strings.ReplaceAll(parentPath, " ", "-") + expected := translate(dashParentPath) + expected = expected + "(" + header.Section + ")" + checkStringContains(t, output, expected) + + checkStringContains(t, output, translate(echoCmd.Name())) + checkStringContains(t, output, translate(echoCmd.Name())) + checkStringContains(t, output, "boolone") + checkStringOmits(t, output, "rootflag") + checkStringContains(t, output, translate(rootCmd.Name())) + checkStringContains(t, output, translate(echoSubCmd.Name())) + checkStringOmits(t, output, translate(deprecatedCmd.Name())) + checkStringContains(t, output, translate("Auto generated")) + checkStringOmits(t, output, "OPTIONS INHERITED FROM PARENT COMMANDS") +} + +func TestGenManNoGenTag(t *testing.T) { + echoCmd.DisableAutoGenTag = true + defer func() { echoCmd.DisableAutoGenTag = false }() + + header := &GenManHeader{ + Title: "Project", + Section: "2", + } + + // We generate on a subcommand so we have both subcommands and parents + buf := new(bytes.Buffer) + if err := GenMan(echoCmd, header, buf); err != nil { + t.Fatal(err) + } + output := buf.String() + + unexpected := translate("#HISTORY") + checkStringOmits(t, output, unexpected) + unexpected = translate("Auto generated by spf13/cobra") + checkStringOmits(t, output, unexpected) +} + +func TestGenManSeeAlso(t *testing.T) { + rootCmd := &cobra.Command{Use: "root", Run: emptyRun} + aCmd := &cobra.Command{Use: "aaa", Run: emptyRun, Hidden: true} // #229 + bCmd := &cobra.Command{Use: "bbb", Run: emptyRun} + cCmd := &cobra.Command{Use: "ccc", Run: emptyRun} + rootCmd.AddCommand(aCmd, bCmd, cCmd) + + buf := new(bytes.Buffer) + header := &GenManHeader{} + if err := GenMan(rootCmd, header, buf); err != nil { + t.Fatal(err) + } + scanner := bufio.NewScanner(buf) + + if err := assertLineFound(scanner, ".SH SEE ALSO"); err != nil { + t.Fatalf("Couldn't find SEE ALSO section header: %v", err) + } + if err := assertNextLineEquals(scanner, ".PP"); err != nil { + t.Fatalf("First line after SEE ALSO wasn't break-indent: %v", err) + } + if err := assertNextLineEquals(scanner, `\fBroot-bbb(1)\fP, \fBroot-ccc(1)\fP`); err != nil { + t.Fatalf("Second line after SEE ALSO wasn't correct: %v", err) + } +} + +func TestManPrintFlagsHidesShortDeprecated(t *testing.T) { + c := &cobra.Command{} + c.Flags().StringP("foo", "f", "default", "Foo flag") + assertNoErr(t, c.Flags().MarkShorthandDeprecated("foo", "don't use it no more")) + + buf := new(bytes.Buffer) + manPrintFlags(buf, c.Flags()) + + got := buf.String() + expected := "**--foo**=\"default\"\n\tFoo flag\n\n" + if got != expected { + t.Errorf("Expected %v, got %v", expected, got) + } +} + +func TestGenManTree(t *testing.T) { + c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"} + header := &GenManHeader{Section: "2"} + tmpdir, err := ioutil.TempDir("", "test-gen-man-tree") + if err != nil { + t.Fatalf("Failed to create tmpdir: %s", err.Error()) + } + defer os.RemoveAll(tmpdir) + + if err := GenManTree(c, header, tmpdir); err != nil { + t.Fatalf("GenManTree failed: %s", err.Error()) + } + + if _, err := os.Stat(filepath.Join(tmpdir, "do.2")); err != nil { + t.Fatalf("Expected file 'do.2' to exist") + } + + if header.Title != "" { + t.Fatalf("Expected header.Title to be unmodified") + } +} + +func assertLineFound(scanner *bufio.Scanner, expectedLine string) error { + for scanner.Scan() { + line := scanner.Text() + if line == expectedLine { + return nil + } + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("scan failed: %s", err) + } + + return fmt.Errorf("hit EOF before finding %v", expectedLine) +} + +func assertNextLineEquals(scanner *bufio.Scanner, expectedLine string) error { + if scanner.Scan() { + line := scanner.Text() + if line == expectedLine { + return nil + } + return fmt.Errorf("got %v, not %v", line, expectedLine) + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("scan failed: %v", err) + } + + return fmt.Errorf("hit EOF before finding %v", expectedLine) +} + +func BenchmarkGenManToFile(b *testing.B) { + file, err := ioutil.TempFile("", "") + if err != nil { + b.Fatal(err) + } + defer os.Remove(file.Name()) + defer file.Close() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := GenMan(rootCmd, nil, file); err != nil { + b.Fatal(err) + } + } +} diff --git a/doc/man_examples_test.go b/doc/man_examples_test.go new file mode 100644 index 0000000..873b2b6 --- /dev/null +++ b/doc/man_examples_test.go @@ -0,0 +1,49 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package doc_test + +import ( + "bytes" + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" +) + +func ExampleGenManTree() { + cmd := &cobra.Command{ + Use: "test", + Short: "my test program", + } + header := &doc.GenManHeader{ + Title: "MINE", + Section: "3", + } + cobra.CheckErr(doc.GenManTree(cmd, header, "/tmp")) +} + +func ExampleGenMan() { + cmd := &cobra.Command{ + Use: "test", + Short: "my test program", + } + header := &doc.GenManHeader{ + Title: "MINE", + Section: "3", + } + out := new(bytes.Buffer) + cobra.CheckErr(doc.GenMan(cmd, header, out)) + fmt.Print(out.String()) +} diff --git a/doc/md_docs.go b/doc/md_docs.go new file mode 100644 index 0000000..f98fe2a --- /dev/null +++ b/doc/md_docs.go @@ -0,0 +1,158 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package doc + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/spf13/cobra" +) + +const markdownExtension = ".md" + +func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) error { + flags := cmd.NonInheritedFlags() + flags.SetOutput(buf) + if flags.HasAvailableFlags() { + buf.WriteString("### Options\n\n```\n") + flags.PrintDefaults() + buf.WriteString("```\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") + } + return nil +} + +// GenMarkdown creates markdown output. +func GenMarkdown(cmd *cobra.Command, w io.Writer) error { + return GenMarkdownCustom(cmd, w, func(s string) string { return s }) +} + +// GenMarkdownCustom creates custom markdown output. +func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error { + cmd.InitDefaultHelpCmd() + cmd.InitDefaultHelpFlag() + + 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") + } + + if cmd.Runnable() { + buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.UseLine())) + } + + if len(cmd.Example) > 0 { + buf.WriteString("### Examples\n\n") + buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.Example)) + } + + if err := printOptions(buf, cmd, name); err != nil { + return err + } + if hasSeeAlso(cmd) { + buf.WriteString("### SEE ALSO\n\n") + if cmd.HasParent() { + parent := cmd.Parent() + pname := parent.CommandPath() + link := pname + markdownExtension + link = strings.ReplaceAll(link, " ", "_") + buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", pname, linkHandler(link), parent.Short)) + cmd.VisitParents(func(c *cobra.Command) { + if c.DisableAutoGenTag { + cmd.DisableAutoGenTag = c.DisableAutoGenTag + } + }) + } + + children := cmd.Commands() + sort.Sort(byName(children)) + + for _, child := range children { + if !child.IsAvailableCommand() || child.IsAdditionalHelpTopicCommand() { + continue + } + cname := name + " " + child.Name() + link := cname + markdownExtension + link = strings.ReplaceAll(link, " ", "_") + buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", cname, linkHandler(link), child.Short)) + } + buf.WriteString("\n") + } + if !cmd.DisableAutoGenTag { + buf.WriteString("###### Auto generated by spf13/cobra on " + time.Now().Format("2-Jan-2006") + "\n") + } + _, err := buf.WriteTo(w) + return err +} + +// GenMarkdownTree will generate a markdown page for this command and all +// descendants in the directory given. The header may be nil. +// This function may not work correctly if your command names have `-` in them. +// If you have `cmd` with two subcmds, `sub` and `sub-third`, +// and `sub` has a subcommand called `third`, it is undefined which +// help output will be in the file `cmd-sub-third.1`. +func GenMarkdownTree(cmd *cobra.Command, dir string) error { + identity := func(s string) string { return s } + emptyStr := func(s string) string { return "" } + return GenMarkdownTreeCustom(cmd, dir, emptyStr, identity) +} + +// GenMarkdownTreeCustom is the the same as GenMarkdownTree, but +// with custom filePrepender and linkHandler. +func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandler func(string) string) error { + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { + continue + } + if err := GenMarkdownTreeCustom(c, dir, filePrepender, linkHandler); err != nil { + return err + } + } + + basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + markdownExtension + filename := filepath.Join(dir, basename) + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.WriteString(f, filePrepender(filename)); err != nil { + return err + } + if err := GenMarkdownCustom(cmd, f, linkHandler); err != nil { + return err + } + return nil +} diff --git a/doc/md_docs_test.go b/doc/md_docs_test.go new file mode 100644 index 0000000..e70cad8 --- /dev/null +++ b/doc/md_docs_test.go @@ -0,0 +1,126 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package doc + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/spf13/cobra" +) + +func TestGenMdDoc(t *testing.T) { + // We generate on subcommand so we have both subcommands and parents. + buf := new(bytes.Buffer) + if err := GenMarkdown(echoCmd, buf); err != nil { + t.Fatal(err) + } + output := buf.String() + + checkStringContains(t, output, echoCmd.Long) + checkStringContains(t, output, echoCmd.Example) + checkStringContains(t, output, "boolone") + checkStringContains(t, output, "rootflag") + checkStringContains(t, output, rootCmd.Short) + checkStringContains(t, output, echoSubCmd.Short) + checkStringOmits(t, output, deprecatedCmd.Short) + checkStringContains(t, output, "Options inherited from parent commands") +} + +func TestGenMdDocWithNoLongOrSynopsis(t *testing.T) { + // We generate on subcommand so we have both subcommands and parents. + buf := new(bytes.Buffer) + if err := GenMarkdown(dummyCmd, buf); err != nil { + t.Fatal(err) + } + output := buf.String() + + checkStringContains(t, output, dummyCmd.Example) + checkStringContains(t, output, dummyCmd.Short) + checkStringContains(t, output, "Options inherited from parent commands") + checkStringOmits(t, output, "### Synopsis") +} + +func TestGenMdNoHiddenParents(t *testing.T) { + // We generate on subcommand so we have both subcommands and parents. + for _, name := range []string{"rootflag", "strtwo"} { + f := rootCmd.PersistentFlags().Lookup(name) + f.Hidden = true + defer func() { f.Hidden = false }() + } + buf := new(bytes.Buffer) + if err := GenMarkdown(echoCmd, buf); err != nil { + t.Fatal(err) + } + output := buf.String() + + checkStringContains(t, output, echoCmd.Long) + checkStringContains(t, output, echoCmd.Example) + checkStringContains(t, output, "boolone") + checkStringOmits(t, output, "rootflag") + checkStringContains(t, output, rootCmd.Short) + checkStringContains(t, output, echoSubCmd.Short) + checkStringOmits(t, output, deprecatedCmd.Short) + checkStringOmits(t, output, "Options inherited from parent commands") +} + +func TestGenMdNoTag(t *testing.T) { + rootCmd.DisableAutoGenTag = true + defer func() { rootCmd.DisableAutoGenTag = false }() + + buf := new(bytes.Buffer) + if err := GenMarkdown(rootCmd, buf); err != nil { + t.Fatal(err) + } + output := buf.String() + + checkStringOmits(t, output, "Auto generated") +} + +func TestGenMdTree(t *testing.T) { + c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"} + tmpdir, err := ioutil.TempDir("", "test-gen-md-tree") + if err != nil { + t.Fatalf("Failed to create tmpdir: %v", err) + } + defer os.RemoveAll(tmpdir) + + if err := GenMarkdownTree(c, tmpdir); err != nil { + t.Fatalf("GenMarkdownTree failed: %v", err) + } + + if _, err := os.Stat(filepath.Join(tmpdir, "do.md")); err != nil { + t.Fatalf("Expected file 'do.md' to exist") + } +} + +func BenchmarkGenMarkdownToFile(b *testing.B) { + file, err := ioutil.TempFile("", "") + if err != nil { + b.Fatal(err) + } + defer os.Remove(file.Name()) + defer file.Close() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := GenMarkdown(rootCmd, file); err != nil { + b.Fatal(err) + } + } +} diff --git a/doc/rest_docs.go b/doc/rest_docs.go new file mode 100644 index 0000000..2cca6fd --- /dev/null +++ b/doc/rest_docs.go @@ -0,0 +1,186 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package doc + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/spf13/cobra" +) + +func printOptionsReST(buf *bytes.Buffer, cmd *cobra.Command, name string) error { + flags := cmd.NonInheritedFlags() + flags.SetOutput(buf) + if flags.HasAvailableFlags() { + buf.WriteString("Options\n") + buf.WriteString("~~~~~~~\n\n::\n\n") + flags.PrintDefaults() + buf.WriteString("\n") + } + + parentFlags := cmd.InheritedFlags() + parentFlags.SetOutput(buf) + if parentFlags.HasAvailableFlags() { + buf.WriteString("Options inherited from parent commands\n") + buf.WriteString("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n::\n\n") + parentFlags.PrintDefaults() + buf.WriteString("\n") + } + return nil +} + +// defaultLinkHandler for default ReST hyperlink markup +func defaultLinkHandler(name, ref string) string { + return fmt.Sprintf("`%s <%s.rst>`_", name, ref) +} + +// GenReST creates reStructured Text output. +func GenReST(cmd *cobra.Command, w io.Writer) error { + return GenReSTCustom(cmd, w, defaultLinkHandler) +} + +// GenReSTCustom creates custom reStructured Text output. +func GenReSTCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string, string) string) error { + cmd.InitDefaultHelpCmd() + cmd.InitDefaultHelpFlag() + + buf := new(bytes.Buffer) + name := cmd.CommandPath() + + short := cmd.Short + long := cmd.Long + if len(long) == 0 { + long = short + } + ref := strings.ReplaceAll(name, " ", "_") + + buf.WriteString(".. _" + ref + ":\n\n") + buf.WriteString(name + "\n") + buf.WriteString(strings.Repeat("-", len(name)) + "\n\n") + buf.WriteString(short + "\n\n") + buf.WriteString("Synopsis\n") + buf.WriteString("~~~~~~~~\n\n") + buf.WriteString("\n" + long + "\n\n") + + if cmd.Runnable() { + buf.WriteString(fmt.Sprintf("::\n\n %s\n\n", cmd.UseLine())) + } + + if len(cmd.Example) > 0 { + buf.WriteString("Examples\n") + buf.WriteString("~~~~~~~~\n\n") + buf.WriteString(fmt.Sprintf("::\n\n%s\n\n", indentString(cmd.Example, " "))) + } + + if err := printOptionsReST(buf, cmd, name); err != nil { + return err + } + if hasSeeAlso(cmd) { + buf.WriteString("SEE ALSO\n") + buf.WriteString("~~~~~~~~\n\n") + if cmd.HasParent() { + parent := cmd.Parent() + pname := parent.CommandPath() + ref = strings.ReplaceAll(pname, " ", "_") + buf.WriteString(fmt.Sprintf("* %s \t - %s\n", linkHandler(pname, ref), parent.Short)) + cmd.VisitParents(func(c *cobra.Command) { + if c.DisableAutoGenTag { + cmd.DisableAutoGenTag = c.DisableAutoGenTag + } + }) + } + + children := cmd.Commands() + sort.Sort(byName(children)) + + for _, child := range children { + if !child.IsAvailableCommand() || child.IsAdditionalHelpTopicCommand() { + continue + } + cname := name + " " + child.Name() + ref = strings.ReplaceAll(cname, " ", "_") + buf.WriteString(fmt.Sprintf("* %s \t - %s\n", linkHandler(cname, ref), child.Short)) + } + buf.WriteString("\n") + } + if !cmd.DisableAutoGenTag { + buf.WriteString("*Auto generated by spf13/cobra on " + time.Now().Format("2-Jan-2006") + "*\n") + } + _, err := buf.WriteTo(w) + return err +} + +// GenReSTTree will generate a ReST page for this command and all +// descendants in the directory given. +// This function may not work correctly if your command names have `-` in them. +// If you have `cmd` with two subcmds, `sub` and `sub-third`, +// and `sub` has a subcommand called `third`, it is undefined which +// help output will be in the file `cmd-sub-third.1`. +func GenReSTTree(cmd *cobra.Command, dir string) error { + emptyStr := func(s string) string { return "" } + return GenReSTTreeCustom(cmd, dir, emptyStr, defaultLinkHandler) +} + +// GenReSTTreeCustom is the the same as GenReSTTree, but +// with custom filePrepender and linkHandler. +func GenReSTTreeCustom(cmd *cobra.Command, dir string, filePrepender func(string) string, linkHandler func(string, string) string) error { + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { + continue + } + if err := GenReSTTreeCustom(c, dir, filePrepender, linkHandler); err != nil { + return err + } + } + + basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + ".rst" + filename := filepath.Join(dir, basename) + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.WriteString(f, filePrepender(filename)); err != nil { + return err + } + if err := GenReSTCustom(cmd, f, linkHandler); err != nil { + return err + } + return nil +} + +// indentString adapted from: https://github.com/kr/text/blob/main/indent.go +func indentString(s, p string) string { + var res []byte + b := []byte(s) + prefix := []byte(p) + bol := true + for _, c := range b { + if bol && c != '\n' { + res = append(res, prefix...) + } + res = append(res, c) + bol = c == '\n' + } + return string(res) +} diff --git a/doc/rest_docs_test.go b/doc/rest_docs_test.go new file mode 100644 index 0000000..1a3ea9d --- /dev/null +++ b/doc/rest_docs_test.go @@ -0,0 +1,113 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package doc + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/spf13/cobra" +) + +func TestGenRSTDoc(t *testing.T) { + // We generate on a subcommand so we have both subcommands and parents + buf := new(bytes.Buffer) + if err := GenReST(echoCmd, buf); err != nil { + t.Fatal(err) + } + output := buf.String() + + checkStringContains(t, output, echoCmd.Long) + checkStringContains(t, output, echoCmd.Example) + checkStringContains(t, output, "boolone") + checkStringContains(t, output, "rootflag") + checkStringContains(t, output, rootCmd.Short) + checkStringContains(t, output, echoSubCmd.Short) + checkStringOmits(t, output, deprecatedCmd.Short) +} + +func TestGenRSTNoHiddenParents(t *testing.T) { + // We generate on a subcommand so we have both subcommands and parents + for _, name := range []string{"rootflag", "strtwo"} { + f := rootCmd.PersistentFlags().Lookup(name) + f.Hidden = true + defer func() { f.Hidden = false }() + } + buf := new(bytes.Buffer) + if err := GenReST(echoCmd, buf); err != nil { + t.Fatal(err) + } + output := buf.String() + + checkStringContains(t, output, echoCmd.Long) + checkStringContains(t, output, echoCmd.Example) + checkStringContains(t, output, "boolone") + checkStringOmits(t, output, "rootflag") + checkStringContains(t, output, rootCmd.Short) + checkStringContains(t, output, echoSubCmd.Short) + checkStringOmits(t, output, deprecatedCmd.Short) + checkStringOmits(t, output, "Options inherited from parent commands") +} + +func TestGenRSTNoTag(t *testing.T) { + rootCmd.DisableAutoGenTag = true + defer func() { rootCmd.DisableAutoGenTag = false }() + + buf := new(bytes.Buffer) + if err := GenReST(rootCmd, buf); err != nil { + t.Fatal(err) + } + output := buf.String() + + unexpected := "Auto generated" + checkStringOmits(t, output, unexpected) +} + +func TestGenRSTTree(t *testing.T) { + c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"} + + tmpdir, err := ioutil.TempDir("", "test-gen-rst-tree") + if err != nil { + t.Fatalf("Failed to create tmpdir: %s", err.Error()) + } + defer os.RemoveAll(tmpdir) + + if err := GenReSTTree(c, tmpdir); err != nil { + t.Fatalf("GenReSTTree failed: %s", err.Error()) + } + + if _, err := os.Stat(filepath.Join(tmpdir, "do.rst")); err != nil { + t.Fatalf("Expected file 'do.rst' to exist") + } +} + +func BenchmarkGenReSTToFile(b *testing.B) { + file, err := ioutil.TempFile("", "") + if err != nil { + b.Fatal(err) + } + defer os.Remove(file.Name()) + defer file.Close() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := GenReST(rootCmd, file); err != nil { + b.Fatal(err) + } + } +} diff --git a/doc/util.go b/doc/util.go new file mode 100644 index 0000000..0aaa07a --- /dev/null +++ b/doc/util.go @@ -0,0 +1,52 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package doc + +import ( + "strings" + + "github.com/spf13/cobra" +) + +// Test to see if we have a reason to print See Also information in docs +// Basically this is a test for a parent command or a subcommand which is +// both not deprecated and not the autogenerated help command. +func hasSeeAlso(cmd *cobra.Command) bool { + if cmd.HasParent() { + return true + } + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { + continue + } + return true + } + return false +} + +// Temporary workaround for yaml lib generating incorrect yaml with long strings +// that do not contain \n. +func forceMultiLine(s string) string { + if len(s) > 60 && !strings.Contains(s, "\n") { + s = s + "\n" + } + return s +} + +type byName []*cobra.Command + +func (s byName) Len() int { return len(s) } +func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() } diff --git a/doc/yaml_docs.go b/doc/yaml_docs.go new file mode 100644 index 0000000..2b26d6e --- /dev/null +++ b/doc/yaml_docs.go @@ -0,0 +1,175 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package doc + +import ( + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "gopkg.in/yaml.v3" +) + +type cmdOption struct { + Name string + Shorthand string `yaml:",omitempty"` + DefaultValue string `yaml:"default_value,omitempty"` + Usage string `yaml:",omitempty"` +} + +type cmdDoc struct { + Name string + Synopsis string `yaml:",omitempty"` + Description string `yaml:",omitempty"` + Usage string `yaml:",omitempty"` + Options []cmdOption `yaml:",omitempty"` + InheritedOptions []cmdOption `yaml:"inherited_options,omitempty"` + Example string `yaml:",omitempty"` + SeeAlso []string `yaml:"see_also,omitempty"` +} + +// GenYamlTree creates yaml structured ref files for this command and all descendants +// in the directory given. This function may not work +// correctly if your command names have `-` in them. If you have `cmd` with two +// subcmds, `sub` and `sub-third`, and `sub` has a subcommand called `third` +// it is undefined which help output will be in the file `cmd-sub-third.1`. +func GenYamlTree(cmd *cobra.Command, dir string) error { + identity := func(s string) string { return s } + emptyStr := func(s string) string { return "" } + return GenYamlTreeCustom(cmd, dir, emptyStr, identity) +} + +// GenYamlTreeCustom creates yaml structured ref files. +func GenYamlTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandler func(string) string) error { + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { + continue + } + if err := GenYamlTreeCustom(c, dir, filePrepender, linkHandler); err != nil { + return err + } + } + + basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + ".yaml" + filename := filepath.Join(dir, basename) + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.WriteString(f, filePrepender(filename)); err != nil { + return err + } + if err := GenYamlCustom(cmd, f, linkHandler); err != nil { + return err + } + return nil +} + +// GenYaml creates yaml output. +func GenYaml(cmd *cobra.Command, w io.Writer) error { + return GenYamlCustom(cmd, w, func(s string) string { return s }) +} + +// GenYamlCustom creates custom yaml output. +func GenYamlCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error { + cmd.InitDefaultHelpCmd() + cmd.InitDefaultHelpFlag() + + yamlDoc := cmdDoc{} + yamlDoc.Name = cmd.CommandPath() + + yamlDoc.Synopsis = forceMultiLine(cmd.Short) + yamlDoc.Description = forceMultiLine(cmd.Long) + + if cmd.Runnable() { + yamlDoc.Usage = cmd.UseLine() + } + + if len(cmd.Example) > 0 { + yamlDoc.Example = cmd.Example + } + + flags := cmd.NonInheritedFlags() + if flags.HasFlags() { + yamlDoc.Options = genFlagResult(flags) + } + flags = cmd.InheritedFlags() + if flags.HasFlags() { + yamlDoc.InheritedOptions = genFlagResult(flags) + } + + if hasSeeAlso(cmd) { + result := []string{} + if cmd.HasParent() { + parent := cmd.Parent() + result = append(result, parent.CommandPath()+" - "+parent.Short) + } + children := cmd.Commands() + sort.Sort(byName(children)) + for _, child := range children { + if !child.IsAvailableCommand() || child.IsAdditionalHelpTopicCommand() { + continue + } + result = append(result, child.CommandPath()+" - "+child.Short) + } + yamlDoc.SeeAlso = result + } + + final, err := yaml.Marshal(&yamlDoc) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + if _, err := w.Write(final); err != nil { + return err + } + return nil +} + +func genFlagResult(flags *pflag.FlagSet) []cmdOption { + var result []cmdOption + + flags.VisitAll(func(flag *pflag.Flag) { + // Todo, when we mark a shorthand is deprecated, but specify an empty message. + // The flag.ShorthandDeprecated is empty as the shorthand is deprecated. + // Using len(flag.ShorthandDeprecated) > 0 can't handle this, others are ok. + if !(len(flag.ShorthandDeprecated) > 0) && len(flag.Shorthand) > 0 { + opt := cmdOption{ + flag.Name, + flag.Shorthand, + flag.DefValue, + forceMultiLine(flag.Usage), + } + result = append(result, opt) + } else { + opt := cmdOption{ + Name: flag.Name, + DefaultValue: forceMultiLine(flag.DefValue), + Usage: forceMultiLine(flag.Usage), + } + result = append(result, opt) + } + }) + + return result +} diff --git a/doc/yaml_docs_test.go b/doc/yaml_docs_test.go new file mode 100644 index 0000000..1a6fa7c --- /dev/null +++ b/doc/yaml_docs_test.go @@ -0,0 +1,101 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package doc + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/spf13/cobra" +) + +func TestGenYamlDoc(t *testing.T) { + // We generate on s subcommand so we have both subcommands and parents + buf := new(bytes.Buffer) + if err := GenYaml(echoCmd, buf); err != nil { + t.Fatal(err) + } + output := buf.String() + + checkStringContains(t, output, echoCmd.Long) + checkStringContains(t, output, echoCmd.Example) + checkStringContains(t, output, "boolone") + checkStringContains(t, output, "rootflag") + checkStringContains(t, output, rootCmd.Short) + checkStringContains(t, output, echoSubCmd.Short) + checkStringContains(t, output, fmt.Sprintf("- %s - %s", echoSubCmd.CommandPath(), echoSubCmd.Short)) +} + +func TestGenYamlNoTag(t *testing.T) { + rootCmd.DisableAutoGenTag = true + defer func() { rootCmd.DisableAutoGenTag = false }() + + buf := new(bytes.Buffer) + if err := GenYaml(rootCmd, buf); err != nil { + t.Fatal(err) + } + output := buf.String() + + checkStringOmits(t, output, "Auto generated") +} + +func TestGenYamlTree(t *testing.T) { + c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"} + + tmpdir, err := ioutil.TempDir("", "test-gen-yaml-tree") + if err != nil { + t.Fatalf("Failed to create tmpdir: %s", err.Error()) + } + defer os.RemoveAll(tmpdir) + + if err := GenYamlTree(c, tmpdir); err != nil { + t.Fatalf("GenYamlTree failed: %s", err.Error()) + } + + if _, err := os.Stat(filepath.Join(tmpdir, "do.yaml")); err != nil { + t.Fatalf("Expected file 'do.yaml' to exist") + } +} + +func TestGenYamlDocRunnable(t *testing.T) { + // Testing a runnable command: should contain the "usage" field + buf := new(bytes.Buffer) + if err := GenYaml(rootCmd, buf); err != nil { + t.Fatal(err) + } + output := buf.String() + + checkStringContains(t, output, "usage: "+rootCmd.Use) +} + +func BenchmarkGenYamlToFile(b *testing.B) { + file, err := ioutil.TempFile("", "") + if err != nil { + b.Fatal(err) + } + defer os.Remove(file.Name()) + defer file.Close() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := GenYaml(rootCmd, file); err != nil { + b.Fatal(err) + } + } +} |