diff --git a/documentation.go b/documentation.go index ed18aa52..f777cd69 100644 --- a/documentation.go +++ b/documentation.go @@ -328,7 +328,7 @@ func (c *documentationCommand) writeIndex(w io.Writer) error { } // TODO: handle subcommands ?? } - _, err = fmt.Fprintf(w, "---\n\n") + _, err = fmt.Fprintf(w, "\n---\n\n") return err } @@ -357,7 +357,7 @@ func (c *documentationCommand) linkForCommand(cmd string) string { func (c *documentationCommand) formatCommand(ref commandReference, title bool, commandSeq []string) string { var fmtedTitle string if title { - fmtedTitle = strings.ToUpper(strings.Join(commandSeq[1:], " ")) + fmtedTitle = strings.ToLower(strings.Join(commandSeq, " ")) } var buf bytes.Buffer @@ -380,7 +380,7 @@ func (c *documentationCommand) formatCommand(ref commandReference, title bool, c return fmt.Sprintf("%s%s", prefix, target) }, LinkForSubcommand: func(s string) string { - return c.linkForCommand(strings.Join(append(commandSeq[1:], s), "_")) + return c.linkForCommand(strings.Join(append(commandSeq[1:], s), "-")) }, }) return buf.String() diff --git a/markdown.go b/markdown.go index 8693e574..2a9784b8 100644 --- a/markdown.go +++ b/markdown.go @@ -73,15 +73,13 @@ func PrintMarkdown(w io.Writer, cmd InfoCommand, opts MarkdownOptions) error { fmt.Fprintln(&doc) // Usage - if strings.TrimSpace(info.Args) != "" { - fmt.Fprintln(&doc, "## Usage") - fmt.Fprintf(&doc, "```") - fmt.Fprint(&doc, opts.UsagePrefix) - fmt.Fprintf(&doc, "%s [%ss] %s", info.Name, getFlagsName(info.FlagKnownAs), info.Args) - fmt.Fprintf(&doc, "```") - fmt.Fprintln(&doc) - fmt.Fprintln(&doc) - } + fmt.Fprintln(&doc, "## Usage") + fmt.Fprintf(&doc, "```") + fmt.Fprint(&doc, opts.UsagePrefix) + fmt.Fprintf(&doc, "%s [%ss] %s", info.Name, getFlagsName(info.FlagKnownAs), info.Args) + fmt.Fprintf(&doc, "```") + fmt.Fprintln(&doc) + fmt.Fprintln(&doc) // Options printFlags(&doc, cmd) diff --git a/markdown_test.go b/markdown_test.go index 3d85f26b..654bdd29 100644 --- a/markdown_test.go +++ b/markdown_test.go @@ -105,3 +105,59 @@ func (*markdownSuite) TestOutput(c *gc.C) { c.Assert(err, jc.ErrorIsNil) c.Check(buf.String(), gc.Equals, string(expected)) } + +// TestOutputWithoutArgs tests that the output of the PrintMarkdown function is +// correct when a command does not need arguments, e.g. list commands. +func (*markdownSuite) TestOutputWithoutArgs(c *gc.C) { + seeAlso := []string{"add-cloud", "update-cloud", "remove-cloud", "update-credential"} + subcommands := map[string]string{ + "foo": "foo the bar baz", + "bar": "bar the baz foo", + "baz": "baz the foo bar", + } + + command := &docTestCommand{ + info: &cmd.Info{ + Name: "clouds", + Args: "", //Empty args should still result in a usage field. + Purpose: "List clouds.", + Doc: "details for clouds...", + Examples: "examples for clouds...", + SeeAlso: seeAlso, + Aliases: []string{"list-clouds"}, + Subcommands: subcommands, + }, + } + + // These functions verify the provided argument is in the expected set. + linkForCommand := func(s string) string { + for _, cmd := range seeAlso { + if cmd == s { + return "https://docs.com/" + cmd + } + } + c.Fatalf("linkForCommand called with unexpected command %q", s) + return "" + } + + linkForSubcommand := func(s string) string { + _, ok := subcommands[s] + if !ok { + c.Fatalf("linkForSubcommand called with unexpected subcommand %q", s) + } + return "https://docs.com/clouds/" + s + } + + expected, err := os.ReadFile("testdata/list-clouds.md") + c.Assert(err, jc.ErrorIsNil) + + var buf bytes.Buffer + err = cmd.PrintMarkdown(&buf, command, cmd.MarkdownOptions{ + Title: `Command "juju clouds"`, + UsagePrefix: "juju ", + LinkForCommand: linkForCommand, + LinkForSubcommand: linkForSubcommand, + }) + c.Assert(err, jc.ErrorIsNil) + c.Check(buf.String(), gc.Equals, string(expected)) +} diff --git a/supercommand.go b/supercommand.go index 3317d2aa..dc67f1cb 100644 --- a/supercommand.go +++ b/supercommand.go @@ -4,6 +4,7 @@ package cmd import ( + stderr "errors" "fmt" "io/ioutil" "sort" @@ -17,6 +18,11 @@ import ( var logger = loggo.GetLogger("cmd") +// ErrCommandMissing can be returned during the Init() method +// of a command to trigger the super command's missingCallback +// if one is set. +var ErrCommandMissing = stderr.New("missing command") + type topic struct { short string long func() string @@ -458,24 +464,30 @@ func (c *SuperCommand) Init(args []string) error { } found := false + setupMissingCallback := func() { + c.action = commandReference{ + command: &missingCommand{ + callback: c.missingCallback, + superName: c.Name, + name: args[0], + args: args[1:], + }, + } + } + // Look for the command. if c.action, found = c.subcmds[args[0]]; !found { if c.missingCallback != nil { - c.action = commandReference{ - command: &missingCommand{ - callback: c.missingCallback, - superName: c.Name, - name: args[0], - args: args[1:], - }, - } + setupMissingCallback() // Yes return here, no Init called on missing Command. return nil } return fmt.Errorf("unrecognized command: %s %s", c.Name, args[0]) } - args = args[1:] + // Keep the original args + cleanArgs := make([]string, len(args[1:])) + copy(cleanArgs, args[1:]) subcmd := c.action.command if subcmd.IsSuperCommand() { f := gnuflag.NewFlagSetWithFlagKnownAs(c.Info().Name, gnuflag.ContinueOnError, FlagAlias(subcmd, "flag")) @@ -484,17 +496,27 @@ func (c *SuperCommand) Init(args []string) error { } else { subcmd.SetFlags(c.commonflags) } - if err := c.commonflags.Parse(subcmd.AllowInterspersedFlags(), args); err != nil { + if err := c.commonflags.Parse(subcmd.AllowInterspersedFlags(), cleanArgs); err != nil { return err } - args = c.commonflags.Args() + cleanArgs = c.commonflags.Args() if c.showHelp { // We want to treat help for the command the same way we would if we went "help foo". - args = []string{c.action.name} + cleanArgs = []string{c.action.name} c.action = c.subcmds["help"] } - return c.action.command.Init(args) + + err := c.action.command.Init(cleanArgs) + + // Commands may intentionally return a command missing + // error during init to trigger their missing callback. + if !stderr.Is(err, ErrCommandMissing) || c.missingCallback == nil { + return err + } + + setupMissingCallback() + return nil } // Run executes the subcommand that was selected in Init. diff --git a/supercommand_test.go b/supercommand_test.go index c9053996..3942bd51 100644 --- a/supercommand_test.go +++ b/supercommand_test.go @@ -391,6 +391,52 @@ func (s *SuperCommandSuite) TestMissingCallbackContextWiredIn(c *gc.C) { c.Assert(cmdtesting.Stderr(s.ctx), gc.Equals, "this is std err") } +type simpleWithInitError struct { + cmd.CommandBase + name string + initError error +} + +var _ cmd.Command = (*simpleWithInitError)(nil) + +func (s *simpleWithInitError) Info() *cmd.Info { + return &cmd.Info{Name: s.name, Purpose: "to be simple"} +} + +func (s *simpleWithInitError) Init(args []string) error { + return s.initError +} + +func (s *simpleWithInitError) Run(_ *cmd.Context) error { + return errors.New("unexpected-error") +} + +func (s *SuperCommandSuite) TestMissingCallbackSetOnError(c *gc.C) { + callback := func(ctx *cmd.Context, subcommand string, args []string) error { + fmt.Fprint(ctx.Stdout, "reached callback: "+strings.Join(args, " ")) + return nil + } + + jc := cmd.NewSuperCommand(cmd.SuperCommandParams{ + Name: "jujutest", + Log: &cmd.Log{}, + MissingCallback: callback, + }) + jc.Register(&simpleWithInitError{name: "foo", initError: cmd.ErrCommandMissing}) + jc.Register(&simpleWithInitError{name: "bar", initError: errors.New("my-fake-error")}) + + code := cmd.Main(jc, s.ctx, []string{"bar"}) + c.Assert(code, gc.Equals, 2) + c.Assert(cmdtesting.Stderr(s.ctx), gc.Equals, "ERROR my-fake-error\n") + + // Verify that a call to foo, which returns a ErrCommandMissing error + // triggers the command missing callback and ensure all expected + // args were correctly sent to the callback. + code = cmd.Main(jc, s.ctx, []string{"foo", "bar", "baz", "--debug"}) + c.Assert(code, gc.Equals, 0) + c.Assert(cmdtesting.Stdout(s.ctx), gc.Equals, "reached callback: bar baz --debug") +} + func (s *SuperCommandSuite) TestSupercommandAliases(c *gc.C) { jc := cmd.NewSuperCommand(cmd.SuperCommandParams{ Name: "jujutest", diff --git a/testdata/list-clouds.md b/testdata/list-clouds.md new file mode 100644 index 00000000..16d58419 --- /dev/null +++ b/testdata/list-clouds.md @@ -0,0 +1,23 @@ +# Command "juju clouds" + +> See also: [add-cloud](https://docs.com/add-cloud), [update-cloud](https://docs.com/update-cloud), [remove-cloud](https://docs.com/remove-cloud), [update-credential](https://docs.com/update-credential) + +**Aliases:** list-clouds + +## Summary +List clouds. + +## Usage +```juju clouds [options] ``` + +## Examples +examples for clouds... + +## Details +details for clouds... + +## Subcommands +- [bar](https://docs.com/clouds/bar) +- [baz](https://docs.com/clouds/baz) +- [foo](https://docs.com/clouds/foo) +