Skip to content

Commit

Permalink
Merge pull request #83 from juju/JUJU-2217_support_extended_help
Browse files Browse the repository at this point in the history
[JUJU-2217] Extended help documentation command
  • Loading branch information
juanmanuel-tirado authored Jan 5, 2023
2 parents 9e7de1c + 427fa9e commit 790481d
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 15 deletions.
6 changes: 6 additions & 0 deletions cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,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

Expand Down
7 changes: 7 additions & 0 deletions cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

var _ = gc.Suite(&CmdSuite{})
var _ = gc.Suite(&CmdHelpSuite{})
var _ = gc.Suite(&CmdDocumentationSuite{})

type CmdSuite struct {
testing.LoggingCleanupSuite
Expand Down Expand Up @@ -384,3 +385,9 @@ Details:
command details
`[1:])
}

type CmdDocumentationSuite struct {
testing.LoggingCleanupSuite

targetCmd cmd.Command
}
229 changes: 229 additions & 0 deletions documentation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
// 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 <target-file> --noindex",
Purpose: "Generate the documentation for all 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, "no-index", 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 {
fmt.Printf("No commands found for %s", c.super.Name)
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"

// Usage
if ref.command.Info().Args != "" {
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
doc := ref.command.Info().Doc
if doc != "" {
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"

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 {
flagsAlias := FlagAlias(c, "")
if flagsAlias == "" {
// For backward compatibility, the default is 'flag'.
flagsAlias = "flag"
}
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) {
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)
}
1 change: 1 addition & 0 deletions help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions supercommand.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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 {
Expand All @@ -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() {
Expand All @@ -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".
Expand Down
35 changes: 20 additions & 15 deletions supercommand_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,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) SetUpTest(c *gc.C) {
s.IsolationSuite.SetUpTest(c)
Expand All @@ -70,7 +71,7 @@ func (s *SuperCommandSuite) TestDispatch(c *gc.C) {
info = jc.Info()
c.Assert(info.Name, gc.Equals, "jujutest")
c.Assert(info.Args, gc.Equals, "<command> ...")
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)
Expand Down Expand Up @@ -140,16 +141,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",
Expand Down Expand Up @@ -455,9 +458,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
Expand Down Expand Up @@ -520,10 +524,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
Expand Down

0 comments on commit 790481d

Please sign in to comment.