diff --git a/.travis.yml b/.travis.yml index 2949d18..41697ed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,11 +7,11 @@ cache: - node_modules go: -- 1.2.2 -- 1.3.3 -- 1.4 -- 1.5.4 -- 1.6.2 +- 1.2.x +- 1.3.x +- 1.4.2 +- 1.5.x +- 1.6.x - master env: pip_install="pip install --user" @@ -20,17 +20,12 @@ matrix: allow_failures: - go: master include: - - go: 1.6.2 + - go: 1.6.x os: osx env: pip_install="sudo pip install" before_script: - $pip_install flake8 -- go get github.com/urfave/gfmrun/... -- go get golang.org/x/tools/cmd/goimports || true -- if [ ! -f node_modules/.bin/markdown-toc ] ; then - npm install markdown-toc ; - fi - mkdir -p ${GOPATH%%:*}/src/gopkg.in/urfave - rm -rvf ${GOPATH%%:*}/src/gopkg.in/urfave/cli.v2 - rm -rvf ${GOPATH%%:*}/pkg/*/gopkg.in/urfave/cli.v2.a @@ -38,10 +33,4 @@ before_script: script: - flake8 runtests cli-v1-to-v2 generate-flag-types -- ./runtests gen -- ./runtests vet -- ./runtests test -- ./runtests gfmrun -- ./cli-v1-to-v2 --selftest -- ./runtests migrations -- ./runtests toc +- make all diff --git a/CHANGELOG.md b/CHANGELOG.md index febed88..f5fc7f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ ## [Unreleased] - (1.x series) ### Added - 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 - Raise minimum tested/supported Go version to 1.2+ diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 0000000..4543b19 --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,37 @@ +default: test + +deps: + go get golang.org/x/tools/cmd/goimports || true + go get github.com/urfave/gfmrun/... || true + go list ./... \ + | xargs go list -f '{{ join .Deps "\n" }}{{ printf "\n" }}{{ join .TestImports "\n" }}' \ + | grep -v github.com/urfave/cli \ + | xargs go get + @if [ ! -f node_modules/.bin/markdown-toc ]; then \ + npm install markdown-toc ; \ + fi + +gen: deps + ./runtests gen + +vet: + ./runtests vet + +gfmrun: + ./runtests gfmrun + +v1-to-v2: + ./cli-v1-to-v2 --selftest + +migrations: + ./runtests migrations + +toc: + ./runtests toc + +test: deps + ./runtests test + +all: gen vet test gfmrun v1-to-v2 migrations toc + +.PHONY: default gen vet test gfmrun migrations toc v1-to-v2 deps all diff --git a/README.md b/README.md index e63b9f5..4fc1f55 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ applications in an expressive way. + [Placeholder Values](#placeholder-values) + [Alternate Names](#alternate-names) + [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) + + [Default Values for help output](#default-values-for-help-output) * [Subcommands](#subcommands) * [Subcommands categories](#subcommands-categories) * [Exit code](#exit-code) @@ -520,10 +521,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 -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 code would be added to wrap an existing cli.Flag like below: @@ -545,9 +550,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 for this code snipped to work. -Currently only YAML and JSON files are supported but developers can add support -for other input sources by implementing the altsrc.InputSourceContext for their -given sources. +Currently only the aboved specified formats are supported but developers can +add support for other input sources by implementing the +altsrc.InputSourceContext for their given sources. Here is a more complete sample of a command using YAML support: @@ -585,6 +590,48 @@ func main() { } ``` +#### Default Values for help output + +Sometimes it's useful to specify a flag's default help-text value within the flag declaration. This can be useful if the default value for a flag is a computed value. The default value can be set via the `DefaultText` struct field. + +For example this: + + +```go +package main + +import ( + "os" + + "gopkg.in/urfave/cli.v2" +) + +func main() { + app := &cli.App{ + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "port", + Usage: "Use a randomized port", + Value: 0, + DefaultText: "random", + }, + }, + } + + app.Run(os.Args) +} +``` + +Will result in help output like: + +``` +--port value Use a randomized port (default: random) +``` + + ### Subcommands Subcommands can be defined for a more git-like command line app. diff --git a/altsrc/flag.go b/altsrc/flag.go index c378ac4..879708c 100644 --- a/altsrc/flag.go +++ b/altsrc/flag.go @@ -5,7 +5,7 @@ import ( "os" "strconv" - "github.com/urfave/cli" + "gopkg.in/urfave/cli.v2" ) // FlagInputSourceExtension is an extension interface of cli.Flag that diff --git a/altsrc/flag_generated.go b/altsrc/flag_generated.go index 4edb0a7..7ca8dac 100644 --- a/altsrc/flag_generated.go +++ b/altsrc/flag_generated.go @@ -3,7 +3,7 @@ package altsrc import ( "flag" - "github.com/urfave/cli" + "gopkg.in/urfave/cli.v2" ) // WARNING: This file is generated! diff --git a/altsrc/flag_test.go b/altsrc/flag_test.go index e5b4c05..f077487 100644 --- a/altsrc/flag_test.go +++ b/altsrc/flag_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/urfave/cli" + "gopkg.in/urfave/cli.v2" ) type testApplyInputSource struct { diff --git a/altsrc/input_source_context.go b/altsrc/input_source_context.go index 56603cf..c45ba5c 100644 --- a/altsrc/input_source_context.go +++ b/altsrc/input_source_context.go @@ -3,7 +3,7 @@ package altsrc import ( "time" - "github.com/urfave/cli" + "gopkg.in/urfave/cli.v2" ) // InputSourceContext is an interface used to allow diff --git a/altsrc/json_command_test.go b/altsrc/json_command_test.go index fcca5fc..ccb5041 100644 --- a/altsrc/json_command_test.go +++ b/altsrc/json_command_test.go @@ -6,7 +6,7 @@ import ( "os" "testing" - "github.com/urfave/cli" + "gopkg.in/urfave/cli.v2" ) const ( diff --git a/altsrc/json_source_context.go b/altsrc/json_source_context.go index c027a53..a197d87 100644 --- a/altsrc/json_source_context.go +++ b/altsrc/json_source_context.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/urfave/cli" + "gopkg.in/urfave/cli.v2" ) // NewJSONSourceFromFlagFunc returns a func that takes a cli.Context diff --git a/altsrc/map_input_source.go b/altsrc/map_input_source.go index 68a749c..26d6e81 100644 --- a/altsrc/map_input_source.go +++ b/altsrc/map_input_source.go @@ -6,7 +6,7 @@ import ( "strings" "time" - "github.com/urfave/cli" + "gopkg.in/urfave/cli.v2" ) // MapInputSource implements InputSourceContext to return diff --git a/altsrc/toml_command_test.go b/altsrc/toml_command_test.go new file mode 100644 index 0000000..2f28d22 --- /dev/null +++ b/altsrc/toml_command_test.go @@ -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" + + "gopkg.in/urfave/cli.v2" +) + +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) +} diff --git a/altsrc/toml_file_loader.go b/altsrc/toml_file_loader.go new file mode 100644 index 0000000..0fa2dbb --- /dev/null +++ b/altsrc/toml_file_loader.go @@ -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" + "gopkg.in/urfave/cli.v2" +) + +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 +} diff --git a/altsrc/yaml_command_test.go b/altsrc/yaml_command_test.go index 2bcea40..5290e84 100644 --- a/altsrc/yaml_command_test.go +++ b/altsrc/yaml_command_test.go @@ -11,7 +11,7 @@ import ( "os" "testing" - "github.com/urfave/cli" + "gopkg.in/urfave/cli.v2" ) func TestCommandYamlFileTest(t *testing.T) { diff --git a/altsrc/yaml_file_loader.go b/altsrc/yaml_file_loader.go index b4e3365..ada90a8 100644 --- a/altsrc/yaml_file_loader.go +++ b/altsrc/yaml_file_loader.go @@ -12,7 +12,7 @@ import ( "net/url" "os" - "github.com/urfave/cli" + "gopkg.in/urfave/cli.v2" "gopkg.in/yaml.v2" ) diff --git a/app_test.go b/app_test.go index a7d325e..911e83f 100644 --- a/app_test.go +++ b/app_test.go @@ -13,6 +13,19 @@ import ( "testing" ) +var ( + lastExitCode = 0 + fakeOsExiter = func(rc int) { + lastExitCode = rc + } + fakeErrWriter = &bytes.Buffer{} +) + +func init() { + OsExiter = fakeOsExiter + ErrWriter = fakeErrWriter +} + type opCounts struct { Total, ShellComplete, OnUsageError, Before, CommandNotFound, Action, After, SubCommand int } diff --git a/errors.go b/errors.go index ba01537..549e1d3 100644 --- a/errors.go +++ b/errors.go @@ -98,5 +98,11 @@ func HandleExitCoder(err error) { for _, merr := range multiErr.Errors() { HandleExitCoder(merr) } + return + } + + if err.Error() != "" { + fmt.Fprintln(ErrWriter, err) } + OsExiter(1) } diff --git a/errors_test.go b/errors_test.go index 5b4981f..69f2b48 100644 --- a/errors_test.go +++ b/errors_test.go @@ -1,8 +1,8 @@ package cli import ( + "bytes" "errors" - "os" "testing" ) @@ -15,7 +15,7 @@ func TestHandleExitCoder_nil(t *testing.T) { called = true } - defer func() { OsExiter = os.Exit }() + defer func() { OsExiter = fakeOsExiter }() HandleExitCoder(nil) @@ -32,7 +32,7 @@ func TestHandleExitCoder_ExitCoder(t *testing.T) { called = true } - defer func() { OsExiter = os.Exit }() + defer func() { OsExiter = fakeOsExiter }() HandleExitCoder(Exit("galactic perimeter breach", 9)) @@ -49,7 +49,7 @@ func TestHandleExitCoder_MultiErrorWithExitCoder(t *testing.T) { called = true } - defer func() { OsExiter = os.Exit }() + defer func() { OsExiter = fakeOsExiter }() exitErr := Exit("galactic perimeter breach", 9) 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, 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(), "") +} diff --git a/flag.go b/flag.go index ec798fd..a38473a 100644 --- a/flag.go +++ b/flag.go @@ -736,7 +736,6 @@ func stringifyFlag(f Flag) string { needsPlaceholder := false defaultValueString := "" val := fv.FieldByName("Value") - if val.IsValid() { needsPlaceholder = val.Kind() != reflect.Bool defaultValueString = fmt.Sprintf(" (default: %v)", val.Interface()) @@ -746,6 +745,12 @@ func stringifyFlag(f Flag) string { } } + helpText := fv.FieldByName("DefaultText") + if helpText.IsValid() && helpText.String() != "" { + needsPlaceholder = val.Kind() != reflect.Bool + defaultValueString = fmt.Sprintf(" (default: %s)", helpText.String()) + } + if defaultValueString == " (default: )" { defaultValueString = "" } diff --git a/flag_generated.go b/flag_generated.go index e224fb8..1f48d9f 100644 --- a/flag_generated.go +++ b/flag_generated.go @@ -16,6 +16,7 @@ type BoolFlag struct { EnvVars []string Hidden bool Value bool + DefaultText string Destination *bool } @@ -59,6 +60,7 @@ type DurationFlag struct { EnvVars []string Hidden bool Value time.Duration + DefaultText string Destination *time.Duration } @@ -102,6 +104,7 @@ type Float64Flag struct { EnvVars []string Hidden bool Value float64 + DefaultText string Destination *float64 } @@ -139,12 +142,13 @@ func lookupFloat64(name string, set *flag.FlagSet) float64 { // GenericFlag is a flag with type Generic type GenericFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value Generic + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + Value Generic + DefaultText string } // String returns a readable representation of this value @@ -187,6 +191,7 @@ type Int64Flag struct { EnvVars []string Hidden bool Value int64 + DefaultText string Destination *int64 } @@ -230,6 +235,7 @@ type IntFlag struct { EnvVars []string Hidden bool Value int + DefaultText string Destination *int } @@ -267,12 +273,13 @@ func lookupInt(name string, set *flag.FlagSet) int { // IntSliceFlag is a flag with type *IntSlice type IntSliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value *IntSlice + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + Value *IntSlice + DefaultText string } // String returns a readable representation of this value @@ -309,12 +316,13 @@ func lookupIntSlice(name string, set *flag.FlagSet) []int { // Int64SliceFlag is a flag with type *Int64Slice type Int64SliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value *Int64Slice + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + Value *Int64Slice + DefaultText string } // String returns a readable representation of this value @@ -351,12 +359,13 @@ func lookupInt64Slice(name string, set *flag.FlagSet) []int64 { // Float64SliceFlag is a flag with type *Float64Slice type Float64SliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value *Float64Slice + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + Value *Float64Slice + DefaultText string } // String returns a readable representation of this value @@ -399,6 +408,7 @@ type StringFlag struct { EnvVars []string Hidden bool Value string + DefaultText string Destination *string } @@ -436,12 +446,13 @@ func lookupString(name string, set *flag.FlagSet) string { // StringSliceFlag is a flag with type *StringSlice type StringSliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value *StringSlice + Name string + Aliases []string + Usage string + EnvVars []string + Hidden bool + Value *StringSlice + DefaultText string } // String returns a readable representation of this value @@ -484,6 +495,7 @@ type Uint64Flag struct { EnvVars []string Hidden bool Value uint64 + DefaultText string Destination *uint64 } @@ -527,6 +539,7 @@ type UintFlag struct { EnvVars []string Hidden bool Value uint + DefaultText string Destination *uint } diff --git a/flag_test.go b/flag_test.go index e8c8bcd..ccb1d45 100644 --- a/flag_test.go +++ b/flag_test.go @@ -67,6 +67,16 @@ func TestStringFlagHelpOutput(t *testing.T) { } } +func TestStringFlagDefaultText(t *testing.T) { + flag := &StringFlag{Name: "foo", Aliases: nil, Usage: "amount of `foo` requested", Value: "none", DefaultText: "all of it"} + expected := "--foo foo\tamount of foo requested (default: all of it)" + output := flag.String() + + if output != expected { + t.Errorf("%q does not match %q", output, expected) + } +} + func TestStringFlagWithEnvVarHelpOutput(t *testing.T) { os.Clearenv() os.Setenv("APP_FOO", "derp") @@ -482,7 +492,6 @@ func TestFloat64FlagApply_SetsAllNames(t *testing.T) { expect(t, v, float64(43.33333)) } - var float64SliceFlagTests = []struct { name string aliases []string @@ -523,8 +532,6 @@ func TestFloat64SliceFlagWithEnvVarHelpOutput(t *testing.T) { } } - - var genericFlagTests = []struct { name string value Generic @@ -1100,7 +1107,6 @@ func TestParseMultiFloat64FromEnvCascade(t *testing.T) { a.Run([]string{"run"}) } - func TestParseMultiFloat64SliceFromEnv(t *testing.T) { os.Clearenv() os.Setenv("APP_INTERVALS", "0.1,-10.5") @@ -1141,7 +1147,6 @@ func TestParseMultiFloat64SliceFromEnvCascade(t *testing.T) { }).Run([]string{"run"}) } - func TestParseMultiBool(t *testing.T) { a := App{ Flags: []Flag{ diff --git a/generate-flag-types b/generate-flag-types index 6244ff9..3213dd0 100755 --- a/generate-flag-types +++ b/generate-flag-types @@ -145,6 +145,7 @@ def _write_cli_flag_types(outfile, types): EnvVars []string Hidden bool Value {type} + DefaultText string """.format(**typedef)) if typedef['dest']: @@ -193,6 +194,8 @@ def _write_altsrc_flag_types(outfile, types): _fwrite(outfile, """\ package altsrc + import "gopkg.in/urfave/cli.v2" + // WARNING: This file is generated! """) diff --git a/runtests b/runtests index 8e4f97f..bcd8f4c 100755 --- a/runtests +++ b/runtests @@ -61,6 +61,10 @@ def _test(): @_target def _gfmrun(): + go_version = check_output('go version'.split()).split()[2] + if go_version < 'go1.3': + print('runtests: skip on {}'.format(go_version), file=sys.stderr) + return _run(['gfmrun', '-c', str(_gfmrun_count()), '-s', 'README.md']) @@ -71,6 +75,11 @@ def _vet(): @_target def _migrations(): + go_version = check_output('go version'.split()).split()[2] + if go_version < 'go1.3': + print('runtests: skip on {}'.format(go_version), file=sys.stderr) + return + migration_script = os.path.abspath( os.environ.get('V1TOV2', './cli-v1-to-v2') )