Repository: urfave/cli
Pull request #1377 (2022-04-26 02:47:31)

Accept multi-value input on slice flags (#1241)
<!--
  This template provides some ideas of things to include in your PR description.
  To start, try providing a short summary of your changes in the Title above.
  If a section of the PR template does not apply to this PR, then delete that section.
 -->

## What type of PR is this?

- bug

## What this PR does / why we need it:

See #1241 


Patch:
diff --git a/flag.go b/flag.go
index b85bafd..d2a33f0 100644
--- a/flag.go
+++ b/flag.go
@@ -402,3 +402,7 @@ func flagFromEnvOrFile(envVars []string, filePath string) (val string, ok bool)
 	}
 	return "", false
 }
+
+func flagSplitMultiValues(val string) []string {
+	return strings.Split(val, ",")
+}
diff --git a/flag_float64_slice.go b/flag_float64_slice.go
index d573ae5..219c501 100644
--- a/flag_float64_slice.go
+++ b/flag_float64_slice.go
@@ -43,12 +43,14 @@ func (f *Float64Slice) Set(value string) error {
 		return nil
 	}
 
-	tmp, err := strconv.ParseFloat(value, 64)
-	if err != nil {
-		return err
-	}
+	for _, s := range flagSplitMultiValues(value) {
+		tmp, err := strconv.ParseFloat(strings.TrimSpace(s), 64)
+		if err != nil {
+			return err
+		}
 
-	f.slice = append(f.slice, tmp)
+		f.slice = append(f.slice, tmp)
+	}
 	return nil
 }
 
