From e77dd7bb6816628722681373965a1ce3a34e0810 Mon Sep 17 00:00:00 2001 From: Joseph Cumines Date: Tue, 7 Jun 2022 08:54:57 +1000 Subject: [PATCH 1/2] Add SliceFlag wrapper and fix bugs in existing implementations The SliceFlag implementation and associated aliases (MultiStringFlag, etc) extend the existing slice implementations (StringSliceFlag, etc) to support actual slices as the flag value and destination. This change also fixes various bugs in the existing implementation. Notably, the StringSliceFlag.Apply implementation would modify the input (default) Value, if an env var was set, and no destination was provided. The bugs fixed in the other three implementations were all already fixed in either StringSliceFlag, or in one case (ignoring empty env var) in Float64SliceFlag. --- flag_float64_slice.go | 55 ++- flag_int64_slice.go | 53 ++- flag_int_slice.go | 53 ++- flag_string_slice.go | 48 +- flag_test.go | 90 +++- sliceflag.go | 228 ++++++++++ sliceflag_test.go | 1002 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1473 insertions(+), 56 deletions(-) create mode 100644 sliceflag.go create mode 100644 sliceflag_test.go diff --git a/flag_float64_slice.go b/flag_float64_slice.go index bc347ccdb2..8452d29245 100644 --- a/flag_float64_slice.go +++ b/flag_float64_slice.go @@ -56,7 +56,12 @@ func (f *Float64Slice) Set(value string) error { // String returns a readable representation of this value (for usage defaults) func (f *Float64Slice) String() string { - return fmt.Sprintf("%#v", f.slice) + v := f.slice + if v == nil { + // treat nil the same as zero length non-nil + v = make([]float64, 0) + } + return fmt.Sprintf("%#v", v) } // Serialize allows Float64Slice to fulfill Serializer @@ -120,34 +125,60 @@ func (f *Float64SliceFlag) GetEnvVars() []string { // Apply populates the flag given the flag set and environment func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error { + // apply any default + if f.Destination != nil && f.Value != nil { + f.Destination.slice = make([]float64, len(f.Value.slice)) + copy(f.Destination.slice, f.Value.slice) + } + + // resolve setValue (what we will assign to the set) + var setValue *Float64Slice + switch { + case f.Destination != nil: + setValue = f.Destination + case f.Value != nil: + setValue = f.Value.clone() + default: + setValue = new(Float64Slice) + } + if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if val != "" { - f.Value = &Float64Slice{} - for _, s := range flagSplitMultiValues(val) { - if err := f.Value.Set(strings.TrimSpace(s)); err != nil { - return fmt.Errorf("could not parse %q as float64 slice value from %s for flag %s: %s", f.Value, source, f.Name, err) + if err := setValue.Set(strings.TrimSpace(s)); err != nil { + return fmt.Errorf("could not parse %q as float64 slice value from %s for flag %s: %s", val, source, f.Name, err) } } // Set this to false so that we reset the slice if we then set values from // flags that have already been set by the environment. - f.Value.hasBeenSet = false + setValue.hasBeenSet = false f.HasBeenSet = true } } - if f.Value == nil { - f.Value = &Float64Slice{} - } - copyValue := f.Value.clone() for _, name := range f.Names() { - set.Var(copyValue, name, f.Usage) + set.Var(setValue, name, f.Usage) } return nil } +func (f *Float64SliceFlag) SetValue(slice []float64) { + f.Value = newSliceFlagValue(NewFloat64Slice, slice) +} + +func (f *Float64SliceFlag) SetDestination(slice []float64) { + f.Destination = newSliceFlagValue(NewFloat64Slice, slice) +} + +func (f *Float64SliceFlag) GetDestination() []float64 { + if destination := f.Destination; destination != nil { + return destination.Value() + } + return nil +} + // Get returns the flag’s value in the given Context. func (f *Float64SliceFlag) Get(ctx *Context) []float64 { return ctx.Float64Slice(f.Name) @@ -165,7 +196,7 @@ func (cCtx *Context) Float64Slice(name string) []float64 { func lookupFloat64Slice(name string, set *flag.FlagSet) []float64 { f := set.Lookup(name) if f != nil { - if slice, ok := f.Value.(*Float64Slice); ok { + if slice, ok := unwrapFlagValue(f.Value).(*Float64Slice); ok { return slice.Value() } } diff --git a/flag_int64_slice.go b/flag_int64_slice.go index 5f3d5cd4ea..0efea54006 100644 --- a/flag_int64_slice.go +++ b/flag_int64_slice.go @@ -57,7 +57,12 @@ func (i *Int64Slice) Set(value string) error { // String returns a readable representation of this value (for usage defaults) func (i *Int64Slice) String() string { - return fmt.Sprintf("%#v", i.slice) + v := i.slice + if v == nil { + // treat nil the same as zero length non-nil + v = make([]int64, 0) + } + return fmt.Sprintf("%#v", v) } // Serialize allows Int64Slice to fulfill Serializer @@ -121,32 +126,58 @@ func (f *Int64SliceFlag) GetEnvVars() []string { // Apply populates the flag given the flag set and environment func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error { - if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { - f.Value = &Int64Slice{} + // apply any default + if f.Destination != nil && f.Value != nil { + f.Destination.slice = make([]int64, len(f.Value.slice)) + copy(f.Destination.slice, f.Value.slice) + } + + // resolve setValue (what we will assign to the set) + var setValue *Int64Slice + switch { + case f.Destination != nil: + setValue = f.Destination + case f.Value != nil: + setValue = f.Value.clone() + default: + setValue = new(Int64Slice) + } + if val, source, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok && val != "" { for _, s := range flagSplitMultiValues(val) { - if err := f.Value.Set(strings.TrimSpace(s)); err != nil { + if err := setValue.Set(strings.TrimSpace(s)); err != nil { return fmt.Errorf("could not parse %q as int64 slice value from %s for flag %s: %s", val, source, f.Name, err) } } // Set this to false so that we reset the slice if we then set values from // flags that have already been set by the environment. - f.Value.hasBeenSet = false + setValue.hasBeenSet = false f.HasBeenSet = true } - if f.Value == nil { - f.Value = &Int64Slice{} - } - copyValue := f.Value.clone() for _, name := range f.Names() { - set.Var(copyValue, name, f.Usage) + set.Var(setValue, name, f.Usage) } return nil } +func (f *Int64SliceFlag) SetValue(slice []int64) { + f.Value = newSliceFlagValue(NewInt64Slice, slice) +} + +func (f *Int64SliceFlag) SetDestination(slice []int64) { + f.Destination = newSliceFlagValue(NewInt64Slice, slice) +} + +func (f *Int64SliceFlag) GetDestination() []int64 { + if destination := f.Destination; destination != nil { + return destination.Value() + } + return nil +} + // Get returns the flag’s value in the given Context. func (f *Int64SliceFlag) Get(ctx *Context) []int64 { return ctx.Int64Slice(f.Name) @@ -164,7 +195,7 @@ func (cCtx *Context) Int64Slice(name string) []int64 { func lookupInt64Slice(name string, set *flag.FlagSet) []int64 { f := set.Lookup(name) if f != nil { - if slice, ok := f.Value.(*Int64Slice); ok { + if slice, ok := unwrapFlagValue(f.Value).(*Int64Slice); ok { return slice.Value() } } diff --git a/flag_int_slice.go b/flag_int_slice.go index 2ddf805967..28f8c90303 100644 --- a/flag_int_slice.go +++ b/flag_int_slice.go @@ -68,7 +68,12 @@ func (i *IntSlice) Set(value string) error { // String returns a readable representation of this value (for usage defaults) func (i *IntSlice) String() string { - return fmt.Sprintf("%#v", i.slice) + v := i.slice + if v == nil { + // treat nil the same as zero length non-nil + v = make([]int, 0) + } + return fmt.Sprintf("%#v", v) } // Serialize allows IntSlice to fulfill Serializer @@ -132,32 +137,58 @@ func (f *IntSliceFlag) GetEnvVars() []string { // Apply populates the flag given the flag set and environment func (f *IntSliceFlag) Apply(set *flag.FlagSet) error { - if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { - f.Value = &IntSlice{} + // apply any default + if f.Destination != nil && f.Value != nil { + f.Destination.slice = make([]int, len(f.Value.slice)) + copy(f.Destination.slice, f.Value.slice) + } + + // resolve setValue (what we will assign to the set) + var setValue *IntSlice + switch { + case f.Destination != nil: + setValue = f.Destination + case f.Value != nil: + setValue = f.Value.clone() + default: + setValue = new(IntSlice) + } + if val, source, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok && val != "" { for _, s := range flagSplitMultiValues(val) { - if err := f.Value.Set(strings.TrimSpace(s)); err != nil { + if err := setValue.Set(strings.TrimSpace(s)); err != nil { return fmt.Errorf("could not parse %q as int slice value from %s for flag %s: %s", val, source, f.Name, err) } } // Set this to false so that we reset the slice if we then set values from // flags that have already been set by the environment. - f.Value.hasBeenSet = false + setValue.hasBeenSet = false f.HasBeenSet = true } - if f.Value == nil { - f.Value = &IntSlice{} - } - copyValue := f.Value.clone() for _, name := range f.Names() { - set.Var(copyValue, name, f.Usage) + set.Var(setValue, name, f.Usage) } return nil } +func (f *IntSliceFlag) SetValue(slice []int) { + f.Value = newSliceFlagValue(NewIntSlice, slice) +} + +func (f *IntSliceFlag) SetDestination(slice []int) { + f.Destination = newSliceFlagValue(NewIntSlice, slice) +} + +func (f *IntSliceFlag) GetDestination() []int { + if destination := f.Destination; destination != nil { + return destination.Value() + } + return nil +} + // Get returns the flag’s value in the given Context. func (f *IntSliceFlag) Get(ctx *Context) []int { return ctx.IntSlice(f.Name) @@ -175,7 +206,7 @@ func (cCtx *Context) IntSlice(name string) []int { func lookupIntSlice(name string, set *flag.FlagSet) []int { f := set.Lookup(name) if f != nil { - if slice, ok := f.Value.(*IntSlice); ok { + if slice, ok := unwrapFlagValue(f.Value).(*IntSlice); ok { return slice.Value() } } diff --git a/flag_string_slice.go b/flag_string_slice.go index 599f42c7fb..2be2af62d7 100644 --- a/flag_string_slice.go +++ b/flag_string_slice.go @@ -115,41 +115,36 @@ func (f *StringSliceFlag) GetEnvVars() []string { // Apply populates the flag given the flag set and environment func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { - + // apply any default if f.Destination != nil && f.Value != nil { f.Destination.slice = make([]string, len(f.Value.slice)) copy(f.Destination.slice, f.Value.slice) + } + // resolve setValue (what we will assign to the set) + var setValue *StringSlice + switch { + case f.Destination != nil: + setValue = f.Destination + case f.Value != nil: + setValue = f.Value.clone() + default: + setValue = new(StringSlice) } if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { - if f.Value == nil { - f.Value = &StringSlice{} - } - destination := f.Value - if f.Destination != nil { - destination = f.Destination - } - for _, s := range flagSplitMultiValues(val) { - if err := destination.Set(strings.TrimSpace(s)); err != nil { + if err := setValue.Set(strings.TrimSpace(s)); err != nil { return fmt.Errorf("could not parse %q as string value from %s for flag %s: %s", val, source, f.Name, err) } } // Set this to false so that we reset the slice if we then set values from // flags that have already been set by the environment. - destination.hasBeenSet = false + setValue.hasBeenSet = false f.HasBeenSet = true } - if f.Value == nil { - f.Value = &StringSlice{} - } - setValue := f.Destination - if f.Destination == nil { - setValue = f.Value.clone() - } for _, name := range f.Names() { set.Var(setValue, name, f.Usage) } @@ -157,6 +152,21 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { return nil } +func (f *StringSliceFlag) SetValue(slice []string) { + f.Value = newSliceFlagValue(NewStringSlice, slice) +} + +func (f *StringSliceFlag) SetDestination(slice []string) { + f.Destination = newSliceFlagValue(NewStringSlice, slice) +} + +func (f *StringSliceFlag) GetDestination() []string { + if destination := f.Destination; destination != nil { + return destination.Value() + } + return nil +} + // Get returns the flag’s value in the given Context. func (f *StringSliceFlag) Get(ctx *Context) []string { return ctx.StringSlice(f.Name) @@ -174,7 +184,7 @@ func (cCtx *Context) StringSlice(name string) []string { func lookupStringSlice(name string, set *flag.FlagSet) []string { f := set.Lookup(name) if f != nil { - if slice, ok := f.Value.(*StringSlice); ok { + if slice, ok := unwrapFlagValue(f.Value).(*StringSlice); ok { return slice.Value() } } diff --git a/flag_test.go b/flag_test.go index ba90e91c05..0bb893afb6 100644 --- a/flag_test.go +++ b/flag_test.go @@ -114,7 +114,7 @@ func TestFlagsFromEnv(t *testing.T) { {"foobar", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int value from environment variable "SECONDS" for flag seconds: .*`}, {"1.0,2", newSetFloat64Slice(1, 2), &Float64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, - {"foobar", newSetFloat64Slice(), &Float64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "\[\]float64{}" as float64 slice value from environment variable "SECONDS" for flag seconds: .*`}, + {"foobar", newSetFloat64Slice(), &Float64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as float64 slice value from environment variable "SECONDS" for flag seconds: .*`}, {"1,2", newSetIntSlice(1, 2), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, {"1.2,2", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as int slice value from environment variable "SECONDS" for flag seconds: .*`}, @@ -604,7 +604,7 @@ func TestStringSliceFlagApply_SetsAllNames(t *testing.T) { expect(t, err, nil) } -func TestStringSliceFlagApply_UsesEnvValues(t *testing.T) { +func TestStringSliceFlagApply_UsesEnvValues_noDefault(t *testing.T) { defer resetEnv(os.Environ()) os.Clearenv() _ = os.Setenv("MY_GOAT", "vincent van goat,scape goat") @@ -615,7 +615,22 @@ func TestStringSliceFlagApply_UsesEnvValues(t *testing.T) { err := set.Parse(nil) expect(t, err, nil) - expect(t, val.Value(), NewStringSlice("vincent van goat", "scape goat").Value()) + expect(t, val.Value(), []string(nil)) + expect(t, set.Lookup("goat").Value.(*StringSlice).Value(), []string{"vincent van goat", "scape goat"}) +} + +func TestStringSliceFlagApply_UsesEnvValues_withDefault(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("MY_GOAT", "vincent van goat,scape goat") + val := NewStringSlice(`some default`, `values here`) + fl := StringSliceFlag{Name: "goat", EnvVars: []string{"MY_GOAT"}, Value: val} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + err := set.Parse(nil) + expect(t, err, nil) + expect(t, val.Value(), []string{`some default`, `values here`}) + expect(t, set.Lookup("goat").Value.(*StringSlice).Value(), []string{"vincent van goat", "scape goat"}) } func TestStringSliceFlagApply_DefaultValueWithDestination(t *testing.T) { @@ -1406,6 +1421,75 @@ func TestParseMultiStringSliceWithDestinationAndEnv(t *testing.T) { }).Run([]string{"run", "-s", "10", "-s", "20"}) } +func TestParseMultiFloat64SliceWithDestinationAndEnv(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("APP_INTERVALS", "20,30,40") + + dest := &Float64Slice{} + _ = (&App{ + Flags: []Flag{ + &Float64SliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: dest, EnvVars: []string{"APP_INTERVALS"}}, + }, + Action: func(ctx *Context) error { + expected := []float64{10, 20} + if !reflect.DeepEqual(dest.slice, expected) { + t.Errorf("main name not set: %v != %v", expected, ctx.StringSlice("serve")) + } + if !reflect.DeepEqual(dest.slice, expected) { + t.Errorf("short name not set: %v != %v", expected, ctx.StringSlice("s")) + } + return nil + }, + }).Run([]string{"run", "-s", "10", "-s", "20"}) +} + +func TestParseMultiInt64SliceWithDestinationAndEnv(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("APP_INTERVALS", "20,30,40") + + dest := &Int64Slice{} + _ = (&App{ + Flags: []Flag{ + &Int64SliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: dest, EnvVars: []string{"APP_INTERVALS"}}, + }, + Action: func(ctx *Context) error { + expected := []int64{10, 20} + if !reflect.DeepEqual(dest.slice, expected) { + t.Errorf("main name not set: %v != %v", expected, ctx.StringSlice("serve")) + } + if !reflect.DeepEqual(dest.slice, expected) { + t.Errorf("short name not set: %v != %v", expected, ctx.StringSlice("s")) + } + return nil + }, + }).Run([]string{"run", "-s", "10", "-s", "20"}) +} + +func TestParseMultiIntSliceWithDestinationAndEnv(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("APP_INTERVALS", "20,30,40") + + dest := &IntSlice{} + _ = (&App{ + Flags: []Flag{ + &IntSliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: dest, EnvVars: []string{"APP_INTERVALS"}}, + }, + Action: func(ctx *Context) error { + expected := []int{10, 20} + if !reflect.DeepEqual(dest.slice, expected) { + t.Errorf("main name not set: %v != %v", expected, ctx.StringSlice("serve")) + } + if !reflect.DeepEqual(dest.slice, expected) { + t.Errorf("short name not set: %v != %v", expected, ctx.StringSlice("s")) + } + return nil + }, + }).Run([]string{"run", "-s", "10", "-s", "20"}) +} + func TestParseMultiStringSliceWithDefaultsUnset(t *testing.T) { _ = (&App{ Flags: []Flag{ diff --git a/sliceflag.go b/sliceflag.go new file mode 100644 index 0000000000..80063efc5a --- /dev/null +++ b/sliceflag.go @@ -0,0 +1,228 @@ +package cli + +import ( + "flag" + "reflect" +) + +type ( + // SliceFlag extends implementations like StringSliceFlag and IntSliceFlag with support for using slices directly, + // as Value and/or Destination. + // See also SliceFlagTarget, MultiStringFlag, MultiFloat64Flag, MultiInt64Flag, MultiIntFlag. + SliceFlag[T SliceFlagTarget[E], S ~[]E, E any] struct { + Target T + Value S + Destination *S + } + + // SliceFlagTarget models a target implementation for use with SliceFlag. + // The three methods, SetValue, SetDestination, and GetDestination, are necessary to propagate Value and + // Destination, where Value is propagated inwards (initially), and Destination is propagated outwards (on every + // update). + SliceFlagTarget[E any] interface { + Flag + RequiredFlag + DocGenerationFlag + VisibleFlag + CategorizableFlag + + // SetValue should propagate the given slice to the target, ideally as a new value. + // Note that a nil slice should nil/clear any existing value (modelled as ~[]E). + SetValue(slice []E) + // SetDestination should propagate the given slice to the target, ideally as a new value. + // Note that a nil slice should nil/clear any existing value (modelled as ~*[]E). + SetDestination(slice []E) + // GetDestination should return the current value referenced by any destination, or nil if nil/unset. + GetDestination() []E + } + + // MultiStringFlag extends StringSliceFlag with support for using slices directly, as Value and/or Destination. + // See also SliceFlag. + MultiStringFlag = SliceFlag[*StringSliceFlag, []string, string] + + // MultiFloat64Flag extends Float64SliceFlag with support for using slices directly, as Value and/or Destination. + // See also SliceFlag. + MultiFloat64Flag = SliceFlag[*Float64SliceFlag, []float64, float64] + + // MultiInt64Flag extends Int64SliceFlag with support for using slices directly, as Value and/or Destination. + // See also SliceFlag. + MultiInt64Flag = SliceFlag[*Int64SliceFlag, []int64, int64] + + // MultiIntFlag extends IntSliceFlag with support for using slices directly, as Value and/or Destination. + // See also SliceFlag. + MultiIntFlag = SliceFlag[*IntSliceFlag, []int, int] + + flagValueHook struct { + value Generic + hook func() + } +) + +var ( + // compile time assertions + + _ SliceFlagTarget[string] = (*StringSliceFlag)(nil) + _ SliceFlagTarget[string] = (*SliceFlag[*StringSliceFlag, []string, string])(nil) + _ SliceFlagTarget[string] = (*MultiStringFlag)(nil) + _ SliceFlagTarget[float64] = (*MultiFloat64Flag)(nil) + _ SliceFlagTarget[int64] = (*MultiInt64Flag)(nil) + _ SliceFlagTarget[int] = (*MultiIntFlag)(nil) + + _ Generic = (*flagValueHook)(nil) + _ Serializer = (*flagValueHook)(nil) +) + +func (x *SliceFlag[T, S, E]) Apply(set *flag.FlagSet) error { + x.Target.SetValue(x.convertSlice(x.Value)) + + destination := x.Destination + if destination == nil { + x.Target.SetDestination(nil) + + return x.Target.Apply(set) + } + + x.Target.SetDestination(x.convertSlice(*destination)) + + return applyFlagValueHook(set, x.Target.Apply, func() { + *destination = x.Target.GetDestination() + }) +} + +func (x *SliceFlag[T, S, E]) convertSlice(slice S) []E { + result := make([]E, len(slice)) + copy(result, slice) + return result +} + +func (x *SliceFlag[T, S, E]) SetValue(slice S) { + x.Value = slice +} + +func (x *SliceFlag[T, S, E]) SetDestination(slice S) { + if slice != nil { + x.Destination = &slice + } else { + x.Destination = nil + } +} + +func (x *SliceFlag[T, S, E]) GetDestination() S { + if destination := x.Destination; destination != nil { + return *destination + } + return nil +} + +func (x *SliceFlag[T, S, E]) String() string { return x.Target.String() } +func (x *SliceFlag[T, S, E]) Names() []string { return x.Target.Names() } +func (x *SliceFlag[T, S, E]) IsSet() bool { return x.Target.IsSet() } +func (x *SliceFlag[T, S, E]) IsRequired() bool { return x.Target.IsRequired() } +func (x *SliceFlag[T, S, E]) TakesValue() bool { return x.Target.TakesValue() } +func (x *SliceFlag[T, S, E]) GetUsage() string { return x.Target.GetUsage() } +func (x *SliceFlag[T, S, E]) GetValue() string { return x.Target.GetValue() } +func (x *SliceFlag[T, S, E]) GetDefaultText() string { return x.Target.GetDefaultText() } +func (x *SliceFlag[T, S, E]) GetEnvVars() []string { return x.Target.GetEnvVars() } +func (x *SliceFlag[T, S, E]) IsVisible() bool { return x.Target.IsVisible() } +func (x *SliceFlag[T, S, E]) GetCategory() string { return x.Target.GetCategory() } + +func (x *flagValueHook) Set(value string) error { + if err := x.value.Set(value); err != nil { + return err + } + x.hook() + return nil +} + +func (x *flagValueHook) String() string { + // note: this is necessary due to the way Go's flag package handles defaults + isZeroValue := func(f flag.Value, v string) bool { + /* + https://cs.opensource.google/go/go/+/refs/tags/go1.18.3:src/flag/flag.go;drc=2580d0e08d5e9f979b943758d3c49877fb2324cb;l=453 + + Copyright (c) 2009 The Go Authors. All rights reserved. + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + // Build a zero value of the flag's Value type, and see if the + // result of calling its String method equals the value passed in. + // This works unless the Value type is itself an interface type. + typ := reflect.TypeOf(f) + var z reflect.Value + if typ.Kind() == reflect.Pointer { + z = reflect.New(typ.Elem()) + } else { + z = reflect.Zero(typ) + } + return v == z.Interface().(flag.Value).String() + } + if x.value != nil { + // only return non-empty if not the same string as returned by the zero value + if s := x.value.String(); !isZeroValue(x.value, s) { + return s + } + } + return `` +} + +func (x *flagValueHook) Serialize() string { + if value, ok := x.value.(Serializer); ok { + return value.Serialize() + } + return x.String() +} + +// applyFlagValueHook wraps calls apply then wraps flags to call a hook function on update and after initial apply. +func applyFlagValueHook(set *flag.FlagSet, apply func(set *flag.FlagSet) error, hook func()) error { + if apply == nil || set == nil || hook == nil { + panic(`invalid input`) + } + var tmp flag.FlagSet + if err := apply(&tmp); err != nil { + return err + } + tmp.VisitAll(func(f *flag.Flag) { set.Var(&flagValueHook{value: f.Value, hook: hook}, f.Name, f.Usage) }) + hook() + return nil +} + +// newSliceFlagValue is for implementing SliceFlagTarget.SetValue and SliceFlagTarget.SetDestination. +// It's e.g. as part of StringSliceFlag.SetValue, using the factory NewStringSlice. +func newSliceFlagValue[R any, S ~[]E, E any](factory func(defaults ...E) *R, defaults S) *R { + if defaults == nil { + return nil + } + return factory(defaults...) +} + +// unwrapFlagValue strips any/all *flagValueHook wrappers. +func unwrapFlagValue(v flag.Value) flag.Value { + for { + h, ok := v.(*flagValueHook) + if !ok { + return v + } + v = h.value + } +} diff --git a/sliceflag_test.go b/sliceflag_test.go new file mode 100644 index 0000000000..8ff905583b --- /dev/null +++ b/sliceflag_test.go @@ -0,0 +1,1002 @@ +package cli + +import ( + "bytes" + "flag" + "fmt" + "os" + "reflect" + "testing" +) + +func ExampleMultiStringFlag() { + run := func(args ...string) { + // add $0 (the command being run) + args = append([]string{`-`}, args...) + type CustomStringSlice []string + type Config struct { + FlagOne []string + Two CustomStringSlice + } + cfg := Config{ + Two: []string{ + `default value 1`, + `default value 2`, + }, + } + if err := (&App{ + Flags: []Flag{ + &MultiStringFlag{ + Target: &StringSliceFlag{ + Name: `flag-one`, + Category: `category1`, + Usage: `this is the first flag`, + Aliases: []string{`1`}, + EnvVars: []string{`FLAG_ONE`}, + }, + Value: cfg.FlagOne, + Destination: &cfg.FlagOne, + }, + &SliceFlag[*StringSliceFlag, CustomStringSlice, string]{ + Target: &StringSliceFlag{ + Name: `two`, + Category: `category2`, + Usage: `this is the second flag`, + Aliases: []string{`2`}, + EnvVars: []string{`TWO`}, + }, + Value: cfg.Two, + Destination: &cfg.Two, + }, + &MultiStringFlag{ + Target: &StringSliceFlag{ + Name: `flag-three`, + Category: `category1`, + Usage: `this is the third flag`, + Aliases: []string{`3`}, + EnvVars: []string{`FLAG_THREE`}, + }, + Value: []string{`some value`}, + }, + &StringSliceFlag{ + Name: `flag-four`, + Category: `category2`, + Usage: `this is the fourth flag`, + Aliases: []string{`4`}, + EnvVars: []string{`FLAG_FOUR`}, + Value: NewStringSlice(`d1`, `d2`), + }, + }, + Action: func(c *Context) error { + fmt.Printf("Flag names: %q\n", c.FlagNames()) + fmt.Printf("Local flag names: %q\n", c.LocalFlagNames()) + fmt.Println(`Context values:`) + for _, name := range [...]string{`flag-one`, `two`, `flag-three`, `flag-four`} { + fmt.Printf("%q=%q\n", name, c.StringSlice(name)) + } + fmt.Println(`Destination values:`) + fmt.Printf("cfg.FlagOne=%q\n", cfg.FlagOne) + fmt.Printf("cfg.Two=%q\n", cfg.Two) + return nil + }, + Writer: os.Stdout, + ErrWriter: os.Stdout, + Name: `app-name`, + }).Run(args); err != nil { + panic(err) + } + } + + fmt.Printf("Show defaults...\n\n") + run() + + fmt.Printf("---\nSetting all flags via command line...\n\n") + allFlagsArgs := []string{ + `-1`, `v 1`, + `-1`, `v 2`, + `-2`, `v 3`, + `-2`, `v 4`, + `-3`, `v 5`, + `-3`, `v 6`, + `-4`, `v 7`, + `-4`, `v 8`, + } + run(allFlagsArgs...) + + func() { + defer resetEnv(os.Environ()) + os.Clearenv() + for _, args := range [...][2]string{ + {`FLAG_ONE`, `v 9, v 10`}, + {`TWO`, `v 11, v 12`}, + {`FLAG_THREE`, `v 13, v 14`}, + {`FLAG_FOUR`, `v 15, v 16`}, + } { + if err := os.Setenv(args[0], args[1]); err != nil { + panic(err) + } + } + + fmt.Printf("---\nSetting all flags via environment...\n\n") + run() + + fmt.Printf("---\nWith the same environment + args from the previous example...\n\n") + run(allFlagsArgs...) + }() + + //output: + //Show defaults... + // + //Flag names: [] + //Local flag names: [] + //Context values: + //"flag-one"=[] + //"two"=["default value 1" "default value 2"] + //"flag-three"=["some value"] + //"flag-four"=["d1" "d2"] + //Destination values: + //cfg.FlagOne=[] + //cfg.Two=["default value 1" "default value 2"] + //--- + //Setting all flags via command line... + // + //Flag names: ["1" "2" "3" "4" "flag-four" "flag-one" "flag-three" "two"] + //Local flag names: ["1" "2" "3" "4" "flag-four" "flag-one" "flag-three" "two"] + //Context values: + //"flag-one"=["v 1" "v 2"] + //"two"=["v 3" "v 4"] + //"flag-three"=["v 5" "v 6"] + //"flag-four"=["v 7" "v 8"] + //Destination values: + //cfg.FlagOne=["v 1" "v 2"] + //cfg.Two=["v 3" "v 4"] + //--- + //Setting all flags via environment... + // + //Flag names: [] + //Local flag names: [] + //Context values: + //"flag-one"=["v 9" "v 10"] + //"two"=["v 11" "v 12"] + //"flag-three"=["v 13" "v 14"] + //"flag-four"=["v 15" "v 16"] + //Destination values: + //cfg.FlagOne=["v 9" "v 10"] + //cfg.Two=["v 11" "v 12"] + //--- + //With the same environment + args from the previous example... + // + //Flag names: ["1" "2" "3" "4" "flag-four" "flag-one" "flag-three" "two"] + //Local flag names: ["1" "2" "3" "4" "flag-four" "flag-one" "flag-three" "two"] + //Context values: + //"flag-one"=["v 1" "v 2"] + //"two"=["v 3" "v 4"] + //"flag-three"=["v 5" "v 6"] + //"flag-four"=["v 7" "v 8"] + //Destination values: + //cfg.FlagOne=["v 1" "v 2"] + //cfg.Two=["v 3" "v 4"] +} + +func TestSliceFlag_Apply_string(t *testing.T) { + normalise := func(v any) any { + switch v := v.(type) { + case *[]string: + if v == nil { + return nil + } + return *v + case *StringSlice: + if v == nil { + return nil + } + return v.Value() + } + return v + } + expectEqual := func(t *testing.T, actual, expected any) { + t.Helper() + actual = normalise(actual) + expected = normalise(expected) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("actual: %#v\nexpected: %#v", actual, expected) + } + } + type Config struct { + Flag SliceFlagTarget[string] + Value *[]string + Destination **[]string + Context *Context + Check func() + } + for _, tc := range [...]struct { + Name string + Factory func(t *testing.T, f *StringSliceFlag) Config + }{ + { + Name: `once`, + Factory: func(t *testing.T, f *StringSliceFlag) Config { + v := SliceFlag[*StringSliceFlag, []string, string]{Target: f} + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + }, + } + }, + }, + { + Name: `twice`, + Factory: func(t *testing.T, f *StringSliceFlag) Config { + v := SliceFlag[*SliceFlag[*StringSliceFlag, []string, string], []string, string]{ + Target: &SliceFlag[*StringSliceFlag, []string, string]{Target: f}, + } + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Destination) + }, + } + }, + }, + { + Name: `thrice`, + Factory: func(t *testing.T, f *StringSliceFlag) Config { + v := SliceFlag[*SliceFlag[*SliceFlag[*StringSliceFlag, []string, string], []string, string], []string, string]{ + Target: &SliceFlag[*SliceFlag[*StringSliceFlag, []string, string], []string, string]{ + Target: &SliceFlag[*StringSliceFlag, []string, string]{Target: f}, + }, + } + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Target.Destination) + }, + } + }, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + t.Run(`destination`, func(t *testing.T) { + c := tc.Factory(t, &StringSliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []string{`one`, ``, ``, `two`, ``} + var vTarget []string + *c.Value = vDefault + *c.Destination = &vTarget + if err := (&App{Action: func(c *Context) error { return nil }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=`, `--a=three`, `--a=`, `--a=`, `--a=four`, `--a=`, `--a=`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []string{`one`, ``, ``, `two`, ``}) + expectEqual(t, vTarget, []string{"", "three", "", "", "four", "", ""}) + }) + t.Run(`context`, func(t *testing.T) { + c := tc.Factory(t, &StringSliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []string{`one`, ``, ``, `two`, ``} + *c.Value = vDefault + var vTarget []string + if err := (&App{Action: func(c *Context) error { + vTarget = c.StringSlice(`a`) + return nil + }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=`, `--a=three`, `--a=`, `--a=`, `--a=four`, `--a=`, `--a=`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []string{`one`, ``, ``, `two`, ``}) + expectEqual(t, vTarget, []string{"", "three", "", "", "four", "", ""}) + }) + t.Run(`context with destination`, func(t *testing.T) { + c := tc.Factory(t, &StringSliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []string{`one`, ``, ``, `two`, ``} + *c.Value = vDefault + var vTarget []string + var destination []string + *c.Destination = &destination + if err := (&App{Action: func(c *Context) error { + vTarget = c.StringSlice(`a`) + return nil + }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=`, `--a=three`, `--a=`, `--a=`, `--a=four`, `--a=`, `--a=`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []string{`one`, ``, ``, `two`, ``}) + expectEqual(t, vTarget, []string{"", "three", "", "", "four", "", ""}) + expectEqual(t, destination, []string{"", "three", "", "", "four", "", ""}) + }) + t.Run(`stdlib flag usage with default`, func(t *testing.T) { + c := tc.Factory(t, &StringSliceFlag{Name: `a`}) + *c.Value = []string{`one`, `two`} + var vTarget []string + *c.Destination = &vTarget + set := flag.NewFlagSet(`flagset`, flag.ContinueOnError) + var output bytes.Buffer + set.SetOutput(&output) + if err := c.Flag.Apply(set); err != nil { + t.Fatal(err) + } + if err := set.Parse([]string{`-h`}); err != flag.ErrHelp { + t.Fatal(err) + } + if s := output.String(); s != "Usage of flagset:\n -a value\n \t (default [one two])\n" { + t.Errorf("unexpected output: %q\n%s", s, s) + } + }) + { + test := func(t *testing.T, value []string) { + c := tc.Factory(t, &StringSliceFlag{Name: `a`}) + *c.Value = value + var vTarget []string + *c.Destination = &vTarget + set := flag.NewFlagSet(`flagset`, flag.ContinueOnError) + var output bytes.Buffer + set.SetOutput(&output) + if err := c.Flag.Apply(set); err != nil { + t.Fatal(err) + } + if err := set.Parse([]string{`-h`}); err != flag.ErrHelp { + t.Fatal(err) + } + if s := output.String(); s != "Usage of flagset:\n -a value\n \t\n" { + t.Errorf("unexpected output: %q\n%s", s, s) + } + } + t.Run(`stdlib flag usage without default nil`, func(t *testing.T) { + test(t, nil) + }) + t.Run(`stdlib flag usage without default empty`, func(t *testing.T) { + test(t, make([]string, 0)) + }) + } + }) + } +} + +func TestSliceFlag_Apply_float64(t *testing.T) { + normalise := func(v any) any { + switch v := v.(type) { + case *[]float64: + if v == nil { + return nil + } + return *v + case *Float64Slice: + if v == nil { + return nil + } + return v.Value() + } + return v + } + expectEqual := func(t *testing.T, actual, expected any) { + t.Helper() + actual = normalise(actual) + expected = normalise(expected) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("actual: %#v\nexpected: %#v", actual, expected) + } + } + type Config struct { + Flag SliceFlagTarget[float64] + Value *[]float64 + Destination **[]float64 + Context *Context + Check func() + } + for _, tc := range [...]struct { + Name string + Factory func(t *testing.T, f *Float64SliceFlag) Config + }{ + { + Name: `once`, + Factory: func(t *testing.T, f *Float64SliceFlag) Config { + v := SliceFlag[*Float64SliceFlag, []float64, float64]{Target: f} + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + }, + } + }, + }, + { + Name: `twice`, + Factory: func(t *testing.T, f *Float64SliceFlag) Config { + v := SliceFlag[*SliceFlag[*Float64SliceFlag, []float64, float64], []float64, float64]{ + Target: &SliceFlag[*Float64SliceFlag, []float64, float64]{Target: f}, + } + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Destination) + }, + } + }, + }, + { + Name: `thrice`, + Factory: func(t *testing.T, f *Float64SliceFlag) Config { + v := SliceFlag[*SliceFlag[*SliceFlag[*Float64SliceFlag, []float64, float64], []float64, float64], []float64, float64]{ + Target: &SliceFlag[*SliceFlag[*Float64SliceFlag, []float64, float64], []float64, float64]{ + Target: &SliceFlag[*Float64SliceFlag, []float64, float64]{Target: f}, + }, + } + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Target.Destination) + }, + } + }, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + t.Run(`destination`, func(t *testing.T) { + c := tc.Factory(t, &Float64SliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []float64{1, 2, 3} + var vTarget []float64 + *c.Value = vDefault + *c.Destination = &vTarget + if err := (&App{Action: func(c *Context) error { return nil }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []float64{1, 2, 3}) + expectEqual(t, vTarget, []float64{4, 5}) + }) + t.Run(`context`, func(t *testing.T) { + c := tc.Factory(t, &Float64SliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []float64{1, 2, 3} + *c.Value = vDefault + var vTarget []float64 + if err := (&App{Action: func(c *Context) error { + vTarget = c.Float64Slice(`a`) + return nil + }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []float64{1, 2, 3}) + expectEqual(t, vTarget, []float64{4, 5}) + }) + t.Run(`context with destination`, func(t *testing.T) { + c := tc.Factory(t, &Float64SliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []float64{1, 2, 3} + *c.Value = vDefault + var vTarget []float64 + var destination []float64 + *c.Destination = &destination + if err := (&App{Action: func(c *Context) error { + vTarget = c.Float64Slice(`a`) + return nil + }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []float64{1, 2, 3}) + expectEqual(t, vTarget, []float64{4, 5}) + expectEqual(t, destination, []float64{4, 5}) + }) + t.Run(`stdlib flag usage with default`, func(t *testing.T) { + c := tc.Factory(t, &Float64SliceFlag{Name: `a`}) + *c.Value = []float64{1, 2} + var vTarget []float64 + *c.Destination = &vTarget + set := flag.NewFlagSet(`flagset`, flag.ContinueOnError) + var output bytes.Buffer + set.SetOutput(&output) + if err := c.Flag.Apply(set); err != nil { + t.Fatal(err) + } + if err := set.Parse([]string{`-h`}); err != flag.ErrHelp { + t.Fatal(err) + } + if s := output.String(); s != "Usage of flagset:\n -a value\n \t (default []float64{1, 2})\n" { + t.Errorf("unexpected output: %q\n%s", s, s) + } + }) + { + test := func(t *testing.T, value []float64) { + c := tc.Factory(t, &Float64SliceFlag{Name: `a`}) + *c.Value = value + var vTarget []float64 + *c.Destination = &vTarget + set := flag.NewFlagSet(`flagset`, flag.ContinueOnError) + var output bytes.Buffer + set.SetOutput(&output) + if err := c.Flag.Apply(set); err != nil { + t.Fatal(err) + } + if err := set.Parse([]string{`-h`}); err != flag.ErrHelp { + t.Fatal(err) + } + if s := output.String(); s != "Usage of flagset:\n -a value\n \t\n" { + t.Errorf("unexpected output: %q\n%s", s, s) + } + } + t.Run(`stdlib flag usage without default nil`, func(t *testing.T) { + test(t, nil) + }) + t.Run(`stdlib flag usage without default empty`, func(t *testing.T) { + test(t, make([]float64, 0)) + }) + } + }) + } +} + +func TestSliceFlag_Apply_int64(t *testing.T) { + normalise := func(v any) any { + switch v := v.(type) { + case *[]int64: + if v == nil { + return nil + } + return *v + case *Int64Slice: + if v == nil { + return nil + } + return v.Value() + } + return v + } + expectEqual := func(t *testing.T, actual, expected any) { + t.Helper() + actual = normalise(actual) + expected = normalise(expected) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("actual: %#v\nexpected: %#v", actual, expected) + } + } + type Config struct { + Flag SliceFlagTarget[int64] + Value *[]int64 + Destination **[]int64 + Context *Context + Check func() + } + for _, tc := range [...]struct { + Name string + Factory func(t *testing.T, f *Int64SliceFlag) Config + }{ + { + Name: `once`, + Factory: func(t *testing.T, f *Int64SliceFlag) Config { + v := SliceFlag[*Int64SliceFlag, []int64, int64]{Target: f} + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + }, + } + }, + }, + { + Name: `twice`, + Factory: func(t *testing.T, f *Int64SliceFlag) Config { + v := SliceFlag[*SliceFlag[*Int64SliceFlag, []int64, int64], []int64, int64]{ + Target: &SliceFlag[*Int64SliceFlag, []int64, int64]{Target: f}, + } + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Destination) + }, + } + }, + }, + { + Name: `thrice`, + Factory: func(t *testing.T, f *Int64SliceFlag) Config { + v := SliceFlag[*SliceFlag[*SliceFlag[*Int64SliceFlag, []int64, int64], []int64, int64], []int64, int64]{ + Target: &SliceFlag[*SliceFlag[*Int64SliceFlag, []int64, int64], []int64, int64]{ + Target: &SliceFlag[*Int64SliceFlag, []int64, int64]{Target: f}, + }, + } + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Target.Destination) + }, + } + }, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + t.Run(`destination`, func(t *testing.T) { + c := tc.Factory(t, &Int64SliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []int64{1, 2, 3} + var vTarget []int64 + *c.Value = vDefault + *c.Destination = &vTarget + if err := (&App{Action: func(c *Context) error { return nil }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []int64{1, 2, 3}) + expectEqual(t, vTarget, []int64{4, 5}) + }) + t.Run(`context`, func(t *testing.T) { + c := tc.Factory(t, &Int64SliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []int64{1, 2, 3} + *c.Value = vDefault + var vTarget []int64 + if err := (&App{Action: func(c *Context) error { + vTarget = c.Int64Slice(`a`) + return nil + }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []int64{1, 2, 3}) + expectEqual(t, vTarget, []int64{4, 5}) + }) + t.Run(`context with destination`, func(t *testing.T) { + c := tc.Factory(t, &Int64SliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []int64{1, 2, 3} + *c.Value = vDefault + var vTarget []int64 + var destination []int64 + *c.Destination = &destination + if err := (&App{Action: func(c *Context) error { + vTarget = c.Int64Slice(`a`) + return nil + }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []int64{1, 2, 3}) + expectEqual(t, vTarget, []int64{4, 5}) + expectEqual(t, destination, []int64{4, 5}) + }) + t.Run(`stdlib flag usage with default`, func(t *testing.T) { + c := tc.Factory(t, &Int64SliceFlag{Name: `a`}) + *c.Value = []int64{1, 2} + var vTarget []int64 + *c.Destination = &vTarget + set := flag.NewFlagSet(`flagset`, flag.ContinueOnError) + var output bytes.Buffer + set.SetOutput(&output) + if err := c.Flag.Apply(set); err != nil { + t.Fatal(err) + } + if err := set.Parse([]string{`-h`}); err != flag.ErrHelp { + t.Fatal(err) + } + if s := output.String(); s != "Usage of flagset:\n -a value\n \t (default []int64{1, 2})\n" { + t.Errorf("unexpected output: %q\n%s", s, s) + } + }) + { + test := func(t *testing.T, value []int64) { + c := tc.Factory(t, &Int64SliceFlag{Name: `a`}) + *c.Value = value + var vTarget []int64 + *c.Destination = &vTarget + set := flag.NewFlagSet(`flagset`, flag.ContinueOnError) + var output bytes.Buffer + set.SetOutput(&output) + if err := c.Flag.Apply(set); err != nil { + t.Fatal(err) + } + if err := set.Parse([]string{`-h`}); err != flag.ErrHelp { + t.Fatal(err) + } + if s := output.String(); s != "Usage of flagset:\n -a value\n \t\n" { + t.Errorf("unexpected output: %q\n%s", s, s) + } + } + t.Run(`stdlib flag usage without default nil`, func(t *testing.T) { + test(t, nil) + }) + t.Run(`stdlib flag usage without default empty`, func(t *testing.T) { + test(t, make([]int64, 0)) + }) + } + }) + } +} + +func TestSliceFlag_Apply_int(t *testing.T) { + normalise := func(v any) any { + switch v := v.(type) { + case *[]int: + if v == nil { + return nil + } + return *v + case *IntSlice: + if v == nil { + return nil + } + return v.Value() + } + return v + } + expectEqual := func(t *testing.T, actual, expected any) { + t.Helper() + actual = normalise(actual) + expected = normalise(expected) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("actual: %#v\nexpected: %#v", actual, expected) + } + } + type Config struct { + Flag SliceFlagTarget[int] + Value *[]int + Destination **[]int + Context *Context + Check func() + } + for _, tc := range [...]struct { + Name string + Factory func(t *testing.T, f *IntSliceFlag) Config + }{ + { + Name: `once`, + Factory: func(t *testing.T, f *IntSliceFlag) Config { + v := SliceFlag[*IntSliceFlag, []int, int]{Target: f} + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + }, + } + }, + }, + { + Name: `twice`, + Factory: func(t *testing.T, f *IntSliceFlag) Config { + v := SliceFlag[*SliceFlag[*IntSliceFlag, []int, int], []int, int]{ + Target: &SliceFlag[*IntSliceFlag, []int, int]{Target: f}, + } + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Destination) + }, + } + }, + }, + { + Name: `thrice`, + Factory: func(t *testing.T, f *IntSliceFlag) Config { + v := SliceFlag[*SliceFlag[*SliceFlag[*IntSliceFlag, []int, int], []int, int], []int, int]{ + Target: &SliceFlag[*SliceFlag[*IntSliceFlag, []int, int], []int, int]{ + Target: &SliceFlag[*IntSliceFlag, []int, int]{Target: f}, + }, + } + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Target.Destination) + }, + } + }, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + t.Run(`destination`, func(t *testing.T) { + c := tc.Factory(t, &IntSliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []int{1, 2, 3} + var vTarget []int + *c.Value = vDefault + *c.Destination = &vTarget + if err := (&App{Action: func(c *Context) error { return nil }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []int{1, 2, 3}) + expectEqual(t, vTarget, []int{4, 5}) + }) + t.Run(`context`, func(t *testing.T) { + c := tc.Factory(t, &IntSliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []int{1, 2, 3} + *c.Value = vDefault + var vTarget []int + if err := (&App{Action: func(c *Context) error { + vTarget = c.IntSlice(`a`) + return nil + }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []int{1, 2, 3}) + expectEqual(t, vTarget, []int{4, 5}) + }) + t.Run(`context with destination`, func(t *testing.T) { + c := tc.Factory(t, &IntSliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []int{1, 2, 3} + *c.Value = vDefault + var vTarget []int + var destination []int + *c.Destination = &destination + if err := (&App{Action: func(c *Context) error { + vTarget = c.IntSlice(`a`) + return nil + }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []int{1, 2, 3}) + expectEqual(t, vTarget, []int{4, 5}) + expectEqual(t, destination, []int{4, 5}) + }) + t.Run(`stdlib flag usage with default`, func(t *testing.T) { + c := tc.Factory(t, &IntSliceFlag{Name: `a`}) + *c.Value = []int{1, 2} + var vTarget []int + *c.Destination = &vTarget + set := flag.NewFlagSet(`flagset`, flag.ContinueOnError) + var output bytes.Buffer + set.SetOutput(&output) + if err := c.Flag.Apply(set); err != nil { + t.Fatal(err) + } + if err := set.Parse([]string{`-h`}); err != flag.ErrHelp { + t.Fatal(err) + } + if s := output.String(); s != "Usage of flagset:\n -a value\n \t (default []int{1, 2})\n" { + t.Errorf("unexpected output: %q\n%s", s, s) + } + }) + { + test := func(t *testing.T, value []int) { + c := tc.Factory(t, &IntSliceFlag{Name: `a`}) + *c.Value = value + var vTarget []int + *c.Destination = &vTarget + set := flag.NewFlagSet(`flagset`, flag.ContinueOnError) + var output bytes.Buffer + set.SetOutput(&output) + if err := c.Flag.Apply(set); err != nil { + t.Fatal(err) + } + if err := set.Parse([]string{`-h`}); err != flag.ErrHelp { + t.Fatal(err) + } + if s := output.String(); s != "Usage of flagset:\n -a value\n \t\n" { + t.Errorf("unexpected output: %q\n%s", s, s) + } + } + t.Run(`stdlib flag usage without default nil`, func(t *testing.T) { + test(t, nil) + }) + t.Run(`stdlib flag usage without default empty`, func(t *testing.T) { + test(t, make([]int, 0)) + }) + } + }) + } +} + +type intSliceWrapperDefaultingNil struct { + *IntSlice +} + +func (x intSliceWrapperDefaultingNil) String() string { + if x.IntSlice != nil { + return x.IntSlice.String() + } + return NewIntSlice().String() +} + +func TestFlagValueHook_String_struct(t *testing.T) { + wrap := func(values ...int) *flagValueHook { + return &flagValueHook{value: intSliceWrapperDefaultingNil{NewIntSlice(values...)}} + } + if s := wrap().String(); s != `` { + t.Error(s) + } + if s := wrap(1).String(); s != `[]int{1}` { + t.Error(s) + } +} From 4f795e3870f4edd720f02585c904ca4111466117 Mon Sep 17 00:00:00 2001 From: Joseph Cumines Date: Mon, 13 Jun 2022 08:23:41 +1000 Subject: [PATCH 2/2] Fix build for go < 1.18 --- flag_float64_slice.go | 15 ---------- flag_int64_slice.go | 15 ---------- flag_int_slice.go | 15 ---------- flag_string_slice.go | 15 ---------- sliceflag.go | 65 +++++++++++++++++++++++++++++++++++++++++++ sliceflag_pre18.go | 10 +++++++ sliceflag_test.go | 3 ++ 7 files changed, 78 insertions(+), 60 deletions(-) create mode 100644 sliceflag_pre18.go diff --git a/flag_float64_slice.go b/flag_float64_slice.go index 8452d29245..031ec1d1aa 100644 --- a/flag_float64_slice.go +++ b/flag_float64_slice.go @@ -164,21 +164,6 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error { return nil } -func (f *Float64SliceFlag) SetValue(slice []float64) { - f.Value = newSliceFlagValue(NewFloat64Slice, slice) -} - -func (f *Float64SliceFlag) SetDestination(slice []float64) { - f.Destination = newSliceFlagValue(NewFloat64Slice, slice) -} - -func (f *Float64SliceFlag) GetDestination() []float64 { - if destination := f.Destination; destination != nil { - return destination.Value() - } - return nil -} - // Get returns the flag’s value in the given Context. func (f *Float64SliceFlag) Get(ctx *Context) []float64 { return ctx.Float64Slice(f.Name) diff --git a/flag_int64_slice.go b/flag_int64_slice.go index 0efea54006..657aaaaf33 100644 --- a/flag_int64_slice.go +++ b/flag_int64_slice.go @@ -163,21 +163,6 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error { return nil } -func (f *Int64SliceFlag) SetValue(slice []int64) { - f.Value = newSliceFlagValue(NewInt64Slice, slice) -} - -func (f *Int64SliceFlag) SetDestination(slice []int64) { - f.Destination = newSliceFlagValue(NewInt64Slice, slice) -} - -func (f *Int64SliceFlag) GetDestination() []int64 { - if destination := f.Destination; destination != nil { - return destination.Value() - } - return nil -} - // Get returns the flag’s value in the given Context. func (f *Int64SliceFlag) Get(ctx *Context) []int64 { return ctx.Int64Slice(f.Name) diff --git a/flag_int_slice.go b/flag_int_slice.go index 28f8c90303..7c383935ac 100644 --- a/flag_int_slice.go +++ b/flag_int_slice.go @@ -174,21 +174,6 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) error { return nil } -func (f *IntSliceFlag) SetValue(slice []int) { - f.Value = newSliceFlagValue(NewIntSlice, slice) -} - -func (f *IntSliceFlag) SetDestination(slice []int) { - f.Destination = newSliceFlagValue(NewIntSlice, slice) -} - -func (f *IntSliceFlag) GetDestination() []int { - if destination := f.Destination; destination != nil { - return destination.Value() - } - return nil -} - // Get returns the flag’s value in the given Context. func (f *IntSliceFlag) Get(ctx *Context) []int { return ctx.IntSlice(f.Name) diff --git a/flag_string_slice.go b/flag_string_slice.go index 2be2af62d7..bcdfd4c554 100644 --- a/flag_string_slice.go +++ b/flag_string_slice.go @@ -152,21 +152,6 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { return nil } -func (f *StringSliceFlag) SetValue(slice []string) { - f.Value = newSliceFlagValue(NewStringSlice, slice) -} - -func (f *StringSliceFlag) SetDestination(slice []string) { - f.Destination = newSliceFlagValue(NewStringSlice, slice) -} - -func (f *StringSliceFlag) GetDestination() []string { - if destination := f.Destination; destination != nil { - return destination.Value() - } - return nil -} - // Get returns the flag’s value in the given Context. func (f *StringSliceFlag) Get(ctx *Context) []string { return ctx.StringSlice(f.Name) diff --git a/sliceflag.go b/sliceflag.go index 80063efc5a..7dea3576a3 100644 --- a/sliceflag.go +++ b/sliceflag.go @@ -1,3 +1,6 @@ +//go:build go1.18 +// +build go1.18 + package cli import ( @@ -226,3 +229,65 @@ func unwrapFlagValue(v flag.Value) flag.Value { v = h.value } } + +// NOTE: the methods below are in this file to make use of the build constraint + +func (f *Float64SliceFlag) SetValue(slice []float64) { + f.Value = newSliceFlagValue(NewFloat64Slice, slice) +} + +func (f *Float64SliceFlag) SetDestination(slice []float64) { + f.Destination = newSliceFlagValue(NewFloat64Slice, slice) +} + +func (f *Float64SliceFlag) GetDestination() []float64 { + if destination := f.Destination; destination != nil { + return destination.Value() + } + return nil +} + +func (f *Int64SliceFlag) SetValue(slice []int64) { + f.Value = newSliceFlagValue(NewInt64Slice, slice) +} + +func (f *Int64SliceFlag) SetDestination(slice []int64) { + f.Destination = newSliceFlagValue(NewInt64Slice, slice) +} + +func (f *Int64SliceFlag) GetDestination() []int64 { + if destination := f.Destination; destination != nil { + return destination.Value() + } + return nil +} + +func (f *IntSliceFlag) SetValue(slice []int) { + f.Value = newSliceFlagValue(NewIntSlice, slice) +} + +func (f *IntSliceFlag) SetDestination(slice []int) { + f.Destination = newSliceFlagValue(NewIntSlice, slice) +} + +func (f *IntSliceFlag) GetDestination() []int { + if destination := f.Destination; destination != nil { + return destination.Value() + } + return nil +} + +func (f *StringSliceFlag) SetValue(slice []string) { + f.Value = newSliceFlagValue(NewStringSlice, slice) +} + +func (f *StringSliceFlag) SetDestination(slice []string) { + f.Destination = newSliceFlagValue(NewStringSlice, slice) +} + +func (f *StringSliceFlag) GetDestination() []string { + if destination := f.Destination; destination != nil { + return destination.Value() + } + return nil +} diff --git a/sliceflag_pre18.go b/sliceflag_pre18.go new file mode 100644 index 0000000000..1173ae7402 --- /dev/null +++ b/sliceflag_pre18.go @@ -0,0 +1,10 @@ +//go:build !go1.18 +// +build !go1.18 + +package cli + +import ( + "flag" +) + +func unwrapFlagValue(v flag.Value) flag.Value { return v } diff --git a/sliceflag_test.go b/sliceflag_test.go index 8ff905583b..179020b14f 100644 --- a/sliceflag_test.go +++ b/sliceflag_test.go @@ -1,3 +1,6 @@ +//go:build go1.18 +// +build go1.18 + package cli import (