Merge pull request #141 from meatballhat/multi-env-var

Adding support for multiple env var "cascade"
This commit is contained in:
Jesse Szwedko 2014-12-03 16:09:45 -05:00
commit fbda1ce02d
3 changed files with 251 additions and 45 deletions

View File

@ -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 #### Values from the Environment
You can also have the default value set from the environment via `EnvVar`. e.g. 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 ### Subcommands

111
flag.go
View File

@ -74,8 +74,12 @@ func (f GenericFlag) String() string {
func (f GenericFlag) Apply(set *flag.FlagSet) { func (f GenericFlag) Apply(set *flag.FlagSet) {
val := f.Value val := f.Value
if f.EnvVar != "" { if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" { for _, envVar := range strings.Split(f.EnvVar, ",") {
val.Set(envVal) 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) { func (f StringSliceFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" { if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" { for _, envVar := range strings.Split(f.EnvVar, ",") {
newVal := &StringSlice{} envVar = strings.TrimSpace(envVar)
for _, s := range strings.Split(envVal, ",") { if envVal := os.Getenv(envVar); envVal != "" {
newVal.Set(s) 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) { func (f IntSliceFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" { if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" { for _, envVar := range strings.Split(f.EnvVar, ",") {
newVal := &IntSlice{} envVar = strings.TrimSpace(envVar)
for _, s := range strings.Split(envVal, ",") { if envVal := os.Getenv(envVar); envVal != "" {
err := newVal.Set(s) newVal := &IntSlice{}
if err != nil { for _, s := range strings.Split(envVal, ",") {
fmt.Fprintf(os.Stderr, err.Error()) 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) { func (f BoolFlag) Apply(set *flag.FlagSet) {
val := false val := false
if f.EnvVar != "" { if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" { for _, envVar := range strings.Split(f.EnvVar, ",") {
envValBool, err := strconv.ParseBool(envVal) envVar = strings.TrimSpace(envVar)
if err == nil { if envVal := os.Getenv(envVar); envVal != "" {
val = envValBool 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) { func (f BoolTFlag) Apply(set *flag.FlagSet) {
val := true val := true
if f.EnvVar != "" { if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" { for _, envVar := range strings.Split(f.EnvVar, ",") {
envValBool, err := strconv.ParseBool(envVal) envVar = strings.TrimSpace(envVar)
if err == nil { if envVal := os.Getenv(envVar); envVal != "" {
val = envValBool 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) { func (f StringFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" { if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" { for _, envVar := range strings.Split(f.EnvVar, ",") {
f.Value = envVal 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) { func (f IntFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" { if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" { for _, envVar := range strings.Split(f.EnvVar, ",") {
envValInt, err := strconv.ParseUint(envVal, 10, 64) envVar = strings.TrimSpace(envVar)
if err == nil { if envVal := os.Getenv(envVar); envVal != "" {
f.Value = int(envValInt) 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) { func (f DurationFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" { if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" { for _, envVar := range strings.Split(f.EnvVar, ",") {
envValDuration, err := time.ParseDuration(envVal) envVar = strings.TrimSpace(envVar)
if err == nil { if envVal := os.Getenv(envVar); envVal != "" {
f.Value = envValDuration 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) { func (f Float64Flag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" { if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" { for _, envVar := range strings.Split(f.EnvVar, ",") {
envValFloat, err := strconv.ParseFloat(envVal, 10) envVar = strings.TrimSpace(envVar)
if err == nil { if envVal := os.Getenv(envVar); envVal != "" {
f.Value = float64(envValFloat) 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 { func withEnvHint(envVar, str string) string {
envText := "" envText := ""
if envVar != "" { if envVar != "" {
envText = fmt.Sprintf(" [$%s]", envVar) envText = fmt.Sprintf(" [$%s]", strings.Join(strings.Split(envVar, ","), ", $"))
} }
return str + envText return str + envText
} }

View File

@ -54,7 +54,7 @@ func TestStringFlagHelpOutput(t *testing.T) {
} }
func TestStringFlagWithEnvVarHelpOutput(t *testing.T) { func TestStringFlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
os.Setenv("APP_FOO", "derp") os.Setenv("APP_FOO", "derp")
for _, test := range stringFlagTests { for _, test := range stringFlagTests {
flag := cli.StringFlag{Name: test.name, Value: test.value, EnvVar: "APP_FOO"} 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) { func TestStringSliceFlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
os.Setenv("APP_QWWX", "11,4") os.Setenv("APP_QWWX", "11,4")
for _, test := range stringSliceFlagTests { for _, test := range stringSliceFlagTests {
flag := cli.StringSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_QWWX"} 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) { func TestIntFlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
os.Setenv("APP_BAR", "2") os.Setenv("APP_BAR", "2")
for _, test := range intFlagTests { for _, test := range intFlagTests {
flag := cli.IntFlag{Name: test.name, EnvVar: "APP_BAR"} flag := cli.IntFlag{Name: test.name, EnvVar: "APP_BAR"}
@ -172,7 +172,7 @@ func TestDurationFlagHelpOutput(t *testing.T) {
} }
func TestDurationFlagWithEnvVarHelpOutput(t *testing.T) { func TestDurationFlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
os.Setenv("APP_BAR", "2h3m6s") os.Setenv("APP_BAR", "2h3m6s")
for _, test := range durationFlagTests { for _, test := range durationFlagTests {
flag := cli.DurationFlag{Name: test.name, EnvVar: "APP_BAR"} flag := cli.DurationFlag{Name: test.name, EnvVar: "APP_BAR"}
@ -212,7 +212,7 @@ func TestIntSliceFlagHelpOutput(t *testing.T) {
} }
func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) { func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
os.Setenv("APP_SMURF", "42,3") os.Setenv("APP_SMURF", "42,3")
for _, test := range intSliceFlagTests { for _, test := range intSliceFlagTests {
flag := cli.IntSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_SMURF"} 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) { func TestFloat64FlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
os.Setenv("APP_BAZ", "99.4") os.Setenv("APP_BAZ", "99.4")
for _, test := range float64FlagTests { for _, test := range float64FlagTests {
flag := cli.Float64Flag{Name: test.name, EnvVar: "APP_BAZ"} flag := cli.Float64Flag{Name: test.name, EnvVar: "APP_BAZ"}
@ -280,7 +280,7 @@ func TestGenericFlagHelpOutput(t *testing.T) {
} }
func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) { func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) {
os.Clearenv()
os.Setenv("APP_ZAP", "3") os.Setenv("APP_ZAP", "3")
for _, test := range genericFlagTests { for _, test := range genericFlagTests {
flag := cli.GenericFlag{Name: test.name, EnvVar: "APP_ZAP"} flag := cli.GenericFlag{Name: test.name, EnvVar: "APP_ZAP"}
@ -309,6 +309,7 @@ func TestParseMultiString(t *testing.T) {
} }
func TestParseMultiStringFromEnv(t *testing.T) { func TestParseMultiStringFromEnv(t *testing.T) {
os.Clearenv()
os.Setenv("APP_COUNT", "20") os.Setenv("APP_COUNT", "20")
(&cli.App{ (&cli.App{
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -325,6 +326,24 @@ func TestParseMultiStringFromEnv(t *testing.T) {
}).Run([]string{"run"}) }).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) { func TestParseMultiStringSlice(t *testing.T) {
(&cli.App{ (&cli.App{
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -342,6 +361,7 @@ func TestParseMultiStringSlice(t *testing.T) {
} }
func TestParseMultiStringSliceFromEnv(t *testing.T) { func TestParseMultiStringSliceFromEnv(t *testing.T) {
os.Clearenv()
os.Setenv("APP_INTERVALS", "20,30,40") os.Setenv("APP_INTERVALS", "20,30,40")
(&cli.App{ (&cli.App{
@ -359,6 +379,25 @@ func TestParseMultiStringSliceFromEnv(t *testing.T) {
}).Run([]string{"run"}) }).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) { func TestParseMultiInt(t *testing.T) {
a := cli.App{ a := cli.App{
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -377,6 +416,7 @@ func TestParseMultiInt(t *testing.T) {
} }
func TestParseMultiIntFromEnv(t *testing.T) { func TestParseMultiIntFromEnv(t *testing.T) {
os.Clearenv()
os.Setenv("APP_TIMEOUT_SECONDS", "10") os.Setenv("APP_TIMEOUT_SECONDS", "10")
a := cli.App{ a := cli.App{
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -394,6 +434,25 @@ func TestParseMultiIntFromEnv(t *testing.T) {
a.Run([]string{"run"}) 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) { func TestParseMultiIntSlice(t *testing.T) {
(&cli.App{ (&cli.App{
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -411,6 +470,7 @@ func TestParseMultiIntSlice(t *testing.T) {
} }
func TestParseMultiIntSliceFromEnv(t *testing.T) { func TestParseMultiIntSliceFromEnv(t *testing.T) {
os.Clearenv()
os.Setenv("APP_INTERVALS", "20,30,40") os.Setenv("APP_INTERVALS", "20,30,40")
(&cli.App{ (&cli.App{
@ -428,6 +488,25 @@ func TestParseMultiIntSliceFromEnv(t *testing.T) {
}).Run([]string{"run"}) }).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) { func TestParseMultiFloat64(t *testing.T) {
a := cli.App{ a := cli.App{
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -446,6 +525,7 @@ func TestParseMultiFloat64(t *testing.T) {
} }
func TestParseMultiFloat64FromEnv(t *testing.T) { func TestParseMultiFloat64FromEnv(t *testing.T) {
os.Clearenv()
os.Setenv("APP_TIMEOUT_SECONDS", "15.5") os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
a := cli.App{ a := cli.App{
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -463,6 +543,25 @@ func TestParseMultiFloat64FromEnv(t *testing.T) {
a.Run([]string{"run"}) 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) { func TestParseMultiBool(t *testing.T) {
a := cli.App{ a := cli.App{
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -481,6 +580,7 @@ func TestParseMultiBool(t *testing.T) {
} }
func TestParseMultiBoolFromEnv(t *testing.T) { func TestParseMultiBoolFromEnv(t *testing.T) {
os.Clearenv()
os.Setenv("APP_DEBUG", "1") os.Setenv("APP_DEBUG", "1")
a := cli.App{ a := cli.App{
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -498,6 +598,25 @@ func TestParseMultiBoolFromEnv(t *testing.T) {
a.Run([]string{"run"}) 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) { func TestParseMultiBoolT(t *testing.T) {
a := cli.App{ a := cli.App{
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -516,6 +635,7 @@ func TestParseMultiBoolT(t *testing.T) {
} }
func TestParseMultiBoolTFromEnv(t *testing.T) { func TestParseMultiBoolTFromEnv(t *testing.T) {
os.Clearenv()
os.Setenv("APP_DEBUG", "0") os.Setenv("APP_DEBUG", "0")
a := cli.App{ a := cli.App{
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -533,6 +653,25 @@ func TestParseMultiBoolTFromEnv(t *testing.T) {
a.Run([]string{"run"}) 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 type Parser [2]string
func (p *Parser) Set(value string) error { func (p *Parser) Set(value string) error {
@ -569,6 +708,7 @@ func TestParseGeneric(t *testing.T) {
} }
func TestParseGenericFromEnv(t *testing.T) { func TestParseGenericFromEnv(t *testing.T) {
os.Clearenv()
os.Setenv("APP_SERVE", "20,30") os.Setenv("APP_SERVE", "20,30")
a := cli.App{ a := cli.App{
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -585,3 +725,19 @@ func TestParseGenericFromEnv(t *testing.T) {
} }
a.Run([]string{"run"}) 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"})
}