diff --git a/app_test.go b/app_test.go index b05e8e62d5..ccd63b9ef8 100644 --- a/app_test.go +++ b/app_test.go @@ -428,7 +428,7 @@ func ExampleApp_Run_sliceValues() { // 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} + // 3-stringSclice cli.StringSlice{slice:[]string{"parsed1", "parsed2", "parsed3", "parsed4"}, separator:cli.separatorSpec{sep:"", disabled:false, customized:false}, hasBeenSet:true, keepSpace:false} // error: } diff --git a/flag-spec.yaml b/flag-spec.yaml index ed4db985d0..03d82e7011 100644 --- a/flag-spec.yaml +++ b/flag-spec.yaml @@ -101,6 +101,8 @@ flag_types: type: bool - name: Action type: "func(*Context, []string) error" + - name: KeepSpace + type: bool time.Duration: struct_fields: - name: Action diff --git a/flag_string_slice.go b/flag_string_slice.go index 82410dbc8b..28f4798f55 100644 --- a/flag_string_slice.go +++ b/flag_string_slice.go @@ -13,6 +13,7 @@ type StringSlice struct { slice []string separator separatorSpec hasBeenSet bool + keepSpace bool } // NewStringSlice creates a *StringSlice with default values @@ -45,6 +46,9 @@ func (s *StringSlice) Set(value string) error { } for _, t := range s.separator.flagSplitMultiValues(value) { + if !s.keepSpace { + t = strings.TrimSpace(t) + } s.slice = append(s.slice, t) } @@ -149,9 +153,14 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { setValue.WithSeparatorSpec(f.separator) } + setValue.keepSpace = f.KeepSpace + if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { for _, s := range f.separator.flagSplitMultiValues(val) { - if err := setValue.Set(strings.TrimSpace(s)); err != nil { + if !f.KeepSpace { + s = strings.TrimSpace(s) + } + if err := setValue.Set(s); err != nil { return fmt.Errorf("could not parse %q as string value from %s for flag %s: %s", val, source, f.Name, err) } } diff --git a/flag_test.go b/flag_test.go index 132a9ede4a..60370aceb2 100644 --- a/flag_test.go +++ b/flag_test.go @@ -144,6 +144,11 @@ func TestFlagsFromEnv(t *testing.T) { s.hasBeenSet = false return *s } + newSetStringSliceKeepSpace := func(defaults ...string) StringSlice { + s := newSetStringSlice(defaults...) + s.keepSpace = true + return s + } var flagTests = []struct { input string @@ -198,6 +203,8 @@ func TestFlagsFromEnv(t *testing.T) { {"path", "path", &PathFlag{Name: "path", EnvVars: []string{"PATH"}}, ""}, {"foo,bar", newSetStringSlice("foo", "bar"), &StringSliceFlag{Name: "names", EnvVars: []string{"NAMES"}}, ""}, + {" space ", newSetStringSliceKeepSpace(" space "), &StringSliceFlag{Name: "names", KeepSpace: true, EnvVars: []string{"NAMES"}}, ""}, + {" no space ", newSetStringSlice("no space"), &StringSliceFlag{Name: "names", EnvVars: []string{"NAMES"}}, ""}, {"1", uint(1), &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, {"08", uint(8), &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}, Base: 10}, ""}, @@ -774,7 +781,7 @@ func TestStringSliceFlag_MatchStringFlagBehavior(t *testing.T) { app := App{ Flags: []Flag{ &StringFlag{Name: "string"}, - &StringSliceFlag{Name: "slice"}, + &StringSliceFlag{Name: "slice", KeepSpace: true}, }, Action: func(ctx *Context) error { f1, f2 := ctx.String("string"), ctx.StringSlice("slice") @@ -797,6 +804,52 @@ func TestStringSliceFlag_MatchStringFlagBehavior(t *testing.T) { } } +func TestStringSliceFlag_TrimSpace(t *testing.T) { + t.Parallel() + + tests := []struct { + in, out string + }{ + {" asd", "asd"}, + {"123 ", "123"}, + {" asd ", "asd"}, + } + for testNum, tt := range tests { + tt := tt + t.Run(fmt.Sprintf("%d", testNum), func(t *testing.T) { + t.Parallel() + + app := App{ + Flags: []Flag{ + &StringSliceFlag{Name: "trim"}, + &StringSliceFlag{Name: "no-trim", KeepSpace: true}, + }, + Action: func(ctx *Context) error { + flagTrim, flagNoTrim := ctx.StringSlice("trim"), ctx.StringSlice("no-trim") + if l := len(flagTrim); l != 1 { + t.Fatalf("slice flag 'trim' should result in exactly one value, got %d", l) + } + if l := len(flagNoTrim); l != 1 { + t.Fatalf("slice flag 'no-trim' should result in exactly one value, got %d", l) + } + + if v := flagTrim[0]; v != tt.out { + t.Errorf("Expected trimmed value %q, got %q", tt.out, v) + } + if v := flagNoTrim[0]; v != tt.in { + t.Errorf("Expected no trimmed value%q, got %q", tt.out, v) + } + return nil + }, + } + + if err := app.Run([]string{"", "--trim", tt.in, "--no-trim", tt.in}); err != nil { + t.Errorf("app run error: %s", err) + } + }) + } +} + var intFlagTests = []struct { name string expected string diff --git a/godoc-current.txt b/godoc-current.txt index 599332b4f2..bd5e1defcd 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -1827,6 +1827,8 @@ type StringSliceFlag struct { TakesFile bool Action func(*Context, []string) error + + KeepSpace bool // Has unexported fields. } StringSliceFlag is a flag with type *StringSlice diff --git a/testdata/godoc-v2.x.txt b/testdata/godoc-v2.x.txt index 599332b4f2..bd5e1defcd 100644 --- a/testdata/godoc-v2.x.txt +++ b/testdata/godoc-v2.x.txt @@ -1827,6 +1827,8 @@ type StringSliceFlag struct { TakesFile bool Action func(*Context, []string) error + + KeepSpace bool // Has unexported fields. } StringSliceFlag is a flag with type *StringSlice diff --git a/zz_generated.flags.go b/zz_generated.flags.go index 8c29f6ee03..73e771451a 100644 --- a/zz_generated.flags.go +++ b/zz_generated.flags.go @@ -270,6 +270,8 @@ type StringSliceFlag struct { TakesFile bool Action func(*Context, []string) error + + KeepSpace bool } // IsSet returns whether or not the flag has been set through env or file