Merge remote-tracking branch 'origin/master' into merging-from-v1

This commit is contained in:
Dan Buch 2016-08-22 14:41:56 -04:00
commit 81c242dda6
No known key found for this signature in database
GPG Key ID: FAEF12936DD3E3EC
8 changed files with 513 additions and 19 deletions

View File

@ -7,11 +7,11 @@ cache:
- node_modules - node_modules
go: go:
- 1.2.2 - 1.2.x
- 1.3.3 - 1.3.x
- 1.4 - 1.4.2
- 1.5.4 - 1.5.x
- 1.6.2 - 1.6.x
- master - master
env: pip_install="pip install --user" env: pip_install="pip install --user"
@ -20,14 +20,14 @@ matrix:
allow_failures: allow_failures:
- go: master - go: master
include: include:
- go: 1.6.2 - go: 1.6.x
os: osx os: osx
env: pip_install="sudo pip install" env: pip_install="sudo pip install"
before_script: before_script:
- $pip_install flake8 - $pip_install flake8
- go get github.com/urfave/gfmrun/... - go get github.com/urfave/gfmrun/...
- go get golang.org/x/tools/cmd/goimports || true - go get golang.org/x/tools/... || true
- if [ ! -f node_modules/.bin/markdown-toc ] ; then - if [ ! -f node_modules/.bin/markdown-toc ] ; then
npm install markdown-toc ; npm install markdown-toc ;
fi fi

View File

@ -35,6 +35,8 @@
## [Unreleased] - (1.x series) ## [Unreleased] - (1.x series)
### Added ### Added
- Flag type code generation via `go generate` - Flag type code generation via `go generate`
- Write to stderr and exit 1 if action returns non-nil error
- Added support for TOML to the `altsrc` loader
### Changed ### Changed
- Raise minimum tested/supported Go version to 1.2+ - Raise minimum tested/supported Go version to 1.2+

View File

