Skip to content

Commit

Permalink
Merge pull request #2005 from dearchap/add_generic_flag
Browse files Browse the repository at this point in the history
Re-add generic flag back
  • Loading branch information
dearchap authored Nov 8, 2024
2 parents 18c557e + b9f05fb commit ef45965
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 8 deletions.
6 changes: 0 additions & 6 deletions flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -307,7 +302,6 @@ func stringifyFlag(f Flag) string {
if !ok {
return ""
}

placeholder, usage := unquoteUsage(df.GetUsage())
needsPlaceholder := df.TakesValue()

Expand Down
67 changes: 67 additions & 0 deletions flag_generic.go
Original file line number Diff line number Diff line change
@@ -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
}
10 changes: 9 additions & 1 deletion flag_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down
162 changes: 161 additions & 1 deletion flag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"},
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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())
}
5 changes: 5 additions & 0 deletions godoc-current.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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]
Expand Down
5 changes: 5 additions & 0 deletions testdata/godoc-v3.x.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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]
Expand Down

0 comments on commit ef45965

Please sign in to comment.