Skip to content

Commit

Permalink
Merge pull request #102 from barrettj12/doc-subcmd
Browse files Browse the repository at this point in the history
[JUJU-4255][documentation command] handle subcommands
  • Loading branch information
barrettj12 authored Aug 14, 2023
2 parents a0647fc + 9137ebd commit 5229d4b
Show file tree
Hide file tree
Showing 9 changed files with 271 additions and 112 deletions.
39 changes: 39 additions & 0 deletions cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"os"
"os/signal"
"path/filepath"
"sort"
"strings"

"github.com/juju/ansiterm"
Expand Down Expand Up @@ -284,6 +285,9 @@ type Info struct {
// Doc is the long documentation for the Command.
Doc string

// Subcommands stores the name and description of each subcommand.
Subcommands map[string]string

// Examples is a collection of running examples.
Examples string

Expand Down Expand Up @@ -374,6 +378,9 @@ func (i *Info) HelpWithSuperFlags(superF *gnuflag.FlagSet, f *gnuflag.FlagSet) [
if len(i.Examples) > 0 {
fmt.Fprintf(buf, "\nExamples:\n%s", i.Examples)
}
if len(i.Subcommands) > 0 {
fmt.Fprintf(buf, "\n%s", i.describeCommands())
}
if len(i.SeeAlso) > 0 {
fmt.Fprintf(buf, "\nSee also:\n")
for _, entry := range i.SeeAlso {
Expand All @@ -384,6 +391,38 @@ func (i *Info) HelpWithSuperFlags(superF *gnuflag.FlagSet, f *gnuflag.FlagSet) [
return buf.Bytes()
}

// Default commands should be hidden from the help output.
func isDefaultCommand(cmd string) bool {
switch cmd {
case "documentation", "help", "version":
return true
}
return false
}

func (i *Info) describeCommands() string {
// Sort command names, and work out length of the longest one
cmdNames := make([]string, 0, len(i.Subcommands))
longest := 0
for name := range i.Subcommands {
if isDefaultCommand(name) {
continue
}
if len(name) > longest {
longest = len(name)
}
cmdNames = append(cmdNames, name)
}
sort.Strings(cmdNames)

descr := "Subcommands:\n"
for _, name := range cmdNames {
purpose := i.Subcommands[name]
descr += fmt.Sprintf(" %-*s - %s\n", longest, name, purpose)
}
return descr
}

// Errors from commands can be ErrSilent (don't print an error message),
// ErrHelp (show the help) or some other error related to needed flags
// missing, or needed positional args missing, in which case we should
Expand Down
33 changes: 33 additions & 0 deletions cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,39 @@ command details
`[1:])
}

func (s *CmdHelpSuite) TestSuperShowsSubcommands(c *gc.C) {
s.info.Subcommands = map[string]string{
"application": "Wait for an application to reach a specified state.",
"machine": "Wait for a machine to reach a specified state.",
"model": "Wait for a model to reach a specified state.",
"unit": "Wait for a unit to reach a specified state.",
}

s.assertHelp(c, `
Usage: verb [flags] <something>
Summary:
command purpose
Flags:
--five (= "")
option-doc
--one (= "")
option-doc
--three (= "")
option-doc
Details:
command details
Subcommands:
application - Wait for an application to reach a specified state.
machine - Wait for a machine to reach a specified state.
model - Wait for a model to reach a specified state.
unit - Wait for a unit to reach a specified state.
`[1:])
}

type CmdDocumentationSuite struct {
testing.LoggingCleanupSuite

Expand Down
143 changes: 105 additions & 38 deletions documentation.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"

Expand Down Expand Up @@ -151,6 +152,11 @@ func (c *documentationCommand) computeReverseAliases() {
// dumpSeveralFiles is invoked when every command is dumped into
// a separated entity
func (c *documentationCommand) dumpSeveralFiles() error {
if len(c.super.subcmds) == 0 {
fmt.Printf("No commands found for %s", c.super.Name)
return nil
}

// Attempt to create output directory. This will fail if:
// - we don't have permission to create the dir
// - a file already exists at the given path
Expand All @@ -159,16 +165,6 @@ func (c *documentationCommand) dumpSeveralFiles() error {
return err
}

if len(c.super.subcmds) == 0 {
fmt.Printf("No commands found for %s", c.super.Name)
return nil
}

sorted := c.getSortedListCommands()
c.computeReverseAliases()

// if the ids were provided, we must have the same
// number of commands and ids
if c.idsPath != "" {
// get the list of ids
c.ids, err = c.readFileIds(c.idsPath)
Expand All @@ -186,31 +182,52 @@ func (c *documentationCommand) dumpSeveralFiles() error {
}

writer := bufio.NewWriter(f)
_, err = writer.WriteString(c.commandsIndex(sorted))
_, err = writer.WriteString(c.commandsIndex())
if err != nil {
return err
}
f.Close()
}

folder := c.out + "/%s.md"
for _, command := range sorted {
target := fmt.Sprintf(folder, command)
return c.writeDocs(c.out, []string{c.super.Name}, true)
}

// writeDocs (recursively) writes docs for all commands in the given folder.
func (c *documentationCommand) writeDocs(folder string, superCommands []string, printDefaultCommands bool) error {
c.computeReverseAliases()

for name, ref := range c.super.subcmds {
if !printDefaultCommands && isDefaultCommand(name) {
continue
}
commandSeq := append(superCommands, name)
target := fmt.Sprintf("%s.md", strings.Join(commandSeq[1:], "_"))
target = strings.ReplaceAll(target, " ", "_")
target = filepath.Join(folder, target)

f, err := os.Create(target)
if err != nil {
return err
}
writer := bufio.NewWriter(f)
formatted := c.formatCommand(c.super.subcmds[command], false)
formatted := c.formatCommand(ref, false, commandSeq)
_, err = writer.WriteString(formatted)
if err != nil {
return err
}
writer.Flush()
f.Close()

// Handle subcommands
if sc, ok := ref.command.(*SuperCommand); ok {
err = sc.documentation.writeDocs(folder, commandSeq, false)
if err != nil {
return err
}
}
}

return err
return nil
}

func (c *documentationCommand) readFileIds(path string) (map[string]string, error) {
Expand Down Expand Up @@ -240,27 +257,58 @@ func (c *documentationCommand) dumpEntries(writer *bufio.Writer) error {
return nil
}

sorted := c.getSortedListCommands()

if !c.noIndex {
_, err := writer.WriteString(c.commandsIndex(sorted))
_, err := writer.WriteString(c.commandsIndex())
if err != nil {
return err
}
}

var err error
for _, nameCmd := range sorted {
_, err = writer.WriteString(c.formatCommand(c.super.subcmds[nameCmd], true))
return c.writeSections(writer, []string{c.super.Name}, true)
}

// writeSections (recursively) writes sections for all commands to the given file.
func (c *documentationCommand) writeSections(writer *bufio.Writer, superCommands []string, printDefaultCommands bool) error {
sorted := c.getSortedListCommands()
for _, name := range sorted {
if !printDefaultCommands && isDefaultCommand(name) {
continue
}
ref := c.super.subcmds[name]
commandSeq := append(superCommands, name)
_, err := writer.WriteString(c.formatCommand(ref, true, commandSeq))
if err != nil {
return err
}

// Handle subcommands
if sc, ok := ref.command.(*SuperCommand); ok {
err = sc.documentation.writeSections(writer, commandSeq, false)
if err != nil {
return err
}
}
}
return nil
}

func (c *documentationCommand) commandsIndex(listCommands []string) string {
func (c *documentationCommand) commandsIndex() string {
index := "# Index\n"

listCommands := c.getSortedListCommands()
for id, name := range listCommands {
if isDefaultCommand(name) {
continue
}
index += fmt.Sprintf("%d. [%s](%s)\n", id, name, c.linkForCommand(name))
// TODO: handle subcommands ??
}
index += "---\n\n"
return index
}

// Return the URL/location for the given command
func (c *documentationCommand) linkForCommand(cmd string) string {
prefix := "#"
if c.ids != nil {
prefix = "/t/"
Expand All @@ -269,28 +317,22 @@ func (c *documentationCommand) commandsIndex(listCommands []string) string {
prefix = c.url + "/"
}

for id, name := range listCommands {
if name == "documentation" || name == "help" {
continue
}
target, err := c.getTargetCmd(name)
if err != nil {
fmt.Printf("[ERROR] command [%s] has no id, please add it to the list\n", name)
}
index += fmt.Sprintf("%d. [%s](%s%s)\n", id, name, prefix, target)
target, err := c.getTargetCmd(cmd)
if err != nil {
fmt.Printf("[ERROR] command [%s] has no id, please add it to the list\n", cmd)
return ""
}
index += "---\n\n"
return index
return prefix + target
}

// formatCommand returns a string representation of the information contained
// by a command in Markdown format. The title param can be used to set
// whether the command name should be a title or not. This is particularly
// handy when splitting the commands in different files.
func (c *documentationCommand) formatCommand(ref commandReference, title bool) string {
func (c *documentationCommand) formatCommand(ref commandReference, title bool, commandSeq []string) string {
formatted := ""
if title {
formatted = "# " + strings.ToUpper(ref.name) + "\n"
formatted = "# " + strings.ToUpper(strings.Join(commandSeq[1:], " ")) + "\n"
}

var info *Info
Expand Down Expand Up @@ -338,9 +380,9 @@ func (c *documentationCommand) formatCommand(ref commandReference, title bool) s
// Usage
if strings.TrimSpace(info.Args) != "" {
formatted += fmt.Sprintf(`## Usage
`+"```"+`%s %s [options] %s`+"```"+`
`+"```"+`%s [options] %s`+"```"+`
`, c.super.Name, info.Name, info.Args)
`, strings.Join(commandSeq, " "), info.Args)
}

// Options
Expand All @@ -361,6 +403,7 @@ func (c *documentationCommand) formatCommand(ref commandReference, title bool) s
formatted += "## Details\n" + doc + "\n\n"
}

formatted += c.formatSubcommands(info.Subcommands, commandSeq)
formatted += "---\n\n"

return formatted
Expand Down Expand Up @@ -524,3 +567,27 @@ func EscapeMarkdown(raw string) string {

return escaped.String()
}

func (c *documentationCommand) formatSubcommands(subcommands map[string]string, commandSeq []string) string {
var output string

sorted := []string{}
for name := range subcommands {
if isDefaultCommand(name) {
continue
}
sorted = append(sorted, name)
}
sort.Strings(sorted)

if len(sorted) > 0 {
output += "## Subcommands\n"
for _, name := range sorted {
output += fmt.Sprintf("- [%s](%s)\n", name,
c.linkForCommand(strings.Join(append(commandSeq[1:], name), "_")))
}
output += "\n"
}

return output
}
1 change: 1 addition & 0 deletions documentation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ insert details here...
t.command,
&cmd.SuperCommand{Name: "juju"},
t.title,
[]string{"juju", t.command.Info().Name},
)
c.Check(output, gc.Equals, t.expected)
}
Expand Down
4 changes: 2 additions & 2 deletions export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ func NewVersionCommand(version string, versionDetail interface{}) Command {
return newVersionCommand(version, versionDetail)
}

func FormatCommand(command Command, super *SuperCommand, title bool) string {
func FormatCommand(command Command, super *SuperCommand, title bool, commandSeq []string) string {
docCmd := &documentationCommand{super: super}
ref := commandReference{command: command}
return docCmd.formatCommand(ref, title)
return docCmd.formatCommand(ref, title, commandSeq)
}
Loading

0 comments on commit 5229d4b

Please sign in to comment.