@ -31,7 +31,7 @@ applications in an expressive way.
+ [Placeholder Values](#placeholder-values) + [Placeholder Values](#placeholder-values)
+ [Alternate Names](#alternate-names) + [Alternate Names](#alternate-names)
+ [Values from the Environment](#values-from-the-environment) + [Values from the Environment](#values-from-the-environment)
+ [Values from alternate input sources (YAML and others)](#values-from-alternate-input-sources-yaml-and-others) + [Values from alternate input sources (YAML, TOML, and others)](#values-from-alternate-input-sources-yaml-toml-and-others)
* [Subcommands](#subcommands) * [Subcommands](#subcommands)
* [Subcommands categories](#subcommands-categories) * [Subcommands categories](#subcommands-categories)
* [Exit code](#exit-code) * [Exit code](#exit-code)
@ -520,10 +520,14 @@ func main() {
} }
``` ```
#### Values from alternate input sources (YAML and others) #### Values from alternate input sources (YAML, TOML, and others)
There is a separate package altsrc that adds support for getting flag values There is a separate package altsrc that adds support for getting flag values
from other input sources like YAML. from other file input sources.
Currently supported input source formats:
* YAML
* TOML
In order to get values for a flag from an alternate input source the following In order to get values for a flag from an alternate input source the following
code would be added to wrap an existing cli.Flag like below: code would be added to wrap an existing cli.Flag like below:
@ -545,9 +549,9 @@ the yaml input source for any flags that are defined on that command. As a note
the "load" flag used would also have to be defined on the command flags in order the "load" flag used would also have to be defined on the command flags in order
for this code snipped to work. for this code snipped to work.
Currently only YAML and JSON files are supported but developers can add support Currently only the aboved specified formats are supported but developers can
for other input sources by implementing the altsrc.InputSourceContext for their add support for other input sources by implementing the
given sources. altsrc.InputSourceContext for their given sources.
Here is a more complete sample of a command using YAML support: Here is a more complete sample of a command using YAML support:
@ -963,7 +967,7 @@ setting `cli.VersionFlag`, e.g.:
<!-- { <!-- {
"args": ["&#45;&#45print-version"], "args": ["&#45;&#45print-version"],
"output": "partay version v19\\.99\\.0" "output": "partay version 19\\.99\\.0"
} --> } -->
``` go ``` go
package main package main
@ -992,7 +996,7 @@ Alternatively, the version printer at `cli.VersionPrinter` may be overridden, e.
<!-- { <!-- {
"args": ["&#45;&#45version"], "args": ["&#45;&#45version"],
"output": "version=v19\\.99\\.0 revision=fafafaf" "output": "version=19\\.99\\.0 revision=fafafaf"
} --> } -->
``` go ``` go
package main package main

310
altsrc/toml_command_test.go Normal file
View File

@ -0,0 +1,310 @@
// Disabling building of toml support in cases where golang is 1.0 or 1.1
// as the encoding library is not implemented or supported.
// +build go1.2
package altsrc
import (
"flag"
"io/ioutil"
"os"
"testing"
"github.com/urfave/cli"
)
func TestCommandTomFileTest(t *testing.T) {
app := (&cli.App{})
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte("test = 15"), 0666)
defer os.Remove("current.toml")
test := []string{"test-cmd", "--load", "current.toml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 15)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "test"}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandTomlFileTestGlobalEnvVarWins(t *testing.T) {
app := (&cli.App{})
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte("test = 15"), 0666)
defer os.Remove("current.toml")
os.Setenv("THE_TEST", "10")
defer os.Setenv("THE_TEST", "")
test := []string{"test-cmd", "--load", "current.toml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 10)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "test", EnvVars: []string{"THE_TEST"}}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandTomlFileTestGlobalEnvVarWinsNested(t *testing.T) {
app := (&cli.App{})
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666)
defer os.Remove("current.toml")
os.Setenv("THE_TEST", "10")
defer os.Setenv("THE_TEST", "")
test := []string{"test-cmd", "--load", "current.toml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("top.test")
expect(t, val, 10)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "top.test", EnvVars: []string{"THE_TEST"}}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandTomlFileTestSpecifiedFlagWins(t *testing.T) {
app := (&cli.App{})
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte("test = 15"), 0666)
defer os.Remove("current.toml")
test := []string{"test-cmd", "--load", "current.toml", "--test", "7"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 7)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "test"}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandTomlFileTestSpecifiedFlagWinsNested(t *testing.T) {
app := (&cli.App{})
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte(`[top]
test = 15`), 0666)
defer os.Remove("current.toml")
test := []string{"test-cmd", "--load", "current.toml", "--top.test", "7"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("top.test")
expect(t, val, 7)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "top.test"}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandTomlFileTestDefaultValueFileWins(t *testing.T) {
app := (&cli.App{})
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte("test = 15"), 0666)
defer os.Remove("current.toml")
test := []string{"test-cmd", "--load", "current.toml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 15)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "test", Value: 7}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandTomlFileTestDefaultValueFileWinsNested(t *testing.T) {
app := (&cli.App{})
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666)
defer os.Remove("current.toml")
test := []string{"test-cmd", "--load", "current.toml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("top.test")
expect(t, val, 15)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "top.test", Value: 7}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandTomlFileFlagHasDefaultGlobalEnvTomlSetGlobalEnvWins(t *testing.T) {
app := (&cli.App{})
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte("test = 15"), 0666)
defer os.Remove("current.toml")
os.Setenv("THE_TEST", "11")
defer os.Setenv("THE_TEST", "")
test := []string{"test-cmd", "--load", "current.toml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 11)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "test", Value: 7, EnvVars: []string{"THE_TEST"}}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandTomlFileFlagHasDefaultGlobalEnvTomlSetGlobalEnvWinsNested(t *testing.T) {
app := (&cli.App{})
set := flag.NewFlagSet("test", 0)
ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666)
defer os.Remove("current.toml")
os.Setenv("THE_TEST", "11")
defer os.Setenv("THE_TEST", "")
test := []string{"test-cmd", "--load", "current.toml"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("top.test")
expect(t, val, 11)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "top.test", Value: 7, EnvVars: []string{"THE_TEST"}}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}

113
altsrc/toml_file_loader.go Normal file
View File

@ -0,0 +1,113 @@
// Disabling building of toml support in cases where golang is 1.0 or 1.1
// as the encoding library is not implemented or supported.
// +build go1.2
package altsrc
import (
"fmt"
"reflect"
"github.com/BurntSushi/toml"
"github.com/urfave/cli"
)
type tomlMap struct {
Map map[interface{}]interface{}
}
func unmarshalMap(i interface{}) (ret map[interface{}]interface{}, err error) {
ret = make(map[interface{}]interface{})
m := i.(map[string]interface{})
for key, val := range m {
v := reflect.ValueOf(val)
switch v.Kind() {
case reflect.Bool:
ret[key] = val.(bool)
case reflect.String:
ret[key] = val.(string)
case reflect.Int:
ret[key] = int(val.(int))
case reflect.Int8:
ret[key] = int(val.(int8))
case reflect.Int16:
ret[key] = int(val.(int16))
case reflect.Int32:
ret[key] = int(val.(int32))
case reflect.Int64:
ret[key] = int(val.(int64))
case reflect.Uint:
ret[key] = int(val.(uint))
case reflect.Uint8:
ret[key] = int(val.(uint8))
case reflect.Uint16:
ret[key] = int(val.(uint16))
case reflect.Uint32:
ret[key] = int(val.(uint32))
case reflect.Uint64:
ret[key] = int(val.(uint64))
case reflect.Float32:
ret[key] = float64(val.(float32))
case reflect.Float64:
ret[key] = float64(val.(float64))
case reflect.Map:
if tmp, err := unmarshalMap(val); err == nil {
ret[key] = tmp
} else {
return nil, err
}
case reflect.Array:
fallthrough // [todo] - Support array type
default:
return nil, fmt.Errorf("Unsupported: type = %#v", v.Kind())
}
}
return ret, nil
}
func (self *tomlMap) UnmarshalTOML(i interface{}) error {
if tmp, err := unmarshalMap(i); err == nil {
self.Map = tmp
} else {
return err
}
return nil
}
type tomlSourceContext struct {
FilePath string
}
// NewTomlSourceFromFile creates a new TOML InputSourceContext from a filepath.
func NewTomlSourceFromFile(file string) (InputSourceContext, error) {
tsc := &tomlSourceContext{FilePath: file}
var results tomlMap = tomlMap{}
if err := readCommandToml(tsc.FilePath, &results); err != nil {
return nil, fmt.Errorf("Unable to load TOML file '%s': inner error: \n'%v'", tsc.FilePath, err.Error())
}
return &MapInputSource{valueMap: results.Map}, nil
}
// NewTomlSourceFromFlagFunc creates a new TOML InputSourceContext from a provided flag name and source context.
func NewTomlSourceFromFlagFunc(flagFileName string) func(context *cli.Context) (InputSourceContext, error) {
return func(context *cli.Context) (InputSourceContext, error) {
filePath := context.String(flagFileName)
return NewTomlSourceFromFile(filePath)
}
}
func readCommandToml(filePath string, container interface{}) (err error) {
b, err := loadDataFrom(filePath)
if err != nil {
return err
}
err = toml.Unmarshal(b, container)
if err != nil {
return err
}
err = nil
return
}

View File

@ -13,6 +13,19 @@ import (
"testing" "testing"
) )
var (
lastExitCode = 0
fakeOsExiter = func(rc int) {
lastExitCode = rc
}
fakeErrWriter = &bytes.Buffer{}
)
func init() {
OsExiter = fakeOsExiter
ErrWriter = fakeErrWriter
}
type opCounts struct { type opCounts struct {
Total, BashComplete, OnUsageError, Before, CommandNotFound, Action, After, SubCommand int Total, BashComplete, OnUsageError, Before, CommandNotFound, Action, After, SubCommand int
} }

View File

@ -98,5 +98,11 @@ func HandleExitCoder(err error) {
for _, merr := range multiErr.Errors() { for _, merr := range multiErr.Errors() {
HandleExitCoder(merr) HandleExitCoder(merr)
} }
return
} }
if err.Error() != "" {
fmt.Fprintln(ErrWriter, err)
}
OsExiter(1)
} }

