My urfave/cli version is
v3.7.0 (also reproduced on latest main at commit a107ee4)
Checklist
Dependency Management
- My project is using go modules.
- My project is using vendoring.
Describe the bug
StringSliceFlag (and StringMapFlag) with Local: true only retains the last value when multiple values are passed via repeated flags. For example, -p /a -p /b -p /c results in [/c] instead of [/a /b /c].
The cause is FlagBase.Set() in flag_impl.go:188. For Local flags, Set() re-runs PreParse() on every invocation:
if !f.applied || f.Local {
if err := f.PreParse(); err != nil {
return err
}
f.applied = true
}
PreParse() calls f.creator.Create(newVal, f.Destination, f.Config), which for SliceBase unconditionally resets the destination to empty (flag_slice_base.go:20):
func (i SliceBase[T, C, VC]) Create(val []T, p *[]T, c C) Value {
*p = []T{} // resets destination
*p = append(*p, val...) // copies in default (empty)
...
}
So each -p call: resets dest = [] via PreParse, then appends the single new value. Only the last survives.
For non-Local flags this doesn't happen because f.applied is true after the first Set(), short-circuiting the condition. The f.Local disjunction defeats that guard.
This affects all multi-value flag types (StringSliceFlag, IntSliceFlag, StringMapFlag, etc.) when combined with Local: true. Single-value flags are unaffected because overwriting is idempotent for them.
To reproduce
package main
import (
"context"
"fmt"
"os"
cli "github.com/urfave/cli/v3"
)
func main() {
var paths []string
app := &cli.Command{
Name: "app",
Commands: []*cli.Command{
{
Name: "sub",
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "paths",
Aliases: []string{"p"},
Local: true,
Destination: &paths,
},
},
Action: func(_ context.Context, cmd *cli.Command) error {
fmt.Printf("Destination: %v (len=%d)\n", paths, len(paths))
fmt.Printf("cmd.StringSlice: %v\n", cmd.StringSlice("paths"))
return nil
},
},
},
}
app.Run(context.Background(), os.Args)
}
go run . sub -p /a -p /b -p /c
Observed behavior
Destination: [/c] (len=1)
cmd.StringSlice: [/c]
Only the last value is retained. Both Destination and cmd.StringSlice() return the same broken result — the bug is in parsing, not in reading.
DisableSliceFlagSeparator: true does not help.
Expected behavior
Destination: [/a /b /c] (len=3)
cmd.StringSlice: [/a /b /c]
All three values should accumulate. Removing Local: true from the flag definition produces the correct result.
Additional context
Test against latest main (a107ee4):
func TestLocalSliceFlagAccumulation(t *testing.T) {
var got []string
app := &cli.Command{
Name: "app",
Commands: []*cli.Command{
{
Name: "sub",
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "paths",
Aliases: []string{"p"},
Local: true,
Destination: &got,
},
},
Action: func(_ context.Context, cmd *cli.Command) error {
return nil
},
},
},
}
err := app.Run(context.Background(), []string{"app", "sub", "-p", "/a", "-p", "/b", "-p", "/c"})
if err != nil {
t.Fatal(err)
}
if len(got) != 3 {
t.Errorf("expected 3 values, got %d: %v", len(got), got)
}
}
--- FAIL: TestLocalSliceFlagAccumulation (0.00s)
local_slice_test.go:39: expected 3 values, got 1: [/c]
The fix is straightforward — Set() should not re-run PreParse() for an already-initialized multi-value flag. Something like:
if !f.applied || (f.Local && !f.IsMultiValueFlag()) {
Or alternatively, guard inside PreParse / SliceBase.Create to not reset when the value is already initialized. Happy to open a PR.
Want to fix this yourself?
Yes. The fix is a one-line change in flag_impl.go:188. I have a failing test and a working patch ready.
Run go version and paste its output here
go version go1.26.1 linux/amd64
Run go env and paste its output here
n/a — bug is in urfave/cli internals, not environment-specific
My urfave/cli version is
v3.7.0 (also reproduced on latest
mainat commita107ee4)Checklist
Dependency Management
Describe the bug
StringSliceFlag(andStringMapFlag) withLocal: trueonly retains the last value when multiple values are passed via repeated flags. For example,-p /a -p /b -p /cresults in[/c]instead of[/a /b /c].The cause is
FlagBase.Set()inflag_impl.go:188. ForLocalflags,Set()re-runsPreParse()on every invocation:PreParse()callsf.creator.Create(newVal, f.Destination, f.Config), which forSliceBaseunconditionally resets the destination to empty (flag_slice_base.go:20):So each
-pcall: resetsdest = []viaPreParse, then appends the single new value. Only the last survives.For non-
Localflags this doesn't happen becausef.appliedistrueafter the firstSet(), short-circuiting the condition. Thef.Localdisjunction defeats that guard.This affects all multi-value flag types (
StringSliceFlag,IntSliceFlag,StringMapFlag, etc.) when combined withLocal: true. Single-value flags are unaffected because overwriting is idempotent for them.To reproduce
go run . sub -p /a -p /b -p /cObserved behavior
Only the last value is retained. Both
Destinationandcmd.StringSlice()return the same broken result — the bug is in parsing, not in reading.DisableSliceFlagSeparator: truedoes not help.Expected behavior
All three values should accumulate. Removing
Local: truefrom the flag definition produces the correct result.Additional context
Test against latest
main(a107ee4):The fix is straightforward —
Set()should not re-runPreParse()for an already-initialized multi-value flag. Something like:Or alternatively, guard inside
PreParse/SliceBase.Createto not reset when the value is already initialized. Happy to open a PR.Want to fix this yourself?
Yes. The fix is a one-line change in
flag_impl.go:188. I have a failing test and a working patch ready.Run
go versionand paste its output hereRun
go envand paste its output here