diff --git a/flag_float64_slice.go b/flag_float64_slice.go index bc347ccdb2..031ec1d1aa 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,29 +125,40 @@ 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 @@ -165,7 +181,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..657aaaaf33 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,27 +126,38 @@ 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 @@ -164,7 +180,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..7c383935ac 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,27 +137,38 @@ 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 @@ -175,7 +191,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..bcdfd4c554 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) } @@ -174,7 +169,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..7dea3576a3 --- /dev/null +++ b/sliceflag.go @@ -0,0 +1,293 @@ +//go:build go1.18 +// +build go1.18 + +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 + } +} + +// 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 new file mode 100644 index 0000000000..179020b14f --- /dev/null +++ b/sliceflag_test.go @@ -0,0 +1,1005 @@ +//go:build go1.18 +// +build go1.18 + +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) + } +}