@@ -151,7 +153,7 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error {
 		if val != "" {
 			f.Value = &Float64Slice{}
 
-			for _, s := range strings.Split(val, ",") {
+			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 for flag %s: %s", f.Value, f.Name, err)
 				}
diff --git a/flag_int64_slice.go b/flag_int64_slice.go
index 16f5665..6ea7ae8 100644
--- a/flag_int64_slice.go
+++ b/flag_int64_slice.go
@@ -43,12 +43,14 @@ func (i *Int64Slice) Set(value string) error {
 		return nil
 	}
 
-	tmp, err := strconv.ParseInt(value, 0, 64)
-	if err != nil {
-		return err
-	}
+	for _, s := range flagSplitMultiValues(value) {
+		tmp, err := strconv.ParseInt(strings.TrimSpace(s), 0, 64)
+		if err != nil {
+			return err
+		}
 
-	i.slice = append(i.slice, tmp)
+		i.slice = append(i.slice, tmp)
+	}
 
 	return nil
 }
@@ -151,7 +153,7 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error {
 	if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
 		f.Value = &Int64Slice{}
 
-		for _, s := range strings.Split(val, ",") {
+		for _, s := range flagSplitMultiValues(val) {
 			if err := f.Value.Set(strings.TrimSpace(s)); err != nil {
 				return fmt.Errorf("could not parse %q as int64 slice value for flag %s: %s", val, f.Name, err)
 			}
diff --git a/flag_int_slice.go b/flag_int_slice.go
index b003229..38f6a33 100644
--- a/flag_int_slice.go
+++ b/flag_int_slice.go
@@ -54,12 +54,14 @@ func (i *IntSlice) Set(value string) error {
 		return nil
 	}
 
-	tmp, err := strconv.ParseInt(value, 0, 64)
-	if err != nil {
-		return err
-	}
+	for _, s := range flagSplitMultiValues(value) {
+		tmp, err := strconv.ParseInt(strings.TrimSpace(s), 0, 64)
+		if err != nil {
+			return err
+		}
 
-	i.slice = append(i.slice, int(tmp))
+		i.slice = append(i.slice, int(tmp))
+	}
 
 	return nil
 }
@@ -162,7 +164,7 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) error {
 	if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
 		f.Value = &IntSlice{}
 
-		for _, s := range strings.Split(val, ",") {
+		for _, s := range flagSplitMultiValues(val) {
 			if err := f.Value.Set(strings.TrimSpace(s)); err != nil {
 				return fmt.Errorf("could not parse %q as int slice value for flag %s: %s", val, f.Name, err)
 			}
diff --git a/flag_string_slice.go b/flag_string_slice.go
index 3879235..ff0a4b7 100644
--- a/flag_string_slice.go
+++ b/flag_string_slice.go
@@ -42,7 +42,9 @@ func (s *StringSlice) Set(value string) error {
 		return nil
 	}
 
-	s.slice = append(s.slice, value)
+	for _, t := range flagSplitMultiValues(value) {
+		s.slice = append(s.slice, strings.TrimSpace(t))
+	}
 
 	return nil
 }
@@ -160,7 +162,7 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
 			destination = f.Destination
 		}
 
-		for _, s := range strings.Split(val, ",") {
+		for _, s := range flagSplitMultiValues(val) {
 			if err := destination.Set(strings.TrimSpace(s)); err != nil {
 				return fmt.Errorf("could not parse %q as string value for flag %s: %s", val, f.Name, err)
 			}


Test patch:
diff --git a/app_test.go b/app_test.go
index 0829ac6..28b497c 100644
--- a/app_test.go
+++ b/app_test.go
@@ -390,6 +390,40 @@ func ExampleApp_Run_zshComplete() {
 	// h:Shows a list of commands or help for one command
 }
 
+func ExampleApp_Run_sliceValues() {
+	// set args for examples sake
+	os.Args = []string{"multi_values",
+		"--stringSclice", "parsed1,parsed2", "--stringSclice", "parsed3,parsed4",
+		"--float64Sclice", "13.3,14.4", "--float64Sclice", "15.5,16.6",
+		"--int64Sclice", "13,14", "--int64Sclice", "15,16",
+		"--intSclice", "13,14", "--intSclice", "15,16",
+	}
+	app := NewApp()
+	app.Name = "multi_values"
+	app.Flags = []Flag{
+		&StringSliceFlag{Name: "stringSclice"},
+		&Float64SliceFlag{Name: "float64Sclice"},
+		&Int64SliceFlag{Name: "int64Sclice"},
+		&IntSliceFlag{Name: "intSclice"},
+	}
+	app.Action = func(ctx *Context) error {
+		for i, v := range ctx.FlagNames() {
+			fmt.Printf("%d-%s %#v\n", i, v, ctx.Value(v))
+		}
+		err := ctx.Err()
+		fmt.Println("error:", err)
+		return err
+	}
+
+	_ = app.Run(os.Args)
+	// Output:
+	// 0-float64Sclice cli.Float64Slice{slice:[]float64{13.3, 14.4, 15.5, 16.6}, hasBeenSet:true}
+	// 1-int64Sclice cli.Int64Slice{slice:[]int64{13, 14, 15, 16}, hasBeenSet:true}
+	// 2-intSclice cli.IntSlice{slice:[]int{13, 14, 15, 16}, hasBeenSet:true}
+	// 3-stringSclice cli.StringSlice{slice:[]string{"parsed1", "parsed2", "parsed3", "parsed4"}, hasBeenSet:true}
+	// error: <nil>
+}
+
 func TestApp_Run(t *testing.T) {
 	s := ""
 
diff --git a/flag_test.go b/flag_test.go
index 843f646..e385276 100644
--- a/flag_test.go
+++ b/flag_test.go
@@ -2230,6 +2230,54 @@ func TestFlagDefaultValue(t *testing.T) {
 	}
 }
 
+type flagValueTestCase struct {
+	name    string
+	flag    Flag
+	toParse []string
+	expect  string
+}
+
+func TestFlagValue(t *testing.T) {
+	cases := []*flagValueTestCase{
+		&flagValueTestCase{
+			name:    "stringSclice",
+			flag:    &StringSliceFlag{Name: "flag", Value: NewStringSlice("default1", "default2")},
+			toParse: []string{"--flag", "parsed,parsed2", "--flag", "parsed3,parsed4"},
+			expect:  `[parsed parsed2 parsed3 parsed4]`,
+		},
+		&flagValueTestCase{
+			name:    "float64Sclice",
+			flag:    &Float64SliceFlag{Name: "flag", Value: NewFloat64Slice(1.1, 2.2)},
+			toParse: []string{"--flag", "13.3,14.4", "--flag", "15.5,16.6"},
+			expect:  `[]float64{13.3, 14.4, 15.5, 16.6}`,
+		},
+		&flagValueTestCase{
+			name:    "int64Sclice",
+			flag:    &Int64SliceFlag{Name: "flag", Value: NewInt64Slice(1, 2)},
+			toParse: []string{"--flag", "13,14", "--flag", "15,16"},
+			expect:  `[]int64{13, 14, 15, 16}`,
+		},
+		&flagValueTestCase{
+			name:    "intSclice",
+			flag:    &IntSliceFlag{Name: "flag", Value: NewIntSlice(1, 2)},
+			toParse: []string{"--flag", "13,14", "--flag", "15,16"},
+			expect:  `[]int{13, 14, 15, 16}`,
+		},
+	}
+	for i, v := range cases {
+		set := flag.NewFlagSet("test", 0)
+		set.SetOutput(ioutil.Discard)
+		_ = v.flag.Apply(set)
+		if err := set.Parse(v.toParse); err != nil {
+			t.Error(err)
+		}
+		f := set.Lookup("flag")
+		if got := f.Value.String(); got != v.expect {
+			t.Errorf("TestFlagValue %d-%s\nexpect:%s\ngot:%s", i, v.name, v.expect, got)
+		}
+	}
+}
+
 func TestTimestampFlagApply_WithDestination(t *testing.T) {
 	var destination Timestamp
 	expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")