diff --git a/README.md b/README.md index d361e86..3a075f8 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A minimal, command-oriented CLI package. - Very small, simple API. - Support for POSIX flags. - Only external dependency is [spf13/pflag](https://github.com/spf13/pflag). -- Subcommands. +- Subcommands and subcommand aliases. - Auto-generated help. ## Install @@ -135,5 +135,5 @@ Usage: subcommand [flags] This is a simple example of subcommands. Commands: - s,sub - This is a simple subcommand. + s, sub - This is a simple subcommand. ``` diff --git a/command_test.go b/command_test.go index 0640898..ef9f900 100644 --- a/command_test.go +++ b/command_test.go @@ -9,28 +9,24 @@ import ( "github.com/spf13/pflag" ) -const ( - success = "test successful" - - expectedParentCmdHelpOutput = `Usage: mockParentCmd Mock parent command usage. - -Mock parent command description. - -Commands: - s,sc,sub,mockSubCmd - A simple mock subcommand with aliases. -` -) - -var subCmd = &mockSubCmd{buf: new(bytes.Buffer)} +var subCmd = new(mockSubCmd) type ( + // Mock for root/parent command. mockParentCmd struct{} - mockSubCmd struct{ buf *bytes.Buffer } + // Mock subcommand with aliases and a nested sucommand of its own. + mockSubCmd struct { + buf *bytes.Buffer + } + // Mock subcommand with aliases and no nested subcommands. + mockSubCmdNoNested struct{} ) func (c *mockParentCmd) Run(fl *pflag.FlagSet) {} -func (c *mockParentCmd) Subcommands() []Command { return []Command{subCmd} } +func (c *mockParentCmd) Subcommands() []Command { + return []Command{subCmd} +} func (c *mockParentCmd) Spec() CommandSpec { return CommandSpec{ @@ -42,64 +38,119 @@ func (c *mockParentCmd) Spec() CommandSpec { func (c *mockSubCmd) Run(fl *pflag.FlagSet) { c.buf = new(bytes.Buffer) - if _, err := c.Write([]byte(success)); err != nil { + _, err := c.WriteString("success") + if err != nil { fl.Usage() } } -func (c *mockSubCmd) Write(b []byte) (int, error) { return c.buf.Write(b) } +func (c *mockSubCmd) Subcommands() []Command { + return []Command{new(mockSubCmd)} +} + +func (c *mockSubCmd) WriteString(s string) (int, error) { + return c.buf.WriteString(s) +} func (c *mockSubCmd) Spec() CommandSpec { return CommandSpec{ Name: "mockSubCmd", Usage: "Test a subcommand.", Aliases: []string{"s", "sc", "sub"}, - Desc: "A simple mock subcommand with aliases.", + Desc: "A simple mock subcommand with aliases and its own subcommand.", + } +} + +func (c *mockSubCmdNoNested) Run(fl *pflag.FlagSet) {} + +func (c *mockSubCmdNoNested) Spec() CommandSpec { + return CommandSpec{ + Name: "mockSubCmdNoNested", + Usage: "Used for help output tests.", + Aliases: []string{"s", "sc", "sub"}, + Desc: "A simple mock subcommand with aliases and no nested subcommands.", } } func TestSubCmdAliases(t *testing.T) { + for _, alias := range []string{"s", "sc", "sub"} { + t.Run(alias, func(t *testing.T) { + // Setup command. + cmd := new(mockParentCmd) + os.Args = []string{cmd.Spec().Name, alias} + // Run command. + RunRoot(cmd) + // If "success" isn't written into the buffer + // then we failed to find the subcommand by alias. + got := subCmd.buf.String() + assert.Equal(t, t.Name(), "success", got) + }) + } +} + +func TestCmdHelpOutput(t *testing.T) { + t.Run(t.Name(), func(t *testing.T) { + expected := `Usage: mockParentCmd Mock parent command usage. + +Description: Mock parent command description. + +Commands: + s, sc, sub, mockSubCmd - A simple mock subcommand with aliases and its own subcommand. +` + buf := new(bytes.Buffer) + cmd := new(mockParentCmd) + name := cmd.Spec().Name + fl := pflag.NewFlagSet(name, pflag.ExitOnError) + // If the help output doesn't contain the subcommand and + // isn't formatted the way we expect the test will fail. + renderHelp(buf, name, cmd, fl) + got := buf.String() + assert.Equal(t, t.Name(), expected, got) + }) +} + +func TestSubCmdHelpOutput(t *testing.T) { + withNested := `Usage: mockSubCmd Test a subcommand. + +Aliases: s, sc, sub + +Description: A simple mock subcommand with aliases and its own subcommand. + +Commands: + s, sc, sub, mockSubCmd - A simple mock subcommand with aliases and its own subcommand. +` + + noNested := `Usage: mockSubCmdNoNested Used for help output tests. + +Aliases: s, sc, sub + +Description: A simple mock subcommand with aliases and no nested subcommands. +` + for _, test := range []struct { name, expected string + cmd Command }{ { - name: "s", - expected: success, - }, - { - name: "sc", - expected: success, + name: "subcmd w/nested subcmd.", + expected: withNested, + cmd: new(mockSubCmd), }, { - name: "sub", - expected: success, + name: "subcmd w/no nested subcmds.", + expected: noNested, + cmd: new(mockSubCmdNoNested), }, } { t.Run(test.name, func(t *testing.T) { - // Since the alias is the name of the test - // we can just pass it as the alias arg. - os.Args = []string{"mockParentCmd", test.name} - // Based on os.Args, when splitArgs is invoked, - // it should be able to deduce the subcommand we want - // based on the new alias map it's being passed. - RunRoot(&mockParentCmd{}) - // The success const is never written into the buffer - // if the subcommand fails to be invoked by alias. - got := string(subCmd.buf.Bytes()) - assert.Equal(t, test.name, test.expected, got) + buf := new(bytes.Buffer) + name := test.cmd.Spec().Name + fl := pflag.NewFlagSet(name, pflag.ExitOnError) + // If the help output is not written to the buffer + // in the format we expect then the test will fail. + renderHelp(buf, name, test.cmd, fl) + got := buf.String() + assert.Equal(t, t.Name(), test.expected, got) }) } } - -func TestSubcmdAliasesInParentCmdHelpOutput(t *testing.T) { - buf := new(bytes.Buffer) - cmd := &mockParentCmd{} - name := cmd.Spec().Name - fl := pflag.NewFlagSet(name, pflag.ExitOnError) - // If the help output is not written to the buffer - // in the format we expect then the test will fail. - renderHelp(name, cmd, fl, buf) - got := string(buf.Bytes()) - expected := expectedParentCmdHelpOutput - assert.Equal(t, "display_subcmd_aliases_in_parentcmd_help_output", expected, got) -} diff --git a/help.go b/help.go index c04e2e5..8b90c2f 100644 --- a/help.go +++ b/help.go @@ -45,18 +45,17 @@ func renderFlagHelp(fullName string, fl *pflag.FlagSet, w io.Writer) { } // renderHelp generates a command's help page. -func renderHelp(fullName string, cmd Command, fl *pflag.FlagSet, w io.Writer) { +func renderHelp(w io.Writer, fullName string, cmd Command, fl *pflag.FlagSet) { var b strings.Builder spec := cmd.Spec() fmt.Fprintf(&b, "Usage: %v %v\n\n", fullName, spec.Usage) // If the command has aliases, add them to the output as a comma-separated list. if spec.HasAliases() { - fmt.Fprintf(&b, "Aliases: %v", spec.Aliases) + fmt.Fprintf(&b, "Aliases: %s\n\n", strings.Join(spec.Aliases, ", ")) } - // Render usage and description. - usageAndDesc := fmt.Sprintf("%s%s\n", b.String(), spec.Desc) - fmt.Fprint(w, usageAndDesc) + // Print usage and description. + fmt.Fprintf(w, "%sDescription: %s\n", b.String(), spec.Desc) tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', tabwriter.StripEscape) defer tw.Flush() @@ -80,7 +79,7 @@ func renderHelp(fullName string, cmd Command, fl *pflag.FlagSet, w io.Writer) { continue } - allNames := strings.Join(append(spec.Aliases, spec.Name), ",") + allNames := strings.Join(append(spec.Aliases, spec.Name), ", ") fmt.Fprintf(tw, tabEscape+"\t"+tabEscape+"%v\t- %v\n", diff --git a/run.go b/run.go index 34341a9..59d444a 100644 --- a/run.go +++ b/run.go @@ -94,7 +94,7 @@ func Run(cmd Command, args []string, parent string) { // Reassign the usage now that we've parsed the args // so that we can render it manually. fl.Usage = func() { - renderHelp(name, cmd, fl, os.Stderr) + renderHelp(os.Stderr, name, cmd, fl) } if err != nil { fl.Usage()