Repository: urfave/cli (go)

Issue:
Race condition on disableSliceFlagSeparator in flag.go
## My urfave/cli version is

`github.com/urfave/cli/v2 v2.24.1`

## 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/main/docs/v2/manual.md)
- [X] Did you perform a search about this problem? 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.

## Dependency Management

- My project is using go modules.

## Describe the bug

Running integration tests by passing fake command line arguments to `.RunContext()`. Allows test to use assertion instead of `exec` an binary.

## To reproduce

1. A function is used to create a clean `*cli.Command` every time:
```go
func Generate() *cli.Command {
	return &cli.Command{
		Name:  "server",
// [...]
```
2. A helper function is used to compile into an `*cli.App`:
```go
func compileApp(cmd *cli.Command) (*cli.App, *observer.ObservedLogs) {
	observedZapCore, observedLogs := observer.New(zap.DebugLevel)
	observedLogger := zap.New(observedZapCore)
	return &cli.App{
		Name: "specter",
		Commands: []*cli.Command{
			cmd,
		},
		Before: func(ctx *cli.Context) error {
			ctx.App.Metadata["logger"] = observedLogger
			return nil
		},
		Metadata: make(map[string]interface{}),
	}, observedLogs
}
// [...]
```
3. Then create servers dynamically in test:
```go
	serverReturnCtx, serverStopped := context.WithCancel(ctx)
	defer serverStopped()

	t.Logf("Starting servers\n")

	serverLogs := make([]*observer.ObservedLogs, len(serverPorts))
	for i, args := range serverArgs {
		sApp, sLogs := compileApp(server.Generate())
		serverLogs[i] = sLogs
		args := args
		go func(app *cli.App) {
			if err := app.RunContext(ctx, args); err != nil {
				as.NoError(err)
			}
			serverStopped()
		}(sApp)
	}

	t.Logf("Waiting for servers to be started\n")
// [helper function not shown: checking if servers are started]
```
4. Then start the test:
`GO_RUN_INTEGRATION=1 go test -v -timeout 30s ./integrations -v -race`

## Observed behavior

```
==================
WARNING: DATA RACE
Write at 0x000001bb3d53 by goroutine 11:
  github.com/urfave/cli/v2.(*App).Setup()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/app.go:270 +0x1764
  github.com/urfave/cli/v2.(*App).RunContext()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/app.go:317 +0x7a
  kon.nect.sh/specter/integrations.TestTunnel.func3()
      /home/rachel/code/specter/integrations/tunnel_test.go:164 +0x88
  kon.nect.sh/specter/integrations.TestTunnel.func13()
      /home/rachel/code/specter/integrations/tunnel_test.go:168 +0x47

Previous write at 0x000001bb3d53 by goroutine 10:
  github.com/urfave/cli/v2.(*App).Setup()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/app.go:270 +0x1764
  github.com/urfave/cli/v2.(*App).RunContext()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/app.go:317 +0x7a
  kon.nect.sh/specter/integrations.TestTunnel.func3()
      /home/rachel/code/specter/integrations/tunnel_test.go:164 +0x88
  kon.nect.sh/specter/integrations.TestTunnel.func13()
      /home/rachel/code/specter/integrations/tunnel_test.go:168 +0x47

Goroutine 11 (running) created at:
  kon.nect.sh/specter/integrations.TestTunnel()
      /home/rachel/code/specter/integrations/tunnel_test.go:163 +0x2689
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1446 +0x216
  testing.(*T).Run.func1()
      /usr/local/go/src/testing/testing.go:1493 +0x47

Goroutine 10 (running) created at:
  kon.nect.sh/specter/integrations.TestTunnel()
      /home/rachel/code/specter/integrations/tunnel_test.go:163 +0x2689
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1446 +0x216
  testing.(*T).Run.func1()
      /usr/local/go/src/testing/testing.go:1493 +0x47
==================
==================
WARNING: DATA RACE
Write at 0x000001b74e30 by goroutine 11:
  github.com/urfave/cli/v2.(*BoolFlag).Apply()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/flag_bool.go:107 +0x84
  github.com/urfave/cli/v2.flagSet()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/flag.go:174 +0x25b
  github.com/urfave/cli/v2.(*Command).newFlagSet()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/command.go:278 +0xc9
  github.com/urfave/cli/v2.(*Command).parseFlags()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/command.go:311 +0x5a
  github.com/urfave/cli/v2.(*Command).Run()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/command.go:152 +0x14c
  github.com/urfave/cli/v2.(*App).RunContext()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/app.go:333 +0x10e4
  kon.nect.sh/specter/integrations.TestTunnel.func3()
      /home/rachel/code/specter/integrations/tunnel_test.go:164 +0x88
  kon.nect.sh/specter/integrations.TestTunnel.func13()
      /home/rachel/code/specter/integrations/tunnel_test.go:168 +0x47

Previous write at 0x000001b74e30 by goroutine 10:
  github.com/urfave/cli/v2.(*BoolFlag).Apply()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/flag_bool.go:107 +0x84
  github.com/urfave/cli/v2.flagSet()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/flag.go:174 +0x25b
  github.com/urfave/cli/v2.(*Command).newFlagSet()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/command.go:278 +0xc9
  github.com/urfave/cli/v2.(*Command).parseFlags()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/command.go:311 +0x5a
  github.com/urfave/cli/v2.(*Command).Run()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/command.go:152 +0x14c
  github.com/urfave/cli/v2.(*App).RunContext()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/app.go:333 +0x10e4
  kon.nect.sh/specter/integrations.TestTunnel.func3()
      /home/rachel/code/specter/integrations/tunnel_test.go:164 +0x88
  kon.nect.sh/specter/integrations.TestTunnel.func13()
      /home/rachel/code/specter/integrations/tunnel_test.go:168 +0x47

Goroutine 11 (running) created at:
  kon.nect.sh/specter/integrations.TestTunnel()
      /home/rachel/code/specter/integrations/tunnel_test.go:163 +0x2689
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1446 +0x216
  testing.(*T).Run.func1()
      /usr/local/go/src/testing/testing.go:1493 +0x47

Goroutine 10 (running) created at:
  kon.nect.sh/specter/integrations.TestTunnel()
      /home/rachel/code/specter/integrations/tunnel_test.go:163 +0x2689
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1446 +0x216
  testing.(*T).Run.func1()
      /usr/local/go/src/testing/testing.go:1493 +0x47
==================
==================
WARNING: DATA RACE
Read at 0x000001b75b28 by goroutine 11:
  github.com/urfave/cli/v2.(*Command).setup()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/command.go:133 +0x8d6
  github.com/urfave/cli/v2.(*Command).Run()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/command.go:148 +0xa5
  github.com/urfave/cli/v2.(*Command).Run()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/command.go:264 +0x128b
  github.com/urfave/cli/v2.(*App).RunContext()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/app.go:333 +0x10e4
  kon.nect.sh/specter/integrations.TestTunnel.func3()
      /home/rachel/code/specter/integrations/tunnel_test.go:164 +0x88
  kon.nect.sh/specter/integrations.TestTunnel.func13()
      /home/rachel/code/specter/integrations/tunnel_test.go:168 +0x47

Previous write at 0x000001b75b28 by goroutine 12:
  github.com/urfave/cli/v2.(*Command).setup()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/command.go:134 +0x9be
  github.com/urfave/cli/v2.(*Command).Run()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/command.go:148 +0xa5
  github.com/urfave/cli/v2.(*Command).Run()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/command.go:264 +0x128b
  github.com/urfave/cli/v2.(*App).RunContext()
      /home/rachel/go/pkg/mod/github.com/urfave/cli/v2@v2.24.1/app.go:333 +0x10e4
  kon.nect.sh/specter/integrations.TestTunnel.func3()
      /home/rachel/code/specter/integrations/tunnel_test.go:164 +0x88
  kon.nect.sh/specter/integrations.TestTunnel.func13()
      /home/rachel/code/specter/integrations/tunnel_test.go:168 +0x47

Goroutine 11 (running) created at:
  kon.nect.sh/specter/integrations.TestTunnel()
      /home/rachel/code/specter/integrations/tunnel_test.go:163 +0x2689
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1446 +0x216
  testing.(*T).Run.func1()
      /usr/local/go/src/testing/testing.go:1493 +0x47

Goroutine 12 (running) created at:
  kon.nect.sh/specter/integrations.TestTunnel()
      /home/rachel/code/specter/integrations/tunnel_test.go:163 +0x2689
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1446 +0x216
  testing.(*T).Run.func1()
      /usr/local/go/src/testing/testing.go:1493 +0x47
==================
```

