Skip to content
This repository has been archived by the owner on Jul 22, 2024. It is now read-only.

Add SubcommandChooser #20

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 32 additions & 18 deletions cli.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"fmt"
"io"
"os"
"sync"
Expand All @@ -26,6 +27,8 @@ type CLI struct {
// Version of the CLI.
Version string

SubcommandChooser func(*CLI) (CommandFactory, error)

// HelpFunc and HelpWriter are used to output help information, if
// requested.
//
Expand All @@ -47,6 +50,28 @@ type CLI struct {
isVersion bool
}

// DefaultSubcommandChooser is the default SubcommandChooser. It will return
// the proper subcommand CommandFactory, or if that cannot be found and
// IsVersion() return true, return versionCommanFactory. The fallback of
// helpCommandFactory will be returned when there's nothing matched. error will
// be non-nil if a proper subcommand can't be found, so client user can wrap
// this function according to error value.
func DefaultSubcommandChooser(c *CLI) (CommandFactory, error) {
if commandFunc, ok := c.Commands[c.Subcommand()]; ok {
return commandFunc, nil
} else if c.IsVersion() {
versionCommandFactory := func() (Command, error) {
return OutputTextCommand{c.HelpWriter, c.Version}, nil
}
return versionCommandFactory, fmt.Errorf("Failed to find subcommand")
} else {
helpCommandFactory := func() (Command, error) {
return OutputTextCommand{c.HelpWriter, c.HelpFunc(c.Commands)}, nil
}
return helpCommandFactory, fmt.Errorf("Failed to find subcommand")
}
}

// NewClI returns a new CLI instance with sensible defaults.
func NewCLI(app, version string) *CLI {
return &CLI{
Expand Down Expand Up @@ -75,12 +100,6 @@ func (c *CLI) IsVersion() bool {
func (c *CLI) Run() (int, error) {
c.once.Do(c.init)

// Just show the version and exit if instructed.
if c.IsVersion() && c.Version != "" {
c.HelpWriter.Write([]byte(c.Version + "\n"))
return 1, nil
}

// If there is an invalid flag, then error
if len(c.topFlags) > 0 {
c.HelpWriter.Write([]byte(
Expand All @@ -90,25 +109,16 @@ func (c *CLI) Run() (int, error) {
return 1, nil
}

// Attempt to get the factory function for creating the command
// implementation. If the command is invalid or blank, it is an error.
commandFunc, ok := c.Commands[c.Subcommand()]
if !ok {
c.HelpWriter.Write([]byte(c.HelpFunc(c.Commands) + "\n"))
return 1, nil
}
commandFunc, _ := c.SubcommandChooser(c)

command, err := commandFunc()

if err != nil {
return 0, err
}

// If we've been instructed to just print the help, then print it
if c.IsHelp() {
c.HelpWriter.Write([]byte(command.Help() + "\n"))
return 1, nil
command = OutputTextCommand{c.HelpWriter, command.Help()}
}

return command.Run(c.SubcommandArgs()), nil
}

Expand Down Expand Up @@ -140,6 +150,10 @@ func (c *CLI) init() {
c.HelpWriter = os.Stderr
}

if c.SubcommandChooser == nil {
c.SubcommandChooser = DefaultSubcommandChooser
}

c.processArgs()
}

Expand Down
83 changes: 79 additions & 4 deletions cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,81 @@ func TestCLIRun_printHelp(t *testing.T) {
continue
}

if !strings.Contains(buf.String(), helpText) {
t.Errorf("Args: %#v. Text: %v", testCase, buf.String())
expect := strings.TrimSpace(buf.String())
got := strings.TrimSpace(helpText)

if !strings.Contains(expect, got) {
t.Errorf("Args: %#v, expect: %#v, got %#v", testCase, expect, got)
}
}
}

func TestCLIRun_printBasicHelpFunc(t *testing.T) {
defaultExpect := `usage: app [--version] [--help] <command> [<args>]

Available commands are:
bar bar command
foo foo command
`
tests := []struct {
input []string
expect string
}{
{
[]string{}, defaultExpect,
},
{
[]string{"-h"}, defaultExpect,
},
{
[]string{"i-dont-exist"}, defaultExpect,
},
{
[]string{"-bad-flag", "foo"},
`Invalid flags before the subcommand. If these flags are for
the subcommand, please put them after the subcommand.

usage: app [--version] [--help] <command> [<args>]

Available commands are:
bar bar command
foo foo command

`,
},
}

for _, test := range tests {
buf := new(bytes.Buffer)

cli := &CLI{
Args: test.input,
Commands: map[string]CommandFactory{
"foo": func() (Command, error) {
return &MockCommand{SynopsisText: "foo command"}, nil
},
"bar": func() (Command, error) {
return &MockCommand{SynopsisText: "bar command"}, nil
},
},
HelpFunc: BasicHelpFunc("app"),
HelpWriter: buf,
}

code, err := cli.Run()
if err != nil {
t.Errorf("Args: %#v. Error: %s", test.input, err)
continue
}

if code != 1 {
t.Errorf("Args: %#v. Code: %d", test.input, code)
continue
}

if got := buf.String(); got != test.expect {
t.Errorf(
"Args: %#v, expect: %#v, got %#v", test.input, test.expect, got)
}
}
}
Expand Down Expand Up @@ -195,8 +268,10 @@ func TestCLIRun_printCommandHelp(t *testing.T) {
t.Fatalf("bad exit code: %d", exitCode)
}

if buf.String() != (command.HelpText + "\n") {
t.Fatalf("bad: %#v", buf.String())
expect := strings.TrimSpace(command.HelpText)
got := strings.TrimSpace(buf.String())
if expect != got {
t.Fatalf("Expect %#v, got %#v.", expect, got)
}
}
}
Expand Down
25 changes: 25 additions & 0 deletions command.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package cli

import "io"

// A command is a runnable sub-command of a CLI.
type Command interface {
// Help should return long-form help text that includes the command-line
Expand All @@ -21,3 +23,26 @@ type Command interface {
// We need a factory because we may need to setup some state on the
// struct that implements the command itself.
type CommandFactory func() (Command, error)

// OutputTextCommand implemented Command interface and is used to write text to
// given writer
type OutputTextCommand struct {
writer io.Writer
text string
}

// Help is part of Command interface
func (c OutputTextCommand) Help() string {
return c.text
}

// Synopsis is part of Command interface. Return Help()
func (c OutputTextCommand) Synopsis() string {
return c.Help()
}

// Run is part of Command interface. Args will be ignored.
func (c OutputTextCommand) Run(_ []string) int {
c.writer.Write([]byte(c.Help()))
return 1
}