diff --git a/flag.go b/flag.go index 420ea5e939..20d47c1e66 100644 --- a/flag.go +++ b/flag.go @@ -180,11 +180,6 @@ type LocalFlag interface { IsLocal() bool } -// IsDefaultVisible returns true if the flag is not hidden, otherwise false -func (f *FlagBase[T, C, V]) IsDefaultVisible() bool { - return !f.HideDefault -} - func newFlagSet(name string, flags []Flag) (*flag.FlagSet, error) { set := flag.NewFlagSet(name, flag.ContinueOnError) @@ -307,7 +302,6 @@ func stringifyFlag(f Flag) string { if !ok { return "" } - placeholder, usage := unquoteUsage(df.GetUsage()) needsPlaceholder := df.TakesValue() diff --git a/flag_generic.go b/flag_generic.go new file mode 100644 index 0000000000..9618409ee8 --- /dev/null +++ b/flag_generic.go @@ -0,0 +1,67 @@ +package cli + +type GenericFlag = FlagBase[Value, NoConfig, genericValue] + +// -- Value Value +type genericValue struct { + val Value +} + +// Below functions are to satisfy the ValueCreator interface + +func (f genericValue) Create(val Value, p *Value, c NoConfig) Value { + *p = val + return &genericValue{ + val: *p, + } +} + +func (f genericValue) ToString(b Value) string { + if b != nil { + return b.String() + } + return "" +} + +// Below functions are to satisfy the flag.Value interface + +func (f *genericValue) Set(s string) error { + if f.val != nil { + return f.val.Set(s) + } + return nil +} + +func (f *genericValue) Get() any { + if f.val != nil { + return f.val.Get() + } + return nil +} + +func (f *genericValue) String() string { + if f.val != nil { + return f.val.String() + } + return "" +} + +func (f *genericValue) IsBoolFlag() bool { + if f.val == nil { + return false + } + bf, ok := f.val.(boolFlag) + return ok && bf.IsBoolFlag() +} + +// Generic looks up the value of a local GenericFlag, returns +// nil if not found +func (cmd *Command) Generic(name string) Value { + if v, ok := cmd.Value(name).(Value); ok { + tracef("generic available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) + return v + } + + tracef("generic NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) + return nil +} diff --git a/flag_impl.go b/flag_impl.go index 06ca8573c4..3b3780d269 100644 --- a/flag_impl.go +++ b/flag_impl.go @@ -174,6 +174,11 @@ func (f *FlagBase[T, C, V]) Apply(set *flag.FlagSet) error { return nil } +// IsDefaultVisible returns true if the flag is not hidden, otherwise false +func (f *FlagBase[T, C, V]) IsDefaultVisible() bool { + return !f.HideDefault +} + // String returns a readable representation of this value (for usage defaults) func (f *FlagBase[T, C, V]) String() string { return FlagStringer(f) @@ -221,7 +226,7 @@ func (f *FlagBase[T, C, V]) GetEnvVars() []string { // TakesValue returns true if the flag takes a value, otherwise false func (f *FlagBase[T, C, V]) TakesValue() bool { var t T - return reflect.TypeOf(t).Kind() != reflect.Bool + return reflect.TypeOf(t) == nil || reflect.TypeOf(t).Kind() != reflect.Bool } // GetDefaultText returns the default text for this flag @@ -246,6 +251,9 @@ func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error { // values from cmd line. This is true for slice and map type flags func (f *FlagBase[T, C, VC]) IsMultiValueFlag() bool { // TBD how to specify + if reflect.TypeOf(f.Value) == nil { + return false + } kind := reflect.TypeOf(f.Value).Kind() return kind == reflect.Slice || kind == reflect.Map } diff --git a/flag_test.go b/flag_test.go index bffac56913..801c36a406 100644 --- a/flag_test.go +++ b/flag_test.go @@ -238,7 +238,12 @@ func TestFlagsFromEnv(t *testing.T) { errContains: `could not parse "foobar" as []float64 value from environment ` + `variable "SECONDS" for flag seconds:`, }, - + { + name: "Generic", + input: "foo,bar", + output: &Parser{"foo", "bar"}, + fl: &GenericFlag{Name: "names", Value: &Parser{}, Sources: EnvVars("NAMES")}, + }, { name: "IntSliceFlag valid", input: "1,2", @@ -461,6 +466,16 @@ func TestFlagStringifying(t *testing.T) { fl: &FloatSliceFlag{Name: "pepperonis", DefaultText: "shaved"}, expected: "--pepperonis value [ --pepperonis value ]\t(default: shaved)", }, + { + name: "generic-flag", + fl: &GenericFlag{Name: "yogurt"}, + expected: "--yogurt value\t", + }, + { + name: "generic-flag-with-default-text", + fl: &GenericFlag{Name: "ricotta", DefaultText: "plops"}, + expected: "--ricotta value\t(default: plops)", + }, { name: "int-flag", fl: &IntFlag{Name: "grubs"}, @@ -1558,6 +1573,85 @@ func TestFloat64SliceFlagApply_ParentCommand(t *testing.T) { }).Run(buildTestContext(t), []string{"run", "child"}) } +var genericFlagTests = []struct { + name string + value Value + expected string +}{ + {"toads", &Parser{"abc", "def"}, "--toads value\ttest flag (default: abc,def)"}, + {"t", &Parser{"abc", "def"}, "-t value\ttest flag (default: abc,def)"}, +} + +func TestGenericFlagHelpOutput(t *testing.T) { + for _, test := range genericFlagTests { + fl := &GenericFlag{Name: test.name, Value: test.value, Usage: "test flag"} + // create a temporary flag set to apply + tfs := flag.NewFlagSet("test", 0) + assert.NoError(t, fl.Apply(tfs)) + assert.Equal(t, test.expected, fl.String()) + } +} + +func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("APP_ZAP", "3") + + for _, test := range genericFlagTests { + fl := &GenericFlag{Name: test.name, Sources: EnvVars("APP_ZAP")} + output := fl.String() + + expectedSuffix := withEnvHint([]string{"APP_ZAP"}, "") + if !strings.HasSuffix(output, expectedSuffix) { + t.Errorf("%s does not end with"+expectedSuffix, output) + } + } +} + +func TestGenericFlagApply_SetsAllNames(t *testing.T) { + fl := GenericFlag{Name: "orbs", Aliases: []string{"O", "obrs"}, Value: &Parser{}} + set := flag.NewFlagSet("test", 0) + assert.NoError(t, fl.Apply(set)) + assert.NoError(t, set.Parse([]string{"--orbs", "eleventy,3", "-O", "4,bloop", "--obrs", "19,s"})) +} + +func TestGenericFlagValueFromCommand(t *testing.T) { + cmd := &Command{ + Name: "foo", + Flags: []Flag{ + &GenericFlag{Name: "myflag", Value: &Parser{}}, + }, + } + + assert.NoError(t, cmd.Run(buildTestContext(t), []string{"foo", "--myflag", "abc,def"})) + assert.Equal(t, &Parser{"abc", "def"}, cmd.Generic("myflag")) + assert.Nil(t, cmd.Generic("someother")) +} + +func TestParseGenericFromEnv(t *testing.T) { + t.Setenv("APP_SERVE", "20,30") + cmd := &Command{ + Flags: []Flag{ + &GenericFlag{ + Name: "serve", + Aliases: []string{"s"}, + Value: &Parser{}, + Sources: EnvVars("APP_SERVE"), + }, + }, + Action: func(ctx context.Context, cmd *Command) error { + if !reflect.DeepEqual(cmd.Generic("serve"), &Parser{"20", "30"}) { + t.Errorf("main name not set from env") + } + if !reflect.DeepEqual(cmd.Generic("s"), &Parser{"20", "30"}) { + t.Errorf("short name not set from env") + } + return nil + }, + } + assert.NoError(t, cmd.Run(buildTestContext(t), []string{"run"})) +} + func TestParseMultiString(t *testing.T) { _ = (&Command{ Flags: []Flag{ @@ -2756,6 +2850,16 @@ func TestFlagDefaultValueWithEnv(t *testing.T) { "ssflag": "some-other-env_value=", }, }, + // TODO + /*{ + name: "generic", + flag: &GenericFlag{Name: "flag", Value: &Parser{"11", "12"}, Sources: EnvVars("gflag")}, + toParse: []string{"--flag", "15,16"}, + expect: `--flag value (default: 11,12)` + withEnvHint([]string{"gflag"}, ""), + environ: map[string]string{ + "gflag": "13,14", + }, + },*/ } for _, v := range cases { for key, val := range v.environ { @@ -3133,3 +3237,59 @@ func TestDocGetValue(t *testing.T) { assert.Equal(t, "", (&BoolFlag{Name: "foo", Value: false}).GetValue()) assert.Equal(t, "bar", (&StringFlag{Name: "foo", Value: "bar"}).GetValue()) } + +func TestGenericFlag_SatisfiesFlagInterface(t *testing.T) { + var f Flag = &GenericFlag{} + + _ = f.IsSet() + _ = f.Names() +} + +func TestGenericValue_SatisfiesBoolInterface(t *testing.T) { + var f boolFlag = &genericValue{} + + assert.False(t, f.IsBoolFlag()) + + fv := floatValue(0) + f = &genericValue{ + val: &fv, + } + + assert.False(t, f.IsBoolFlag()) + + f = &genericValue{ + val: &boolValue{}, + } + assert.True(t, f.IsBoolFlag()) +} + +func TestGenericFlag_SatisfiesFmtStringerInterface(t *testing.T) { + var f fmt.Stringer = &GenericFlag{} + + _ = f.String() +} + +func TestGenericFlag_SatisfiesRequiredFlagInterface(t *testing.T) { + var f RequiredFlag = &GenericFlag{} + + _ = f.IsRequired() +} + +func TestGenericFlag_SatisfiesVisibleFlagInterface(t *testing.T) { + var f VisibleFlag = &GenericFlag{} + + _ = f.IsVisible() +} + +func TestGenericFlag_SatisfiesDocFlagInterface(t *testing.T) { + var f DocGenerationFlag = &GenericFlag{} + + _ = f.GetUsage() +} + +func TestGenericValue(t *testing.T) { + g := &genericValue{} + assert.NoError(t, g.Set("something")) + assert.Nil(t, g.Get()) + assert.Empty(t, g.String()) +} diff --git a/godoc-current.txt b/godoc-current.txt index a49558e1c3..079b09b459 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -443,6 +443,9 @@ func (cmd *Command) FullName() string FullName returns the full name of the command. For commands with parents this ensures that the parent commands are part of the command path. +func (cmd *Command) Generic(name string) Value + Generic looks up the value of a local GenericFlag, returns nil if not found + func (cmd *Command) HasName(name string) bool HasName returns true if Command.Name matches given name @@ -793,6 +796,8 @@ type FloatSlice = SliceBase[float64, NoConfig, floatValue] type FloatSliceFlag = FlagBase[[]float64, NoConfig, FloatSlice] +type GenericFlag = FlagBase[Value, NoConfig, genericValue] + type IntArg = ArgumentBase[int64, IntegerConfig, intValue] type IntFlag = FlagBase[int64, IntegerConfig, intValue] diff --git a/testdata/godoc-v3.x.txt b/testdata/godoc-v3.x.txt index a49558e1c3..079b09b459 100644 --- a/testdata/godoc-v3.x.txt +++ b/testdata/godoc-v3.x.txt @@ -443,6 +443,9 @@ func (cmd *Command) FullName() string FullName returns the full name of the command. For commands with parents this ensures that the parent commands are part of the command path. +func (cmd *Command) Generic(name string) Value + Generic looks up the value of a local GenericFlag, returns nil if not found + func (cmd *Command) HasName(name string) bool HasName returns true if Command.Name matches given name @@ -793,6 +796,8 @@ type FloatSlice = SliceBase[float64, NoConfig, floatValue] type FloatSliceFlag = FlagBase[[]float64, NoConfig, FloatSlice] +type GenericFlag = FlagBase[Value, NoConfig, genericValue] + type IntArg = ArgumentBase[int64, IntegerConfig, intValue] type IntFlag = FlagBase[int64, IntegerConfig, intValue]