Skip to content

Commit 60abda2

Browse files
committed
feat: support flags with array types
Basic array types are supported: - strings - ints - floats - bools
1 parent 48fdfc3 commit 60abda2

3 files changed

Lines changed: 123 additions & 0 deletions

File tree

flag_def.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package command
22

33
import (
44
"cmp"
5+
"encoding/csv"
56
"errors"
67
"fmt"
78
"reflect"
89
"strconv"
10+
"strings"
911
)
1012

1113
type ErrInvalidValue struct {
@@ -104,6 +106,47 @@ func (fd *flagDef) setValue(sv string) error {
104106
}
105107
case reflect.String:
106108
fv.SetString(sv)
109+
case reflect.Slice:
110+
r := csv.NewReader(strings.NewReader(sv))
111+
r.LazyQuotes = true
112+
r.TrimLeadingSpace = true
113+
rec, err := r.Read()
114+
if err != nil {
115+
return &ErrInvalidValue{Cause: err, Value: sv, Flag: fd.Name}
116+
}
117+
118+
inValue := reflect.ValueOf(rec)
119+
120+
targetType := fv.Type().Elem()
121+
outSlice := reflect.MakeSlice(reflect.SliceOf(targetType), inValue.Len(), inValue.Len())
122+
for i, inElem := range rec {
123+
var outElem interface{}
124+
var err error
125+
switch targetType.Kind() {
126+
case reflect.String:
127+
outElem = inElem
128+
case reflect.Int:
129+
outElem, err = strconv.Atoi(inElem)
130+
case reflect.Float32:
131+
if f64, parseErr := strconv.ParseFloat(inElem, 32); parseErr == nil {
132+
outElem = float32(f64)
133+
} else {
134+
outElem = nil
135+
err = parseErr
136+
}
137+
case reflect.Float64:
138+
outElem, err = strconv.ParseFloat(inElem, 64)
139+
case reflect.Bool:
140+
outElem, err = strconv.ParseBool(inElem)
141+
default:
142+
return fmt.Errorf("%w: field kind is '%s'", errors.ErrUnsupported, fv.Kind())
143+
}
144+
if err != nil {
145+
return &ErrInvalidValue{Cause: err, Value: inElem, Flag: fd.Name}
146+
}
147+
outSlice.Index(i).Set(reflect.ValueOf(outElem).Convert(outSlice.Type().Elem()))
148+
}
149+
fv.Set(outSlice)
107150
default:
108151
return fmt.Errorf("%w: field kind is '%s'", errors.ErrUnsupported, fv.Kind())
109152
}

flag_set.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,17 @@ func (fs *flagSet) readFlagFromField(fieldValue reflect.Value, structField refle
253253
case reflect.String:
254254
fd.HasValue = true
255255
fd.DefaultValue = fieldValue.String()
256+
case reflect.Slice:
257+
fd.HasValue = true
258+
var defaultValues []string
259+
for i := 0; i < fieldValue.Len(); i++ {
260+
defaultValues = append(defaultValues, fieldValue.Index(i).String())
261+
}
262+
if defaultValues != nil {
263+
fd.DefaultValue = strings.Join(defaultValues, ",")
264+
} else {
265+
fd.DefaultValue = ""
266+
}
256267
default:
257268
// Unsupported flag field type
258269
return fmt.Errorf("unsupported field type: %s", fieldValue.Kind())

flag_set_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,30 @@ func TestNewFlagSet(t *testing.T) {
609609
}
610610
}
611611

612+
func TestFlagSetWithArrays(t *testing.T) {
613+
t.Parallel()
614+
615+
config := &struct {
616+
MyArray []string `flag:"true" `
617+
}{MyArray: []string{"v1", "v2"}}
618+
619+
valueOfConfig := reflect.ValueOf(config)
620+
fs, err := newFlagSet(nil, valueOfConfig)
621+
With(t).Verify(err).Will(BeNil()).OrFail()
622+
if len(fs.flags) != 1 {
623+
t.Fatalf("Expected 1 flag, got %d", len(fs.flags))
624+
}
625+
626+
f := fs.flags[0]
627+
With(t).Verify(f.Name).Will(EqualTo("my-array")).OrFail()
628+
With(t).Verify(f.EnvVarName).Will(BeNil()).OrFail()
629+
With(t).Verify(f.HasValue).Will(EqualTo(true)).OrFail()
630+
With(t).Verify(f.ValueName).Will(BeNil()).OrFail()
631+
With(t).Verify(f.Description).Will(BeNil()).OrFail()
632+
With(t).Verify(f.Required).Will(BeNil()).OrFail()
633+
With(t).Verify(f.DefaultValue).Will(EqualTo("v1,v2")).OrFail()
634+
}
635+
612636
func TestFlagSetGetMergedFlagDefs(t *testing.T) {
613637
t.Parallel()
614638
type testCase struct {
@@ -891,6 +915,51 @@ func TestFlagSetApply(t *testing.T) {
891915
expectedError string
892916
}
893917
testCases := map[string]testCase{
918+
"all types are supported from CLI": {
919+
config: &struct {
920+
String string `flag:"true"`
921+
Int int `flag:"true"`
922+
Float32 float32 `flag:"true"`
923+
Float64 float64 `flag:"true"`
924+
Bool bool `flag:"true"`
925+
StringArray []string `flag:"true"`
926+
IntArray []int `flag:"true"`
927+
Float32Array []float32 `flag:"true"`
928+
Float64Array []float64 `flag:"true"`
929+
}{},
930+
args: []string{
931+
"--string", "s1",
932+
"--int", "9",
933+
"--float32", "1.2",
934+
"--float64", "123.456",
935+
"--bool",
936+
"--string-array", `sa1,"s with space",sa3,,,"`,
937+
"--int-array", `1,2,3,5,8`,
938+
"--float32array", `1.2,3.4,5.6`,
939+
"--float64array", `11.22,33.44,55.66`,
940+
},
941+
expectedConfig: &struct {
942+
String string `flag:"true"`
943+
Int int `flag:"true"`
944+
Float32 float32 `flag:"true"`
945+
Float64 float64 `flag:"true"`
946+
Bool bool `flag:"true"`
947+
StringArray []string `flag:"true"`
948+
IntArray []int `flag:"true"`
949+
Float32Array []float32 `flag:"true"`
950+
Float64Array []float64 `flag:"true"`
951+
}{
952+
String: "s1",
953+
Int: 9,
954+
Float32: 1.2,
955+
Float64: 123.456,
956+
Bool: true,
957+
StringArray: []string{"sa1", "s with space", "sa3", "", "", ""},
958+
IntArray: []int{1, 2, 3, 5, 8},
959+
Float32Array: []float32{1.2, 3.4, 5.6},
960+
Float64Array: []float64{11.22, 33.44, 55.66},
961+
},
962+
},
894963
"CLI overrides environment variables": {
895964
config: &struct {
896965
F1 string `name:"my-field1"`

0 commit comments

Comments
 (0)