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

Commit

Permalink
Add SubcommandChooser
Browse files Browse the repository at this point in the history
The SubcommandChooser can be used to customize the behavior when the
default map lookup cannot find the subcommand you want.

Example usage:

1. busybox-style subcommand invocation

```
// A symbolic link `ls` to binary `busybox` will invoke `busybox ls`
mySubcommandChooser = func(*c cli.CLI) (CommandFactory, error){
    subcommand = os.Args[0]
    if subcommand != c.Name() {
        if subcommandFunc, ok := c.Commands[subcommand]; ok {
            return subcommandFunc, nil
        }
    }
    return cli.DefaultSubcommandChooser(c)
}

c := cli.NewCLI("busybox", "1.0.0")
c.Args = os.Args[1:]
c.Commands = map[string]cli.CommandFactory{
    "ls": lsCommandFactory,
    "cat": catCommandFactory,
}
c.SubcommandChooser = mySubcommandChooser
```

2. default subcommand

```
// `myapp --help` == `myapp default --help`
mySubcommandChooser = func(*c cli.CLI) (CommandFactory, error) {
    if c.Subcommand() == "" {
        return c.Commands["default"], nil
    }
    return cli.DefaultSubcommandChooser(c)
}
c := cli.NewCLI("myapp", "1.0.0")
c.Args = os.Args[1:]
c.Commands = map[string]cli.CommandFactory{
    "default": defaultCommandFactory,
}
c.SubcommandChooser = mySubcommandChooser
```
  • Loading branch information
yegle committed Jun 4, 2015
1 parent 76e2780 commit 91e3762
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 22 deletions.
46 changes: 28 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,24 @@ type CLI struct {
isVersion bool
}

func DefaultSubcommandChooser(c *CLI) (CommandFactory, error) {
helpCommandFactory := func() (Command, error) {
return HelpCommand{c.HelpFunc, c.HelpWriter, c.Commands}, nil
}
versionCommand := OutputTextCommand{c.HelpWriter, c.Version}
versionCommandFactory := func() (Command, error) {
return versionCommand, nil
}

if commandFunc, ok := c.Commands[c.Subcommand()]; ok {
return commandFunc, nil
} else if c.IsVersion() {
return versionCommandFactory, fmt.Errorf("Failed to find subcommand")
} else {
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 +96,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 +105,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 +146,10 @@ func (c *CLI) init() {
c.HelpWriter = os.Stderr
}

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

c.processArgs()
}

Expand Down
13 changes: 9 additions & 4 deletions cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,11 @@ 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)
}
}
}
Expand Down Expand Up @@ -195,8 +198,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
20 changes: 20 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,21 @@ 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)

type OutputTextCommand struct {
writer io.Writer
text string
}

func (c OutputTextCommand) Help() string {
return c.text
}

func (c OutputTextCommand) Synopsis() string {
return c.Help()
}

func (c OutputTextCommand) Run(_ []string) int {
c.writer.Write([]byte(c.Help()))
return 1
}
18 changes: 18 additions & 0 deletions help.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cli
import (
"bytes"
"fmt"
"io"
"log"
"sort"
"strings"
Expand Down Expand Up @@ -77,3 +78,20 @@ func FilteredHelpFunc(include []string, f HelpFunc) HelpFunc {
return f(filtered)
}
}

type HelpCommand struct {
helpFunc HelpFunc
writer io.Writer
subcommands map[string]CommandFactory
}

func (c HelpCommand) Help() string {
return c.helpFunc(c.subcommands) + "\n"
}
func (c HelpCommand) Synopsis() string {
return c.Help()
}
func (c HelpCommand) Run(_ []string) int {
c.writer.Write([]byte(c.Help()))
return 1
}

0 comments on commit 91e3762

Please sign in to comment.