Skip to content

Commit 5229d4b

Browse files
authored
Merge pull request #102 from barrettj12/doc-subcmd
[JUJU-4255][documentation command] handle subcommands
2 parents a0647fc + 9137ebd commit 5229d4b

9 files changed

+271
-112
lines changed

cmd.go

+39
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"os"
1414
"os/signal"
1515
"path/filepath"
16+
"sort"
1617
"strings"
1718

1819
"github.com/juju/ansiterm"
@@ -284,6 +285,9 @@ type Info struct {
284285
// Doc is the long documentation for the Command.
285286
Doc string
286287

288+
// Subcommands stores the name and description of each subcommand.
289+
Subcommands map[string]string
290+
287291
// Examples is a collection of running examples.
288292
Examples string
289293

@@ -374,6 +378,9 @@ func (i *Info) HelpWithSuperFlags(superF *gnuflag.FlagSet, f *gnuflag.FlagSet) [
374378
if len(i.Examples) > 0 {
375379
fmt.Fprintf(buf, "\nExamples:\n%s", i.Examples)
376380
}
381+
if len(i.Subcommands) > 0 {
382+
fmt.Fprintf(buf, "\n%s", i.describeCommands())
383+
}
377384
if len(i.SeeAlso) > 0 {
378385
fmt.Fprintf(buf, "\nSee also:\n")
379386
for _, entry := range i.SeeAlso {
@@ -384,6 +391,38 @@ func (i *Info) HelpWithSuperFlags(superF *gnuflag.FlagSet, f *gnuflag.FlagSet) [
384391
return buf.Bytes()
385392
}
386393

394+
// Default commands should be hidden from the help output.
395+
func isDefaultCommand(cmd string) bool {
396+
switch cmd {
397+
case "documentation", "help", "version":
398+
return true
399+
}
400+
return false
401+
}
402+
403+
func (i *Info) describeCommands() string {
404+
// Sort command names, and work out length of the longest one
405+
cmdNames := make([]string, 0, len(i.Subcommands))
406+
longest := 0
407+
for name := range i.Subcommands {
408+
if isDefaultCommand(name) {
409+
continue
410+
}
411+
if len(name) > longest {
412+
longest = len(name)
413+
}
414+
cmdNames = append(cmdNames, name)
415+
}
416+
sort.Strings(cmdNames)
417+
418+
descr := "Subcommands:\n"
419+
for _, name := range cmdNames {
420+
purpose := i.Subcommands[name]
421+
descr += fmt.Sprintf(" %-*s - %s\n", longest, name, purpose)
422+
}
423+
return descr
424+
}
425+
387426
// Errors from commands can be ErrSilent (don't print an error message),
388427
// ErrHelp (show the help) or some other error related to needed flags
389428
// missing, or needed positional args missing, in which case we should

cmd_test.go

+33
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,39 @@ command details
386386
`[1:])
387387
}
388388

389+
func (s *CmdHelpSuite) TestSuperShowsSubcommands(c *gc.C) {
390+
s.info.Subcommands = map[string]string{
391+
"application": "Wait for an application to reach a specified state.",
392+
"machine": "Wait for a machine to reach a specified state.",
393+
"model": "Wait for a model to reach a specified state.",
394+
"unit": "Wait for a unit to reach a specified state.",
395+
}
396+
397+
s.assertHelp(c, `
398+
Usage: verb [flags] <something>
399+
400+
Summary:
401+
command purpose
402+
403+
Flags:
404+
--five (= "")
405+
option-doc
406+
--one (= "")
407+
option-doc
408+
--three (= "")
409+
option-doc
410+
411+
Details:
412+
command details
413+
414+
Subcommands:
415+
application - Wait for an application to reach a specified state.
416+
machine - Wait for a machine to reach a specified state.
417+
model - Wait for a model to reach a specified state.
418+
unit - Wait for a unit to reach a specified state.
419+
`[1:])
420+
}
421+
389422
type CmdDocumentationSuite struct {
390423
testing.LoggingCleanupSuite
391424

documentation.go

+105-38
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"errors"
99
"fmt"
1010
"os"
11+
"path/filepath"
1112
"sort"
1213
"strings"
1314

@@ -151,6 +152,11 @@ func (c *documentationCommand) computeReverseAliases() {
151152
// dumpSeveralFiles is invoked when every command is dumped into
152153
// a separated entity
153154
func (c *documentationCommand) dumpSeveralFiles() error {
155+
if len(c.super.subcmds) == 0 {
156+
fmt.Printf("No commands found for %s", c.super.Name)
157+
return nil
158+
}
159+
154160
// Attempt to create output directory. This will fail if:
155161
// - we don't have permission to create the dir
156162
// - a file already exists at the given path
@@ -159,16 +165,6 @@ func (c *documentationCommand) dumpSeveralFiles() error {
159165
return err
160166
}
161167

162-
if len(c.super.subcmds) == 0 {
163-
fmt.Printf("No commands found for %s", c.super.Name)
164-
return nil
165-
}
166-
167-
sorted := c.getSortedListCommands()
168-
c.computeReverseAliases()
169-
170-
// if the ids were provided, we must have the same
171-
// number of commands and ids
172168
if c.idsPath != "" {
173169
// get the list of ids
174170
c.ids, err = c.readFileIds(c.idsPath)
@@ -186,31 +182,52 @@ func (c *documentationCommand) dumpSeveralFiles() error {
186182
}
187183

188184
writer := bufio.NewWriter(f)
189-
_, err = writer.WriteString(c.commandsIndex(sorted))
185+
_, err = writer.WriteString(c.commandsIndex())
190186
if err != nil {
191187
return err
192188
}
193189
f.Close()
194190
}
195191

196-
folder := c.out + "/%s.md"
197-
for _, command := range sorted {
198-
target := fmt.Sprintf(folder, command)
192+
return c.writeDocs(c.out, []string{c.super.Name}, true)
193+
}
194+
195+
// writeDocs (recursively) writes docs for all commands in the given folder.
196+
func (c *documentationCommand) writeDocs(folder string, superCommands []string, printDefaultCommands bool) error {
197+
c.computeReverseAliases()
198+
199+
for name, ref := range c.super.subcmds {
200+
if !printDefaultCommands && isDefaultCommand(name) {
201+
continue
202+
}
203+
commandSeq := append(superCommands, name)
204+
target := fmt.Sprintf("%s.md", strings.Join(commandSeq[1:], "_"))
205+
target = strings.ReplaceAll(target, " ", "_")
206+
target = filepath.Join(folder, target)
207+
199208
f, err := os.Create(target)
200209
if err != nil {
201210
return err
202211
}
203212
writer := bufio.NewWriter(f)
204-
formatted := c.formatCommand(c.super.subcmds[command], false)
213+
formatted := c.formatCommand(ref, false, commandSeq)
205214
_, err = writer.WriteString(formatted)
206215
if err != nil {
207216
return err
208217
}
209218
writer.Flush()
210219
f.Close()
220+
221+
// Handle subcommands
222+
if sc, ok := ref.command.(*SuperCommand); ok {
223+
err = sc.documentation.writeDocs(folder, commandSeq, false)
224+
if err != nil {
225+
return err
226+
}
227+
}
211228
}
212229

213-
return err
230+
return nil
214231
}
215232

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

243-
sorted := c.getSortedListCommands()
244-
245260
if !c.noIndex {
246-
_, err := writer.WriteString(c.commandsIndex(sorted))
261+
_, err := writer.WriteString(c.commandsIndex())
247262
if err != nil {
248263
return err
249264
}
250265
}
251266

252-
var err error
253-
for _, nameCmd := range sorted {
254-
_, err = writer.WriteString(c.formatCommand(c.super.subcmds[nameCmd], true))
267+
return c.writeSections(writer, []string{c.super.Name}, true)
268+
}
269+
270+
// writeSections (recursively) writes sections for all commands to the given file.
271+
func (c *documentationCommand) writeSections(writer *bufio.Writer, superCommands []string, printDefaultCommands bool) error {
272+
sorted := c.getSortedListCommands()
273+
for _, name := range sorted {
274+
if !printDefaultCommands && isDefaultCommand(name) {
275+
continue
276+
}
277+
ref := c.super.subcmds[name]
278+
commandSeq := append(superCommands, name)
279+
_, err := writer.WriteString(c.formatCommand(ref, true, commandSeq))
255280
if err != nil {
256281
return err
257282
}
283+
284+
// Handle subcommands
285+
if sc, ok := ref.command.(*SuperCommand); ok {
286+
err = sc.documentation.writeSections(writer, commandSeq, false)
287+
if err != nil {
288+
return err
289+
}
290+
}
258291
}
259292
return nil
260293
}
261294

262-
func (c *documentationCommand) commandsIndex(listCommands []string) string {
295+
func (c *documentationCommand) commandsIndex() string {
263296
index := "# Index\n"
297+
298+
listCommands := c.getSortedListCommands()
299+
for id, name := range listCommands {
300+
if isDefaultCommand(name) {
301+
continue
302+
}
303+
index += fmt.Sprintf("%d. [%s](%s)\n", id, name, c.linkForCommand(name))
304+
// TODO: handle subcommands ??
305+
}
306+
index += "---\n\n"
307+
return index
308+
}
309+
310+
// Return the URL/location for the given command
311+
func (c *documentationCommand) linkForCommand(cmd string) string {
264312
prefix := "#"
265313
if c.ids != nil {
266314
prefix = "/t/"
@@ -269,28 +317,22 @@ func (c *documentationCommand) commandsIndex(listCommands []string) string {
269317
prefix = c.url + "/"
270318
}
271319

272-
for id, name := range listCommands {
273-
if name == "documentation" || name == "help" {
274-
continue
275-
}
276-
target, err := c.getTargetCmd(name)
277-
if err != nil {
278-
fmt.Printf("[ERROR] command [%s] has no id, please add it to the list\n", name)
279-
}
280-
index += fmt.Sprintf("%d. [%s](%s%s)\n", id, name, prefix, target)
320+
target, err := c.getTargetCmd(cmd)
321+
if err != nil {
322+
fmt.Printf("[ERROR] command [%s] has no id, please add it to the list\n", cmd)
323+
return ""
281324
}
282-
index += "---\n\n"
283-
return index
325+
return prefix + target
284326
}
285327

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

296338
var info *Info
@@ -338,9 +380,9 @@ func (c *documentationCommand) formatCommand(ref commandReference, title bool) s
338380
// Usage
339381
if strings.TrimSpace(info.Args) != "" {
340382
formatted += fmt.Sprintf(`## Usage
341-
`+"```"+`%s %s [options] %s`+"```"+`
383+
`+"```"+`%s [options] %s`+"```"+`
342384
343-
`, c.super.Name, info.Name, info.Args)
385+
`, strings.Join(commandSeq, " "), info.Args)
344386
}
345387

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

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

366409
return formatted
@@ -524,3 +567,27 @@ func EscapeMarkdown(raw string) string {
524567

525568
return escaped.String()
526569
}
570+
571+
func (c *documentationCommand) formatSubcommands(subcommands map[string]string, commandSeq []string) string {
572+
var output string
573+
574+
sorted := []string{}
575+
for name := range subcommands {
576+
if isDefaultCommand(name) {
577+
continue
578+
}
579+
sorted = append(sorted, name)
580+
}
581+
sort.Strings(sorted)
582+
583+
if len(sorted) > 0 {
584+
output += "## Subcommands\n"
585+
for _, name := range sorted {
586+
output += fmt.Sprintf("- [%s](%s)\n", name,
587+
c.linkForCommand(strings.Join(append(commandSeq[1:], name), "_")))
588+
}
589+
output += "\n"
590+
}
591+
592+
return output
593+
}

documentation_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ insert details here...
9595
t.command,
9696
&cmd.SuperCommand{Name: "juju"},
9797
t.title,
98+
[]string{"juju", t.command.Info().Name},
9899
)
99100
c.Check(output, gc.Equals, t.expected)
100101
}

export_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ func NewVersionCommand(version string, versionDetail interface{}) Command {
77
return newVersionCommand(version, versionDetail)
88
}
99

10-
func FormatCommand(command Command, super *SuperCommand, title bool) string {
10+
func FormatCommand(command Command, super *SuperCommand, title bool, commandSeq []string) string {
1111
docCmd := &documentationCommand{super: super}
1212
ref := commandReference{command: command}
13-
return docCmd.formatCommand(ref, title)
13+
return docCmd.formatCommand(ref, title, commandSeq)
1414
}

0 commit comments

Comments
 (0)