From 4761857fb3bd33d4c50134458418b88fdc0286ed Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 18 May 2022 18:21:54 +0300 Subject: [PATCH] [#1396] cmd: Allow to autogenerate documentation for all commands Signed-off-by: Evgenii Stratonikov --- CHANGELOG.md | 1 + cmd/neofs-adm/internal/modules/root.go | 2 + cmd/neofs-cli/modules/root.go | 2 + cmd/neofs-lens/root.go | 2 + go.mod | 2 +- pkg/util/gendoc/gendoc.go | 144 +++++++++++++++++++++++++ 6 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 pkg/util/gendoc/gendoc.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 65ca7558d..7456d004f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Changelog for NeoFS Node ### Added - Config examples for Inner ring application (#1358) +- Command for documentation generation for `neofs-cli`, `neofs-adm` and `neofs-lens` (#1396) ### Fixed - Do not ask for contract wallet password twice (#1346) diff --git a/cmd/neofs-adm/internal/modules/root.go b/cmd/neofs-adm/internal/modules/root.go index 4322e6c70..2234fc3d6 100644 --- a/cmd/neofs-adm/internal/modules/root.go +++ b/cmd/neofs-adm/internal/modules/root.go @@ -9,6 +9,7 @@ import ( "github.com/nspcc-dev/neofs-node/cmd/neofs-adm/internal/modules/storagecfg" "github.com/nspcc-dev/neofs-node/misc" "github.com/nspcc-dev/neofs-node/pkg/util/autocomplete" + "github.com/nspcc-dev/neofs-node/pkg/util/gendoc" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -42,6 +43,7 @@ func init() { rootCmd.AddCommand(storagecfg.RootCmd) rootCmd.AddCommand(autocomplete.Command("neofs-adm")) + rootCmd.AddCommand(gendoc.Command(rootCmd)) } func Execute() error { diff --git a/cmd/neofs-cli/modules/root.go b/cmd/neofs-cli/modules/root.go index 11a7d5851..49b798e1c 100644 --- a/cmd/neofs-cli/modules/root.go +++ b/cmd/neofs-cli/modules/root.go @@ -18,6 +18,7 @@ import ( bearerCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/bearer" sessionCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/session" "github.com/nspcc-dev/neofs-node/misc" + "github.com/nspcc-dev/neofs-node/pkg/util/gendoc" "github.com/nspcc-dev/neofs-sdk-go/bearer" "github.com/nspcc-dev/neofs-sdk-go/client" "github.com/nspcc-dev/neofs-sdk-go/owner" @@ -93,6 +94,7 @@ func init() { rootCmd.AddCommand(bearerCli.Cmd) rootCmd.AddCommand(sessionCli.Cmd) rootCmd.AddCommand(accountingCli.Cmd) + rootCmd.AddCommand(gendoc.Command(rootCmd)) } func entryPoint(cmd *cobra.Command, _ []string) { diff --git a/cmd/neofs-lens/root.go b/cmd/neofs-lens/root.go index f27ae7744..8cfdd9c39 100644 --- a/cmd/neofs-lens/root.go +++ b/cmd/neofs-lens/root.go @@ -7,6 +7,7 @@ import ( "github.com/nspcc-dev/neofs-node/cmd/neofs-lens/internal/commands/inspect" cmdlist "github.com/nspcc-dev/neofs-node/cmd/neofs-lens/internal/commands/list" "github.com/nspcc-dev/neofs-node/misc" + "github.com/nspcc-dev/neofs-node/pkg/util/gendoc" "github.com/spf13/cobra" ) @@ -36,6 +37,7 @@ func init() { command.AddCommand( cmdlist.Command, inspect.Command, + gendoc.Command(command), ) } diff --git a/go.mod b/go.mod index b7044cf26..55b3304ef 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/prometheus/client_golang v1.11.0 github.com/spf13/cast v1.3.1 github.com/spf13/cobra v1.1.3 + github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.8.1 github.com/stretchr/testify v1.7.0 go.etcd.io/bbolt v1.3.6 @@ -80,7 +81,6 @@ require ( github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/afero v1.6.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.2.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 // indirect github.com/twmb/murmur3 v1.1.5 // indirect diff --git a/pkg/util/gendoc/gendoc.go b/pkg/util/gendoc/gendoc.go new file mode 100644 index 000000000..59c36fd34 --- /dev/null +++ b/pkg/util/gendoc/gendoc.go @@ -0,0 +1,144 @@ +package gendoc + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" + "github.com/spf13/pflag" +) + +const ( + gendocTypeFlag = "type" + + gendocMarkdown = "md" + gendocMan = "man" + + depthFlag = "depth" + extensionFlag = "extension" +) + +// Command returns command which generates user documentation for the argument. +func Command(rootCmd *cobra.Command) *cobra.Command { + gendocCmd := &cobra.Command{ + Use: "gendoc ", + Short: "Generate documentation for this command.", + Long: `Generate documentation for this command. If the template is not provided, +builtin cobra generator is used and each subcommand is placed in +a separate file in the same directory. + +The last optional argument specifies the template to use with text/template. +In this case there is a number of helper functions which can be used: + replace STR FROM TO -- same as strings.ReplaceAll + join ARRAY SEPARATOR -- same as strings.Join + split STR SEPARATOR -- same as strings.Split + fullUse CMD -- slice of all command names starting from the parent + listFlags CMD -- list of command flags +`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + _ = cmd.Usage() + os.Exit(1) + } + + err := os.MkdirAll(args[0], os.ModePerm) + if err != nil { + return fmt.Errorf("can't create directory: %w", err) + } + + if len(args) == 2 { + data, err := ioutil.ReadFile(args[1]) + if err != nil { + return fmt.Errorf("can't read the template '%s': %w", args[1], err) + } + + return generateTemplate(cmd, rootCmd, args[0], data) + } + + typ, _ := cmd.Flags().GetString(gendocTypeFlag) + switch typ { + case gendocMarkdown: + return doc.GenMarkdownTree(rootCmd, args[0]) + case gendocMan: + hdr := &doc.GenManHeader{ + Section: "1", + Source: "NSPCC & Morphbits", + } + return doc.GenManTree(rootCmd, hdr, args[0]) + default: + return errors.New("type must be 'md' or 'man'") + } + }, + } + + ff := gendocCmd.Flags() + ff.StringP(gendocTypeFlag, "t", gendocMarkdown, "type for the documentation ('md' or 'man')") + ff.Int(depthFlag, 1, "if template is specified, unify all commands starting from depth in a single file. Default: 1.") + ff.StringP(extensionFlag, "e", "", "if the template is specified, string to append to the output file names") + return gendocCmd +} + +func generateTemplate(cmd *cobra.Command, rootCmd *cobra.Command, outDir string, tmpl []byte) error { + depth, _ := cmd.Flags().GetInt(depthFlag) + ext, _ := cmd.Flags().GetString(extensionFlag) + + tm := template.New("doc") + tm.Funcs(template.FuncMap{ + "replace": strings.ReplaceAll, + "split": strings.Split, + "join": strings.Join, + "fullUse": fullUse, + "listFlags": listFlags, + }) + + tm, err := tm.Parse(string(tmpl)) + if err != nil { + return err + } + + return visit(rootCmd, outDir, ext, depth, tm) +} + +func visit(rootCmd *cobra.Command, outDir string, ext string, depth int, tm *template.Template) error { + if depth == 0 { + name := strings.Join(fullUse(rootCmd), "-") + name = strings.TrimSpace(name) + name = strings.ReplaceAll(name, " ", "-") + name = filepath.Join(outDir, name) + ext + f, err := os.Create(name) + if err != nil { + return fmt.Errorf("can't create file '%s': %w", name, err) + } + defer f.Close() + return tm.Execute(f, rootCmd) + } + + for _, c := range rootCmd.Commands() { + err := visit(c, outDir, ext, depth-1, tm) + if err != nil { + return err + } + } + return nil +} + +func fullUse(c *cobra.Command) []string { + if c == nil { + return nil + } + return append(fullUse(c.Parent()), c.Name()) +} + +func listFlags(c *cobra.Command) []*pflag.Flag { + var res []*pflag.Flag + c.Flags().VisitAll(func(f *pflag.Flag) { + res = append(res, f) + }) + return res +}