Commit af8a711e authored by Alexandre Viau's avatar Alexandre Viau

Imported Upstream version 0.0~git20150618.0.8102d0e

parents
language: go
go:
- 1.1
- 1.2
- 1.3
- tip
matrix:
allow_failures:
- go: tip
This diff is collapsed.
# Go CLI Library [![GoDoc](https://godoc.org/github.com/mitchellh/cli?status.png)](https://godoc.org/github.com/mitchellh/cli)
cli is a library for implementing powerful command-line interfaces in Go.
cli is the library that powers the CLI for
[Packer](https://github.com/mitchellh/packer),
[Serf](https://github.com/hashicorp/serf), and
[Consul](https://github.com/hashicorp/consul).
## Features
* Easy sub-command based CLIs: `cli foo`, `cli bar`, etc.
* Optional support for default subcommands so `cli` does something
other than error.
* Automatic help generation for listing subcommands
* Automatic help flag recognition of `-h`, `--help`, etc.
* Automatic version flag recognition of `-v`, `--version`.
* Helpers for interacting with the terminal, such as outputting information,
asking for input, etc. These are optional, you can always interact with the
terminal however you choose.
* Use of Go interfaces/types makes augmenting various parts of the library a
piece of cake.
## Example
Below is a simple example of creating and running a CLI
```go
package main
import (
"log"
"os"
"github.com/mitchellh/cli"
)
func main() {
c := cli.NewCLI("app", "1.0.0")
c.Args = os.Args[1:]
c.Commands = map[string]cli.CommandFactory{
"foo": fooCommandFactory,
"bar": barCommandFactory,
}
exitStatus, err := c.Run()
if err != nil {
log.Println(err)
}
os.Exit(exitStatus)
}
```
package cli
import (
"io"
"os"
"sync"
)
// CLI contains the state necessary to run subcommands and parse the
// command line arguments.
type CLI struct {
// Args is the list of command-line arguments received excluding
// the name of the app. For example, if the command "./cli foo bar"
// was invoked, then Args should be []string{"foo", "bar"}.
Args []string
// Commands is a mapping of subcommand names to a factory function
// for creating that Command implementation. If there is a command
// with a blank string "", then it will be used as the default command
// if no subcommand is specified.
Commands map[string]CommandFactory
// Name defines the name of the CLI.
Name string
// Version of the CLI.
Version string
// HelpFunc and HelpWriter are used to output help information, if
// requested.
//
// HelpFunc is the function called to generate the generic help
// text that is shown if help must be shown for the CLI that doesn't
// pertain to a specific command.
//
// HelpWriter is the Writer where the help text is outputted to. If
// not specified, it will default to Stderr.
HelpFunc HelpFunc
HelpWriter io.Writer
once sync.Once
isHelp bool
subcommand string
subcommandArgs []string
topFlags []string
isVersion bool
}
// NewClI returns a new CLI instance with sensible defaults.
func NewCLI(app, version string) *CLI {
return &CLI{
Name: app,
Version: version,
HelpFunc: BasicHelpFunc(app),
}
}
// IsHelp returns whether or not the help flag is present within the
// arguments.
func (c *CLI) IsHelp() bool {
c.once.Do(c.init)
return c.isHelp
}
// IsVersion returns whether or not the version flag is present within the
// arguments.
func (c *CLI) IsVersion() bool {
c.once.Do(c.init)
return c.isVersion
}
// Run runs the actual CLI based on the arguments given.
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(
"Invalid flags before the subcommand. If these flags are for\n" +
"the subcommand, please put them after the subcommand.\n\n"))
c.HelpWriter.Write([]byte(c.HelpFunc(c.Commands) + "\n"))
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
}
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
}
return command.Run(c.SubcommandArgs()), nil
}
// Subcommand returns the subcommand that the CLI would execute. For
// example, a CLI from "--version version --help" would return a Subcommand
// of "version"
func (c *CLI) Subcommand() string {
c.once.Do(c.init)
return c.subcommand
}
// SubcommandArgs returns the arguments that will be passed to the
// subcommand.
func (c *CLI) SubcommandArgs() []string {
c.once.Do(c.init)
return c.subcommandArgs
}
func (c *CLI) init() {
if c.HelpFunc == nil {
c.HelpFunc = BasicHelpFunc("app")
if c.Name != "" {
c.HelpFunc = BasicHelpFunc(c.Name)
}
}
if c.HelpWriter == nil {
c.HelpWriter = os.Stderr
}
c.processArgs()
}
func (c *CLI) processArgs() {
for i, arg := range c.Args {
if c.subcommand == "" {
// Check for version and help flags if not in a subcommand
if arg == "-v" || arg == "-version" || arg == "--version" {
c.isVersion = true
continue
}
if arg == "-h" || arg == "-help" || arg == "--help" {
c.isHelp = true
continue
}
if arg != "" && arg[0] == '-' {
// Record the arg...
c.topFlags = append(c.topFlags, arg)
}
}
// If we didn't find a subcommand yet and this is the first non-flag
// argument, then this is our subcommand. j
if c.subcommand == "" && arg != "" && arg[0] != '-' {
c.subcommand = arg
// The remaining args the subcommand arguments
c.subcommandArgs = c.Args[i+1:]
}
}
// If we never found a subcommand and support a default command, then
// switch to using that.
if c.subcommand == "" {
if _, ok := c.Commands[""]; ok {
args := c.topFlags
args = append(args, c.subcommandArgs...)
c.topFlags = nil
c.subcommandArgs = args
}
}
}
package cli
import (
"bytes"
"reflect"
"strings"
"testing"
)
func TestCLIIsHelp(t *testing.T) {
testCases := []struct {
args []string
isHelp bool
}{
{[]string{"-h"}, true},
{[]string{"-help"}, true},
{[]string{"--help"}, true},
{[]string{"-h", "foo"}, true},
{[]string{"foo", "bar"}, false},
{[]string{"-v", "bar"}, false},
{[]string{"foo", "-h"}, false},
{[]string{"foo", "-help"}, false},
{[]string{"foo", "--help"}, false},
}
for _, testCase := range testCases {
cli := &CLI{Args: testCase.args}
result := cli.IsHelp()
if result != testCase.isHelp {
t.Errorf("Expected '%#v'. Args: %#v", testCase.isHelp, testCase.args)
}
}
}
func TestCLIIsVersion(t *testing.T) {
testCases := []struct {
args []string
isVersion bool
}{
{[]string{"-v"}, true},
{[]string{"-version"}, true},
{[]string{"--version"}, true},
{[]string{"-v", "foo"}, true},
{[]string{"foo", "bar"}, false},
{[]string{"-h", "bar"}, false},
{[]string{"foo", "-v"}, false},
{[]string{"foo", "-version"}, false},
{[]string{"foo", "--version"}, false},
}
for _, testCase := range testCases {
cli := &CLI{Args: testCase.args}
result := cli.IsVersion()
if result != testCase.isVersion {
t.Errorf("Expected '%#v'. Args: %#v", testCase.isVersion, testCase.args)
}
}
}
func TestCLIRun(t *testing.T) {
command := new(MockCommand)
cli := &CLI{
Args: []string{"foo", "-bar", "-baz"},
Commands: map[string]CommandFactory{
"foo": func() (Command, error) {
return command, nil
},
},
}
exitCode, err := cli.Run()
if err != nil {
t.Fatalf("err: %s", err)
}
if exitCode != command.RunResult {
t.Fatalf("bad: %d", exitCode)
}
if !command.RunCalled {
t.Fatalf("run should be called")
}
if !reflect.DeepEqual(command.RunArgs, []string{"-bar", "-baz"}) {
t.Fatalf("bad args: %#v", command.RunArgs)
}
}
func TestCLIRun_blank(t *testing.T) {
command := new(MockCommand)
cli := &CLI{
Args: []string{"", "foo", "-bar", "-baz"},
Commands: map[string]CommandFactory{
"foo": func() (Command, error) {
return command, nil
},
},
}
exitCode, err := cli.Run()
if err != nil {
t.Fatalf("err: %s", err)
}
if exitCode != command.RunResult {
t.Fatalf("bad: %d", exitCode)
}
if !command.RunCalled {
t.Fatalf("run should be called")
}
if !reflect.DeepEqual(command.RunArgs, []string{"-bar", "-baz"}) {
t.Fatalf("bad args: %#v", command.RunArgs)
}
}
func TestCLIRun_default(t *testing.T) {
commandBar := new(MockCommand)
commandBar.RunResult = 42
cli := &CLI{
Args: []string{"-bar", "-baz"},
Commands: map[string]CommandFactory{
"": func() (Command, error) {
return commandBar, nil
},
"foo": func() (Command, error) {
return new(MockCommand), nil
},
},
}
exitCode, err := cli.Run()
if err != nil {
t.Fatalf("err: %s", err)
}
if exitCode != commandBar.RunResult {
t.Fatalf("bad: %d", exitCode)
}
if !commandBar.RunCalled {
t.Fatalf("run should be called")
}
if !reflect.DeepEqual(commandBar.RunArgs, []string{"-bar", "-baz"}) {
t.Fatalf("bad args: %#v", commandBar.RunArgs)
}
}
func TestCLIRun_printHelp(t *testing.T) {
testCases := [][]string{
{},
{"-h"},
{"i-dont-exist"},
{"-bad-flag", "foo"},
}
for _, testCase := range testCases {
buf := new(bytes.Buffer)
helpText := "foo"
cli := &CLI{
Args: testCase,
Commands: map[string]CommandFactory{
"foo": func() (Command, error) {
return new(MockCommand), nil
},
},
HelpFunc: func(map[string]CommandFactory) string {
return helpText
},
HelpWriter: buf,
}
code, err := cli.Run()
if err != nil {
t.Errorf("Args: %#v. Error: %s", testCase, err)
continue
}
if code != 1 {
t.Errorf("Args: %#v. Code: %d", testCase, code)
continue
}
if !strings.Contains(buf.String(), helpText) {
t.Errorf("Args: %#v. Text: %v", testCase, buf.String())
}
}
}
func TestCLIRun_printCommandHelp(t *testing.T) {
testCases := [][]string{
{"--help", "foo"},
{"-h", "foo"},
}
for _, args := range testCases {
command := &MockCommand{
HelpText: "donuts",
}
buf := new(bytes.Buffer)
cli := &CLI{
Args: args,
Commands: map[string]CommandFactory{
"foo": func() (Command, error) {
return command, nil
},
},
HelpWriter: buf,
}
exitCode, err := cli.Run()
if err != nil {
t.Fatalf("err: %s", err)
}
if exitCode != 1 {
t.Fatalf("bad exit code: %d", exitCode)
}
if buf.String() != (command.HelpText + "\n") {
t.Fatalf("bad: %#v", buf.String())
}
}
}
func TestCLISubcommand(t *testing.T) {
testCases := []struct {
args []string
subcommand string
}{
{[]string{"bar"}, "bar"},
{[]string{"foo", "-h"}, "foo"},
{[]string{"-h", "bar"}, "bar"},
}
for _, testCase := range testCases {
cli := &CLI{Args: testCase.args}
result := cli.Subcommand()
if result != testCase.subcommand {
t.Errorf("Expected %#v, got %#v. Args: %#v",
testCase.subcommand, result, testCase.args)
}
}
}
package cli
// 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
// usage, a brief few sentences explaining the function of the command,
// and the complete list of flags the command accepts.
Help() string
// Run should run the actual command with the given CLI instance and
// command-line arguments. It should return the exit status when it is
// finished.
Run(args []string) int
// Synopsis should return a one-line, short synopsis of the command.
// This should be less than 50 characters ideally.
Synopsis() string
}
// CommandFactory is a type of function that is a factory for commands.
// 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)
package cli
// MockCommand is an implementation of Command that can be used for tests.
// It is publicly exported from this package in case you want to use it
// externally.
type MockCommand struct {
// Settable
HelpText string
RunResult int
SynopsisText string
// Set by the command
RunCalled bool
RunArgs []string
}
func (c *MockCommand) Help() string {
return c.HelpText
}
func (c *MockCommand) Run(args []string) int {
c.RunCalled = true
c.RunArgs = args
return c.RunResult
}
func (c *MockCommand) Synopsis() string {
return c.SynopsisText
}
package cli
import (
"testing"
)
func TestMockCommand_implements(t *testing.T) {
var _ Command = new(MockCommand)
}
package cli
import (
"bytes"
"fmt"
"log"
"sort"
"strings"
)
// HelpFunc is the type of the function that is responsible for generating
// the help output when the CLI must show the general help text.
type HelpFunc func(map[string]CommandFactory) string
// BasicHelpFunc generates some basic help output that is usually good enough
// for most CLI applications.
func BasicHelpFunc(app string) HelpFunc {
return func(commands map[string]CommandFactory) string {
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf(
"usage: %s [--version] [--help] <command> [<args>]\n\n",
app))
buf.WriteString("Available commands are:\n")
// Get the list of keys so we can sort them, and also get the maximum
// key length so they can be aligned properly.
keys := make([]string, 0, len(commands))
maxKeyLen := 0
for key, _ := range commands {
if len(key) > maxKeyLen {
maxKeyLen = len(key)
}
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
commandFunc, ok := commands[key]
if !ok {
// This should never happen since we JUST built the list of
// keys.
panic("command not found: " + key)
}
command, err := commandFunc()
if err != nil {
log.Printf("[ERR] cli: Command '%s' failed to load: %s",
key, err)
continue
}
key = fmt.Sprintf("%s%s", key, strings.Repeat(" ", maxKeyLen-len(key)))
buf.WriteString(fmt.Sprintf(" %s %s\n", key, command.Synopsis()))
}
return buf.String()
}
}
// FilteredHelpFunc will filter the commands to only include the keys
// in the include parameter.
func FilteredHelpFunc(include []string, f HelpFunc) HelpFunc {
return func(commands map[string]CommandFactory) string {
set := make(map[string]struct{})
for _, k := range include {
set[k] = struct{}{}
}
filtered := make(map[string]CommandFactory)
for k, f := range commands {
if _, ok := set[k]; ok {
filtered[k] = f
}
}
return f(filtered)
}
}
package cli
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/signal"
"strings"
"golang.org/x/crypto/ssh/terminal"
)
// Ui is an interface for interacting with the terminal, or "interface"
// of a CLI. This abstraction doesn't have to be used, but helps provide
// a simple, layerable way to manage user interactions.
type Ui interface {
// Ask asks the user for input using the given query. The response is
// returned as the given string, or an error.
Ask(string) (string, error)
// AskSecret asks the user for input using the given query, but does not echo
// the keystrokes to the terminal.
AskSecret(string) (string, error)
// Output is called for normal standard output.
Output(string)
// Info is called for information related to the previous output.
// In general this may be the exact same as Output, but this gives
// Ui implementors some flexibility with output formats.
Info(string)
// Error is used for any error messages that might appear on standard
// error.
Error(string)
// Warn is used for any warning messages that might appear on standard
// error.
Warn(string)
}
// BasicUi is an implementation of Ui that just outputs to the given
// writer. This UI is not threadsafe by default, but you can wrap it
// in a ConcurrentUi to make it safe.
type BasicUi struct {
Reader io.Reader
Writer io.Writer
ErrorWriter io.Writer
}
func (u *BasicUi) Ask(query string) (string, error) {
return u.ask(query, false)
}
func (u *BasicUi) AskSecret(query string) (string, error) {
return u.ask(query, true)
}
func (u *BasicUi) ask(query string, secret bool) (string, error) {
if _, err := fmt.Fprint(u.Writer, query+" "); err != nil {
return "", err
}
// Register for interrupts so that we can catch it and immediately
// return...
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt)
defer signal.Stop(sigCh)
// Ask for input in a go-routine so that we can ignore it.
errCh := make(chan error, 1)
lineCh := make(chan string, 1)
go func() {
var line string
var err error
stdin := int(os.Stdin.Fd())
if secret && terminal.IsTerminal(stdin) {
var lineBytes []byte
lineBytes, err = terminal.ReadPassword(stdin)
line = string(lineBytes)
} else {
r := bufio.NewReader(u.Reader)
line, err = r.ReadString('\n')
}
if err != nil {
errCh <- err
return
}
lineCh <- strings.TrimRight(line, "\r\n")
}()
select {
case err := <-errCh:
return "", err
case line := <-lineCh: