diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 824bccac11..3b31afc5c0 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -34,10 +34,6 @@ jobs: if: matrix.go == '1.18.x' && matrix.os == 'ubuntu-latest' run: test -z $(gofmt -l .) - # https://github.com/urfave/cli/pull/1405#issuecomment-1135458582 - - name: workaround for golang.org/x/tools error - run: go mod download golang.org/x/tools - - name: vet run: go run internal/build/build.go vet diff --git a/altsrc/map_input_source.go b/altsrc/map_input_source.go index e065c7cc43..07de00fcc6 100644 --- a/altsrc/map_input_source.go +++ b/altsrc/map_input_source.go @@ -32,11 +32,18 @@ func nestedVal(name string, tree map[interface{}]interface{}) (interface{}, bool if !ok { return nil, false } - ctype, ok := child.(map[interface{}]interface{}) - if !ok { + + switch child := child.(type) { + case map[string]interface{}: + node = make(map[interface{}]interface{}, len(child)) + for k, v := range child { + node[k] = v + } + case map[interface{}]interface{}: + node = child + default: return nil, false } - node = ctype } if val, ok := node[sections[len(sections)-1]]; ok { return val, true diff --git a/altsrc/yaml_file_loader.go b/altsrc/yaml_file_loader.go index 4ace1f23ec..315db1885f 100644 --- a/altsrc/yaml_file_loader.go +++ b/altsrc/yaml_file_loader.go @@ -11,7 +11,7 @@ import ( "github.com/urfave/cli/v2" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) type yamlSourceContext struct { diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 7e80bdbf13..b0d9bfc82b 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -85,7 +85,7 @@ step. #### generated code A significant portion of the project's source code is generated, with the goal being to -eliminate repetetive maintenance where other type-safe abstraction is impractical or +eliminate repetitive maintenance where other type-safe abstraction is impractical or impossible with Go versions `< 1.18`. In a future where the eldest Go version supported is `1.18.x`, there will likely be efforts to take advantage of [generics](https://go.dev/doc/tutorial/generics). diff --git a/flag_float64_slice.go b/flag_float64_slice.go index bc347ccdb2..031ec1d1aa 100644 --- a/flag_float64_slice.go +++ b/flag_float64_slice.go @@ -56,7 +56,12 @@ func (f *Float64Slice) Set(value string) error { // String returns a readable representation of this value (for usage defaults) func (f *Float64Slice) String() string { - return fmt.Sprintf("%#v", f.slice) + v := f.slice + if v == nil { + // treat nil the same as zero length non-nil + v = make([]float64, 0) + } + return fmt.Sprintf("%#v", v) } // Serialize allows Float64Slice to fulfill Serializer @@ -120,29 +125,40 @@ func (f *Float64SliceFlag) GetEnvVars() []string { // Apply populates the flag given the flag set and environment func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error { + // apply any default + if f.Destination != nil && f.Value != nil { + f.Destination.slice = make([]float64, len(f.Value.slice)) + copy(f.Destination.slice, f.Value.slice) + } + + // resolve setValue (what we will assign to the set) + var setValue *Float64Slice + switch { + case f.Destination != nil: + setValue = f.Destination + case f.Value != nil: + setValue = f.Value.clone() + default: + setValue = new(Float64Slice) + } + if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if val != "" { - f.Value = &Float64Slice{} - 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 from %s for flag %s: %s", f.Value, source, f.Name, err) + 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) } } // 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. - f.Value.hasBeenSet = false + setValue.hasBeenSet = false f.HasBeenSet = true } } - if f.Value == nil { - f.Value = &Float64Slice{} - } - copyValue := f.Value.clone() for _, name := range f.Names() { - set.Var(copyValue, name, f.Usage) + set.Var(setValue, name, f.Usage) } return nil @@ -165,7 +181,7 @@ func (cCtx *Context) Float64Slice(name string) []float64 { func lookupFloat64Slice(name string, set *flag.FlagSet) []float64 { f := set.Lookup(name) if f != nil { - if slice, ok := f.Value.(*Float64Slice); ok { + if slice, ok := unwrapFlagValue(f.Value).(*Float64Slice); ok { return slice.Value() } } diff --git a/flag_int64_slice.go b/flag_int64_slice.go index 5f3d5cd4ea..657aaaaf33 100644 --- a/flag_int64_slice.go +++ b/flag_int64_slice.go @@ -57,7 +57,12 @@ func (i *Int64Slice) Set(value string) error { // String returns a readable representation of this value (for usage defaults) func (i *Int64Slice) String() string { - return fmt.Sprintf("%#v", i.slice) + v := i.slice + if v == nil { + // treat nil the same as zero length non-nil + v = make([]int64, 0) + } + return fmt.Sprintf("%#v", v) } // Serialize allows Int64Slice to fulfill Serializer @@ -121,27 +126,38 @@ func (f *Int64SliceFlag) GetEnvVars() []string { // Apply populates the flag given the flag set and environment func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error { - if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { - f.Value = &Int64Slice{} + // apply any default + if f.Destination != nil && f.Value != nil { + f.Destination.slice = make([]int64, len(f.Value.slice)) + copy(f.Destination.slice, f.Value.slice) + } + // resolve setValue (what we will assign to the set) + var setValue *Int64Slice + switch { + case f.Destination != nil: + setValue = f.Destination + case f.Value != nil: + setValue = f.Value.clone() + default: + setValue = new(Int64Slice) + } + + if val, source, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok && val != "" { for _, s := range flagSplitMultiValues(val) { - if err := f.Value.Set(strings.TrimSpace(s)); err != nil { + 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) } } // 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. - f.Value.hasBeenSet = false + setValue.hasBeenSet = false f.HasBeenSet = true } - if f.Value == nil { - f.Value = &Int64Slice{} - } - copyValue := f.Value.clone() for _, name := range f.Names() { - set.Var(copyValue, name, f.Usage) + set.Var(setValue, name, f.Usage) } return nil @@ -164,7 +180,7 @@ func (cCtx *Context) Int64Slice(name string) []int64 { func lookupInt64Slice(name string, set *flag.FlagSet) []int64 { f := set.Lookup(name) if f != nil { - if slice, ok := f.Value.(*Int64Slice); ok { + if slice, ok := unwrapFlagValue(f.Value).(*Int64Slice); ok { return slice.Value() } } diff --git a/flag_int_slice.go b/flag_int_slice.go index 2ddf805967..7c383935ac 100644 --- a/flag_int_slice.go +++ b/flag_int_slice.go @@ -68,7 +68,12 @@ func (i *IntSlice) Set(value string) error { // String returns a readable representation of this value (for usage defaults) func (i *IntSlice) String() string { - return fmt.Sprintf("%#v", i.slice) + v := i.slice + if v == nil { + // treat nil the same as zero length non-nil + v = make([]int, 0) + } + return fmt.Sprintf("%#v", v) } // Serialize allows IntSlice to fulfill Serializer @@ -132,27 +137,38 @@ func (f *IntSliceFlag) GetEnvVars() []string { // Apply populates the flag given the flag set and environment func (f *IntSliceFlag) Apply(set *flag.FlagSet) error { - if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { - f.Value = &IntSlice{} + // apply any default + if f.Destination != nil && f.Value != nil { + f.Destination.slice = make([]int, len(f.Value.slice)) + copy(f.Destination.slice, f.Value.slice) + } + // resolve setValue (what we will assign to the set) + var setValue *IntSlice + switch { + case f.Destination != nil: + setValue = f.Destination + case f.Value != nil: + setValue = f.Value.clone() + default: + setValue = new(IntSlice) + } + + if val, source, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok && val != "" { for _, s := range flagSplitMultiValues(val) { - if err := f.Value.Set(strings.TrimSpace(s)); err != nil { + 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) } } // 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. - f.Value.hasBeenSet = false + setValue.hasBeenSet = false f.HasBeenSet = true } - if f.Value == nil { - f.Value = &IntSlice{} - } - copyValue := f.Value.clone() for _, name := range f.Names() { - set.Var(copyValue, name, f.Usage) + set.Var(setValue, name, f.Usage) } return nil @@ -175,7 +191,7 @@ func (cCtx *Context) IntSlice(name string) []int { func lookupIntSlice(name string, set *flag.FlagSet) []int { f := set.Lookup(name) if f != nil { - if slice, ok := f.Value.(*IntSlice); ok { + if slice, ok := unwrapFlagValue(f.Value).(*IntSlice); ok { return slice.Value() } } diff --git a/flag_string_slice.go b/flag_string_slice.go index 599f42c7fb..bcdfd4c554 100644 --- a/flag_string_slice.go +++ b/flag_string_slice.go @@ -115,41 +115,36 @@ func (f *StringSliceFlag) GetEnvVars() []string { // Apply populates the flag given the flag set and environment func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { - + // apply any default if f.Destination != nil && f.Value != nil { f.Destination.slice = make([]string, len(f.Value.slice)) copy(f.Destination.slice, f.Value.slice) + } + // resolve setValue (what we will assign to the set) + var setValue *StringSlice + switch { + case f.Destination != nil: + setValue = f.Destination + case f.Value != nil: + setValue = f.Value.clone() + default: + setValue = new(StringSlice) } if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { - if f.Value == nil { - f.Value = &StringSlice{} - } - destination := f.Value - if f.Destination != nil { - destination = f.Destination - } - for _, s := range flagSplitMultiValues(val) { - if err := destination.Set(strings.TrimSpace(s)); err != nil { + 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) } } // 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 + setValue.hasBeenSet = false f.HasBeenSet = true } - if f.Value == nil { - f.Value = &StringSlice{} - } - setValue := f.Destination - if f.Destination == nil { - setValue = f.Value.clone() - } for _, name := range f.Names() { set.Var(setValue, name, f.Usage) } @@ -174,7 +169,7 @@ func (cCtx *Context) StringSlice(name string) []string { func lookupStringSlice(name string, set *flag.FlagSet) []string { f := set.Lookup(name) if f != nil { - if slice, ok := f.Value.(*StringSlice); ok { + if slice, ok := unwrapFlagValue(f.Value).(*StringSlice); ok { return slice.Value() } } diff --git a/flag_test.go b/flag_test.go index ba90e91c05..0bb893afb6 100644 --- a/flag_test.go +++ b/flag_test.go @@ -114,7 +114,7 @@ func TestFlagsFromEnv(t *testing.T) { {"foobar", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int value from environment variable "SECONDS" for flag seconds: .*`}, {"1.0,2", newSetFloat64Slice(1, 2), &Float64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, - {"foobar", newSetFloat64Slice(), &Float64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "\[\]float64{}" as float64 slice value from environment variable "SECONDS" for flag seconds: .*`}, + {"foobar", newSetFloat64Slice(), &Float64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as float64 slice value from environment variable "SECONDS" for flag seconds: .*`}, {"1,2", newSetIntSlice(1, 2), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, {"1.2,2", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as int slice value from environment variable "SECONDS" for flag seconds: .*`}, @@ -604,7 +604,7 @@ func TestStringSliceFlagApply_SetsAllNames(t *testing.T) { expect(t, err, nil) } -func TestStringSliceFlagApply_UsesEnvValues(t *testing.T) { +func TestStringSliceFlagApply_UsesEnvValues_noDefault(t *testing.T) { defer resetEnv(os.Environ()) os.Clearenv() _ = os.Setenv("MY_GOAT", "vincent van goat,scape goat") @@ -615,7 +615,22 @@ func TestStringSliceFlagApply_UsesEnvValues(t *testing.T) { err := set.Parse(nil) expect(t, err, nil) - expect(t, val.Value(), NewStringSlice("vincent van goat", "scape goat").Value()) + expect(t, val.Value(), []string(nil)) + expect(t, set.Lookup("goat").Value.(*StringSlice).Value(), []string{"vincent van goat", "scape goat"}) +} + +func TestStringSliceFlagApply_UsesEnvValues_withDefault(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("MY_GOAT", "vincent van goat,scape goat") + val := NewStringSlice(`some default`, `values here`) + fl := StringSliceFlag{Name: "goat", EnvVars: []string{"MY_GOAT"}, Value: val} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + err := set.Parse(nil) + expect(t, err, nil) + expect(t, val.Value(), []string{`some default`, `values here`}) + expect(t, set.Lookup("goat").Value.(*StringSlice).Value(), []string{"vincent van goat", "scape goat"}) } func TestStringSliceFlagApply_DefaultValueWithDestination(t *testing.T) { @@ -1406,6 +1421,75 @@ func TestParseMultiStringSliceWithDestinationAndEnv(t *testing.T) { }).Run([]string{"run", "-s", "10", "-s", "20"}) } +func TestParseMultiFloat64SliceWithDestinationAndEnv(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("APP_INTERVALS", "20,30,40") + + dest := &Float64Slice{} + _ = (&App{ + Flags: []Flag{ + &Float64SliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: dest, EnvVars: []string{"APP_INTERVALS"}}, + }, + Action: func(ctx *Context) error { + expected := []float64{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 TestParseMultiInt64SliceWithDestinationAndEnv(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("APP_INTERVALS", "20,30,40") + + dest := &Int64Slice{} + _ = (&App{ + Flags: []Flag{ + &Int64SliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: dest, EnvVars: []string{"APP_INTERVALS"}}, + }, + Action: func(ctx *Context) error { + expected := []int64{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 TestParseMultiIntSliceWithDestinationAndEnv(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("APP_INTERVALS", "20,30,40") + + dest := &IntSlice{} + _ = (&App{ + Flags: []Flag{ + &IntSliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: dest, EnvVars: []string{"APP_INTERVALS"}}, + }, + Action: func(ctx *Context) error { + expected := []int{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{ diff --git a/go.mod b/go.mod index 6343421515..4e39308336 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,10 @@ go 1.18 require ( github.com/BurntSushi/toml v1.1.0 - github.com/cpuguy83/go-md2man/v2 v2.0.1 + github.com/cpuguy83/go-md2man/v2 v2.0.2 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 golang.org/x/text v0.3.7 - gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 ) require github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index 0d18f8c55f..6beae99a6d 100644 --- a/go.sum +++ b/go.sum @@ -2,13 +2,17 @@ github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/godoc-current.txt b/godoc-current.txt index d94e80dce6..a3a7faca17 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -32,16 +32,16 @@ var ( SuggestDidYouMeanTemplate string = suggestDidYouMeanTemplate ) var AppHelpTemplate = `NAME: - {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} + {{$v := offset .Name 6}}{{wrap .Name 3}}{{if .Usage}} - {{wrap .Usage $v}}{{end}} USAGE: - {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} + {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} VERSION: {{.Version}}{{end}}{{end}}{{if .Description}} DESCRIPTION: - {{.Description | nindent 3 | trim}}{{end}}{{if len .Authors}} + {{wrap .Description 3}}{{end}}{{if len .Authors}} AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: {{range $index, $author := .Authors}}{{if $index}} @@ -59,26 +59,26 @@ GLOBAL OPTIONS:{{range .VisibleFlagCategories}} GLOBAL OPTIONS: {{range $index, $option := .VisibleFlags}}{{if $index}} - {{end}}{{$option}}{{end}}{{end}}{{end}}{{if .Copyright}} + {{end}}{{wrap $option.String 6}}{{end}}{{end}}{{end}}{{if .Copyright}} COPYRIGHT: - {{.Copyright}}{{end}} + {{wrap .Copyright 3}}{{end}} ` AppHelpTemplate is the text template for the Default help topic. cli.go uses text/template to render templates. You can render custom help text by setting this variable. var CommandHelpTemplate = `NAME: - {{.HelpName}} - {{.Usage}} + {{$v := offset .HelpName 6}}{{wrap .HelpName 3}}{{if .Usage}} - {{wrap .Usage $v}}{{end}} USAGE: - {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} + {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} CATEGORY: {{.Category}}{{end}}{{if .Description}} DESCRIPTION: - {{.Description | nindent 3 | trim}}{{end}}{{if .VisibleFlagCategories}} + {{wrap .Description 3}}{{end}}{{if .VisibleFlagCategories}} OPTIONS:{{range .VisibleFlagCategories}} {{if .Name}}{{.Name}} @@ -150,10 +150,10 @@ var SubcommandHelpTemplate = `NAME: {{.HelpName}} - {{.Usage}} USAGE: - {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} + {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} DESCRIPTION: - {{.Description | nindent 3 | trim}}{{end}} + {{wrap .Description 3}}{{end}} COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{range .VisibleCommands}} @@ -186,6 +186,11 @@ var HelpPrinterCustom helpPrinterCustom = printHelpCustom the default implementation of HelpPrinter, and may be called directly if the ExtraInfo field is set on an App. + In the default implementation, if the customFuncs argument contains a + "wrapAt" key, which is a function which takes no arguments and returns an + int, this int value will be used to produce a "wrap" function used by the + default template to wrap long lines. + FUNCTIONS @@ -1003,6 +1008,8 @@ func (f *Float64SliceFlag) GetCategory() string func (f *Float64SliceFlag) GetDefaultText() string GetDefaultText returns the default text for this flag +func (f *Float64SliceFlag) GetDestination() []float64 + func (f *Float64SliceFlag) GetEnvVars() []string GetEnvVars returns the env vars for this flag @@ -1025,6 +1032,10 @@ func (f *Float64SliceFlag) IsVisible() bool func (f *Float64SliceFlag) Names() []string Names returns the names of the flag +func (f *Float64SliceFlag) SetDestination(slice []float64) + +func (f *Float64SliceFlag) SetValue(slice []float64) + func (f *Float64SliceFlag) String() string String returns a readable representation of this value (for usage defaults) @@ -1215,6 +1226,8 @@ func (f *Int64SliceFlag) GetCategory() string func (f *Int64SliceFlag) GetDefaultText() string GetDefaultText returns the default text for this flag +func (f *Int64SliceFlag) GetDestination() []int64 + func (f *Int64SliceFlag) GetEnvVars() []string GetEnvVars returns the env vars for this flag @@ -1237,6 +1250,10 @@ func (f *Int64SliceFlag) IsVisible() bool func (f *Int64SliceFlag) Names() []string Names returns the names of the flag +func (f *Int64SliceFlag) SetDestination(slice []int64) + +func (f *Int64SliceFlag) SetValue(slice []int64) + func (f *Int64SliceFlag) String() string String returns a readable representation of this value (for usage defaults) @@ -1362,6 +1379,8 @@ func (f *IntSliceFlag) GetCategory() string func (f *IntSliceFlag) GetDefaultText() string GetDefaultText returns the default text for this flag +func (f *IntSliceFlag) GetDestination() []int + func (f *IntSliceFlag) GetEnvVars() []string GetEnvVars returns the env vars for this flag @@ -1384,6 +1403,10 @@ func (f *IntSliceFlag) IsVisible() bool func (f *IntSliceFlag) Names() []string Names returns the names of the flag +func (f *IntSliceFlag) SetDestination(slice []int) + +func (f *IntSliceFlag) SetValue(slice []int) + func (f *IntSliceFlag) String() string String returns a readable representation of this value (for usage defaults) @@ -1396,6 +1419,22 @@ type MultiError interface { } MultiError is an error that wraps multiple errors. +type MultiFloat64Flag = SliceFlag[*Float64SliceFlag, []float64, float64] + MultiFloat64Flag extends Float64SliceFlag with support for using slices + directly, as Value and/or Destination. See also SliceFlag. + +type MultiInt64Flag = SliceFlag[*Int64SliceFlag, []int64, int64] + MultiInt64Flag extends Int64SliceFlag with support for using slices + directly, as Value and/or Destination. See also SliceFlag. + +type MultiIntFlag = SliceFlag[*IntSliceFlag, []int, int] + MultiIntFlag extends IntSliceFlag with support for using slices directly, as + Value and/or Destination. See also SliceFlag. + +type MultiStringFlag = SliceFlag[*StringSliceFlag, []string, string] + MultiStringFlag extends StringSliceFlag with support for using slices + directly, as Value and/or Destination. See also SliceFlag. + type OnUsageErrorFunc func(cCtx *Context, err error, isSubcommand bool) error OnUsageErrorFunc is executed if a usage error occurs. This is useful for displaying customized usage error messages. This function is able to replace @@ -1480,6 +1519,67 @@ type Serializer interface { } Serializer is used to circumvent the limitations of flag.FlagSet.Set +type SliceFlag[T SliceFlagTarget[E], S ~[]E, E any] struct { + Target T + Value S + Destination *S +} + SliceFlag extends implementations like StringSliceFlag and IntSliceFlag with + support for using slices directly, as Value and/or Destination. See also + SliceFlagTarget, MultiStringFlag, MultiFloat64Flag, MultiInt64Flag, + MultiIntFlag. + +func (x *SliceFlag[T, S, E]) Apply(set *flag.FlagSet) error + +func (x *SliceFlag[T, S, E]) GetCategory() string + +func (x *SliceFlag[T, S, E]) GetDefaultText() string + +func (x *SliceFlag[T, S, E]) GetDestination() S + +func (x *SliceFlag[T, S, E]) GetEnvVars() []string + +func (x *SliceFlag[T, S, E]) GetUsage() string + +func (x *SliceFlag[T, S, E]) GetValue() string + +func (x *SliceFlag[T, S, E]) IsRequired() bool + +func (x *SliceFlag[T, S, E]) IsSet() bool + +func (x *SliceFlag[T, S, E]) IsVisible() bool + +func (x *SliceFlag[T, S, E]) Names() []string + +func (x *SliceFlag[T, S, E]) SetDestination(slice S) + +func (x *SliceFlag[T, S, E]) SetValue(slice S) + +func (x *SliceFlag[T, S, E]) String() string + +func (x *SliceFlag[T, S, E]) TakesValue() bool + +type SliceFlagTarget[E any] interface { + Flag + RequiredFlag + DocGenerationFlag + VisibleFlag + CategorizableFlag + + // SetValue should propagate the given slice to the target, ideally as a new value. + // Note that a nil slice should nil/clear any existing value (modelled as ~[]E). + SetValue(slice []E) + // SetDestination should propagate the given slice to the target, ideally as a new value. + // Note that a nil slice should nil/clear any existing value (modelled as ~*[]E). + SetDestination(slice []E) + // GetDestination should return the current value referenced by any destination, or nil if nil/unset. + GetDestination() []E +} + SliceFlagTarget models a target implementation for use with SliceFlag. The + three methods, SetValue, SetDestination, and GetDestination, are necessary + to propagate Value and Destination, where Value is propagated inwards + (initially), and Destination is propagated outwards (on every update). + type StringFlag struct { Name string @@ -1599,6 +1699,8 @@ func (f *StringSliceFlag) GetCategory() string func (f *StringSliceFlag) GetDefaultText() string GetDefaultText returns the default text for this flag +func (f *StringSliceFlag) GetDestination() []string + func (f *StringSliceFlag) GetEnvVars() []string GetEnvVars returns the env vars for this flag @@ -1621,6 +1723,10 @@ func (f *StringSliceFlag) IsVisible() bool func (f *StringSliceFlag) Names() []string Names returns the names of the flag +func (f *StringSliceFlag) SetDestination(slice []string) + +func (f *StringSliceFlag) SetValue(slice []string) + func (f *StringSliceFlag) String() string String returns a readable representation of this value (for usage defaults) diff --git a/help.go b/help.go index ff59ddc8b8..9a8d2437d5 100644 --- a/help.go +++ b/help.go @@ -64,6 +64,11 @@ var HelpPrinter helpPrinter = printHelp // HelpPrinterCustom is a function that writes the help output. It is used as // the default implementation of HelpPrinter, and may be called directly if // the ExtraInfo field is set on an App. +// +// In the default implementation, if the customFuncs argument contains a +// "wrapAt" key, which is a function which takes no arguments and returns +// an int, this int value will be used to produce a "wrap" function used +// by the default template to wrap long lines. var HelpPrinterCustom helpPrinterCustom = printHelpCustom // VersionPrinter prints the version for the App @@ -286,12 +291,29 @@ func ShowCommandCompletions(ctx *Context, command string) { // The customFuncs map will be combined with a default template.FuncMap to // allow using arbitrary functions in template rendering. func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) { + + const maxLineLength = 10000 + funcMap := template.FuncMap{ "join": strings.Join, "indent": indent, "nindent": nindent, "trim": strings.TrimSpace, + "wrap": func(input string, offset int) string { return wrap(input, offset, maxLineLength) }, + "offset": offset, + } + + if customFuncs["wrapAt"] != nil { + if wa, ok := customFuncs["wrapAt"]; ok { + if waf, ok := wa.(func() int); ok { + wrapAt := waf() + customFuncs["wrap"] = func(input string, offset int) string { + return wrap(input, offset, wrapAt) + } + } + } } + for key, value := range customFuncs { funcMap[key] = value } @@ -402,3 +424,55 @@ func indent(spaces int, v string) string { func nindent(spaces int, v string) string { return "\n" + indent(spaces, v) } + +func wrap(input string, offset int, wrapAt int) string { + var sb strings.Builder + + lines := strings.Split(input, "\n") + + padding := strings.Repeat(" ", offset) + + for i, line := range lines { + if i != 0 { + sb.WriteString(padding) + } + + sb.WriteString(wrapLine(line, offset, wrapAt, padding)) + + if i != len(lines)-1 { + sb.WriteString("\n") + } + } + + return sb.String() +} + +func wrapLine(input string, offset int, wrapAt int, padding string) string { + if wrapAt <= offset || len(input) <= wrapAt-offset { + return input + } + + lineWidth := wrapAt - offset + words := strings.Fields(input) + if len(words) == 0 { + return input + } + + wrapped := words[0] + spaceLeft := lineWidth - len(wrapped) + for _, word := range words[1:] { + if len(word)+1 > spaceLeft { + wrapped += "\n" + padding + word + spaceLeft = lineWidth - len(word) + } else { + wrapped += " " + word + spaceLeft -= 1 + len(word) + } + } + + return wrapped +} + +func offset(input string, fixed int) int { + return len(input) + fixed +} diff --git a/help_test.go b/help_test.go index 17a263deb6..4feb7f05a0 100644 --- a/help_test.go +++ b/help_test.go @@ -1124,3 +1124,225 @@ func TestDefaultCompleteWithFlags(t *testing.T) { }) } } + +func TestWrappedHelp(t *testing.T) { + + // Reset HelpPrinter after this test. + defer func(old helpPrinter) { + HelpPrinter = old + }(HelpPrinter) + + output := new(bytes.Buffer) + app := &App{ + Writer: output, + Flags: []Flag{ + &BoolFlag{Name: "foo", + Aliases: []string{"h"}, + Usage: "here's a really long help text line, let's see where it wraps. blah blah blah and so on.", + }, + }, + Usage: "here's a sample App.Usage string long enough that it should be wrapped in this test", + UsageText: "i'm not sure how App.UsageText differs from App.Usage, but this should also be wrapped in this test", + // TODO: figure out how to make ArgsUsage appear in the help text, and test that + Description: `here's a sample App.Description string long enough that it should be wrapped in this test + +with a newline + and an indented line`, + Copyright: `Here's a sample copyright text string long enough that it should be wrapped. +Including newlines. + And also indented lines. + + +And then another long line. Blah blah blah does anybody ever read these things?`, + } + + c := NewContext(app, nil, nil) + + HelpPrinter = func(w io.Writer, templ string, data interface{}) { + funcMap := map[string]interface{}{ + "wrapAt": func() int { + return 30 + }, + } + + HelpPrinterCustom(w, templ, data, funcMap) + } + + _ = ShowAppHelp(c) + + expected := `NAME: + - here's a sample + App.Usage string long + enough that it should be + wrapped in this test + +USAGE: + i'm not sure how + App.UsageText differs from + App.Usage, but this should + also be wrapped in this + test + +DESCRIPTION: + here's a sample + App.Description string long + enough that it should be + wrapped in this test + + with a newline + and an indented line + +GLOBAL OPTIONS: + --foo, -h here's a + really long help text + line, let's see where it + wraps. blah blah blah + and so on. (default: + false) + +COPYRIGHT: + Here's a sample copyright + text string long enough + that it should be wrapped. + Including newlines. + And also indented lines. + + + And then another long line. + Blah blah blah does anybody + ever read these things? +` + + if output.String() != expected { + t.Errorf("Unexpected wrapping, got:\n%s\nexpected: %s", + output.String(), expected) + } +} + +func TestWrappedCommandHelp(t *testing.T) { + + // Reset HelpPrinter after this test. + defer func(old helpPrinter) { + HelpPrinter = old + }(HelpPrinter) + + output := new(bytes.Buffer) + app := &App{ + Writer: output, + Commands: []*Command{ + { + Name: "add", + Aliases: []string{"a"}, + Usage: "add a task to the list", + UsageText: "this is an even longer way of describing adding a task to the list", + Description: "and a description long enough to wrap in this test case", + Action: func(c *Context) error { + return nil + }, + }, + }, + } + + c := NewContext(app, nil, nil) + + HelpPrinter = func(w io.Writer, templ string, data interface{}) { + funcMap := map[string]interface{}{ + "wrapAt": func() int { + return 30 + }, + } + + HelpPrinterCustom(w, templ, data, funcMap) + } + + _ = ShowCommandHelp(c, "add") + + expected := `NAME: + - add a task to the list + +USAGE: + this is an even longer way + of describing adding a task + to the list + +DESCRIPTION: + and a description long + enough to wrap in this test + case +` + + if output.String() != expected { + t.Errorf("Unexpected wrapping, got:\n%s\nexpected: %s", + output.String(), expected) + } +} + +func TestWrappedSubcommandHelp(t *testing.T) { + + // Reset HelpPrinter after this test. + defer func(old helpPrinter) { + HelpPrinter = old + }(HelpPrinter) + + output := new(bytes.Buffer) + app := &App{ + Name: "cli.test", + Writer: output, + Commands: []*Command{ + { + Name: "bar", + Aliases: []string{"a"}, + Usage: "add a task to the list", + UsageText: "this is an even longer way of describing adding a task to the list", + Description: "and a description long enough to wrap in this test case", + Action: func(c *Context) error { + return nil + }, + Subcommands: []*Command{ + { + Name: "grok", + Usage: "remove an existing template", + UsageText: "longer usage text goes here, la la la, hopefully this is long enough to wrap even more", + Action: func(c *Context) error { + return nil + }, + }, + }, + }, + }, + } + + HelpPrinter = func(w io.Writer, templ string, data interface{}) { + funcMap := map[string]interface{}{ + "wrapAt": func() int { + return 30 + }, + } + + HelpPrinterCustom(w, templ, data, funcMap) + } + + _ = app.Run([]string{"foo", "bar", "grok", "--help"}) + + expected := `NAME: + cli.test bar grok - remove + an + existing + template + +USAGE: + longer usage text goes + here, la la la, hopefully + this is long enough to wrap + even more + +OPTIONS: + --help, -h show help (default: false) + +` + + if output.String() != expected { + t.Errorf("Unexpected wrapping, got:\n%s\nexpected: %s", + output.String(), expected) + } +} diff --git a/internal/genflags/cmd/genflags/main.go b/internal/genflags/cmd/genflags/main.go index cad25088f6..4212e60da1 100644 --- a/internal/genflags/cmd/genflags/main.go +++ b/internal/genflags/cmd/genflags/main.go @@ -15,7 +15,7 @@ import ( "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/internal/genflags" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) const ( diff --git a/sliceflag.go b/sliceflag.go new file mode 100644 index 0000000000..7dea3576a3 --- /dev/null +++ b/sliceflag.go @@ -0,0 +1,293 @@ +//go:build go1.18 +// +build go1.18 + +package cli + +import ( + "flag" + "reflect" +) + +type ( + // SliceFlag extends implementations like StringSliceFlag and IntSliceFlag with support for using slices directly, + // as Value and/or Destination. + // See also SliceFlagTarget, MultiStringFlag, MultiFloat64Flag, MultiInt64Flag, MultiIntFlag. + SliceFlag[T SliceFlagTarget[E], S ~[]E, E any] struct { + Target T + Value S + Destination *S + } + + // SliceFlagTarget models a target implementation for use with SliceFlag. + // The three methods, SetValue, SetDestination, and GetDestination, are necessary to propagate Value and + // Destination, where Value is propagated inwards (initially), and Destination is propagated outwards (on every + // update). + SliceFlagTarget[E any] interface { + Flag + RequiredFlag + DocGenerationFlag + VisibleFlag + CategorizableFlag + + // SetValue should propagate the given slice to the target, ideally as a new value. + // Note that a nil slice should nil/clear any existing value (modelled as ~[]E). + SetValue(slice []E) + // SetDestination should propagate the given slice to the target, ideally as a new value. + // Note that a nil slice should nil/clear any existing value (modelled as ~*[]E). + SetDestination(slice []E) + // GetDestination should return the current value referenced by any destination, or nil if nil/unset. + GetDestination() []E + } + + // MultiStringFlag extends StringSliceFlag with support for using slices directly, as Value and/or Destination. + // See also SliceFlag. + MultiStringFlag = SliceFlag[*StringSliceFlag, []string, string] + + // MultiFloat64Flag extends Float64SliceFlag with support for using slices directly, as Value and/or Destination. + // See also SliceFlag. + MultiFloat64Flag = SliceFlag[*Float64SliceFlag, []float64, float64] + + // MultiInt64Flag extends Int64SliceFlag with support for using slices directly, as Value and/or Destination. + // See also SliceFlag. + MultiInt64Flag = SliceFlag[*Int64SliceFlag, []int64, int64] + + // MultiIntFlag extends IntSliceFlag with support for using slices directly, as Value and/or Destination. + // See also SliceFlag. + MultiIntFlag = SliceFlag[*IntSliceFlag, []int, int] + + flagValueHook struct { + value Generic + hook func() + } +) + +var ( + // compile time assertions + + _ SliceFlagTarget[string] = (*StringSliceFlag)(nil) + _ SliceFlagTarget[string] = (*SliceFlag[*StringSliceFlag, []string, string])(nil) + _ SliceFlagTarget[string] = (*MultiStringFlag)(nil) + _ SliceFlagTarget[float64] = (*MultiFloat64Flag)(nil) + _ SliceFlagTarget[int64] = (*MultiInt64Flag)(nil) + _ SliceFlagTarget[int] = (*MultiIntFlag)(nil) + + _ Generic = (*flagValueHook)(nil) + _ Serializer = (*flagValueHook)(nil) +) + +func (x *SliceFlag[T, S, E]) Apply(set *flag.FlagSet) error { + x.Target.SetValue(x.convertSlice(x.Value)) + + destination := x.Destination + if destination == nil { + x.Target.SetDestination(nil) + + return x.Target.Apply(set) + } + + x.Target.SetDestination(x.convertSlice(*destination)) + + return applyFlagValueHook(set, x.Target.Apply, func() { + *destination = x.Target.GetDestination() + }) +} + +func (x *SliceFlag[T, S, E]) convertSlice(slice S) []E { + result := make([]E, len(slice)) + copy(result, slice) + return result +} + +func (x *SliceFlag[T, S, E]) SetValue(slice S) { + x.Value = slice +} + +func (x *SliceFlag[T, S, E]) SetDestination(slice S) { + if slice != nil { + x.Destination = &slice + } else { + x.Destination = nil + } +} + +func (x *SliceFlag[T, S, E]) GetDestination() S { + if destination := x.Destination; destination != nil { + return *destination + } + return nil +} + +func (x *SliceFlag[T, S, E]) String() string { return x.Target.String() } +func (x *SliceFlag[T, S, E]) Names() []string { return x.Target.Names() } +func (x *SliceFlag[T, S, E]) IsSet() bool { return x.Target.IsSet() } +func (x *SliceFlag[T, S, E]) IsRequired() bool { return x.Target.IsRequired() } +func (x *SliceFlag[T, S, E]) TakesValue() bool { return x.Target.TakesValue() } +func (x *SliceFlag[T, S, E]) GetUsage() string { return x.Target.GetUsage() } +func (x *SliceFlag[T, S, E]) GetValue() string { return x.Target.GetValue() } +func (x *SliceFlag[T, S, E]) GetDefaultText() string { return x.Target.GetDefaultText() } +func (x *SliceFlag[T, S, E]) GetEnvVars() []string { return x.Target.GetEnvVars() } +func (x *SliceFlag[T, S, E]) IsVisible() bool { return x.Target.IsVisible() } +func (x *SliceFlag[T, S, E]) GetCategory() string { return x.Target.GetCategory() } + +func (x *flagValueHook) Set(value string) error { + if err := x.value.Set(value); err != nil { + return err + } + x.hook() + return nil +} + +func (x *flagValueHook) String() string { + // note: this is necessary due to the way Go's flag package handles defaults + isZeroValue := func(f flag.Value, v string) bool { + /* + https://cs.opensource.google/go/go/+/refs/tags/go1.18.3:src/flag/flag.go;drc=2580d0e08d5e9f979b943758d3c49877fb2324cb;l=453 + + Copyright (c) 2009 The Go Authors. All rights reserved. + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + // Build a zero value of the flag's Value type, and see if the + // result of calling its String method equals the value passed in. + // This works unless the Value type is itself an interface type. + typ := reflect.TypeOf(f) + var z reflect.Value + if typ.Kind() == reflect.Pointer { + z = reflect.New(typ.Elem()) + } else { + z = reflect.Zero(typ) + } + return v == z.Interface().(flag.Value).String() + } + if x.value != nil { + // only return non-empty if not the same string as returned by the zero value + if s := x.value.String(); !isZeroValue(x.value, s) { + return s + } + } + return `` +} + +func (x *flagValueHook) Serialize() string { + if value, ok := x.value.(Serializer); ok { + return value.Serialize() + } + return x.String() +} + +// applyFlagValueHook wraps calls apply then wraps flags to call a hook function on update and after initial apply. +func applyFlagValueHook(set *flag.FlagSet, apply func(set *flag.FlagSet) error, hook func()) error { + if apply == nil || set == nil || hook == nil { + panic(`invalid input`) + } + var tmp flag.FlagSet + if err := apply(&tmp); err != nil { + return err + } + tmp.VisitAll(func(f *flag.Flag) { set.Var(&flagValueHook{value: f.Value, hook: hook}, f.Name, f.Usage) }) + hook() + return nil +} + +// newSliceFlagValue is for implementing SliceFlagTarget.SetValue and SliceFlagTarget.SetDestination. +// It's e.g. as part of StringSliceFlag.SetValue, using the factory NewStringSlice. +func newSliceFlagValue[R any, S ~[]E, E any](factory func(defaults ...E) *R, defaults S) *R { + if defaults == nil { + return nil + } + return factory(defaults...) +} + +// unwrapFlagValue strips any/all *flagValueHook wrappers. +func unwrapFlagValue(v flag.Value) flag.Value { + for { + h, ok := v.(*flagValueHook) + if !ok { + return v + } + v = h.value + } +} + +// NOTE: the methods below are in this file to make use of the build constraint + +func (f *Float64SliceFlag) SetValue(slice []float64) { + f.Value = newSliceFlagValue(NewFloat64Slice, slice) +} + +func (f *Float64SliceFlag) SetDestination(slice []float64) { + f.Destination = newSliceFlagValue(NewFloat64Slice, slice) +} + +func (f *Float64SliceFlag) GetDestination() []float64 { + if destination := f.Destination; destination != nil { + return destination.Value() + } + return nil +} + +func (f *Int64SliceFlag) SetValue(slice []int64) { + f.Value = newSliceFlagValue(NewInt64Slice, slice) +} + +func (f *Int64SliceFlag) SetDestination(slice []int64) { + f.Destination = newSliceFlagValue(NewInt64Slice, slice) +} + +func (f *Int64SliceFlag) GetDestination() []int64 { + if destination := f.Destination; destination != nil { + return destination.Value() + } + return nil +} + +func (f *IntSliceFlag) SetValue(slice []int) { + f.Value = newSliceFlagValue(NewIntSlice, slice) +} + +func (f *IntSliceFlag) SetDestination(slice []int) { + f.Destination = newSliceFlagValue(NewIntSlice, slice) +} + +func (f *IntSliceFlag) GetDestination() []int { + if destination := f.Destination; destination != nil { + return destination.Value() + } + return nil +} + +func (f *StringSliceFlag) SetValue(slice []string) { + f.Value = newSliceFlagValue(NewStringSlice, slice) +} + +func (f *StringSliceFlag) SetDestination(slice []string) { + f.Destination = newSliceFlagValue(NewStringSlice, slice) +} + +func (f *StringSliceFlag) GetDestination() []string { + if destination := f.Destination; destination != nil { + return destination.Value() + } + return nil +} diff --git a/sliceflag_pre18.go b/sliceflag_pre18.go new file mode 100644 index 0000000000..1173ae7402 --- /dev/null +++ b/sliceflag_pre18.go @@ -0,0 +1,10 @@ +//go:build !go1.18 +// +build !go1.18 + +package cli + +import ( + "flag" +) + +func unwrapFlagValue(v flag.Value) flag.Value { return v } diff --git a/sliceflag_test.go b/sliceflag_test.go new file mode 100644 index 0000000000..179020b14f --- /dev/null +++ b/sliceflag_test.go @@ -0,0 +1,1005 @@ +//go:build go1.18 +// +build go1.18 + +package cli + +import ( + "bytes" + "flag" + "fmt" + "os" + "reflect" + "testing" +) + +func ExampleMultiStringFlag() { + run := func(args ...string) { + // add $0 (the command being run) + args = append([]string{`-`}, args...) + type CustomStringSlice []string + type Config struct { + FlagOne []string + Two CustomStringSlice + } + cfg := Config{ + Two: []string{ + `default value 1`, + `default value 2`, + }, + } + if err := (&App{ + Flags: []Flag{ + &MultiStringFlag{ + Target: &StringSliceFlag{ + Name: `flag-one`, + Category: `category1`, + Usage: `this is the first flag`, + Aliases: []string{`1`}, + EnvVars: []string{`FLAG_ONE`}, + }, + Value: cfg.FlagOne, + Destination: &cfg.FlagOne, + }, + &SliceFlag[*StringSliceFlag, CustomStringSlice, string]{ + Target: &StringSliceFlag{ + Name: `two`, + Category: `category2`, + Usage: `this is the second flag`, + Aliases: []string{`2`}, + EnvVars: []string{`TWO`}, + }, + Value: cfg.Two, + Destination: &cfg.Two, + }, + &MultiStringFlag{ + Target: &StringSliceFlag{ + Name: `flag-three`, + Category: `category1`, + Usage: `this is the third flag`, + Aliases: []string{`3`}, + EnvVars: []string{`FLAG_THREE`}, + }, + Value: []string{`some value`}, + }, + &StringSliceFlag{ + Name: `flag-four`, + Category: `category2`, + Usage: `this is the fourth flag`, + Aliases: []string{`4`}, + EnvVars: []string{`FLAG_FOUR`}, + Value: NewStringSlice(`d1`, `d2`), + }, + }, + Action: func(c *Context) error { + fmt.Printf("Flag names: %q\n", c.FlagNames()) + fmt.Printf("Local flag names: %q\n", c.LocalFlagNames()) + fmt.Println(`Context values:`) + for _, name := range [...]string{`flag-one`, `two`, `flag-three`, `flag-four`} { + fmt.Printf("%q=%q\n", name, c.StringSlice(name)) + } + fmt.Println(`Destination values:`) + fmt.Printf("cfg.FlagOne=%q\n", cfg.FlagOne) + fmt.Printf("cfg.Two=%q\n", cfg.Two) + return nil + }, + Writer: os.Stdout, + ErrWriter: os.Stdout, + Name: `app-name`, + }).Run(args); err != nil { + panic(err) + } + } + + fmt.Printf("Show defaults...\n\n") + run() + + fmt.Printf("---\nSetting all flags via command line...\n\n") + allFlagsArgs := []string{ + `-1`, `v 1`, + `-1`, `v 2`, + `-2`, `v 3`, + `-2`, `v 4`, + `-3`, `v 5`, + `-3`, `v 6`, + `-4`, `v 7`, + `-4`, `v 8`, + } + run(allFlagsArgs...) + + func() { + defer resetEnv(os.Environ()) + os.Clearenv() + for _, args := range [...][2]string{ + {`FLAG_ONE`, `v 9, v 10`}, + {`TWO`, `v 11, v 12`}, + {`FLAG_THREE`, `v 13, v 14`}, + {`FLAG_FOUR`, `v 15, v 16`}, + } { + if err := os.Setenv(args[0], args[1]); err != nil { + panic(err) + } + } + + fmt.Printf("---\nSetting all flags via environment...\n\n") + run() + + fmt.Printf("---\nWith the same environment + args from the previous example...\n\n") + run(allFlagsArgs...) + }() + + //output: + //Show defaults... + // + //Flag names: [] + //Local flag names: [] + //Context values: + //"flag-one"=[] + //"two"=["default value 1" "default value 2"] + //"flag-three"=["some value"] + //"flag-four"=["d1" "d2"] + //Destination values: + //cfg.FlagOne=[] + //cfg.Two=["default value 1" "default value 2"] + //--- + //Setting all flags via command line... + // + //Flag names: ["1" "2" "3" "4" "flag-four" "flag-one" "flag-three" "two"] + //Local flag names: ["1" "2" "3" "4" "flag-four" "flag-one" "flag-three" "two"] + //Context values: + //"flag-one"=["v 1" "v 2"] + //"two"=["v 3" "v 4"] + //"flag-three"=["v 5" "v 6"] + //"flag-four"=["v 7" "v 8"] + //Destination values: + //cfg.FlagOne=["v 1" "v 2"] + //cfg.Two=["v 3" "v 4"] + //--- + //Setting all flags via environment... + // + //Flag names: [] + //Local flag names: [] + //Context values: + //"flag-one"=["v 9" "v 10"] + //"two"=["v 11" "v 12"] + //"flag-three"=["v 13" "v 14"] + //"flag-four"=["v 15" "v 16"] + //Destination values: + //cfg.FlagOne=["v 9" "v 10"] + //cfg.Two=["v 11" "v 12"] + //--- + //With the same environment + args from the previous example... + // + //Flag names: ["1" "2" "3" "4" "flag-four" "flag-one" "flag-three" "two"] + //Local flag names: ["1" "2" "3" "4" "flag-four" "flag-one" "flag-three" "two"] + //Context values: + //"flag-one"=["v 1" "v 2"] + //"two"=["v 3" "v 4"] + //"flag-three"=["v 5" "v 6"] + //"flag-four"=["v 7" "v 8"] + //Destination values: + //cfg.FlagOne=["v 1" "v 2"] + //cfg.Two=["v 3" "v 4"] +} + +func TestSliceFlag_Apply_string(t *testing.T) { + normalise := func(v any) any { + switch v := v.(type) { + case *[]string: + if v == nil { + return nil + } + return *v + case *StringSlice: + if v == nil { + return nil + } + return v.Value() + } + return v + } + expectEqual := func(t *testing.T, actual, expected any) { + t.Helper() + actual = normalise(actual) + expected = normalise(expected) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("actual: %#v\nexpected: %#v", actual, expected) + } + } + type Config struct { + Flag SliceFlagTarget[string] + Value *[]string + Destination **[]string + Context *Context + Check func() + } + for _, tc := range [...]struct { + Name string + Factory func(t *testing.T, f *StringSliceFlag) Config + }{ + { + Name: `once`, + Factory: func(t *testing.T, f *StringSliceFlag) Config { + v := SliceFlag[*StringSliceFlag, []string, string]{Target: f} + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + }, + } + }, + }, + { + Name: `twice`, + Factory: func(t *testing.T, f *StringSliceFlag) Config { + v := SliceFlag[*SliceFlag[*StringSliceFlag, []string, string], []string, string]{ + Target: &SliceFlag[*StringSliceFlag, []string, string]{Target: f}, + } + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Destination) + }, + } + }, + }, + { + Name: `thrice`, + Factory: func(t *testing.T, f *StringSliceFlag) Config { + v := SliceFlag[*SliceFlag[*SliceFlag[*StringSliceFlag, []string, string], []string, string], []string, string]{ + Target: &SliceFlag[*SliceFlag[*StringSliceFlag, []string, string], []string, string]{ + Target: &SliceFlag[*StringSliceFlag, []string, string]{Target: f}, + }, + } + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Target.Destination) + }, + } + }, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + t.Run(`destination`, func(t *testing.T) { + c := tc.Factory(t, &StringSliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []string{`one`, ``, ``, `two`, ``} + var vTarget []string + *c.Value = vDefault + *c.Destination = &vTarget + if err := (&App{Action: func(c *Context) error { return nil }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=`, `--a=three`, `--a=`, `--a=`, `--a=four`, `--a=`, `--a=`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []string{`one`, ``, ``, `two`, ``}) + expectEqual(t, vTarget, []string{"", "three", "", "", "four", "", ""}) + }) + t.Run(`context`, func(t *testing.T) { + c := tc.Factory(t, &StringSliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []string{`one`, ``, ``, `two`, ``} + *c.Value = vDefault + var vTarget []string + if err := (&App{Action: func(c *Context) error { + vTarget = c.StringSlice(`a`) + return nil + }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=`, `--a=three`, `--a=`, `--a=`, `--a=four`, `--a=`, `--a=`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []string{`one`, ``, ``, `two`, ``}) + expectEqual(t, vTarget, []string{"", "three", "", "", "four", "", ""}) + }) + t.Run(`context with destination`, func(t *testing.T) { + c := tc.Factory(t, &StringSliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []string{`one`, ``, ``, `two`, ``} + *c.Value = vDefault + var vTarget []string + var destination []string + *c.Destination = &destination + if err := (&App{Action: func(c *Context) error { + vTarget = c.StringSlice(`a`) + return nil + }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=`, `--a=three`, `--a=`, `--a=`, `--a=four`, `--a=`, `--a=`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []string{`one`, ``, ``, `two`, ``}) + expectEqual(t, vTarget, []string{"", "three", "", "", "four", "", ""}) + expectEqual(t, destination, []string{"", "three", "", "", "four", "", ""}) + }) + t.Run(`stdlib flag usage with default`, func(t *testing.T) { + c := tc.Factory(t, &StringSliceFlag{Name: `a`}) + *c.Value = []string{`one`, `two`} + var vTarget []string + *c.Destination = &vTarget + set := flag.NewFlagSet(`flagset`, flag.ContinueOnError) + var output bytes.Buffer + set.SetOutput(&output) + if err := c.Flag.Apply(set); err != nil { + t.Fatal(err) + } + if err := set.Parse([]string{`-h`}); err != flag.ErrHelp { + t.Fatal(err) + } + if s := output.String(); s != "Usage of flagset:\n -a value\n \t (default [one two])\n" { + t.Errorf("unexpected output: %q\n%s", s, s) + } + }) + { + test := func(t *testing.T, value []string) { + c := tc.Factory(t, &StringSliceFlag{Name: `a`}) + *c.Value = value + var vTarget []string + *c.Destination = &vTarget + set := flag.NewFlagSet(`flagset`, flag.ContinueOnError) + var output bytes.Buffer + set.SetOutput(&output) + if err := c.Flag.Apply(set); err != nil { + t.Fatal(err) + } + if err := set.Parse([]string{`-h`}); err != flag.ErrHelp { + t.Fatal(err) + } + if s := output.String(); s != "Usage of flagset:\n -a value\n \t\n" { + t.Errorf("unexpected output: %q\n%s", s, s) + } + } + t.Run(`stdlib flag usage without default nil`, func(t *testing.T) { + test(t, nil) + }) + t.Run(`stdlib flag usage without default empty`, func(t *testing.T) { + test(t, make([]string, 0)) + }) + } + }) + } +} + +func TestSliceFlag_Apply_float64(t *testing.T) { + normalise := func(v any) any { + switch v := v.(type) { + case *[]float64: + if v == nil { + return nil + } + return *v + case *Float64Slice: + if v == nil { + return nil + } + return v.Value() + } + return v + } + expectEqual := func(t *testing.T, actual, expected any) { + t.Helper() + actual = normalise(actual) + expected = normalise(expected) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("actual: %#v\nexpected: %#v", actual, expected) + } + } + type Config struct { + Flag SliceFlagTarget[float64] + Value *[]float64 + Destination **[]float64 + Context *Context + Check func() + } + for _, tc := range [...]struct { + Name string + Factory func(t *testing.T, f *Float64SliceFlag) Config + }{ + { + Name: `once`, + Factory: func(t *testing.T, f *Float64SliceFlag) Config { + v := SliceFlag[*Float64SliceFlag, []float64, float64]{Target: f} + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + }, + } + }, + }, + { + Name: `twice`, + Factory: func(t *testing.T, f *Float64SliceFlag) Config { + v := SliceFlag[*SliceFlag[*Float64SliceFlag, []float64, float64], []float64, float64]{ + Target: &SliceFlag[*Float64SliceFlag, []float64, float64]{Target: f}, + } + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Destination) + }, + } + }, + }, + { + Name: `thrice`, + Factory: func(t *testing.T, f *Float64SliceFlag) Config { + v := SliceFlag[*SliceFlag[*SliceFlag[*Float64SliceFlag, []float64, float64], []float64, float64], []float64, float64]{ + Target: &SliceFlag[*SliceFlag[*Float64SliceFlag, []float64, float64], []float64, float64]{ + Target: &SliceFlag[*Float64SliceFlag, []float64, float64]{Target: f}, + }, + } + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Target.Destination) + }, + } + }, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + t.Run(`destination`, func(t *testing.T) { + c := tc.Factory(t, &Float64SliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []float64{1, 2, 3} + var vTarget []float64 + *c.Value = vDefault + *c.Destination = &vTarget + if err := (&App{Action: func(c *Context) error { return nil }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []float64{1, 2, 3}) + expectEqual(t, vTarget, []float64{4, 5}) + }) + t.Run(`context`, func(t *testing.T) { + c := tc.Factory(t, &Float64SliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []float64{1, 2, 3} + *c.Value = vDefault + var vTarget []float64 + if err := (&App{Action: func(c *Context) error { + vTarget = c.Float64Slice(`a`) + return nil + }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []float64{1, 2, 3}) + expectEqual(t, vTarget, []float64{4, 5}) + }) + t.Run(`context with destination`, func(t *testing.T) { + c := tc.Factory(t, &Float64SliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []float64{1, 2, 3} + *c.Value = vDefault + var vTarget []float64 + var destination []float64 + *c.Destination = &destination + if err := (&App{Action: func(c *Context) error { + vTarget = c.Float64Slice(`a`) + return nil + }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []float64{1, 2, 3}) + expectEqual(t, vTarget, []float64{4, 5}) + expectEqual(t, destination, []float64{4, 5}) + }) + t.Run(`stdlib flag usage with default`, func(t *testing.T) { + c := tc.Factory(t, &Float64SliceFlag{Name: `a`}) + *c.Value = []float64{1, 2} + var vTarget []float64 + *c.Destination = &vTarget + set := flag.NewFlagSet(`flagset`, flag.ContinueOnError) + var output bytes.Buffer + set.SetOutput(&output) + if err := c.Flag.Apply(set); err != nil { + t.Fatal(err) + } + if err := set.Parse([]string{`-h`}); err != flag.ErrHelp { + t.Fatal(err) + } + if s := output.String(); s != "Usage of flagset:\n -a value\n \t (default []float64{1, 2})\n" { + t.Errorf("unexpected output: %q\n%s", s, s) + } + }) + { + test := func(t *testing.T, value []float64) { + c := tc.Factory(t, &Float64SliceFlag{Name: `a`}) + *c.Value = value + var vTarget []float64 + *c.Destination = &vTarget + set := flag.NewFlagSet(`flagset`, flag.ContinueOnError) + var output bytes.Buffer + set.SetOutput(&output) + if err := c.Flag.Apply(set); err != nil { + t.Fatal(err) + } + if err := set.Parse([]string{`-h`}); err != flag.ErrHelp { + t.Fatal(err) + } + if s := output.String(); s != "Usage of flagset:\n -a value\n \t\n" { + t.Errorf("unexpected output: %q\n%s", s, s) + } + } + t.Run(`stdlib flag usage without default nil`, func(t *testing.T) { + test(t, nil) + }) + t.Run(`stdlib flag usage without default empty`, func(t *testing.T) { + test(t, make([]float64, 0)) + }) + } + }) + } +} + +func TestSliceFlag_Apply_int64(t *testing.T) { + normalise := func(v any) any { + switch v := v.(type) { + case *[]int64: + if v == nil { + return nil + } + return *v + case *Int64Slice: + if v == nil { + return nil + } + return v.Value() + } + return v + } + expectEqual := func(t *testing.T, actual, expected any) { + t.Helper() + actual = normalise(actual) + expected = normalise(expected) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("actual: %#v\nexpected: %#v", actual, expected) + } + } + type Config struct { + Flag SliceFlagTarget[int64] + Value *[]int64 + Destination **[]int64 + Context *Context + Check func() + } + for _, tc := range [...]struct { + Name string + Factory func(t *testing.T, f *Int64SliceFlag) Config + }{ + { + Name: `once`, + Factory: func(t *testing.T, f *Int64SliceFlag) Config { + v := SliceFlag[*Int64SliceFlag, []int64, int64]{Target: f} + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + }, + } + }, + }, + { + Name: `twice`, + Factory: func(t *testing.T, f *Int64SliceFlag) Config { + v := SliceFlag[*SliceFlag[*Int64SliceFlag, []int64, int64], []int64, int64]{ + Target: &SliceFlag[*Int64SliceFlag, []int64, int64]{Target: f}, + } + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Destination) + }, + } + }, + }, + { + Name: `thrice`, + Factory: func(t *testing.T, f *Int64SliceFlag) Config { + v := SliceFlag[*SliceFlag[*SliceFlag[*Int64SliceFlag, []int64, int64], []int64, int64], []int64, int64]{ + Target: &SliceFlag[*SliceFlag[*Int64SliceFlag, []int64, int64], []int64, int64]{ + Target: &SliceFlag[*Int64SliceFlag, []int64, int64]{Target: f}, + }, + } + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Target.Destination) + }, + } + }, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + t.Run(`destination`, func(t *testing.T) { + c := tc.Factory(t, &Int64SliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []int64{1, 2, 3} + var vTarget []int64 + *c.Value = vDefault + *c.Destination = &vTarget + if err := (&App{Action: func(c *Context) error { return nil }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []int64{1, 2, 3}) + expectEqual(t, vTarget, []int64{4, 5}) + }) + t.Run(`context`, func(t *testing.T) { + c := tc.Factory(t, &Int64SliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []int64{1, 2, 3} + *c.Value = vDefault + var vTarget []int64 + if err := (&App{Action: func(c *Context) error { + vTarget = c.Int64Slice(`a`) + return nil + }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []int64{1, 2, 3}) + expectEqual(t, vTarget, []int64{4, 5}) + }) + t.Run(`context with destination`, func(t *testing.T) { + c := tc.Factory(t, &Int64SliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []int64{1, 2, 3} + *c.Value = vDefault + var vTarget []int64 + var destination []int64 + *c.Destination = &destination + if err := (&App{Action: func(c *Context) error { + vTarget = c.Int64Slice(`a`) + return nil + }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []int64{1, 2, 3}) + expectEqual(t, vTarget, []int64{4, 5}) + expectEqual(t, destination, []int64{4, 5}) + }) + t.Run(`stdlib flag usage with default`, func(t *testing.T) { + c := tc.Factory(t, &Int64SliceFlag{Name: `a`}) + *c.Value = []int64{1, 2} + var vTarget []int64 + *c.Destination = &vTarget + set := flag.NewFlagSet(`flagset`, flag.ContinueOnError) + var output bytes.Buffer + set.SetOutput(&output) + if err := c.Flag.Apply(set); err != nil { + t.Fatal(err) + } + if err := set.Parse([]string{`-h`}); err != flag.ErrHelp { + t.Fatal(err) + } + if s := output.String(); s != "Usage of flagset:\n -a value\n \t (default []int64{1, 2})\n" { + t.Errorf("unexpected output: %q\n%s", s, s) + } + }) + { + test := func(t *testing.T, value []int64) { + c := tc.Factory(t, &Int64SliceFlag{Name: `a`}) + *c.Value = value + var vTarget []int64 + *c.Destination = &vTarget + set := flag.NewFlagSet(`flagset`, flag.ContinueOnError) + var output bytes.Buffer + set.SetOutput(&output) + if err := c.Flag.Apply(set); err != nil { + t.Fatal(err) + } + if err := set.Parse([]string{`-h`}); err != flag.ErrHelp { + t.Fatal(err) + } + if s := output.String(); s != "Usage of flagset:\n -a value\n \t\n" { + t.Errorf("unexpected output: %q\n%s", s, s) + } + } + t.Run(`stdlib flag usage without default nil`, func(t *testing.T) { + test(t, nil) + }) + t.Run(`stdlib flag usage without default empty`, func(t *testing.T) { + test(t, make([]int64, 0)) + }) + } + }) + } +} + +func TestSliceFlag_Apply_int(t *testing.T) { + normalise := func(v any) any { + switch v := v.(type) { + case *[]int: + if v == nil { + return nil + } + return *v + case *IntSlice: + if v == nil { + return nil + } + return v.Value() + } + return v + } + expectEqual := func(t *testing.T, actual, expected any) { + t.Helper() + actual = normalise(actual) + expected = normalise(expected) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("actual: %#v\nexpected: %#v", actual, expected) + } + } + type Config struct { + Flag SliceFlagTarget[int] + Value *[]int + Destination **[]int + Context *Context + Check func() + } + for _, tc := range [...]struct { + Name string + Factory func(t *testing.T, f *IntSliceFlag) Config + }{ + { + Name: `once`, + Factory: func(t *testing.T, f *IntSliceFlag) Config { + v := SliceFlag[*IntSliceFlag, []int, int]{Target: f} + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + }, + } + }, + }, + { + Name: `twice`, + Factory: func(t *testing.T, f *IntSliceFlag) Config { + v := SliceFlag[*SliceFlag[*IntSliceFlag, []int, int], []int, int]{ + Target: &SliceFlag[*IntSliceFlag, []int, int]{Target: f}, + } + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Destination) + }, + } + }, + }, + { + Name: `thrice`, + Factory: func(t *testing.T, f *IntSliceFlag) Config { + v := SliceFlag[*SliceFlag[*SliceFlag[*IntSliceFlag, []int, int], []int, int], []int, int]{ + Target: &SliceFlag[*SliceFlag[*IntSliceFlag, []int, int], []int, int]{ + Target: &SliceFlag[*IntSliceFlag, []int, int]{Target: f}, + }, + } + return Config{ + Flag: &v, + Value: &v.Value, + Destination: &v.Destination, + Check: func() { + expectEqual(t, v.Value, v.Target.Value) + expectEqual(t, v.Destination, v.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Destination) + + expectEqual(t, v.Value, v.Target.Target.Target.Value) + expectEqual(t, v.Destination, v.Target.Target.Target.Destination) + }, + } + }, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + t.Run(`destination`, func(t *testing.T) { + c := tc.Factory(t, &IntSliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []int{1, 2, 3} + var vTarget []int + *c.Value = vDefault + *c.Destination = &vTarget + if err := (&App{Action: func(c *Context) error { return nil }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []int{1, 2, 3}) + expectEqual(t, vTarget, []int{4, 5}) + }) + t.Run(`context`, func(t *testing.T) { + c := tc.Factory(t, &IntSliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []int{1, 2, 3} + *c.Value = vDefault + var vTarget []int + if err := (&App{Action: func(c *Context) error { + vTarget = c.IntSlice(`a`) + return nil + }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []int{1, 2, 3}) + expectEqual(t, vTarget, []int{4, 5}) + }) + t.Run(`context with destination`, func(t *testing.T) { + c := tc.Factory(t, &IntSliceFlag{ + Name: `a`, + EnvVars: []string{`APP_A`}, + }) + defer c.Check() + vDefault := []int{1, 2, 3} + *c.Value = vDefault + var vTarget []int + var destination []int + *c.Destination = &destination + if err := (&App{Action: func(c *Context) error { + vTarget = c.IntSlice(`a`) + return nil + }, Flags: []Flag{c.Flag}}).Run([]string{`-`, `--a=4`, `--a=5`}); err != nil { + t.Fatal(err) + } + expectEqual(t, vDefault, []int{1, 2, 3}) + expectEqual(t, vTarget, []int{4, 5}) + expectEqual(t, destination, []int{4, 5}) + }) + t.Run(`stdlib flag usage with default`, func(t *testing.T) { + c := tc.Factory(t, &IntSliceFlag{Name: `a`}) + *c.Value = []int{1, 2} + var vTarget []int + *c.Destination = &vTarget + set := flag.NewFlagSet(`flagset`, flag.ContinueOnError) + var output bytes.Buffer + set.SetOutput(&output) + if err := c.Flag.Apply(set); err != nil { + t.Fatal(err) + } + if err := set.Parse([]string{`-h`}); err != flag.ErrHelp { + t.Fatal(err) + } + if s := output.String(); s != "Usage of flagset:\n -a value\n \t (default []int{1, 2})\n" { + t.Errorf("unexpected output: %q\n%s", s, s) + } + }) + { + test := func(t *testing.T, value []int) { + c := tc.Factory(t, &IntSliceFlag{Name: `a`}) + *c.Value = value + var vTarget []int + *c.Destination = &vTarget + set := flag.NewFlagSet(`flagset`, flag.ContinueOnError) + var output bytes.Buffer + set.SetOutput(&output) + if err := c.Flag.Apply(set); err != nil { + t.Fatal(err) + } + if err := set.Parse([]string{`-h`}); err != flag.ErrHelp { + t.Fatal(err) + } + if s := output.String(); s != "Usage of flagset:\n -a value\n \t\n" { + t.Errorf("unexpected output: %q\n%s", s, s) + } + } + t.Run(`stdlib flag usage without default nil`, func(t *testing.T) { + test(t, nil) + }) + t.Run(`stdlib flag usage without default empty`, func(t *testing.T) { + test(t, make([]int, 0)) + }) + } + }) + } +} + +type intSliceWrapperDefaultingNil struct { + *IntSlice +} + +func (x intSliceWrapperDefaultingNil) String() string { + if x.IntSlice != nil { + return x.IntSlice.String() + } + return NewIntSlice().String() +} + +func TestFlagValueHook_String_struct(t *testing.T) { + wrap := func(values ...int) *flagValueHook { + return &flagValueHook{value: intSliceWrapperDefaultingNil{NewIntSlice(values...)}} + } + if s := wrap().String(); s != `` { + t.Error(s) + } + if s := wrap(1).String(); s != `[]int{1}` { + t.Error(s) + } +} diff --git a/template.go b/template.go index 264eb856bb..f3116fd2c9 100644 --- a/template.go +++ b/template.go @@ -4,16 +4,16 @@ package cli // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. var AppHelpTemplate = `NAME: - {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} + {{$v := offset .Name 6}}{{wrap .Name 3}}{{if .Usage}} - {{wrap .Usage $v}}{{end}} USAGE: - {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} + {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} VERSION: {{.Version}}{{end}}{{end}}{{if .Description}} DESCRIPTION: - {{.Description | nindent 3 | trim}}{{end}}{{if len .Authors}} + {{wrap .Description 3}}{{end}}{{if len .Authors}} AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: {{range $index, $author := .Authors}}{{if $index}} @@ -31,26 +31,26 @@ GLOBAL OPTIONS:{{range .VisibleFlagCategories}} GLOBAL OPTIONS: {{range $index, $option := .VisibleFlags}}{{if $index}} - {{end}}{{$option}}{{end}}{{end}}{{end}}{{if .Copyright}} + {{end}}{{wrap $option.String 6}}{{end}}{{end}}{{end}}{{if .Copyright}} COPYRIGHT: - {{.Copyright}}{{end}} + {{wrap .Copyright 3}}{{end}} ` // CommandHelpTemplate is the text template for the command help topic. // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. var CommandHelpTemplate = `NAME: - {{.HelpName}} - {{.Usage}} + {{$v := offset .HelpName 6}}{{wrap .HelpName 3}}{{if .Usage}} - {{wrap .Usage $v}}{{end}} USAGE: - {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} + {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} CATEGORY: {{.Category}}{{end}}{{if .Description}} DESCRIPTION: - {{.Description | nindent 3 | trim}}{{end}}{{if .VisibleFlagCategories}} + {{wrap .Description 3}}{{end}}{{if .VisibleFlagCategories}} OPTIONS:{{range .VisibleFlagCategories}} {{if .Name}}{{.Name}} @@ -69,10 +69,10 @@ var SubcommandHelpTemplate = `NAME: {{.HelpName}} - {{.Usage}} USAGE: - {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} + {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} DESCRIPTION: - {{.Description | nindent 3 | trim}}{{end}} + {{wrap .Description 3}}{{end}} COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{range .VisibleCommands}} diff --git a/testdata/godoc-v2.x.txt b/testdata/godoc-v2.x.txt index d94e80dce6..a3a7faca17 100644 --- a/testdata/godoc-v2.x.txt +++ b/testdata/godoc-v2.x.txt @@ -32,16 +32,16 @@ var ( SuggestDidYouMeanTemplate string = suggestDidYouMeanTemplate ) var AppHelpTemplate = `NAME: - {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} + {{$v := offset .Name 6}}{{wrap .Name 3}}{{if .Usage}} - {{wrap .Usage $v}}{{end}} USAGE: - {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} + {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} VERSION: {{.Version}}{{end}}{{end}}{{if .Description}} DESCRIPTION: - {{.Description | nindent 3 | trim}}{{end}}{{if len .Authors}} + {{wrap .Description 3}}{{end}}{{if len .Authors}} AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: {{range $index, $author := .Authors}}{{if $index}} @@ -59,26 +59,26 @@ GLOBAL OPTIONS:{{range .VisibleFlagCategories}} GLOBAL OPTIONS: {{range $index, $option := .VisibleFlags}}{{if $index}} - {{end}}{{$option}}{{end}}{{end}}{{end}}{{if .Copyright}} + {{end}}{{wrap $option.String 6}}{{end}}{{end}}{{end}}{{if .Copyright}} COPYRIGHT: - {{.Copyright}}{{end}} + {{wrap .Copyright 3}}{{end}} ` AppHelpTemplate is the text template for the Default help topic. cli.go uses text/template to render templates. You can render custom help text by setting this variable. var CommandHelpTemplate = `NAME: - {{.HelpName}} - {{.Usage}} + {{$v := offset .HelpName 6}}{{wrap .HelpName 3}}{{if .Usage}} - {{wrap .Usage $v}}{{end}} USAGE: - {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} + {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} CATEGORY: {{.Category}}{{end}}{{if .Description}} DESCRIPTION: - {{.Description | nindent 3 | trim}}{{end}}{{if .VisibleFlagCategories}} + {{wrap .Description 3}}{{end}}{{if .VisibleFlagCategories}} OPTIONS:{{range .VisibleFlagCategories}} {{if .Name}}{{.Name}} @@ -150,10 +150,10 @@ var SubcommandHelpTemplate = `NAME: {{.HelpName}} - {{.Usage}} USAGE: - {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} + {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} DESCRIPTION: - {{.Description | nindent 3 | trim}}{{end}} + {{wrap .Description 3}}{{end}} COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{range .VisibleCommands}} @@ -186,6 +186,11 @@ var HelpPrinterCustom helpPrinterCustom = printHelpCustom the default implementation of HelpPrinter, and may be called directly if the ExtraInfo field is set on an App. + In the default implementation, if the customFuncs argument contains a + "wrapAt" key, which is a function which takes no arguments and returns an + int, this int value will be used to produce a "wrap" function used by the + default template to wrap long lines. + FUNCTIONS @@ -1003,6 +1008,8 @@ func (f *Float64SliceFlag) GetCategory() string func (f *Float64SliceFlag) GetDefaultText() string GetDefaultText returns the default text for this flag +func (f *Float64SliceFlag) GetDestination() []float64 + func (f *Float64SliceFlag) GetEnvVars() []string GetEnvVars returns the env vars for this flag @@ -1025,6 +1032,10 @@ func (f *Float64SliceFlag) IsVisible() bool func (f *Float64SliceFlag) Names() []string Names returns the names of the flag +func (f *Float64SliceFlag) SetDestination(slice []float64) + +func (f *Float64SliceFlag) SetValue(slice []float64) + func (f *Float64SliceFlag) String() string String returns a readable representation of this value (for usage defaults) @@ -1215,6 +1226,8 @@ func (f *Int64SliceFlag) GetCategory() string func (f *Int64SliceFlag) GetDefaultText() string GetDefaultText returns the default text for this flag +func (f *Int64SliceFlag) GetDestination() []int64 + func (f *Int64SliceFlag) GetEnvVars() []string GetEnvVars returns the env vars for this flag @@ -1237,6 +1250,10 @@ func (f *Int64SliceFlag) IsVisible() bool func (f *Int64SliceFlag) Names() []string Names returns the names of the flag +func (f *Int64SliceFlag) SetDestination(slice []int64) + +func (f *Int64SliceFlag) SetValue(slice []int64) + func (f *Int64SliceFlag) String() string String returns a readable representation of this value (for usage defaults) @@ -1362,6 +1379,8 @@ func (f *IntSliceFlag) GetCategory() string func (f *IntSliceFlag) GetDefaultText() string GetDefaultText returns the default text for this flag +func (f *IntSliceFlag) GetDestination() []int + func (f *IntSliceFlag) GetEnvVars() []string GetEnvVars returns the env vars for this flag @@ -1384,6 +1403,10 @@ func (f *IntSliceFlag) IsVisible() bool func (f *IntSliceFlag) Names() []string Names returns the names of the flag +func (f *IntSliceFlag) SetDestination(slice []int) + +func (f *IntSliceFlag) SetValue(slice []int) + func (f *IntSliceFlag) String() string String returns a readable representation of this value (for usage defaults) @@ -1396,6 +1419,22 @@ type MultiError interface { } MultiError is an error that wraps multiple errors. +type MultiFloat64Flag = SliceFlag[*Float64SliceFlag, []float64, float64] + MultiFloat64Flag extends Float64SliceFlag with support for using slices + directly, as Value and/or Destination. See also SliceFlag. + +type MultiInt64Flag = SliceFlag[*Int64SliceFlag, []int64, int64] + MultiInt64Flag extends Int64SliceFlag with support for using slices + directly, as Value and/or Destination. See also SliceFlag. + +type MultiIntFlag = SliceFlag[*IntSliceFlag, []int, int] + MultiIntFlag extends IntSliceFlag with support for using slices directly, as + Value and/or Destination. See also SliceFlag. + +type MultiStringFlag = SliceFlag[*StringSliceFlag, []string, string] + MultiStringFlag extends StringSliceFlag with support for using slices + directly, as Value and/or Destination. See also SliceFlag. + type OnUsageErrorFunc func(cCtx *Context, err error, isSubcommand bool) error OnUsageErrorFunc is executed if a usage error occurs. This is useful for displaying customized usage error messages. This function is able to replace @@ -1480,6 +1519,67 @@ type Serializer interface { } Serializer is used to circumvent the limitations of flag.FlagSet.Set +type SliceFlag[T SliceFlagTarget[E], S ~[]E, E any] struct { + Target T + Value S + Destination *S +} + SliceFlag extends implementations like StringSliceFlag and IntSliceFlag with + support for using slices directly, as Value and/or Destination. See also + SliceFlagTarget, MultiStringFlag, MultiFloat64Flag, MultiInt64Flag, + MultiIntFlag. + +func (x *SliceFlag[T, S, E]) Apply(set *flag.FlagSet) error + +func (x *SliceFlag[T, S, E]) GetCategory() string + +func (x *SliceFlag[T, S, E]) GetDefaultText() string + +func (x *SliceFlag[T, S, E]) GetDestination() S + +func (x *SliceFlag[T, S, E]) GetEnvVars() []string + +func (x *SliceFlag[T, S, E]) GetUsage() string + +func (x *SliceFlag[T, S, E]) GetValue() string + +func (x *SliceFlag[T, S, E]) IsRequired() bool + +func (x *SliceFlag[T, S, E]) IsSet() bool + +func (x *SliceFlag[T, S, E]) IsVisible() bool + +func (x *SliceFlag[T, S, E]) Names() []string + +func (x *SliceFlag[T, S, E]) SetDestination(slice S) + +func (x *SliceFlag[T, S, E]) SetValue(slice S) + +func (x *SliceFlag[T, S, E]) String() string + +func (x *SliceFlag[T, S, E]) TakesValue() bool + +type SliceFlagTarget[E any] interface { + Flag + RequiredFlag + DocGenerationFlag + VisibleFlag + CategorizableFlag + + // SetValue should propagate the given slice to the target, ideally as a new value. + // Note that a nil slice should nil/clear any existing value (modelled as ~[]E). + SetValue(slice []E) + // SetDestination should propagate the given slice to the target, ideally as a new value. + // Note that a nil slice should nil/clear any existing value (modelled as ~*[]E). + SetDestination(slice []E) + // GetDestination should return the current value referenced by any destination, or nil if nil/unset. + GetDestination() []E +} + SliceFlagTarget models a target implementation for use with SliceFlag. The + three methods, SetValue, SetDestination, and GetDestination, are necessary + to propagate Value and Destination, where Value is propagated inwards + (initially), and Destination is propagated outwards (on every update). + type StringFlag struct { Name string @@ -1599,6 +1699,8 @@ func (f *StringSliceFlag) GetCategory() string func (f *StringSliceFlag) GetDefaultText() string GetDefaultText returns the default text for this flag +func (f *StringSliceFlag) GetDestination() []string + func (f *StringSliceFlag) GetEnvVars() []string GetEnvVars returns the env vars for this flag @@ -1621,6 +1723,10 @@ func (f *StringSliceFlag) IsVisible() bool func (f *StringSliceFlag) Names() []string Names returns the names of the flag +func (f *StringSliceFlag) SetDestination(slice []string) + +func (f *StringSliceFlag) SetValue(slice []string) + func (f *StringSliceFlag) String() string String returns a readable representation of this value (for usage defaults)