Merge pull request #1409 from joeycumines/main
Add SliceFlag wrapper and fix bugs in existing implementations
This commit is contained in:
commit
8007c54e1d
@ -56,7 +56,12 @@ func (f *Float64Slice) Set(value string) error {
|
|||||||
|
|
||||||
// String returns a readable representation of this value (for usage defaults)
|
// String returns a readable representation of this value (for usage defaults)
|
||||||
func (f *Float64Slice) String() string {
|
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
|
// 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
|
// Apply populates the flag given the flag set and environment
|
||||||
func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error {
|
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, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
|
||||||
if val != "" {
|
if val != "" {
|
||||||
f.Value = &Float64Slice{}
|
|
||||||
|
|
||||||
for _, s := range flagSplitMultiValues(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 float64 slice value from %s for flag %s: %s", f.Value, source, f.Name, err)
|
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
|
// 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.
|
// flags that have already been set by the environment.
|
||||||
f.Value.hasBeenSet = false
|
setValue.hasBeenSet = false
|
||||||
f.HasBeenSet = true
|
f.HasBeenSet = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Value == nil {
|
|
||||||
f.Value = &Float64Slice{}
|
|
||||||
}
|
|
||||||
copyValue := f.Value.clone()
|
|
||||||
for _, name := range f.Names() {
|
for _, name := range f.Names() {
|
||||||
set.Var(copyValue, name, f.Usage)
|
set.Var(setValue, name, f.Usage)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -165,7 +181,7 @@ func (cCtx *Context) Float64Slice(name string) []float64 {
|
|||||||
func lookupFloat64Slice(name string, set *flag.FlagSet) []float64 {
|
func lookupFloat64Slice(name string, set *flag.FlagSet) []float64 {
|
||||||
f := set.Lookup(name)
|
f := set.Lookup(name)
|
||||||
if f != nil {
|
if f != nil {
|
||||||
if slice, ok := f.Value.(*Float64Slice); ok {
|
if slice, ok := unwrapFlagValue(f.Value).(*Float64Slice); ok {
|
||||||
return slice.Value()
|
return slice.Value()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,12 @@ func (i *Int64Slice) Set(value string) error {
|
|||||||
|
|
||||||
// String returns a readable representation of this value (for usage defaults)
|
// String returns a readable representation of this value (for usage defaults)
|
||||||
func (i *Int64Slice) String() string {
|
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
|
// 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
|
// Apply populates the flag given the flag set and environment
|
||||||
func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error {
|
func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error {
|
||||||
if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
|
// apply any default
|
||||||
f.Value = &Int64Slice{}
|
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) {
|
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)
|
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
|
// 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.
|
// flags that have already been set by the environment.
|
||||||
f.Value.hasBeenSet = false
|
setValue.hasBeenSet = false
|
||||||
f.HasBeenSet = true
|
f.HasBeenSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Value == nil {
|
|
||||||
f.Value = &Int64Slice{}
|
|
||||||
}
|
|
||||||
copyValue := f.Value.clone()
|
|
||||||
for _, name := range f.Names() {
|
for _, name := range f.Names() {
|
||||||
set.Var(copyValue, name, f.Usage)
|
set.Var(setValue, name, f.Usage)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -164,7 +180,7 @@ func (cCtx *Context) Int64Slice(name string) []int64 {
|
|||||||
func lookupInt64Slice(name string, set *flag.FlagSet) []int64 {
|
func lookupInt64Slice(name string, set *flag.FlagSet) []int64 {
|
||||||
f := set.Lookup(name)
|
f := set.Lookup(name)
|
||||||
if f != nil {
|
if f != nil {
|
||||||
if slice, ok := f.Value.(*Int64Slice); ok {
|
if slice, ok := unwrapFlagValue(f.Value).(*Int64Slice); ok {
|
||||||
return slice.Value()
|
return slice.Value()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,12 @@ func (i *IntSlice) Set(value string) error {
|
|||||||
|
|
||||||
// String returns a readable representation of this value (for usage defaults)
|
// String returns a readable representation of this value (for usage defaults)
|
||||||
func (i *IntSlice) String() string {
|
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
|
// 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
|
// Apply populates the flag given the flag set and environment
|
||||||
func (f *IntSliceFlag) Apply(set *flag.FlagSet) error {
|
func (f *IntSliceFlag) Apply(set *flag.FlagSet) error {
|
||||||
if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
|
// apply any default
|
||||||
f.Value = &IntSlice{}
|
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) {
|
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)
|
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
|
// 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.
|
// flags that have already been set by the environment.
|
||||||
f.Value.hasBeenSet = false
|
setValue.hasBeenSet = false
|
||||||
f.HasBeenSet = true
|
f.HasBeenSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Value == nil {
|
|
||||||
f.Value = &IntSlice{}
|
|
||||||
}
|
|
||||||
copyValue := f.Value.clone()
|
|
||||||
for _, name := range f.Names() {
|
for _, name := range f.Names() {
|
||||||
set.Var(copyValue, name, f.Usage)
|
set.Var(setValue, name, f.Usage)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -175,7 +191,7 @@ func (cCtx *Context) IntSlice(name string) []int {
|
|||||||
func lookupIntSlice(name string, set *flag.FlagSet) []int {
|
func lookupIntSlice(name string, set *flag.FlagSet) []int {
|
||||||
f := set.Lookup(name)
|
f := set.Lookup(name)
|
||||||
if f != nil {
|
if f != nil {
|
||||||
if slice, ok := f.Value.(*IntSlice); ok {
|
if slice, ok := unwrapFlagValue(f.Value).(*IntSlice); ok {
|
||||||
return slice.Value()
|
return slice.Value()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,41 +115,36 @@ func (f *StringSliceFlag) GetEnvVars() []string {
|
|||||||
|
|
||||||
// Apply populates the flag given the flag set and environment
|
// Apply populates the flag given the flag set and environment
|
||||||
func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
|
func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
|
||||||
|
// apply any default
|
||||||
if f.Destination != nil && f.Value != nil {
|
if f.Destination != nil && f.Value != nil {
|
||||||
f.Destination.slice = make([]string, len(f.Value.slice))
|
f.Destination.slice = make([]string, len(f.Value.slice))
|
||||||
copy(f.Destination.slice, 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 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) {
|
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)
|
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
|
// 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.
|
// flags that have already been set by the environment.
|
||||||
destination.hasBeenSet = false
|
setValue.hasBeenSet = false
|
||||||
f.HasBeenSet = true
|
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() {
|
for _, name := range f.Names() {
|
||||||
set.Var(setValue, name, f.Usage)
|
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 {
|
func lookupStringSlice(name string, set *flag.FlagSet) []string {
|
||||||
f := set.Lookup(name)
|
f := set.Lookup(name)
|
||||||
if f != nil {
|
if f != nil {
|
||||||
if slice, ok := f.Value.(*StringSlice); ok {
|
if slice, ok := unwrapFlagValue(f.Value).(*StringSlice); ok {
|
||||||
return slice.Value()
|
return slice.Value()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
90
flag_test.go
90
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: .*`},
|
{"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"}}, ""},
|
{"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", 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: .*`},
|
{"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)
|
expect(t, err, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStringSliceFlagApply_UsesEnvValues(t *testing.T) {
|
func TestStringSliceFlagApply_UsesEnvValues_noDefault(t *testing.T) {
|
||||||
defer resetEnv(os.Environ())
|
defer resetEnv(os.Environ())
|
||||||
os.Clearenv()
|
os.Clearenv()
|
||||||
_ = os.Setenv("MY_GOAT", "vincent van goat,scape goat")
|
_ = os.Setenv("MY_GOAT", "vincent van goat,scape goat")
|
||||||
@ -615,7 +615,22 @@ func TestStringSliceFlagApply_UsesEnvValues(t *testing.T) {
|
|||||||
|
|
||||||
err := set.Parse(nil)
|
err := set.Parse(nil)
|
||||||
expect(t, err, 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) {
|
func TestStringSliceFlagApply_DefaultValueWithDestination(t *testing.T) {
|
||||||
@ -1406,6 +1421,75 @@ func TestParseMultiStringSliceWithDestinationAndEnv(t *testing.T) {
|
|||||||
}).Run([]string{"run", "-s", "10", "-s", "20"})
|
}).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) {
|
func TestParseMultiStringSliceWithDefaultsUnset(t *testing.T) {
|
||||||
_ = (&App{
|
_ = (&App{
|
||||||
Flags: []Flag{
|
Flags: []Flag{
|
||||||
|
293
sliceflag.go
Normal file
293
sliceflag.go
Normal file
@ -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
|
||||||
|
}
|
10
sliceflag_pre18.go
Normal file
10
sliceflag_pre18.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
//go:build !go1.18
|
||||||
|
// +build !go1.18
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func unwrapFlagValue(v flag.Value) flag.Value { return v }
|
1005
sliceflag_test.go
Normal file
1005
sliceflag_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user