From 78d497e4cfaed53897f4194671c076077885d136 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 22 Sep 2014 23:24:08 -0400 Subject: [PATCH 1/2] Adding support for multiple env var "cascade" --- flag.go | 111 ++++++++++++++++++++++----------- flag_test.go | 170 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 237 insertions(+), 44 deletions(-) diff --git a/flag.go b/flag.go index b30bca3..ddd6ef8 100644 --- a/flag.go +++ b/flag.go @@ -74,8 +74,12 @@ func (f GenericFlag) String() string { func (f GenericFlag) Apply(set *flag.FlagSet) { val := f.Value if f.EnvVar != "" { - if envVal := os.Getenv(f.EnvVar); envVal != "" { - val.Set(envVal) + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + val.Set(envVal) + break + } } } @@ -118,12 +122,17 @@ func (f StringSliceFlag) String() string { func (f StringSliceFlag) Apply(set *flag.FlagSet) { if f.EnvVar != "" { - if envVal := os.Getenv(f.EnvVar); envVal != "" { - newVal := &StringSlice{} - for _, s := range strings.Split(envVal, ",") { - newVal.Set(s) + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + newVal := &StringSlice{} + for _, s := range strings.Split(envVal, ",") { + s = strings.TrimSpace(s) + newVal.Set(s) + } + f.Value = newVal + break } - f.Value = newVal } } @@ -172,15 +181,20 @@ func (f IntSliceFlag) String() string { func (f IntSliceFlag) Apply(set *flag.FlagSet) { if f.EnvVar != "" { - if envVal := os.Getenv(f.EnvVar); envVal != "" { - newVal := &IntSlice{} - for _, s := range strings.Split(envVal, ",") { - err := newVal.Set(s) - if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + newVal := &IntSlice{} + for _, s := range strings.Split(envVal, ",") { + s = strings.TrimSpace(s) + err := newVal.Set(s) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + } } + f.Value = newVal + break } - f.Value = newVal } } @@ -206,10 +220,14 @@ func (f BoolFlag) String() string { func (f BoolFlag) Apply(set *flag.FlagSet) { val := false if f.EnvVar != "" { - if envVal := os.Getenv(f.EnvVar); envVal != "" { - envValBool, err := strconv.ParseBool(envVal) - if err == nil { - val = envValBool + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + envValBool, err := strconv.ParseBool(envVal) + if err == nil { + val = envValBool + } + break } } } @@ -236,10 +254,14 @@ func (f BoolTFlag) String() string { func (f BoolTFlag) Apply(set *flag.FlagSet) { val := true if f.EnvVar != "" { - if envVal := os.Getenv(f.EnvVar); envVal != "" { - envValBool, err := strconv.ParseBool(envVal) - if err == nil { - val = envValBool + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + envValBool, err := strconv.ParseBool(envVal) + if err == nil { + val = envValBool + break + } } } } @@ -275,8 +297,12 @@ func (f StringFlag) String() string { func (f StringFlag) Apply(set *flag.FlagSet) { if f.EnvVar != "" { - if envVal := os.Getenv(f.EnvVar); envVal != "" { - f.Value = envVal + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + f.Value = envVal + break + } } } @@ -302,10 +328,14 @@ func (f IntFlag) String() string { func (f IntFlag) Apply(set *flag.FlagSet) { if f.EnvVar != "" { - if envVal := os.Getenv(f.EnvVar); envVal != "" { - envValInt, err := strconv.ParseUint(envVal, 10, 64) - if err == nil { - f.Value = int(envValInt) + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + envValInt, err := strconv.ParseUint(envVal, 10, 64) + if err == nil { + f.Value = int(envValInt) + break + } } } } @@ -332,10 +362,14 @@ func (f DurationFlag) String() string { func (f DurationFlag) Apply(set *flag.FlagSet) { if f.EnvVar != "" { - if envVal := os.Getenv(f.EnvVar); envVal != "" { - envValDuration, err := time.ParseDuration(envVal) - if err == nil { - f.Value = envValDuration + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + envValDuration, err := time.ParseDuration(envVal) + if err == nil { + f.Value = envValDuration + break + } } } } @@ -362,10 +396,13 @@ func (f Float64Flag) String() string { func (f Float64Flag) Apply(set *flag.FlagSet) { if f.EnvVar != "" { - if envVal := os.Getenv(f.EnvVar); envVal != "" { - envValFloat, err := strconv.ParseFloat(envVal, 10) - if err == nil { - f.Value = float64(envValFloat) + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + envValFloat, err := strconv.ParseFloat(envVal, 10) + if err == nil { + f.Value = float64(envValFloat) + } } } } @@ -404,7 +441,7 @@ func prefixedNames(fullName string) (prefixed string) { func withEnvHint(envVar, str string) string { envText := "" if envVar != "" { - envText = fmt.Sprintf(" [$%s]", envVar) + envText = fmt.Sprintf(" [$%s]", strings.Join(strings.Split(envVar, ","), ", $")) } return str + envText } diff --git a/flag_test.go b/flag_test.go index bc5059c..4f0ba55 100644 --- a/flag_test.go +++ b/flag_test.go @@ -54,7 +54,7 @@ func TestStringFlagHelpOutput(t *testing.T) { } func TestStringFlagWithEnvVarHelpOutput(t *testing.T) { - + os.Clearenv() os.Setenv("APP_FOO", "derp") for _, test := range stringFlagTests { flag := cli.StringFlag{Name: test.name, Value: test.value, EnvVar: "APP_FOO"} @@ -106,7 +106,7 @@ func TestStringSliceFlagHelpOutput(t *testing.T) { } func TestStringSliceFlagWithEnvVarHelpOutput(t *testing.T) { - + os.Clearenv() os.Setenv("APP_QWWX", "11,4") for _, test := range stringSliceFlagTests { flag := cli.StringSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_QWWX"} @@ -139,7 +139,7 @@ func TestIntFlagHelpOutput(t *testing.T) { } func TestIntFlagWithEnvVarHelpOutput(t *testing.T) { - + os.Clearenv() os.Setenv("APP_BAR", "2") for _, test := range intFlagTests { flag := cli.IntFlag{Name: test.name, EnvVar: "APP_BAR"} @@ -172,7 +172,7 @@ func TestDurationFlagHelpOutput(t *testing.T) { } func TestDurationFlagWithEnvVarHelpOutput(t *testing.T) { - + os.Clearenv() os.Setenv("APP_BAR", "2h3m6s") for _, test := range durationFlagTests { flag := cli.DurationFlag{Name: test.name, EnvVar: "APP_BAR"} @@ -212,7 +212,7 @@ func TestIntSliceFlagHelpOutput(t *testing.T) { } func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) { - + os.Clearenv() os.Setenv("APP_SMURF", "42,3") for _, test := range intSliceFlagTests { flag := cli.IntSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_SMURF"} @@ -245,7 +245,7 @@ func TestFloat64FlagHelpOutput(t *testing.T) { } func TestFloat64FlagWithEnvVarHelpOutput(t *testing.T) { - + os.Clearenv() os.Setenv("APP_BAZ", "99.4") for _, test := range float64FlagTests { flag := cli.Float64Flag{Name: test.name, EnvVar: "APP_BAZ"} @@ -280,7 +280,7 @@ func TestGenericFlagHelpOutput(t *testing.T) { } func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) { - + os.Clearenv() os.Setenv("APP_ZAP", "3") for _, test := range genericFlagTests { flag := cli.GenericFlag{Name: test.name, EnvVar: "APP_ZAP"} @@ -309,6 +309,7 @@ func TestParseMultiString(t *testing.T) { } func TestParseMultiStringFromEnv(t *testing.T) { + os.Clearenv() os.Setenv("APP_COUNT", "20") (&cli.App{ Flags: []cli.Flag{ @@ -325,6 +326,24 @@ func TestParseMultiStringFromEnv(t *testing.T) { }).Run([]string{"run"}) } +func TestParseMultiStringFromEnvCascade(t *testing.T) { + os.Clearenv() + os.Setenv("APP_COUNT", "20") + (&cli.App{ + Flags: []cli.Flag{ + cli.StringFlag{Name: "count, c", EnvVar: "COMPAT_COUNT,APP_COUNT"}, + }, + Action: func(ctx *cli.Context) { + if ctx.String("count") != "20" { + t.Errorf("main name not set") + } + if ctx.String("c") != "20" { + t.Errorf("short name not set") + } + }, + }).Run([]string{"run"}) +} + func TestParseMultiStringSlice(t *testing.T) { (&cli.App{ Flags: []cli.Flag{ @@ -342,6 +361,7 @@ func TestParseMultiStringSlice(t *testing.T) { } func TestParseMultiStringSliceFromEnv(t *testing.T) { + os.Clearenv() os.Setenv("APP_INTERVALS", "20,30,40") (&cli.App{ @@ -359,6 +379,25 @@ func TestParseMultiStringSliceFromEnv(t *testing.T) { }).Run([]string{"run"}) } +func TestParseMultiStringSliceFromEnvCascade(t *testing.T) { + os.Clearenv() + os.Setenv("APP_INTERVALS", "20,30,40") + + (&cli.App{ + Flags: []cli.Flag{ + cli.StringSliceFlag{Name: "intervals, i", Value: &cli.StringSlice{}, EnvVar: "COMPAT_INTERVALS,APP_INTERVALS"}, + }, + Action: func(ctx *cli.Context) { + if !reflect.DeepEqual(ctx.StringSlice("intervals"), []string{"20", "30", "40"}) { + t.Errorf("main name not set from env") + } + if !reflect.DeepEqual(ctx.StringSlice("i"), []string{"20", "30", "40"}) { + t.Errorf("short name not set from env") + } + }, + }).Run([]string{"run"}) +} + func TestParseMultiInt(t *testing.T) { a := cli.App{ Flags: []cli.Flag{ @@ -377,6 +416,7 @@ func TestParseMultiInt(t *testing.T) { } func TestParseMultiIntFromEnv(t *testing.T) { + os.Clearenv() os.Setenv("APP_TIMEOUT_SECONDS", "10") a := cli.App{ Flags: []cli.Flag{ @@ -394,6 +434,25 @@ func TestParseMultiIntFromEnv(t *testing.T) { a.Run([]string{"run"}) } +func TestParseMultiIntFromEnvCascade(t *testing.T) { + os.Clearenv() + os.Setenv("APP_TIMEOUT_SECONDS", "10") + a := cli.App{ + Flags: []cli.Flag{ + cli.IntFlag{Name: "timeout, t", EnvVar: "COMPAT_TIMEOUT_SECONDS,APP_TIMEOUT_SECONDS"}, + }, + Action: func(ctx *cli.Context) { + if ctx.Int("timeout") != 10 { + t.Errorf("main name not set") + } + if ctx.Int("t") != 10 { + t.Errorf("short name not set") + } + }, + } + a.Run([]string{"run"}) +} + func TestParseMultiIntSlice(t *testing.T) { (&cli.App{ Flags: []cli.Flag{ @@ -411,6 +470,7 @@ func TestParseMultiIntSlice(t *testing.T) { } func TestParseMultiIntSliceFromEnv(t *testing.T) { + os.Clearenv() os.Setenv("APP_INTERVALS", "20,30,40") (&cli.App{ @@ -428,6 +488,25 @@ func TestParseMultiIntSliceFromEnv(t *testing.T) { }).Run([]string{"run"}) } +func TestParseMultiIntSliceFromEnvCascade(t *testing.T) { + os.Clearenv() + os.Setenv("APP_INTERVALS", "20,30,40") + + (&cli.App{ + Flags: []cli.Flag{ + cli.IntSliceFlag{Name: "intervals, i", Value: &cli.IntSlice{}, EnvVar: "COMPAT_INTERVALS,APP_INTERVALS"}, + }, + Action: func(ctx *cli.Context) { + if !reflect.DeepEqual(ctx.IntSlice("intervals"), []int{20, 30, 40}) { + t.Errorf("main name not set from env") + } + if !reflect.DeepEqual(ctx.IntSlice("i"), []int{20, 30, 40}) { + t.Errorf("short name not set from env") + } + }, + }).Run([]string{"run"}) +} + func TestParseMultiFloat64(t *testing.T) { a := cli.App{ Flags: []cli.Flag{ @@ -446,6 +525,7 @@ func TestParseMultiFloat64(t *testing.T) { } func TestParseMultiFloat64FromEnv(t *testing.T) { + os.Clearenv() os.Setenv("APP_TIMEOUT_SECONDS", "15.5") a := cli.App{ Flags: []cli.Flag{ @@ -463,6 +543,25 @@ func TestParseMultiFloat64FromEnv(t *testing.T) { a.Run([]string{"run"}) } +func TestParseMultiFloat64FromEnvCascade(t *testing.T) { + os.Clearenv() + os.Setenv("APP_TIMEOUT_SECONDS", "15.5") + a := cli.App{ + Flags: []cli.Flag{ + cli.Float64Flag{Name: "timeout, t", EnvVar: "COMPAT_TIMEOUT_SECONDS,APP_TIMEOUT_SECONDS"}, + }, + Action: func(ctx *cli.Context) { + if ctx.Float64("timeout") != 15.5 { + t.Errorf("main name not set") + } + if ctx.Float64("t") != 15.5 { + t.Errorf("short name not set") + } + }, + } + a.Run([]string{"run"}) +} + func TestParseMultiBool(t *testing.T) { a := cli.App{ Flags: []cli.Flag{ @@ -481,6 +580,7 @@ func TestParseMultiBool(t *testing.T) { } func TestParseMultiBoolFromEnv(t *testing.T) { + os.Clearenv() os.Setenv("APP_DEBUG", "1") a := cli.App{ Flags: []cli.Flag{ @@ -498,6 +598,25 @@ func TestParseMultiBoolFromEnv(t *testing.T) { a.Run([]string{"run"}) } +func TestParseMultiBoolFromEnvCascade(t *testing.T) { + os.Clearenv() + os.Setenv("APP_DEBUG", "1") + a := cli.App{ + Flags: []cli.Flag{ + cli.BoolFlag{Name: "debug, d", EnvVar: "COMPAT_DEBUG,APP_DEBUG"}, + }, + Action: func(ctx *cli.Context) { + if ctx.Bool("debug") != true { + t.Errorf("main name not set from env") + } + if ctx.Bool("d") != true { + t.Errorf("short name not set from env") + } + }, + } + a.Run([]string{"run"}) +} + func TestParseMultiBoolT(t *testing.T) { a := cli.App{ Flags: []cli.Flag{ @@ -516,6 +635,7 @@ func TestParseMultiBoolT(t *testing.T) { } func TestParseMultiBoolTFromEnv(t *testing.T) { + os.Clearenv() os.Setenv("APP_DEBUG", "0") a := cli.App{ Flags: []cli.Flag{ @@ -533,6 +653,25 @@ func TestParseMultiBoolTFromEnv(t *testing.T) { a.Run([]string{"run"}) } +func TestParseMultiBoolTFromEnvCascade(t *testing.T) { + os.Clearenv() + os.Setenv("APP_DEBUG", "0") + a := cli.App{ + Flags: []cli.Flag{ + cli.BoolTFlag{Name: "debug, d", EnvVar: "COMPAT_DEBUG,APP_DEBUG"}, + }, + Action: func(ctx *cli.Context) { + if ctx.BoolT("debug") != false { + t.Errorf("main name not set from env") + } + if ctx.BoolT("d") != false { + t.Errorf("short name not set from env") + } + }, + } + a.Run([]string{"run"}) +} + type Parser [2]string func (p *Parser) Set(value string) error { @@ -569,6 +708,7 @@ func TestParseGeneric(t *testing.T) { } func TestParseGenericFromEnv(t *testing.T) { + os.Clearenv() os.Setenv("APP_SERVE", "20,30") a := cli.App{ Flags: []cli.Flag{ @@ -585,3 +725,19 @@ func TestParseGenericFromEnv(t *testing.T) { } a.Run([]string{"run"}) } + +func TestParseGenericFromEnvCascade(t *testing.T) { + os.Clearenv() + os.Setenv("APP_FOO", "99,2000") + a := cli.App{ + Flags: []cli.Flag{ + cli.GenericFlag{Name: "foos", Value: &Parser{}, EnvVar: "COMPAT_FOO,APP_FOO"}, + }, + Action: func(ctx *cli.Context) { + if !reflect.DeepEqual(ctx.Generic("foos"), &Parser{"99", "2000"}) { + t.Errorf("value not set from env") + } + }, + } + a.Run([]string{"run"}) +} From 1dce44d78134e0d59d0648ffd7ff7e685daad555 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 1 Dec 2014 22:46:57 -0500 Subject: [PATCH 2/2] Adding section in README about env var cascade as well as moving a line back up where it belongs --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e0fdace..0e8327b 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,8 @@ app.Flags = []cli.Flag { } ``` +That flag can then be set with `--lang spanish` or `-l spanish`. Note that giving two different forms of the same flag in the same command invocation is an error. + #### Values from the Environment You can also have the default value set from the environment via `EnvVar`. e.g. @@ -187,7 +189,18 @@ app.Flags = []cli.Flag { } ``` -That flag can then be set with `--lang spanish` or `-l spanish`. Note that giving two different forms of the same flag in the same command invocation is an error. +The `EnvVar` may also be given as a comma-delimited "cascade", where the first environment variable that resolves is used as the default. + +``` go +app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + EnvVar: "LEGACY_COMPAT_LANG,APP_LANG,LANG", + }, +} +``` ### Subcommands