## Expected behavior

No data race. 

## Additional context

This actually doesn't affect the test results (servers started correctly with the correct parameters). However,
```go
var (
	defaultSliceFlagSeparator = ","
	disableSliceFlagSeparator = false
)
```
in `flag.go` are not protected from concurrent access.

## Want to fix this yourself?

Gonna need some pointers

## Run `go version` and paste its output here

```
go version go1.19.4 linux/amd64
```

## Run `go env` and paste its output here

```
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/rachel/.cache/go-build"
GOENV="/home/rachel/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/rachel/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/rachel/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.19.4"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/home/rachel/code/specter/go.mod"
GOWORK=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build1222184028=/tmp/go-build -gno-record-gcc-switches"
```


Pull request:
flag: remove dependencies on shared variables
## What type of PR is this?

<!--
  Delete any of the following that do not apply:
 -->

- bug fix

## What this PR does / why we need it:

Remove the dependencies of shared variable from slice type flags parsing, and instead use a `separatorSpec` for parsing. This should not fail downstream dependencies that may be running multiple `*App` concurrently with race detector on.

## Which issue(s) this PR fixes:

Fixes #1670

## Special notes for your reviewer:

This commit creates a new type separatorSpec that will get passed into flag parser, instead of reading from shared variables. I'm uncertain that if I may have missed some parts of the code to be injected. Please let me know if I overlooked anything.

With `HideVersion` and `HideHelp`, the integration test mentioned in the issue is now passing with race detector on with this patch applied.

## Testing

Should be passing with `separatorSpec`, assuming that the call site has the configured `separatorSpec` from caller.

## Release Notes

```release-note
flag: remove dependencies on shared variables when parsing slices
```


Patch:
diff --git a/app.go b/app.go
index 10198f4..04aa725 100644
--- a/app.go
+++ b/app.go
@@ -121,7 +121,8 @@ type App struct {
 	// Treat all flags as normal arguments if true
 	SkipFlagParsing bool
 
-	didSetup bool
+	didSetup  bool
+	separator separatorSpec
 
 	rootCommand *Command
 }
@@ -216,6 +217,16 @@ func (a *App) Setup() {
 		})
 	}
 