View File

@ -1,8 +1,8 @@
package cli package cli
import ( import (
"bytes"
"errors" "errors"
"os"
"testing" "testing"
) )
@ -15,7 +15,7 @@ func TestHandleExitCoder_nil(t *testing.T) {
called = true called = true
} }
defer func() { OsExiter = os.Exit }() defer func() { OsExiter = fakeOsExiter }()
HandleExitCoder(nil) HandleExitCoder(nil)
@ -32,7 +32,7 @@ func TestHandleExitCoder_ExitCoder(t *testing.T) {
called = true called = true
} }
defer func() { OsExiter = os.Exit }() defer func() { OsExiter = fakeOsExiter }()
HandleExitCoder(Exit("galactic perimeter breach", 9)) HandleExitCoder(Exit("galactic perimeter breach", 9))
@ -49,7 +49,7 @@ func TestHandleExitCoder_MultiErrorWithExitCoder(t *testing.T) {
called = true called = true
} }
defer func() { OsExiter = os.Exit }() defer func() { OsExiter = fakeOsExiter }()
exitErr := Exit("galactic perimeter breach", 9) exitErr := Exit("galactic perimeter breach", 9)
err := newMultiError(errors.New("wowsa"), errors.New("egad"), exitErr) err := newMultiError(errors.New("wowsa"), errors.New("egad"), exitErr)
@ -58,3 +58,49 @@ func TestHandleExitCoder_MultiErrorWithExitCoder(t *testing.T) {
expect(t, exitCode, 9) expect(t, exitCode, 9)
expect(t, called, true) expect(t, called, true)
} }
func TestHandleExitCoder_ErrorWithMessage(t *testing.T) {
exitCode := 0
called := false
OsExiter = func(rc int) {
exitCode = rc
called = true
}
ErrWriter = &bytes.Buffer{}
defer func() {
OsExiter = fakeOsExiter
ErrWriter = fakeErrWriter
}()
err := errors.New("gourd havens")
HandleExitCoder(err)
expect(t, exitCode, 1)
expect(t, called, true)
expect(t, ErrWriter.(*bytes.Buffer).String(), "gourd havens\n")
}
func TestHandleExitCoder_ErrorWithoutMessage(t *testing.T) {
exitCode := 0
called := false
OsExiter = func(rc int) {
exitCode = rc
called = true
}
ErrWriter = &bytes.Buffer{}
defer func() {
OsExiter = fakeOsExiter
ErrWriter = fakeErrWriter
}()
err := errors.New("")
HandleExitCoder(err)
expect(t, exitCode, 1)
expect(t, called, true)
expect(t, ErrWriter.(*bytes.Buffer).String(), "")
}