From e3ccb048f0817a681c63e3ed4f23167fb9d29a8e Mon Sep 17 00:00:00 2001 From: Juan Tirado Date: Mon, 5 Dec 2022 13:19:55 +0100 Subject: [PATCH 1/7] Documentation command. --- cmd_test.go | 18 +++++++ documentation.go | 129 +++++++++++++++++++++++++++++++++++++++++++++++ help.go | 1 + supercommand.go | 11 ++++ 4 files changed, 159 insertions(+) create mode 100644 documentation.go diff --git a/cmd_test.go b/cmd_test.go index 1a8a83d1..7964237a 100644 --- a/cmd_test.go +++ b/cmd_test.go @@ -23,6 +23,7 @@ import ( var _ = gc.Suite(&CmdSuite{}) var _ = gc.Suite(&CmdHelpSuite{}) +var _ = gc.Suite(&CmdDocumentationSuite{}) type CmdSuite struct { testing.LoggingCleanupSuite @@ -381,3 +382,20 @@ Details: command details `[1:]) } + +type CmdDocumentationSuite struct { + testing.LoggingCleanupSuite + + targetCmd cmd.Command +} + +// func (s *CmdDocumentationSuite) TestDocumentationOutput(c *gc.C) { +// subCmdA := &TestCommand{Name: "subCmdA"} +// subCmdB := &TestCommand{Name: "subCmdB"} +// params := cmd.SuperCommandParams{ +// Name: "superCmd", +// Doc: "superCmd-Doc", +// Version: "v1.0.0", +// } +// superCmd := cmd.NewSuperCommand(params) +// } diff --git a/documentation.go b/documentation.go new file mode 100644 index 00000000..dabdd37e --- /dev/null +++ b/documentation.go @@ -0,0 +1,129 @@ +// Copyright 2012-2022 Canonical Ltd. +// Licensed under the LGPLv3, see LICENSE file for details. + +package cmd + +import ( + "bufio" + "fmt" + "os" + "sort" + "strings" + + "github.com/juju/gnuflag" +) + +var doc string = ` +This command generates a markdown formatted document with all the commands, their descriptions, arguments, and examples. +` + +type documentationCommand struct { + CommandBase + super *SuperCommand + out string + noIndex bool +} + +func newDocumentationCommand(s *SuperCommand) *documentationCommand { + return &documentationCommand{super: s} +} + +func (c *documentationCommand) Info() *Info { + return &Info{ + Name: "documentation", + Args: "--out --noindex", + Purpose: "Generate the documentation for current commands", + Doc: doc, + } +} + +// SetFlags adds command specific flags to the flag set. +func (c *documentationCommand) SetFlags(f *gnuflag.FlagSet) { + f.StringVar(&c.out, "out", "", "Documentation output file") + f.BoolVar(&c.noIndex, "noindex", false, "Do not generate the commands index") +} + +func (c *documentationCommand) Run(ctx *Context) error { + var writer *bufio.Writer + if c.out != "" { + f, err := os.Create(c.out) + if err != nil { + return err + } + defer f.Close() + writer = bufio.NewWriter(f) + } else { + writer = bufio.NewWriter(ctx.Stdout) + } + return c.dumpEntries(writer) +} + +func (c *documentationCommand) dumpEntries(writer *bufio.Writer) error { + if len(c.super.subcmds) == 0 { + return nil + } + + // sort the commands + sorted := make([]string, len(c.super.subcmds)) + i := 0 + for k := range c.super.subcmds { + sorted[i] = k + i++ + } + sort.Strings(sorted) + + if !c.noIndex { + _, err := writer.WriteString(c.commandsIndex(sorted)) + if err != nil { + return err + } + } + + var err error + for _, nameCmd := range sorted { + _, err = writer.WriteString(c.formatCommand(c.super.subcmds[nameCmd])) + if err != nil { + return err + } + } + return nil +} + +func (c *documentationCommand) commandsIndex(listCommands []string) string { + index := "# Index\n" + for id, name := range listCommands { + index += fmt.Sprintf("%d. [%s](#%s)\n", id, name, name) + } + index += "---\n\n" + return index +} + +func (c *documentationCommand) formatCommand(ref commandReference) string { + formatted := "# " + strings.ToUpper(ref.name) + "\n" + if ref.alias != "" { + formatted += "**Alias:** " + ref.alias + "\n" + } + if ref.check != nil && ref.check.Obsolete() { + formatted += "*This command is deprecated*\n" + } + formatted += "\n" + + // Description + formatted += "## Summary\n" + ref.command.Info().Purpose + "\n\n" + + // Arguments + if ref.command.Info().Args != "" { + formatted += "## Arguments\n" + ref.command.Info().Args + "\n\n" + } + + // Description + doc := ref.command.Info().Doc + if doc != "" { + formatted += "## Description\n" + ref.command.Info().Doc + "\n" + } + + formatted += "---\n" + + return formatted + +} diff --git a/help.go b/help.go index 20b705f9..cc0e5aee 100644 --- a/help.go +++ b/help.go @@ -113,6 +113,7 @@ func (c *helpCommand) Init(args []string) error { if c.super.notifyHelp != nil { c.super.notifyHelp(args) } + logger.Tracef("helpCommand.Init: %#v", args) if len(args) == 0 { // If there is no help topic specified, print basic usage if it is diff --git a/supercommand.go b/supercommand.go index c559667d..5dc74194 100644 --- a/supercommand.go +++ b/supercommand.go @@ -172,6 +172,7 @@ type SuperCommand struct { userAliases map[string][]string subcmds map[string]commandReference help *helpCommand + documentation *documentationCommand commonflags *gnuflag.FlagSet flags *gnuflag.FlagSet action commandReference @@ -208,9 +209,16 @@ func (c *SuperCommand) init() { super: c, } c.help.init() + + c.documentation = &documentationCommand{ + super: c, + } c.subcmds = map[string]commandReference{ "help": {command: c.help}, + "documentation": {command: newDocumentationCommand(c), + name: "documentation"}, } + if c.version != "" { c.subcmds["version"] = commandReference{ command: newVersionCommand(c.version, c.versionDetail), @@ -447,6 +455,7 @@ func (c *SuperCommand) Init(args []string) error { args = append(userAlias, args[1:]...) } found := false + // Look for the command. if c.action, found = c.subcmds[args[0]]; !found { if c.missingCallback != nil { @@ -463,6 +472,7 @@ func (c *SuperCommand) Init(args []string) error { } return fmt.Errorf("unrecognized command: %s %s", c.Name, args[0]) } + args = args[1:] subcmd := c.action.command if subcmd.IsSuperCommand() { @@ -475,6 +485,7 @@ func (c *SuperCommand) Init(args []string) error { if err := c.commonflags.Parse(subcmd.AllowInterspersedFlags(), args); err != nil { return err } + args = c.commonflags.Args() if c.showHelp { // We want to treat help for the command the same way we would if we went "help foo". From 6376c8aa979fec0a22dc7cdbd8fdca40838fdb37 Mon Sep 17 00:00:00 2001 From: Juan Tirado Date: Mon, 5 Dec 2022 13:24:56 +0100 Subject: [PATCH 2/7] Add quote to arguments section. --- documentation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation.go b/documentation.go index dabdd37e..2c6ec78b 100644 --- a/documentation.go +++ b/documentation.go @@ -113,7 +113,7 @@ func (c *documentationCommand) formatCommand(ref commandReference) string { // Arguments if ref.command.Info().Args != "" { - formatted += "## Arguments\n" + ref.command.Info().Args + "\n\n" + formatted += "## Arguments\n```" + ref.command.Info().Args + "```\n\n" } // Description From 045ecc2246c1d9c5fd002a73f1d1e1c70b902c8a Mon Sep 17 00:00:00 2001 From: Juan Tirado Date: Wed, 4 Jan 2023 14:07:30 +0100 Subject: [PATCH 3/7] Added options section. --- documentation.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/documentation.go b/documentation.go index 2c6ec78b..00ce5e33 100644 --- a/documentation.go +++ b/documentation.go @@ -111,9 +111,15 @@ func (c *documentationCommand) formatCommand(ref commandReference) string { // Description formatted += "## Summary\n" + ref.command.Info().Purpose + "\n\n" - // Arguments + // Usage if ref.command.Info().Args != "" { - formatted += "## Arguments\n```" + ref.command.Info().Args + "```\n\n" + formatted += "## Usage\n```" + ref.command.Info().Args + "```\n\n" + } + + // Options + formattedFlags := c.formatFlags(ref.command) + if len(formattedFlags) > 0 { + formatted += "## Options\n" + formattedFlags + "\n" } // Description @@ -127,3 +133,77 @@ func (c *documentationCommand) formatCommand(ref commandReference) string { return formatted } + +// formatFlags is an internal formatting solution similar to +// the gnuflag.PrintDefaults. The code is extended here +// to permit additional formatting without modifying the +// gnuflag package. +func (d *documentationCommand) formatFlags(c Command) string { + flagsAKA := FlagAlias(c, "") + if flagsAKA == "" { + // For backward compatibility, the default is 'flag'. + flagsAKA = "flag" + } + f := gnuflag.NewFlagSetWithFlagKnownAs(c.Info().Name, gnuflag.ContinueOnError, flagsAKA) + c.SetFlags(f) + // group together all flags for a given value + flags := make(map[interface{}]flagsByLength) + f.VisitAll(func(f *gnuflag.Flag) { + flags[f.Value] = append(flags[f.Value], f) + }) + + // sort the output flags by shortest name for each group. + var byName flagsByName + for _, fl := range flags { + sort.Sort(fl) + byName = append(byName, fl) + } + sort.Sort(byName) + + formatted := "| Flag | Default | Usage |\n" + formatted += "| --- | --- | --- |\n" + for _, fs := range byName { + theFlags := "" + for i, f := range fs { + if i > 0 { + theFlags += ", " + } + theFlags += fmt.Sprintf("`--%s`", f.Name) + } + formatted += fmt.Sprintf("| %s | %s | %s |\n", theFlags, fs[0].DefValue, fs[0].Usage) + } + return formatted +} + +// flagsByLength is a slice of flags implementing sort.Interface, +// sorting primarily by the length of the flag, and secondarily +// alphabetically. +type flagsByLength []*gnuflag.Flag + +func (f flagsByLength) Less(i, j int) bool { + s1, s2 := f[i].Name, f[j].Name + if len(s1) != len(s2) { + return len(s1) < len(s2) + } + return s1 < s2 +} +func (f flagsByLength) Swap(i, j int) { + f[i], f[j] = f[j], f[i] +} +func (f flagsByLength) Len() int { + return len(f) +} + +// flagsByName is a slice of slices of flags implementing sort.Interface, +// alphabetically sorting by the name of the first flag in each slice. +type flagsByName [][]*gnuflag.Flag + +func (f flagsByName) Less(i, j int) bool { + return f[i][0].Name < f[j][0].Name +} +func (f flagsByName) Swap(i, j int) { + f[i], f[j] = f[j], f[i] +} +func (f flagsByName) Len() int { + return len(f) +} From 154740c08a7d23d75c8dc917d937498884ebab44 Mon Sep 17 00:00:00 2001 From: Juan Tirado Date: Wed, 4 Jan 2023 15:54:38 +0100 Subject: [PATCH 4/7] Fix tests --- documentation.go | 2 +- supercommand_test.go | 35 ++++++++++++++++++++--------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/documentation.go b/documentation.go index 00ce5e33..2aeb3210 100644 --- a/documentation.go +++ b/documentation.go @@ -32,7 +32,7 @@ func (c *documentationCommand) Info() *Info { return &Info{ Name: "documentation", Args: "--out --noindex", - Purpose: "Generate the documentation for current commands", + Purpose: "Generate the documentation for all commands", Doc: doc, } } diff --git a/supercommand_test.go b/supercommand_test.go index 3e4e1558..53d27979 100644 --- a/supercommand_test.go +++ b/supercommand_test.go @@ -47,8 +47,9 @@ type SuperCommandSuite struct { var _ = gc.Suite(&SuperCommandSuite{}) +const docText = "\n documentation\\s+- Generate the documentation for all commands" const helpText = "\n help\\s+- Show help on a command or other topic." -const helpCommandsText = "commands:" + helpText +const helpCommandsText = "commands:" + docText + helpText func (s *SuperCommandSuite) TestDispatch(c *gc.C) { jc := cmd.NewSuperCommand(cmd.SuperCommandParams{Name: "jujutest"}) @@ -62,7 +63,7 @@ func (s *SuperCommandSuite) TestDispatch(c *gc.C) { info = jc.Info() c.Assert(info.Name, gc.Equals, "jujutest") c.Assert(info.Args, gc.Equals, " ...") - c.Assert(info.Doc, gc.Matches, "commands:\n defenestrate - defenestrate the juju"+helpText) + c.Assert(info.Doc, gc.Matches, "commands:\n defenestrate - defenestrate the juju"+docText+helpText) jc, tc, err := initDefenestrate([]string{"defenestrate"}) c.Assert(err, gc.IsNil) @@ -132,16 +133,18 @@ func (s *SuperCommandSuite) TestAliasesRegistered(c *gc.C) { info := jc.Info() c.Assert(info.Doc, gc.Equals, `commands: - flap - Alias for 'flip'. - flip - flip the juju - flop - Alias for 'flip'. - help - Show help on a command or other topic.`) + documentation - Generate the documentation for all commands + flap - Alias for 'flip'. + flip - flip the juju + flop - Alias for 'flip'. + help - Show help on a command or other topic.`) } func (s *SuperCommandSuite) TestInfo(c *gc.C) { commandsDoc := `commands: - flapbabble - flapbabble the juju - flip - flip the juju` + documentation - Generate the documentation for all commands + flapbabble - flapbabble the juju + flip - flip the juju` jc := cmd.NewSuperCommand(cmd.SuperCommandParams{ Name: "jujutest", @@ -438,9 +441,10 @@ func (s *SuperCommandSuite) TestRegisterAlias(c *gc.C) { info := jc.Info() // NOTE: deprecated `bar` not shown in commands. c.Assert(info.Doc, gc.Equals, `commands: - foo - Alias for 'test'. - help - Show help on a command or other topic. - test - to be simple`) + documentation - Generate the documentation for all commands + foo - Alias for 'test'. + help - Show help on a command or other topic. + test - to be simple`) for _, test := range []struct { name string @@ -502,10 +506,11 @@ func (s *SuperCommandSuite) TestRegisterSuperAlias(c *gc.C) { info := jc.Info() // NOTE: deprecated `bar` not shown in commands. c.Assert(info.Doc, gc.Equals, `commands: - bar - bar functions - bar-foo - Alias for 'bar foo'. - help - Show help on a command or other topic. - test - to be simple`) + bar - bar functions + bar-foo - Alias for 'bar foo'. + documentation - Generate the documentation for all commands + help - Show help on a command or other topic. + test - to be simple`) for _, test := range []struct { args []string From ccf277c40d3d1c195786125eb082bf03146cb796 Mon Sep 17 00:00:00 2001 From: Juan Tirado Date: Thu, 5 Jan 2023 10:51:32 +0100 Subject: [PATCH 5/7] Added Examples and SeeAlso fields. --- cmd.go | 6 ++++++ documentation.go | 20 +++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/cmd.go b/cmd.go index c2f07719..f621e284 100644 --- a/cmd.go +++ b/cmd.go @@ -268,6 +268,12 @@ type Info struct { // Doc is the long documentation for the Command. Doc string + // Examples is a collection of running examples. + Examples []string + + // SeeAlso is a collection of additional commands to be checked. + SeeAlso []string + // Aliases are other names for the Command. Aliases []string diff --git a/documentation.go b/documentation.go index 2aeb3210..9ed8d4b9 100644 --- a/documentation.go +++ b/documentation.go @@ -125,7 +125,25 @@ func (c *documentationCommand) formatCommand(ref commandReference) string { // Description doc := ref.command.Info().Doc if doc != "" { - formatted += "## Description\n" + ref.command.Info().Doc + "\n" + formatted += "## Description\n" + ref.command.Info().Doc + "\n\n" + } + + // Examples + if len(ref.command.Info().Examples) > 0 { + formatted += "## Examples\n" + for _, e := range ref.command.Info().Examples { + formatted += "`" + e + "`\n" + } + formatted += "\n" + } + + // See Also + if len(ref.command.Info().SeeAlso) > 0 { + formatted += "## See Also\n" + for _, s := range ref.command.Info().SeeAlso { + formatted += fmt.Sprintf("[%s](#%s)\n", s, s) + } + formatted += "\n" } formatted += "---\n" From d5814856a9223da0e41701a5b0a8606e6507da7c Mon Sep 17 00:00:00 2001 From: "Juan M. Tirado" Date: Thu, 5 Jan 2023 11:56:36 +0100 Subject: [PATCH 6/7] Update documentation.go Change noindex for no-index flag Co-authored-by: Simon Richardson --- documentation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation.go b/documentation.go index 9ed8d4b9..218c5a0b 100644 --- a/documentation.go +++ b/documentation.go @@ -40,7 +40,7 @@ func (c *documentationCommand) Info() *Info { // SetFlags adds command specific flags to the flag set. func (c *documentationCommand) SetFlags(f *gnuflag.FlagSet) { f.StringVar(&c.out, "out", "", "Documentation output file") - f.BoolVar(&c.noIndex, "noindex", false, "Do not generate the commands index") + f.BoolVar(&c.noIndex, "no-index", false, "Do not generate the commands index") } func (c *documentationCommand) Run(ctx *Context) error { From 76e02b1952e7c500d1a314c98234d31e28dd2bfc Mon Sep 17 00:00:00 2001 From: Juan Tirado Date: Thu, 5 Jan 2023 13:20:12 +0100 Subject: [PATCH 7/7] Adding comments. --- cmd_test.go | 13 +------------ documentation.go | 10 ++++++---- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/cmd_test.go b/cmd_test.go index 7964237a..1914087e 100644 --- a/cmd_test.go +++ b/cmd_test.go @@ -387,15 +387,4 @@ type CmdDocumentationSuite struct { testing.LoggingCleanupSuite targetCmd cmd.Command -} - -// func (s *CmdDocumentationSuite) TestDocumentationOutput(c *gc.C) { -// subCmdA := &TestCommand{Name: "subCmdA"} -// subCmdB := &TestCommand{Name: "subCmdB"} -// params := cmd.SuperCommandParams{ -// Name: "superCmd", -// Doc: "superCmd-Doc", -// Version: "v1.0.0", -// } -// superCmd := cmd.NewSuperCommand(params) -// } +} \ No newline at end of file diff --git a/documentation.go b/documentation.go index 9ed8d4b9..a3393588 100644 --- a/documentation.go +++ b/documentation.go @@ -60,6 +60,7 @@ func (c *documentationCommand) Run(ctx *Context) error { func (c *documentationCommand) dumpEntries(writer *bufio.Writer) error { if len(c.super.subcmds) == 0 { + fmt.Printf("No commands found for %s", c.super.Name) return nil } @@ -157,13 +158,14 @@ func (c *documentationCommand) formatCommand(ref commandReference) string { // to permit additional formatting without modifying the // gnuflag package. func (d *documentationCommand) formatFlags(c Command) string { - flagsAKA := FlagAlias(c, "") - if flagsAKA == "" { + flagsAlias := FlagAlias(c, "") + if flagsAlias == "" { // For backward compatibility, the default is 'flag'. - flagsAKA = "flag" + flagsAlias = "flag" } - f := gnuflag.NewFlagSetWithFlagKnownAs(c.Info().Name, gnuflag.ContinueOnError, flagsAKA) + f := gnuflag.NewFlagSetWithFlagKnownAs(c.Info().Name, gnuflag.ContinueOnError, flagsAlias) c.SetFlags(f) + // group together all flags for a given value flags := make(map[interface{}]flagsByLength) f.VisitAll(func(f *gnuflag.Flag) {