+	if len(a.SliceFlagSeparator) != 0 {
+		a.separator.customized = true
+		a.separator.sep = a.SliceFlagSeparator
+	}
+
+	if a.DisableSliceFlagSeparator {
+		a.separator.customized = true
+		a.separator.disabled = true
+	}
+
 	var newCommands []*Command
 
 	for _, c := range a.Commands {
@@ -223,8 +234,8 @@ func (a *App) Setup() {
 		if c.HelpName != "" {
 			cname = c.HelpName
 		}
+		c.separator = a.separator
 		c.HelpName = fmt.Sprintf("%s %s", a.HelpName, cname)
-
 		c.flagCategories = newFlagCategoriesFromFlags(c.Flags)
 		newCommands = append(newCommands, c)
 	}
@@ -262,12 +273,6 @@ func (a *App) Setup() {
 	if a.Metadata == nil {
 		a.Metadata = make(map[string]interface{})
 	}
-
-	if len(a.SliceFlagSeparator) != 0 {
-		defaultSliceFlagSeparator = a.SliceFlagSeparator
-	}
-
-	disableSliceFlagSeparator = a.DisableSliceFlagSeparator
 }
 
 func (a *App) newRootCommand() *Command {
@@ -293,11 +298,12 @@ func (a *App) newRootCommand() *Command {
 		categories:             a.categories,
 		SkipFlagParsing:        a.SkipFlagParsing,
 		isRoot:                 true,
+		separator:              a.separator,
 	}
 }
 
 func (a *App) newFlagSet() (*flag.FlagSet, error) {
-	return flagSet(a.Name, a.Flags)
+	return flagSet(a.Name, a.Flags, a.separator)
 }
 
 func (a *App) useShortOptionHandling() bool {
diff --git a/command.go b/command.go
index da9cf53..f978b4a 100644
--- a/command.go
+++ b/command.go
@@ -69,6 +69,8 @@ type Command struct {
 
 	// if this is a root "special" command
 	isRoot bool
+
+	separator separatorSpec
 }
 
 type Commands []*Command
@@ -275,7 +277,7 @@ func (c *Command) Run(cCtx *Context, arguments ...string) (err error) {
 }
 
 func (c *Command) newFlagSet() (*flag.FlagSet, error) {
-	return flagSet(c.Name, c.Flags)
+	return flagSet(c.Name, c.Flags, c.separator)
 }
 
 func (c *Command) useShortOptionHandling() bool {
diff --git a/flag-spec.yaml b/flag-spec.yaml
index cfe47df..ed4db98 100644
--- a/flag-spec.yaml
+++ b/flag-spec.yaml
@@ -20,6 +20,8 @@ flag_types:
     skip_interfaces:
       - fmt.Stringer
     struct_fields:
+      - name: separator
+        type: separatorSpec
       - name: Action
         type: "func(*Context, []float64) error"
   int:
@@ -33,6 +35,8 @@ flag_types:
     skip_interfaces:
       - fmt.Stringer
     struct_fields:
+      - name: separator
+        type: separatorSpec
       - name: Action
         type: "func(*Context, []int) error"
   int64:
@@ -46,6 +50,8 @@ flag_types:
     skip_interfaces:
       - fmt.Stringer
     struct_fields:
+      - name: separator
+        type: separatorSpec
       - name: Action
         type: "func(*Context, []int64) error"
   uint:
@@ -59,6 +65,8 @@ flag_types:
     skip_interfaces:
       - fmt.Stringer
     struct_fields:
+      - name: separator
+        type: separatorSpec
       - name: Action
         type: "func(*Context, []uint) error"
   uint64:
@@ -72,6 +80,8 @@ flag_types:
     skip_interfaces:
       - fmt.Stringer
     struct_fields:
+      - name: separator
+        type: separatorSpec
       - name: Action
         type: "func(*Context, []uint64) error"
   string:
@@ -85,6 +95,8 @@ flag_types:
     skip_interfaces:
       - fmt.Stringer
     struct_fields:
+      - name: separator
+        type: separatorSpec
       - name: TakesFile
         type: bool
       - name: Action
diff --git a/flag.go b/flag.go
index 5260f7f..fc3744e 100644
--- a/flag.go
+++ b/flag.go
@@ -15,7 +15,7 @@ import (
 
 const defaultPlaceholder = "value"
 
-var (
+const (
 	defaultSliceFlagSeparator = ","
 	disableSliceFlagSeparator = false
 )
@@ -167,10 +167,13 @@ type Countable interface {
 	Count() int
 }
 
-func flagSet(name string, flags []Flag) (*flag.FlagSet, error) {
+func flagSet(name string, flags []Flag, spec separatorSpec) (*flag.FlagSet, error) {
 	set := flag.NewFlagSet(name, flag.ContinueOnError)
 
 	for _, f := range flags {
+		if c, ok := f.(customizedSeparator); ok {
+			c.WithSeparatorSpec(spec)
+		}
 		if err := f.Apply(set); err != nil {
 			return nil, err
 		}
@@ -389,10 +392,28 @@ func flagFromEnvOrFile(envVars []string, filePath string) (value string, fromWhe
 	return "", "", false
 }
 
-func flagSplitMultiValues(val string) []string {
-	if disableSliceFlagSeparator {
+type customizedSeparator interface {
+	WithSeparatorSpec(separatorSpec)
+}
+
+type separatorSpec struct {
+	sep        string
+	disabled   bool
+	customized bool
+}
+
+func (s separatorSpec) flagSplitMultiValues(val string) []string {
+	var (
+		disabled bool   = s.disabled
+		sep      string = s.sep
+	)
+	if !s.customized {
+		disabled = disableSliceFlagSeparator
+		sep = defaultSliceFlagSeparator
+	}
+	if disabled {
 		return []string{val}
 	}
 
-	return strings.Split(val, defaultSliceFlagSeparator)
+	return strings.Split(val, sep)
 }
diff --git a/flag_float64_slice.go b/flag_float64_slice.go
index 413aa50..0bc4612 100644
--- a/flag_float64_slice.go
+++ b/flag_float64_slice.go
@@ -11,6 +11,7 @@ import (
 // Float64Slice wraps []float64 to satisfy flag.Value
 type Float64Slice struct {
 	slice      []float64
+	separator  separatorSpec
 	hasBeenSet bool
 }
 
@@ -29,6 +30,10 @@ func (f *Float64Slice) clone() *Float64Slice {
 	return n
 }
 
+func (f *Float64Slice) WithSeparatorSpec(spec separatorSpec) {
+	f.separator = spec
+}
+
 // Set parses the value into a float64 and appends it to the list of values
 func (f *Float64Slice) Set(value string) error {
 	if !f.hasBeenSet {
@@ -43,7 +48,7 @@ func (f *Float64Slice) Set(value string) error {
 		return nil
 	}
 
-	for _, s := range flagSplitMultiValues(value) {
+	for _, s := range f.separator.flagSplitMultiValues(value) {
 		tmp, err := strconv.ParseFloat(strings.TrimSpace(s), 64)
 		if err != nil {
 			return err
@@ -148,11 +153,12 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error {
 		setValue = f.Value.clone()
 	default:
 		setValue = new(Float64Slice)
+		setValue.WithSeparatorSpec(f.separator)
 	}
 
 	if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
 		if val != "" {
-			for _, s := range flagSplitMultiValues(val) {
+			for _, s := range f.separator.flagSplitMultiValues(val) {
 				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)
 				}
@@ -172,6 +178,10 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error {
 	return nil
 }
 
+func (f *Float64SliceFlag) WithSeparatorSpec(spec separatorSpec) {
+	f.separator = spec
+}
+
 // Get returns the flag’s value in the given Context.
 func (f *Float64SliceFlag) Get(ctx *Context) []float64 {
 	return ctx.Float64Slice(f.Name)
diff --git a/flag_int64_slice.go b/flag_int64_slice.go
index c45c43d..d45c2dd 100644
--- a/flag_int64_slice.go
+++ b/flag_int64_slice.go
@@ -11,6 +11,7 @@ import (
 // Int64Slice wraps []int64 to satisfy flag.Value
 type Int64Slice struct {
 	slice      []int64
+	separator  separatorSpec
 	hasBeenSet bool
 }
 
@@ -29,6 +30,10 @@ func (i *Int64Slice) clone() *Int64Slice {
 	return n
 }
 
+func (i *Int64Slice) WithSeparatorSpec(spec separatorSpec) {
+	i.separator = spec
+}
+
 // Set parses the value into an integer and appends it to the list of values
 func (i *Int64Slice) Set(value string) error {
 	if !i.hasBeenSet {
@@ -43,7 +48,7 @@ func (i *Int64Slice) Set(value string) error {
 		return nil
 	}
 
-	for _, s := range flagSplitMultiValues(value) {
+	for _, s := range i.separator.flagSplitMultiValues(value) {
 		tmp, err := strconv.ParseInt(strings.TrimSpace(s), 0, 64)
 		if err != nil {
 			return err
@@ -149,10 +154,11 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error {
 		setValue = f.Value.clone()
 	default:
 		setValue = new(Int64Slice)
+		setValue.WithSeparatorSpec(f.separator)
 	}
 
 	if val, source, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok && val != "" {
-		for _, s := range flagSplitMultiValues(val) {
+		for _, s := range f.separator.flagSplitMultiValues(val) {
 			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)
 			}
@@ -171,6 +177,10 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error {
 	return nil
 }
 
+func (f *Int64SliceFlag) WithSeparatorSpec(spec separatorSpec) {
+	f.separator = spec
+}
+
 // Get returns the flag’s value in the given Context.
 func (f *Int64SliceFlag) Get(ctx *Context) []int64 {
 	return ctx.Int64Slice(f.Name)
diff --git a/flag_int_slice.go b/flag_int_slice.go
index d4006e5..da9c09b 100644
--- a/flag_int_slice.go
+++ b/flag_int_slice.go
@@ -11,6 +11,7 @@ import (
 // IntSlice wraps []int to satisfy flag.Value
 type IntSlice struct {
 	slice      []int
+	separator  separatorSpec
 	hasBeenSet bool
 }
 
@@ -40,6 +41,10 @@ func (i *IntSlice) SetInt(value int) {
 	i.slice = append(i.slice, value)
 }
 
+func (i *IntSlice) WithSeparatorSpec(spec separatorSpec) {
+	i.separator = spec
+}
+
 // Set parses the value into an integer and appends it to the list of values
 func (i *IntSlice) Set(value string) error {
 	if !i.hasBeenSet {
@@ -54,7 +59,7 @@ func (i *IntSlice) Set(value string) error {
 		return nil
 	}
 
-	for _, s := range flagSplitMultiValues(value) {
+	for _, s := range i.separator.flagSplitMultiValues(value) {
 		tmp, err := strconv.ParseInt(strings.TrimSpace(s), 0, 64)
 		if err != nil {
 			return err
@@ -160,10 +165,11 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) error {
 		setValue = f.Value.clone()
 	default:
 		setValue = new(IntSlice)
+		setValue.WithSeparatorSpec(f.separator)
 	}
 
 	if val, source, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok && val != "" {
-		for _, s := range flagSplitMultiValues(val) {
+		for _, s := range f.separator.flagSplitMultiValues(val) {
 			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)
 			}
@@ -182,6 +188,10 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) error {
 	return nil
 }
 
+func (f *IntSliceFlag) WithSeparatorSpec(spec separatorSpec) {
+	f.separator = spec
+}
+
 // Get returns the flag’s value in the given Context.
 func (f *IntSliceFlag) Get(ctx *Context) []int {
 	return ctx.IntSlice(f.Name)
diff --git a/flag_string_slice.go b/flag_string_slice.go
index 82eeac0..82410db 100644
--- a/flag_string_slice.go
+++ b/flag_string_slice.go
@@ -11,6 +11,7 @@ import (
 // StringSlice wraps a []string to satisfy flag.Value
 type StringSlice struct {
 	slice      []string
+	separator  separatorSpec
 	hasBeenSet bool
 }
 
@@ -43,13 +44,17 @@ func (s *StringSlice) Set(value string) error {
 		return nil
 	}
 
-	for _, t := range flagSplitMultiValues(value) {
+	for _, t := range s.separator.flagSplitMultiValues(value) {
 		s.slice = append(s.slice, t)
 	}
 
 	return nil
 }
 
+func (s *StringSlice) WithSeparatorSpec(spec separatorSpec) {
+	s.separator = spec
+}
+
 // String returns a readable representation of this value (for usage defaults)
 func (s *StringSlice) String() string {
 	return fmt.Sprintf("%s", s.slice)
@@ -141,10 +146,11 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
 		setValue = f.Value.clone()
 	default:
 		setValue = new(StringSlice)
+		setValue.WithSeparatorSpec(f.separator)
 	}
 
 	if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
-		for _, s := range flagSplitMultiValues(val) {
+		for _, s := range f.separator.flagSplitMultiValues(val) {
 			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)
 			}
@@ -163,6 +169,10 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
 	return nil
 }
 
+func (f *StringSliceFlag) WithSeparatorSpec(spec separatorSpec) {
+	f.separator = spec
+}
+
 // Get returns the flag’s value in the given Context.
 func (f *StringSliceFlag) Get(ctx *Context) []string {
 	return ctx.StringSlice(f.Name)
diff --git a/flag_uint64_slice.go b/flag_uint64_slice.go
index 61bb30b..e845dd5 100644
--- a/flag_uint64_slice.go
+++ b/flag_uint64_slice.go
@@ -11,6 +11,7 @@ import (
 // Uint64Slice wraps []int64 to satisfy flag.Value
 type Uint64Slice struct {
 	slice      []uint64
+	separator  separatorSpec
 	hasBeenSet bool
 }
 
@@ -43,7 +44,7 @@ func (i *Uint64Slice) Set(value string) error {
 		return nil
 	}
 
-	for _, s := range flagSplitMultiValues(value) {
+	for _, s := range i.separator.flagSplitMultiValues(value) {
 		tmp, err := strconv.ParseUint(strings.TrimSpace(s), 0, 64)
 		if err != nil {
 			return err
@@ -55,6 +56,10 @@ func (i *Uint64Slice) Set(value string) error {
 	return nil
 }
 
+func (i *Uint64Slice) WithSeparatorSpec(spec separatorSpec) {
+	i.separator = spec
+}
+
 // String returns a readable representation of this value (for usage defaults)
 func (i *Uint64Slice) String() string {
 	v := i.slice
@@ -153,10 +158,11 @@ func (f *Uint64SliceFlag) Apply(set *flag.FlagSet) error {
 		setValue = f.Value.clone()
 	default:
 		setValue = new(Uint64Slice)
+		setValue.WithSeparatorSpec(f.separator)
 	}
 
 	if val, source, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok && val != "" {
-		for _, s := range flagSplitMultiValues(val) {
+		for _, s := range f.separator.flagSplitMultiValues(val) {
 			if err := setValue.Set(strings.TrimSpace(s)); err != nil {
 				return fmt.Errorf("could not parse %q as uint64 slice value from %s for flag %s: %s", val, source, f.Name, err)
 			}
@@ -175,6 +181,10 @@ func (f *Uint64SliceFlag) Apply(set *flag.FlagSet) error {
 	return nil
 }
 
+func (f *Uint64SliceFlag) WithSeparatorSpec(spec separatorSpec) {
+	f.separator = spec
+}
+
 // Get returns the flag’s value in the given Context.
 func (f *Uint64SliceFlag) Get(ctx *Context) []uint64 {
 	return ctx.Uint64Slice(f.Name)
diff --git a/flag_uint_slice.go b/flag_uint_slice.go
index 363aa65..d2aed48 100644
--- a/flag_uint_slice.go
+++ b/flag_uint_slice.go
@@ -11,6 +11,7 @@ import (
 // UintSlice wraps []int to satisfy flag.Value
 type UintSlice struct {
 	slice      []uint
+	separator  separatorSpec
 	hasBeenSet bool
 }
 
@@ -54,7 +55,7 @@ func (i *UintSlice) Set(value string) error {
 		return nil
 	}
 
-	for _, s := range flagSplitMultiValues(value) {
+	for _, s := range i.separator.flagSplitMultiValues(value) {
 		tmp, err := strconv.ParseUint(strings.TrimSpace(s), 0, 32)
 		if err != nil {
 			return err
@@ -66,6 +67,10 @@ func (i *UintSlice) Set(value string) error {
 	return nil
 }
 
+func (i *UintSlice) WithSeparatorSpec(spec separatorSpec) {
+	i.separator = spec
+}
+
 // String returns a readable representation of this value (for usage defaults)
 func (i *UintSlice) String() string {
 	v := i.slice
@@ -164,10 +169,11 @@ func (f *UintSliceFlag) Apply(set *flag.FlagSet) error {
 		setValue = f.Value.clone()
 	default:
 		setValue = new(UintSlice)
+		setValue.WithSeparatorSpec(f.separator)
 	}
 
 	if val, source, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok && val != "" {
-		for _, s := range flagSplitMultiValues(val) {
+		for _, s := range f.separator.flagSplitMultiValues(val) {
 			if err := setValue.Set(strings.TrimSpace(s)); err != nil {
 				return fmt.Errorf("could not parse %q as uint slice value from %s for flag %s: %s", val, source, f.Name, err)
 			}
@@ -186,6 +192,10 @@ func (f *UintSliceFlag) Apply(set *flag.FlagSet) error {
 	return nil
 }
 
+func (f *UintSliceFlag) WithSeparatorSpec(spec separatorSpec) {
+	f.separator = spec
+}
+
 // Get returns the flag’s value in the given Context.
 func (f *UintSliceFlag) Get(ctx *Context) []uint {
 	return ctx.UintSlice(f.Name)
diff --git a/godoc-current.txt b/godoc-current.txt
index c1eb170..599332b 100644
--- a/godoc-current.txt
+++ b/godoc-current.txt
@@ -1038,6 +1038,8 @@ func (f *Float64Slice) String() string
 func (f *Float64Slice) Value() []float64
     Value returns the slice of float64s set by this flag
 
+func (f *Float64Slice) WithSeparatorSpec(spec separatorSpec)
+
 type Float64SliceFlag struct {
 	Name string
 
@@ -1113,6 +1115,8 @@ func (f *Float64SliceFlag) String() string
 func (f *Float64SliceFlag) TakesValue() bool
     TakesValue returns true if the flag takes a value, otherwise false
 
+func (f *Float64SliceFlag) WithSeparatorSpec(spec separatorSpec)
+
 type Generic interface {
 	Set(value string) error
 	String() string
@@ -1279,6 +1283,8 @@ func (i *Int64Slice) String() string
 func (i *Int64Slice) Value() []int64
     Value returns the slice of ints set by this flag
 
+func (i *Int64Slice) WithSeparatorSpec(spec separatorSpec)
+
 type Int64SliceFlag struct {
 	Name string
 
@@ -1354,6 +1360,8 @@ func (f *Int64SliceFlag) String() string
 func (f *Int64SliceFlag) TakesValue() bool
     TakesValue returns true of the flag takes a value, otherwise false
 
+func (f *Int64SliceFlag) WithSeparatorSpec(spec separatorSpec)
+
 type IntFlag struct {
 	Name string
 
@@ -1449,6 +1457,8 @@ func (i *IntSlice) String() string
 func (i *IntSlice) Value() []int
     Value returns the slice of ints set by this flag
 
+func (i *IntSlice) WithSeparatorSpec(spec separatorSpec)
+
 type IntSliceFlag struct {
 	Name string
 
@@ -1524,6 +1534,8 @@ func (f *IntSliceFlag) String() string
 func (f *IntSliceFlag) TakesValue() bool
     TakesValue returns true of the flag takes a value, otherwise false
 
+func (f *IntSliceFlag) WithSeparatorSpec(spec separatorSpec)
+
 type InvalidFlagAccessFunc func(*Context, string)
     InvalidFlagAccessFunc is executed when an invalid flag is accessed from the
     context.
@@ -1792,6 +1804,8 @@ func (s *StringSlice) String() string
 func (s *StringSlice) Value() []string
     Value returns the slice of strings set by this flag
 
+func (s *StringSlice) WithSeparatorSpec(spec separatorSpec)
+
 type StringSliceFlag struct {
 	Name string
 
@@ -1869,6 +1883,8 @@ func (f *StringSliceFlag) String() string
 func (f *StringSliceFlag) TakesValue() bool
     TakesValue returns true of the flag takes a value, otherwise false
 
+func (f *StringSliceFlag) WithSeparatorSpec(spec separatorSpec)
+
 type SuggestCommandFunc func(commands []*Command, provided string) string
 
 type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string
@@ -2063,6 +2079,8 @@ func (i *Uint64Slice) String() string
 func (i *Uint64Slice) Value() []uint64
     Value returns the slice of ints set by this flag
 
+func (i *Uint64Slice) WithSeparatorSpec(spec separatorSpec)
+
 type Uint64SliceFlag struct {
 	Name string
 
@@ -2129,6 +2147,8 @@ func (f *Uint64SliceFlag) String() string
 func (f *Uint64SliceFlag) TakesValue() bool
     TakesValue returns true of the flag takes a value, otherwise false
 
+func (f *Uint64SliceFlag) WithSeparatorSpec(spec separatorSpec)
+
 type UintFlag struct {
 	Name string
 
@@ -2224,6 +2244,8 @@ func (i *UintSlice) String() string
 func (i *UintSlice) Value() []uint
     Value returns the slice of ints set by this flag
 
+func (i *UintSlice) WithSeparatorSpec(spec separatorSpec)
+
 type UintSliceFlag struct {
 	Name string
 
@@ -2290,6 +2312,8 @@ func (f *UintSliceFlag) String() string
 func (f *UintSliceFlag) TakesValue() bool
     TakesValue returns true of the flag takes a value, otherwise false
 
+func (f *UintSliceFlag) WithSeparatorSpec(spec separatorSpec)
+
 type VisibleFlag interface {
 	Flag
 
diff --git a/zz_generated.flags.go b/zz_generated.flags.go
index 016951a..8c29f6e 100644
--- a/zz_generated.flags.go
+++ b/zz_generated.flags.go
@@ -25,6 +25,8 @@ type Float64SliceFlag struct {
 
 	defaultValue *Float64Slice
 
+	separator separatorSpec
+
 	Action func(*Context, []float64) error
 }
 
@@ -120,6 +122,8 @@ type Int64SliceFlag struct {
 
 	defaultValue *Int64Slice
 
+	separator separatorSpec
+
 	Action func(*Context, []int64) error
 }
 
@@ -164,6 +168,8 @@ type IntSliceFlag struct {
 
 	defaultValue *IntSlice
 
+	separator separatorSpec
+
 	Action func(*Context, []int) error
 }
 
@@ -259,6 +265,8 @@ type StringSliceFlag struct {
 
 	defaultValue *StringSlice
 
+	separator separatorSpec
+
 	TakesFile bool
 
 	Action func(*Context, []string) error
@@ -358,6 +366,8 @@ type Uint64SliceFlag struct {
 
 	defaultValue *Uint64Slice
 
+	separator separatorSpec
+
 	Action func(*Context, []uint64) error
 }
 
@@ -402,6 +412,8 @@ type UintSliceFlag struct {
 
 	defaultValue *UintSlice
 
+	separator separatorSpec
+
 	Action func(*Context, []uint) error
 }
 


Test patch:
diff --git a/app_test.go b/app_test.go
index 7f2a214..eb13ec1 100644
--- a/app_test.go
+++ b/app_test.go
@@ -425,10 +425,10 @@ func ExampleApp_Run_sliceValues() {
 
 	_ = 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}
+	// 0-float64Sclice cli.Float64Slice{slice:[]float64{13.3, 14.4, 15.5, 16.6}, separator:cli.separatorSpec{sep:"", disabled:false, customized:false}, hasBeenSet:true}
+	// 1-int64Sclice cli.Int64Slice{slice:[]int64{13, 14, 15, 16}, separator:cli.separatorSpec{sep:"", disabled:false, customized:false}, hasBeenSet:true}
+	// 2-intSclice cli.IntSlice{slice:[]int{13, 14, 15, 16}, separator:cli.separatorSpec{sep:"", disabled:false, customized:false}, hasBeenSet:true}
+	// 3-stringSclice cli.StringSlice{slice:[]string{"parsed1", "parsed2", "parsed3", "parsed4"}, separator:cli.separatorSpec{sep:"", disabled:false, customized:false}, hasBeenSet:true}
 	// error: <nil>
 }
 
@@ -2532,7 +2532,7 @@ func TestCustomHelpVersionFlags(t *testing.T) {
 
 func TestHandleExitCoder_Default(t *testing.T) {
 	app := newTestApp()
-	fs, err := flagSet(app.Name, app.Flags)
+	fs, err := flagSet(app.Name, app.Flags, separatorSpec{})
 	if err != nil {
 		t.Errorf("error creating FlagSet: %s", err)
 	}
@@ -2548,7 +2548,7 @@ func TestHandleExitCoder_Default(t *testing.T) {
 
 func TestHandleExitCoder_Custom(t *testing.T) {
 	app := newTestApp()
-	fs, err := flagSet(app.Name, app.Flags)
+	fs, err := flagSet(app.Name, app.Flags, separatorSpec{})
 	if err != nil {
 		t.Errorf("error creating FlagSet: %s", err)
 	}
diff --git a/flag_test.go b/flag_test.go
index c152371..132a9ed 100644
--- a/flag_test.go
+++ b/flag_test.go
@@ -3425,13 +3425,27 @@ func TestSliceShortOptionHandle(t *testing.T) {
 }
 
 // Test issue #1541
+func TestDefaultSliceFlagSeparator(t *testing.T) {
+	separator := separatorSpec{}
+	opts := []string{"opt1", "opt2", "opt3", "opt4"}
+	ret := separator.flagSplitMultiValues(strings.Join(opts, ","))
+	if len(ret) != 4 {
+		t.Fatalf("split slice flag failed, want: 4, but get: %d", len(ret))
+	}
+	for idx, r := range ret {
+		if r != opts[idx] {
+			t.Fatalf("get %dth failed, wanted: %s, but get: %s", idx, opts[idx], r)
+		}
+	}
+}
+
 func TestCustomizedSliceFlagSeparator(t *testing.T) {
-	defaultSliceFlagSeparator = ";"
-	defer func() {
-		defaultSliceFlagSeparator = ","
-	}()
+	separator := separatorSpec{
+		customized: true,
+		sep:        ";",
+	}
 	opts := []string{"opt1", "opt2", "opt3,op", "opt4"}
-	ret := flagSplitMultiValues(strings.Join(opts, ";"))
+	ret := separator.flagSplitMultiValues(strings.Join(opts, ";"))
 	if len(ret) != 4 {
 		t.Fatalf("split slice flag failed, want: 4, but get: %d", len(ret))
 	}
@@ -3443,13 +3457,13 @@ func TestCustomizedSliceFlagSeparator(t *testing.T) {
 }
 
 func TestFlagSplitMultiValues_Disabled(t *testing.T) {
-	disableSliceFlagSeparator = true
-	defer func() {
-		disableSliceFlagSeparator = false
-	}()
+	separator := separatorSpec{
+		customized: true,
+		disabled:   true,
+	}
 
 	opts := []string{"opt1", "opt2", "opt3,op", "opt4"}
-	ret := flagSplitMultiValues(strings.Join(opts, defaultSliceFlagSeparator))
+	ret := separator.flagSplitMultiValues(strings.Join(opts, defaultSliceFlagSeparator))
 	if len(ret) != 1 {
 		t.Fatalf("failed to disable split slice flag, want: 1, but got: %d", len(ret))
 	}
diff --git a/testdata/godoc-v2.x.txt b/testdata/godoc-v2.x.txt
index c1eb170..599332b 100644
--- a/testdata/godoc-v2.x.txt
+++ b/testdata/godoc-v2.x.txt
@@ -1038,6 +1038,8 @@ func (f *Float64Slice) String() string
 func (f *Float64Slice) Value() []float64
     Value returns the slice of float64s set by this flag
 
+func (f *Float64Slice) WithSeparatorSpec(spec separatorSpec)
+
 type Float64SliceFlag struct {
 	Name string
 
@@ -1113,6 +1115,8 @@ func (f *Float64SliceFlag) String() string
 func (f *Float64SliceFlag) TakesValue() bool
     TakesValue returns true if the flag takes a value, otherwise false
 
+func (f *Float64SliceFlag) WithSeparatorSpec(spec separatorSpec)
+
 type Generic interface {
 	Set(value string) error
 	String() string
@@ -1279,6 +1283,8 @@ func (i *Int64Slice) String() string
 func (i *Int64Slice) Value() []int64
     Value returns the slice of ints set by this flag
 
+func (i *Int64Slice) WithSeparatorSpec(spec separatorSpec)
+
 type Int64SliceFlag struct {
 	Name string
 
@@ -1354,6 +1360,8 @@ func (f *Int64SliceFlag) String() string
 func (f *Int64SliceFlag) TakesValue() bool
     TakesValue returns true of the flag takes a value, otherwise false
 
+func (f *Int64SliceFlag) WithSeparatorSpec(spec separatorSpec)
+
 type IntFlag struct {
 	Name string
 
@@ -1449,6 +1457,8 @@ func (i *IntSlice) String() string
 func (i *IntSlice) Value() []int
     Value returns the slice of ints set by this flag
 
+func (i *IntSlice) WithSeparatorSpec(spec separatorSpec)
+
 type IntSliceFlag struct {
 	Name string
 
@@ -1524,6 +1534,8 @@ func (f *IntSliceFlag) String() string
 func (f *IntSliceFlag) TakesValue() bool
     TakesValue returns true of the flag takes a value, otherwise false
 
+func (f *IntSliceFlag) WithSeparatorSpec(spec separatorSpec)
+
 type InvalidFlagAccessFunc func(*Context, string)
     InvalidFlagAccessFunc is executed when an invalid flag is accessed from the
     context.
@@ -1792,6 +1804,8 @@ func (s *StringSlice) String() string
 func (s *StringSlice) Value() []string
     Value returns the slice of strings set by this flag
 
+func (s *StringSlice) WithSeparatorSpec(spec separatorSpec)
+
 type StringSliceFlag struct {
 	Name string
 
@@ -1869,6 +1883,8 @@ func (f *StringSliceFlag) String() string
 func (f *StringSliceFlag) TakesValue() bool
     TakesValue returns true of the flag takes a value, otherwise false
 
+func (f *StringSliceFlag) WithSeparatorSpec(spec separatorSpec)
+
 type SuggestCommandFunc func(commands []*Command, provided string) string
 
 type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string
@@ -2063,6 +2079,8 @@ func (i *Uint64Slice) String() string
 func (i *Uint64Slice) Value() []uint64
     Value returns the slice of ints set by this flag
 
+func (i *Uint64Slice) WithSeparatorSpec(spec separatorSpec)
+
 type Uint64SliceFlag struct {
 	Name string
 
@@ -2129,6 +2147,8 @@ func (f *Uint64SliceFlag) String() string
 func (f *Uint64SliceFlag) TakesValue() bool
     TakesValue returns true of the flag takes a value, otherwise false
 
+func (f *Uint64SliceFlag) WithSeparatorSpec(spec separatorSpec)
+
 type UintFlag struct {
 	Name string
 
@@ -2224,6 +2244,8 @@ func (i *UintSlice) String() string
 func (i *UintSlice) Value() []uint
     Value returns the slice of ints set by this flag
 
+func (i *UintSlice) WithSeparatorSpec(spec separatorSpec)
+
 type UintSliceFlag struct {
 	Name string
 
@@ -2290,6 +2312,8 @@ func (f *UintSliceFlag) String() string
 func (f *UintSliceFlag) TakesValue() bool
     TakesValue returns true of the flag takes a value, otherwise false
 
+func (f *UintSliceFlag) WithSeparatorSpec(spec separatorSpec)
+
 type VisibleFlag interface {
 	Flag