From 028af4bc3503feb6307441b1d2183cde74e850a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Franc=CC=A7a=20dos=20Reis?= Date: Tue, 28 Jun 2016 19:52:25 -0700 Subject: [PATCH] adding support for Float64SliceFlag --- context.go | 17 ++++++++ flag.go | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++ flag_test.go | 85 ++++++++++++++++++++++++++++++++++++ 3 files changed, 221 insertions(+) diff --git a/context.go b/context.go index 2e07469..21a6b67 100644 --- a/context.go +++ b/context.go @@ -84,6 +84,15 @@ func (c *Context) Int64Slice(name string) []int64 { return nil } +// Float64Slice looks up the value of a local float64 slice flag, returns nil if no float +// slice flag exists +func (c *Context) Float64Slice(name string) []float64 { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupFloat64Slice(name, fs) + } + return nil +} + // Bool looks up the value of a local bool flag, returns false if no bool flag exists func (c *Context) Bool(name string) bool { if fs := lookupFlagSet(name, c); fs != nil { @@ -317,6 +326,14 @@ func lookupInt64Slice(name string, set *flag.FlagSet) []int64 { return nil } +func lookupFloat64Slice(name string, set *flag.FlagSet) []float64 { + f := set.Lookup(name) + if f != nil { + return (f.Value.(*Float64Slice)).Value() + } + return nil +} + func lookupGeneric(name string, set *flag.FlagSet) interface{} { f := set.Lookup(name) if f != nil { diff --git a/flag.go b/flag.go index 3ff53ce..2a587da 100644 --- a/flag.go +++ b/flag.go @@ -765,6 +765,111 @@ func (f *Float64Flag) Names() []string { return flagNames(f) } + + + +// NewFloat64Slice makes a *Float64Slice with default values +func NewFloat64Slice(defaults ...float64) *Float64Slice { + return &Float64Slice{slice: append([]float64{}, defaults...)} +} + + +// Float64Slice is an opaque type for []float64 to satisfy flag.Value +type Float64Slice struct { + slice []float64 + hasBeenSet bool +} + +// Set parses the value into a float64 and appends it to the list of values +func (f *Float64Slice) Set(value string) error { + if !f.hasBeenSet { + f.slice = []float64{} + f.hasBeenSet = true + } + + if strings.HasPrefix(value, slPfx) { + // Deserializing assumes overwrite + _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &f.slice) + f.hasBeenSet = true + return nil + } + + tmp, err := strconv.ParseFloat(value, 64) + if err != nil { + return err + } + + f.slice = append(f.slice, tmp) + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (f *Float64Slice) String() string { + return fmt.Sprintf("%#v", f.slice) +} + +// Serialized allows Float64Slice to fulfill Serializeder +func (f *Float64Slice) Serialized() string { + jsonBytes, _ := json.Marshal(f.slice) + return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) +} + +// Value returns the slice of float64s set by this flag +func (f *Float64Slice) Value() []float64 { + return f.slice +} + +// Float64SliceFlag is a float64 flag that can be specified multiple times on the +// command-line +type Float64SliceFlag struct { + Name string + Aliases []string + Value *Float64Slice + Usage string + EnvVars []string + Hidden bool +} + +// String returns the usage +func (f *Float64SliceFlag) String() string { + return FlagStringer(f) +} + +// Apply populates the flag given the flag set and environment +func (f *Float64SliceFlag) Apply(set *flag.FlagSet) { + if f.EnvVars != nil { + for _, envVar := range f.EnvVars { + if envVal := os.Getenv(envVar); envVal != "" { + newVal := NewFloat64Slice() + for _, s := range strings.Split(envVal, ",") { + s = strings.TrimSpace(s) + err := newVal.Set(s) + if err != nil { + fmt.Fprintf(ErrWriter, err.Error()) + } + } + f.Value = newVal + break + } + } + } + + if f.Value == nil { + f.Value = NewFloat64Slice() + } + + for _, name := range f.Names() { + set.Var(f.Value, name, f.Usage) + } +} + +// Names returns the names of the flag. +func (f *Float64SliceFlag) Names() []string { + return flagNames(f) +} + + + func visibleFlags(fl []Flag) []Flag { visible := []Flag{} for _, flag := range fl { @@ -893,6 +998,9 @@ func stringifyFlag(f Flag) string { case *Int64SliceFlag: return withEnvHint(flagStringSliceField(f, "EnvVars"), stringifyInt64SliceFlag(f.(*Int64SliceFlag))) + case *Float64SliceFlag: + return withEnvHint(flagStringSliceField(f, "EnvVars"), + stringifyFloat64SliceFlag(f.(*Float64SliceFlag))) case *StringSliceFlag: return withEnvHint(flagStringSliceField(f, "EnvVars"), stringifyStringSliceFlag(f.(*StringSliceFlag))) @@ -949,6 +1057,17 @@ func stringifyInt64SliceFlag(f *Int64SliceFlag) string { return stringifySliceFlag(f.Usage, f.Names(), defaultVals) } +func stringifyFloat64SliceFlag(f *Float64SliceFlag) string { + defaultVals := []string{} + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, i := range f.Value.Value() { + defaultVals = append(defaultVals, strings.TrimRight(strings.TrimRight(fmt.Sprintf("%f", i), "0"), ".")) + } + } + + return stringifySliceFlag(f.Usage, f.Names(), defaultVals) +} + func stringifyStringSliceFlag(f *StringSliceFlag) string { defaultVals := []string{} if f.Value != nil && len(f.Value.Value()) > 0 { diff --git a/flag_test.go b/flag_test.go index 1b69f51..e8c8bcd 100644 --- a/flag_test.go +++ b/flag_test.go @@ -482,6 +482,49 @@ func TestFloat64FlagApply_SetsAllNames(t *testing.T) { expect(t, v, float64(43.33333)) } + +var float64SliceFlagTests = []struct { + name string + aliases []string + value *Float64Slice + expected string +}{ + {"heads", nil, NewFloat64Slice(), "--heads value\t"}, + {"H", nil, NewFloat64Slice(), "-H value\t"}, + {"heads", []string{"H"}, NewFloat64Slice(float64(0.1234), float64(-10.5)), + "--heads value, -H value\t(default: 0.1234, -10.5)"}, +} + +func TestFloat64SliceFlagHelpOutput(t *testing.T) { + for _, test := range float64SliceFlagTests { + flag := Float64SliceFlag{Name: test.name, Aliases: test.aliases, Value: test.value} + output := flag.String() + + if output != test.expected { + t.Errorf("%q does not match %q", output, test.expected) + } + } +} + +func TestFloat64SliceFlagWithEnvVarHelpOutput(t *testing.T) { + os.Clearenv() + os.Setenv("APP_SMURF", "0.1234,-10.5") + for _, test := range float64SliceFlagTests { + flag := Float64SliceFlag{Name: test.name, Value: test.value, EnvVars: []string{"APP_SMURF"}} + output := flag.String() + + expectedSuffix := " [$APP_SMURF]" + if runtime.GOOS == "windows" { + expectedSuffix = " [%APP_SMURF%]" + } + if !strings.HasSuffix(output, expectedSuffix) { + t.Errorf("%q does not end with"+expectedSuffix, output) + } + } +} + + + var genericFlagTests = []struct { name string value Generic @@ -1057,6 +1100,48 @@ func TestParseMultiFloat64FromEnvCascade(t *testing.T) { a.Run([]string{"run"}) } + +func TestParseMultiFloat64SliceFromEnv(t *testing.T) { + os.Clearenv() + os.Setenv("APP_INTERVALS", "0.1,-10.5") + + (&App{ + Flags: []Flag{ + &Float64SliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: NewFloat64Slice(), EnvVars: []string{"APP_INTERVALS"}}, + }, + Action: func(ctx *Context) error { + if !reflect.DeepEqual(ctx.Float64Slice("intervals"), []float64{0.1, -10.5}) { + t.Errorf("main name not set from env") + } + if !reflect.DeepEqual(ctx.Float64Slice("i"), []float64{0.1, -10.5}) { + t.Errorf("short name not set from env") + } + return nil + }, + }).Run([]string{"run"}) +} + +func TestParseMultiFloat64SliceFromEnvCascade(t *testing.T) { + os.Clearenv() + os.Setenv("APP_INTERVALS", "0.1234,-10.5") + + (&App{ + Flags: []Flag{ + &Float64SliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: NewFloat64Slice(), EnvVars: []string{"COMPAT_INTERVALS", "APP_INTERVALS"}}, + }, + Action: func(ctx *Context) error { + if !reflect.DeepEqual(ctx.Float64Slice("intervals"), []float64{0.1234, -10.5}) { + t.Errorf("main name not set from env") + } + if !reflect.DeepEqual(ctx.Float64Slice("i"), []float64{0.1234, -10.5}) { + t.Errorf("short name not set from env") + } + return nil + }, + }).Run([]string{"run"}) +} + + func TestParseMultiBool(t *testing.T) { a := App{ Flags: []Flag{