Repository: urfave/cli (go)

Issue:
v2 feature: Add Destination for StringSliceFlag
## Checklist

* [x] Are you running the latest v2 release? The list of releases is [here](https://github.com/urfave/cli/releases).
* [x] Did you check the manual for your release? The v2 manual is [here](https://github.com/urfave/cli/blob/master/docs/v2/manual.md)
* [x] Did you perform a search about this feature? Here's the [Github guide](https://help.github.com/en/github/managing-your-work-on-github/using-search-to-filter-issues-and-pull-requests) about searching.

## What problem does this solve?

- My team want to start using the `Destination` values on flags so we can have our flag values strongly typed rather than stringly typed (i.e calling c.String("flag") etc.). I noticed that the `StringSliceFlag` does not have a `Destination` field like other flags do 
 
## Solution description

- Solution would be to add a `Destination` field to `StringSliceFlag` that works the same as all other `Destination` flag fields do.


Pull request:
Allow specifying a StringSlice destination for StringSliceFlag
## What type of PR is this?

- [ ] bug
- [ ] cleanup  
- [ ] documentation
- [x] feature

## What this PR does / why we need it:

Adds a `Destination` field to the `StringSliceFlag` of type `StringSlice` so that we can use a variable to access the value rather than calling `c.StringSlice`. This adds some additional type safety and can help to avoid scenarios where you have typos calling the `c.StringSlice("name")` method.

## Which issue(s) this PR fixes:

Fixes #1075 

## Testing

By running the code I've seen that the `Destination` gets set, I've also added in some tests for setting the destination value from env and from flags

## Release Notes

```release-note
Adds destination field to StringSliceFlag
```

Patch:
diff --git a/flag_string_slice.go b/flag_string_slice.go
index b24664b..ac363bf 100644
--- a/flag_string_slice.go
+++ b/flag_string_slice.go
@@ -71,6 +71,7 @@ type StringSliceFlag struct {
 	Value       *StringSlice
 	DefaultText string
 	HasBeenSet  bool
+	Destination *StringSlice
 }
 
 // IsSet returns whether or not the flag has been set through env or file
@@ -117,13 +118,20 @@ func (f *StringSliceFlag) GetValue() string {
 func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
 	if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
 		f.Value = &StringSlice{}
+		destination := f.Value
+		if f.Destination != nil {
+			destination = f.Destination
+		}
 
 		for _, s := range strings.Split(val, ",") {
-			if err := f.Value.Set(strings.TrimSpace(s)); err != nil {
+			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)
 			}
 		}
 
+		// 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
 		f.HasBeenSet = true
 	}
 
@@ -131,6 +139,12 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
 		if f.Value == nil {
 			f.Value = &StringSlice{}
 		}
+
+		if f.Destination != nil {
+			set.Var(f.Destination, name, f.Usage)
+			continue
+		}
+
 		set.Var(f.Value, name, f.Usage)
 	}
 


Test patch:
diff --git a/flag_test.go b/flag_test.go
index f0c444e..02f5d7d 100644
--- a/flag_test.go
+++ b/flag_test.go
@@ -59,7 +59,7 @@ func TestFlagsFromEnv(t *testing.T) {
 
 	newSetStringSlice := func(defaults ...string) StringSlice {
 		s := NewStringSlice(defaults...)
-		s.hasBeenSet = true
+		s.hasBeenSet = false
 		return *s
 	}
 
@@ -917,6 +917,47 @@ func TestParseMultiStringSliceWithDefaults(t *testing.T) {
 	}).Run([]string{"run", "-s", "10", "-s", "20"})
 }
 
+func TestParseMultiStringSliceWithDestination(t *testing.T) {
+	dest := &StringSlice{}
+	_ = (&App{
+		Flags: []Flag{
+			&StringSliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: dest},
+		},
+		Action: func(ctx *Context) error {
+			expected := []string{"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 TestParseMultiStringSliceWithDestinationAndEnv(t *testing.T) {
+	os.Clearenv()
+	_ = os.Setenv("APP_INTERVALS", "20,30,40")
+
+	dest := &StringSlice{}
+	_ = (&App{
+		Flags: []Flag{
+			&StringSliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: dest, EnvVars: []string{"APP_INTERVALS"}},
+		},
+		Action: func(ctx *Context) error {
+			expected := []string{"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{
@@ -1014,6 +1055,27 @@ func TestParseMultiStringSliceFromEnvCascadeWithDefaults(t *testing.T) {
 	}).Run([]string{"run"})
 }
 
+func TestParseMultiStringSliceFromEnvWithDestination(t *testing.T) {
+	os.Clearenv()
+	_ = os.Setenv("APP_INTERVALS", "20,30,40")
+
+	dest := &StringSlice{}
+	_ = (&App{
+		Flags: []Flag{
+			&StringSliceFlag{Name: "intervals", Aliases: []string{"i"}, Destination: dest, EnvVars: []string{"APP_INTERVALS"}},
+		},
+		Action: func(ctx *Context) error {
+			if !reflect.DeepEqual(dest.slice, []string{"20", "30", "40"}) {
+				t.Errorf("main name not set from env")
+			}
+			if !reflect.DeepEqual(dest.slice, []string{"20", "30", "40"}) {
+				t.Errorf("short name not set from env")
+			}
+			return nil
+		},
+	}).Run([]string{"run"})
+}
+
 func TestParseMultiInt(t *testing.T) {
 	_ = (&App{
 		Flags: []Flag{