From 3fc3cd64854e64d2b772631a938083e41269c859 Mon Sep 17 00:00:00 2001 From: Yasuhiro KANDA Date: Fri, 22 Jul 2016 22:45:05 +0900 Subject: [PATCH 01/21] Add TOML config file loader --- altsrc/toml_command_test.go | 310 ++++++++++++++++++++++++++++++++++++ altsrc/toml_file_loader.go | 113 +++++++++++++ 2 files changed, 423 insertions(+) create mode 100644 altsrc/toml_command_test.go create mode 100644 altsrc/toml_file_loader.go diff --git a/altsrc/toml_command_test.go b/altsrc/toml_command_test.go new file mode 100644 index 0000000..c887ab2 --- /dev/null +++ b/altsrc/toml_command_test.go @@ -0,0 +1,310 @@ +// Disabling building of tom 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.NewApp() + 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.NewApp() + 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", EnvVar: "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.NewApp() + 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", EnvVar: "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.NewApp() + 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.NewApp() + 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.NewApp() + 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.NewApp() + 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.NewApp() + 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, EnvVar: "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.NewApp() + 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, EnvVar: "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..64250a3 --- /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" + "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 +} From 71a99921b4ed75328f4f3eb887d4718161ad3017 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sat, 23 Jul 2016 17:52:19 -0400 Subject: [PATCH 02/21] Bump tested go versions and (maybe?) take advantage of support for N.x version syntax --- .travis.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 47f25dc..93d3a2e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,18 +7,18 @@ 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.x +- 1.5.x +- 1.6.x - master matrix: allow_failures: - go: master include: - - go: 1.6.2 + - go: 1.6.x os: osx before_script: From 76fb6d2ab73b2919dd9714784b44389508da96f0 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sat, 23 Jul 2016 18:16:45 -0400 Subject: [PATCH 03/21] Pin testing on go 1.4.2 since 1.4.3 is lacking `vet` --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 93d3a2e..d1d820d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ cache: go: - 1.2.x - 1.3.x -- 1.4.x +- 1.4.2 - 1.5.x - 1.6.x - master @@ -23,7 +23,7 @@ matrix: before_script: - 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 npm install markdown-toc ; fi From dd253d122c836210efb731095e85876228f41708 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 24 Jul 2016 11:57:51 -0400 Subject: [PATCH 04/21] Write err to stderr, exit 1 if err != "" Closes #475 --- app_test.go | 13 ++++++++++++ errors.go | 6 ++++++ errors_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/app_test.go b/app_test.go index b0b02e6..23c8aa6 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, BashComplete, OnUsageError, Before, CommandNotFound, Action, After, SubCommand int } diff --git a/errors.go b/errors.go index ea551be..15ac790 100644 --- a/errors.go +++ b/errors.go @@ -88,5 +88,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 8f5f284..e357dc4 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(NewExitError("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 := NewExitError("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, 0) + expect(t, called, false) + expect(t, ErrWriter.(*bytes.Buffer).String(), "") +} From e9688813e4ffad908f2e8a3218d852dc293d8529 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 24 Jul 2016 12:09:05 -0400 Subject: [PATCH 05/21] Refine error handling behavior so that exit 1 happens as long as error is non-nil --- CHANGELOG.md | 1 + errors.go | 2 +- errors_test.go | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4976635..26dd564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## [Unreleased] ### Added - Flag type code generation via `go generate` +- Write to stderr and exit 1 if action returns non-nil error ### Changed - Raise minimum tested/supported Go version to 1.2+ diff --git a/errors.go b/errors.go index 15ac790..c7d8c2f 100644 --- a/errors.go +++ b/errors.go @@ -93,6 +93,6 @@ func HandleExitCoder(err error) { if err.Error() != "" { fmt.Fprintln(ErrWriter, err) - OsExiter(1) } + OsExiter(1) } diff --git a/errors_test.go b/errors_test.go index e357dc4..04df031 100644 --- a/errors_test.go +++ b/errors_test.go @@ -100,7 +100,7 @@ func TestHandleExitCoder_ErrorWithoutMessage(t *testing.T) { err := errors.New("") HandleExitCoder(err) - expect(t, exitCode, 0) - expect(t, called, false) + expect(t, exitCode, 1) + expect(t, called, true) expect(t, ErrWriter.(*bytes.Buffer).String(), "") } From c59ec842c11a6ceb3dd44aa4e6def4320a9b79c2 Mon Sep 17 00:00:00 2001 From: Yi EungJun Date: Mon, 25 Jul 2016 20:09:41 +0900 Subject: [PATCH 06/21] README: Remove unnecessary 'v' from version numbers 'partay version v19.99.0' sounds 'partay version version 19.99.0' because 'v' stands for 'version'. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0ffa92f..615e95d 100644 --- a/README.md +++ b/README.md @@ -953,7 +953,7 @@ setting `cli.VersionFlag`, e.g.: ``` go package main @@ -972,7 +972,7 @@ func main() { app := cli.NewApp() app.Name = "partay" - app.Version = "v19.99.0" + app.Version = "19.99.0" app.Run(os.Args) } ``` @@ -981,7 +981,7 @@ Alternatively, the version printer at `cli.VersionPrinter` may be overridden, e. ``` go package main @@ -1004,7 +1004,7 @@ func main() { app := cli.NewApp() app.Name = "partay" - app.Version = "v19.99.0" + app.Version = "19.99.0" app.Run(os.Args) } ``` @@ -1083,7 +1083,7 @@ func (g *genericType) String() string { func main() { app := cli.NewApp() app.Name = "kənˈtrīv" - app.Version = "v19.99.0" + app.Version = "19.99.0" app.Compiled = time.Now() app.Authors = []cli.Author{ cli.Author{ From 812de9e2507c3ec9ce2750b67e5efe8cc643c0f4 Mon Sep 17 00:00:00 2001 From: kandayasu Date: Tue, 26 Jul 2016 01:37:03 +0900 Subject: [PATCH 07/21] type "TomlMap" to private (rename TomlMap -> tomlMap) --- altsrc/toml_file_loader.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/altsrc/toml_file_loader.go b/altsrc/toml_file_loader.go index 64250a3..bc2c11d 100644 --- a/altsrc/toml_file_loader.go +++ b/altsrc/toml_file_loader.go @@ -13,7 +13,7 @@ import ( "github.com/urfave/cli" ) -type TomlMap struct { +type tomlMap struct { Map map[interface{}]interface{} } @@ -66,7 +66,7 @@ func unmarshalMap(i interface{}) (ret map[interface{}]interface{}, err error) { return ret, nil } -func (self *TomlMap) UnmarshalTOML(i interface{}) error { +func (self *tomlMap) UnmarshalTOML(i interface{}) error { if tmp, err := unmarshalMap(i); err == nil { self.Map = tmp } else { @@ -82,7 +82,7 @@ type tomlSourceContext struct { // NewTomlSourceFromFile creates a new TOML InputSourceContext from a filepath. func NewTomlSourceFromFile(file string) (InputSourceContext, error) { tsc := &tomlSourceContext{FilePath: file} - var results TomlMap = TomlMap{} + 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()) } From 36b7d89bedc1626a65c03df4f9fad9d4a96a4354 Mon Sep 17 00:00:00 2001 From: kandayasu Date: Tue, 26 Jul 2016 01:37:18 +0900 Subject: [PATCH 08/21] Fix typo --- altsrc/toml_command_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/altsrc/toml_command_test.go b/altsrc/toml_command_test.go index c887ab2..1d91d56 100644 --- a/altsrc/toml_command_test.go +++ b/altsrc/toml_command_test.go @@ -1,4 +1,4 @@ -// Disabling building of tom support in cases where golang is 1.0 or 1.1 +// 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 From b616f6088660d2eaa33739718f0583f8d467a178 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Sun, 31 Jul 2016 12:11:52 -0700 Subject: [PATCH 09/21] Note TOML support in README and CHANGELOG --- CHANGELOG.md | 1 + README.md | 16 ++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26dd564..8b0d0ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ### 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/README.md b/README.md index 615e95d..31b25b8 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ 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) * [Subcommands](#subcommands) * [Subcommands categories](#subcommands-categories) * [Exit code](#exit-code) @@ -513,10 +513,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: @@ -538,9 +542,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 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: From 6c1f51aa95e7966c49a5211ffabe337bf4d2f3fb Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Sun, 31 Jul 2016 14:14:46 -0700 Subject: [PATCH 10/21] Fix context.(Global)IsSet to respect environment variables This appeared to be the least messy approach to hack in support for IsSet also checking environment variables to see if a particular cli.Flag was set without making backwards incompatible changes to the interface. I intend to fix this more properly in v2, probably by adding another method to the cli.Flag interface to push the responsibility down as it occurred to me that it was really the `Flag`s themselves that offer support for configuration via the environment as opposed to the `context` or other supporting structures. This opens the door for the anything implementing the `Flag` interface to have additional sources of input while still supporting `context.IsSet`. --- context.go | 79 ++++++++++++++++++++++++++++++++++++++----------- context_test.go | 60 +++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 18 deletions(-) diff --git a/context.go b/context.go index 14ad3f7..5ea4f8f 100644 --- a/context.go +++ b/context.go @@ -3,6 +3,8 @@ package cli import ( "errors" "flag" + "os" + "reflect" "strings" ) @@ -11,12 +13,11 @@ import ( // can be used to retrieve context-specific Args and // parsed command-line options. type Context struct { - App *App - Command Command - flagSet *flag.FlagSet - setFlags map[string]bool - globalSetFlags map[string]bool - parentContext *Context + App *App + Command Command + flagSet *flag.FlagSet + setFlags map[string]bool + parentContext *Context } // NewContext creates a new context. For use in when invoking an App or Command action. @@ -43,28 +44,70 @@ func (c *Context) GlobalSet(name, value string) error { func (c *Context) IsSet(name string) bool { if c.setFlags == nil { c.setFlags = make(map[string]bool) + c.flagSet.Visit(func(f *flag.Flag) { c.setFlags[f.Name] = true }) + + c.flagSet.VisitAll(func(f *flag.Flag) { + if _, ok := c.setFlags[f.Name]; ok { + return + } + c.setFlags[f.Name] = false + }) + + // XXX hack to support IsSet for flags with EnvVar + // + // There isn't an easy way to do this with the current implementation since + // whether a flag was set via an environment variable is very difficult to + // determine here. Instead, we intend to introduce a backwards incompatible + // change in version 2 to add `IsSet` to the Flag interface to push the + // responsibility closer to where the information required to determine + // whether a flag is set by non-standard means such as environment + // variables is avaliable. + // + // See https://github.com/urfave/cli/issues/294 for additional discussion + flags := c.Command.Flags + if c.Command.Name == "" { // cannot == Command{} since it contains slice types + if c.App != nil { + flags = c.App.Flags + } + } + for _, f := range flags { + eachName(f.GetName(), func(name string) { + if isSet, ok := c.setFlags[name]; isSet || !ok { + return + } + + envVars := reflect.ValueOf(f).FieldByName("EnvVar").String() + + eachName(envVars, func(envVar string) { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + c.setFlags[name] = true + return + } + }) + }) + } } - return c.setFlags[name] == true + + return c.setFlags[name] } // GlobalIsSet determines if the global flag was actually set func (c *Context) GlobalIsSet(name string) bool { - if c.globalSetFlags == nil { - c.globalSetFlags = make(map[string]bool) - ctx := c - if ctx.parentContext != nil { - ctx = ctx.parentContext - } - for ; ctx != nil && c.globalSetFlags[name] == false; ctx = ctx.parentContext { - ctx.flagSet.Visit(func(f *flag.Flag) { - c.globalSetFlags[f.Name] = true - }) + ctx := c + if ctx.parentContext != nil { + ctx = ctx.parentContext + } + + for ; ctx != nil; ctx = ctx.parentContext { + if ctx.IsSet(name) { + return true } } - return c.globalSetFlags[name] + return false } // FlagNames returns a slice of flag names used in this context. diff --git a/context_test.go b/context_test.go index 5c68fdd..0cf84d1 100644 --- a/context_test.go +++ b/context_test.go @@ -2,6 +2,7 @@ package cli import ( "flag" + "os" "testing" "time" ) @@ -180,6 +181,33 @@ func TestContext_IsSet(t *testing.T) { expect(t, c.IsSet("myflagGlobal"), false) } +// XXX Corresponds to hack in context.IsSet for flags with EnvVar field +// Should be moved to `flag_test` in v2 +func TestContext_IsSet_fromEnv(t *testing.T) { + var timeoutIsSet, tIsSet, noEnvVarIsSet, nIsSet bool + + os.Clearenv() + os.Setenv("APP_TIMEOUT_SECONDS", "15.5") + a := App{ + Flags: []Flag{ + Float64Flag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"}, + Float64Flag{Name: "no-env-var, n"}, + }, + Action: func(ctx *Context) error { + timeoutIsSet = ctx.IsSet("timeout") + tIsSet = ctx.IsSet("t") + noEnvVarIsSet = ctx.IsSet("no-env-var") + nIsSet = ctx.IsSet("n") + return nil + }, + } + a.Run([]string{"run"}) + expect(t, timeoutIsSet, true) + expect(t, tIsSet, true) + expect(t, noEnvVarIsSet, false) + expect(t, nIsSet, false) +} + func TestContext_GlobalIsSet(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Bool("myflag", false, "doc") @@ -199,6 +227,38 @@ func TestContext_GlobalIsSet(t *testing.T) { expect(t, c.GlobalIsSet("bogusGlobal"), false) } +// XXX Corresponds to hack in context.IsSet for flags with EnvVar field +// Should be moved to `flag_test` in v2 +func TestContext_GlobalIsSet_fromEnv(t *testing.T) { + var timeoutIsSet, tIsSet, noEnvVarIsSet, nIsSet bool + + os.Clearenv() + os.Setenv("APP_TIMEOUT_SECONDS", "15.5") + a := App{ + Flags: []Flag{ + Float64Flag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"}, + Float64Flag{Name: "no-env-var, n"}, + }, + Commands: []Command{ + { + Name: "hello", + Action: func(ctx *Context) error { + timeoutIsSet = ctx.GlobalIsSet("timeout") + tIsSet = ctx.GlobalIsSet("t") + noEnvVarIsSet = ctx.GlobalIsSet("no-env-var") + nIsSet = ctx.GlobalIsSet("n") + return nil + }, + }, + }, + } + a.Run([]string{"run", "hello"}) + expect(t, timeoutIsSet, true) + expect(t, tIsSet, true) + expect(t, noEnvVarIsSet, false) + expect(t, nIsSet, false) +} + func TestContext_NumFlags(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Bool("myflag", false, "doc") From 168c95418e66e019fe17b8f4f5c45aa62ff80e23 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Sun, 31 Jul 2016 20:09:57 -0700 Subject: [PATCH 11/21] Ensure that EnvVar struct field exists before interrogating it Otherwise you end up with `` which, in practice, would probably work, but this is cleaner. --- context.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/context.go b/context.go index 5ea4f8f..15570c5 100644 --- a/context.go +++ b/context.go @@ -79,9 +79,12 @@ func (c *Context) IsSet(name string) bool { return } - envVars := reflect.ValueOf(f).FieldByName("EnvVar").String() + envVarValue := reflect.ValueOf(f).FieldByName("EnvVar") + if !envVarValue.IsValid() { + return + } - eachName(envVars, func(envVar string) { + eachName(envVarValue.String(), func(envVar string) { envVar = strings.TrimSpace(envVar) if envVal := os.Getenv(envVar); envVal != "" { c.setFlags[name] = true From c5d3a341c4d34056d620e638076101b23b0cd806 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 22 Aug 2016 14:56:38 -0400 Subject: [PATCH 12/21] Adjust some gfmrun output matches which I assume became mismatched during merge --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f5e9f63..2f0980e 100644 --- a/README.md +++ b/README.md @@ -967,7 +967,7 @@ setting `cli.VersionFlag`, e.g.: ``` go package main @@ -996,7 +996,7 @@ Alternatively, the version printer at `cli.VersionPrinter` may be overridden, e. ``` go package main From c0cf41eb54ec35f7c325e9b42aaec3c33a36194f Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 22 Aug 2016 15:26:33 -0400 Subject: [PATCH 13/21] Skip gfmrun installation and tests below go1.3 --- .travis.yml | 2 +- runtests | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d1d820d..a6a1386 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ matrix: os: osx before_script: -- go get github.com/urfave/gfmrun/... +- go get github.com/urfave/gfmrun/... || true - go get golang.org/x/tools/... || true - if [ ! -f node_modules/.bin/markdown-toc ] ; then npm install markdown-toc ; diff --git a/runtests b/runtests index e13faf7..ee22bde 100755 --- a/runtests +++ b/runtests @@ -57,6 +57,10 @@ def _test(): 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']) From 812fa64d8e8a5e70a2d23ccd7293ac196ec42f43 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 22 Aug 2016 17:54:51 -0400 Subject: [PATCH 14/21] Skip migrations on go < 1.3 because there won't be a `gfmrun` around to extract examples --- runtests | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/runtests b/runtests index 8a2d3cf..bcd8f4c 100755 --- a/runtests +++ b/runtests @@ -75,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') ) From 3a3228c0e40e35d7ebe5ce1e226bc5541552c8d0 Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Wed, 24 Aug 2016 14:57:01 -0400 Subject: [PATCH 15/21] Abstract dependency and test execution with Makefile Abstract the `runtests` script with a makefile, and update travis tests to use makefile abstraction. --- .travis.yml | 13 +------------ GNUmakefile | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 12 deletions(-) create mode 100644 GNUmakefile diff --git a/.travis.yml b/.travis.yml index 5a45f06..41697ed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,11 +26,6 @@ matrix: before_script: - $pip_install flake8 -- go get github.com/urfave/gfmrun/... || true -- go get golang.org/x/tools/... || 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/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 From b93207160fd4263fd12b9ee6d7f0454fde74e246 Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Sat, 27 Aug 2016 19:14:15 -0400 Subject: [PATCH 16/21] Update altsrc imports to gopkg.in Updates altsrc imports to use `gopkg.in/urfave/cli.v2` Fixes: #505 --- altsrc/flag.go | 2 +- altsrc/flag_generated.go | 2 +- altsrc/flag_test.go | 2 +- altsrc/input_source_context.go | 2 +- altsrc/json_command_test.go | 2 +- altsrc/json_source_context.go | 2 +- altsrc/map_input_source.go | 2 +- altsrc/toml_command_test.go | 2 +- altsrc/toml_file_loader.go | 2 +- altsrc/yaml_command_test.go | 2 +- altsrc/yaml_file_loader.go | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) 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 index 139aa37..2f28d22 100644 --- a/altsrc/toml_command_test.go +++ b/altsrc/toml_command_test.go @@ -11,7 +11,7 @@ import ( "os" "testing" - "github.com/urfave/cli" + "gopkg.in/urfave/cli.v2" ) func TestCommandTomFileTest(t *testing.T) { diff --git a/altsrc/toml_file_loader.go b/altsrc/toml_file_loader.go index bc2c11d..0fa2dbb 100644 --- a/altsrc/toml_file_loader.go +++ b/altsrc/toml_file_loader.go @@ -10,7 +10,7 @@ import ( "reflect" "github.com/BurntSushi/toml" - "github.com/urfave/cli" + "gopkg.in/urfave/cli.v2" ) type tomlMap struct { 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" ) From 2ab83fab2a96a383ee71cb04c2a9cb3c6d033a09 Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Sun, 28 Aug 2016 01:36:49 -0400 Subject: [PATCH 17/21] Force generation of flag types to use gopkg import --- generate-flag-types | 2 ++ 1 file changed, 2 insertions(+) diff --git a/generate-flag-types b/generate-flag-types index 6244ff9..aa51154 100755 --- a/generate-flag-types +++ b/generate-flag-types @@ -193,6 +193,8 @@ def _write_altsrc_flag_types(outfile, types): _fwrite(outfile, """\ package altsrc + import "gopkg.in/urfave/cli.v2" + // WARNING: This file is generated! """) From e0556cf9e8c79d43c2ba13679be64b2cb7b2d6c7 Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Sun, 28 Aug 2016 03:14:39 -0400 Subject: [PATCH 18/21] Add DefaultValue text for flags Allows a user to override the default value of a flag in the displayed help output. Ex: ``` cli.IntFlag{ Name: "foo, f", DefaultText: "random foo", Value: "bar", } ``` Running `(app.name) -h` will now yield: ``` --foo value (default: "random foo") ``` Fixes: #504 --- flag.go | 7 ++++- flag_generated.go | 73 ++++++++++++++++++++++++++------------------- flag_test.go | 15 ++++++---- generate-flag-types | 1 + 4 files changed, 60 insertions(+), 36 deletions(-) diff --git a/flag.go b/flag.go index 807c95f..29010f2 100644 --- a/flag.go +++ b/flag.go @@ -730,7 +730,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()) @@ -740,6 +739,12 @@ func stringifyFlag(f Flag) string { } } + helpText := fv.FieldByName("DefaultText") + if helpText.IsValid() && helpText.String() != "" { + needsPlaceholder = val.Kind() != reflect.Bool + defaultValueString = fmt.Sprintf(" (default: %q)", 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..be126f9 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..276cfc9 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']: From 7d56512ecc02936aab92a89dc8784c44fc614133 Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Tue, 30 Aug 2016 10:52:24 -0400 Subject: [PATCH 19/21] Add documentation, remove quotes by default --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ flag.go | 2 +- flag_test.go | 2 +- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2f0980e..43f2247 100644 --- a/README.md +++ b/README.md @@ -589,6 +589,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/flag.go b/flag.go index 29010f2..10adca8 100644 --- a/flag.go +++ b/flag.go @@ -742,7 +742,7 @@ func stringifyFlag(f Flag) string { helpText := fv.FieldByName("DefaultText") if helpText.IsValid() && helpText.String() != "" { needsPlaceholder = val.Kind() != reflect.Bool - defaultValueString = fmt.Sprintf(" (default: %q)", helpText.String()) + defaultValueString = fmt.Sprintf(" (default: %s)", helpText.String()) } if defaultValueString == " (default: )" { diff --git a/flag_test.go b/flag_test.go index be126f9..ccb1d45 100644 --- a/flag_test.go +++ b/flag_test.go @@ -69,7 +69,7 @@ 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\")" + expected := "--foo foo\tamount of foo requested (default: all of it)" output := flag.String() if output != expected { From 51cebd042ae6c88d8e3083ed0c2980ea94624fc3 Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Tue, 30 Aug 2016 10:56:28 -0400 Subject: [PATCH 20/21] Update TOC in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 43f2247..09f4a57 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ applications in an expressive way. + [Alternate Names](#alternate-names) + [Values from the Environment](#values-from-the-environment) + [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) From c8d66d7edab7375cabe5522889d318738c8345b1 Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Tue, 30 Aug 2016 11:12:22 -0400 Subject: [PATCH 21/21] Lowercase TOC to pass tests --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 09f4a57..f72b7cc 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ applications in an expressive way. + [Alternate Names](#alternate-names) + [Values from the Environment](#values-from-the-environment) + [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) + + [Default Values for help output](#default-values-for-help-output) * [Subcommands](#subcommands) * [Subcommands categories](#subcommands-categories) * [Exit code](#exit-code)