diff --git a/cmd/urfave-cli-genflags/generated.gotmpl b/cmd/urfave-cli-genflags/generated.gotmpl index 8b82ccfb1d..ed58b4f042 100644 --- a/cmd/urfave-cli-genflags/generated.gotmpl +++ b/cmd/urfave-cli-genflags/generated.gotmpl @@ -23,7 +23,7 @@ type {{.TypeName}} struct { EnvVars []string {{range .StructFields}} - {{.Name}} {{.Type}} + {{.Name}} {{if .Pointer}}*{{end}}{{.Type}} {{end}} } diff --git a/cmd/urfave-cli-genflags/main.go b/cmd/urfave-cli-genflags/main.go index 835216a98a..c754147bea 100644 --- a/cmd/urfave-cli-genflags/main.go +++ b/cmd/urfave-cli-genflags/main.go @@ -230,8 +230,9 @@ type FlagTypeConfig struct { } type FlagStructField struct { - Name string - Type string + Name string + Type string + Pointer bool } type FlagType struct { diff --git a/context.go b/context.go index dc0d1ef0e4..0335849a1b 100644 --- a/context.go +++ b/context.go @@ -105,6 +105,16 @@ func (cCtx *Context) Lineage() []*Context { return lineage } +// Count returns the num of occurences of this flag +func (cCtx *Context) Count(name string) int { + if fs := cCtx.lookupFlagSet(name); fs != nil { + if cf, ok := fs.Lookup(name).Value.(Countable); ok { + return cf.Count() + } + } + return 0 +} + // Value returns the value of the flag corresponding to `name` func (cCtx *Context) Value(name string) interface{} { if fs := cCtx.lookupFlagSet(name); fs != nil { diff --git a/docs/v2/examples/flags.md b/docs/v2/examples/flags.md index 2a629f782a..dbb41ea936 100644 --- a/docs/v2/examples/flags.md +++ b/docs/v2/examples/flags.md @@ -101,6 +101,46 @@ func main() { See full list of flags at https://pkg.go.dev/github.com/urfave/cli/v2 +For bool flags you can specify the flag multiple times to get a count(e.g -v -v -v or -vvv) + + +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + var count int + + app := &cli.App{ + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "foo", + Usage: "foo greeting", + Count: &count, + }, + }, + Action: func(cCtx *cli.Context) error { + fmt.Println("count", count) + return nil + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + #### Placeholder Values Sometimes it's useful to specify a flag's value within the usage string itself. diff --git a/flag-spec.yaml b/flag-spec.yaml index 1c594d7231..280f909a3b 100644 --- a/flag-spec.yaml +++ b/flag-spec.yaml @@ -3,7 +3,9 @@ # `Spec` type that maps to this file structure. flag_types: - bool: {} + bool: + struct_fields: + - { name: Count, type: int, pointer: true } float64: {} int64: struct_fields: diff --git a/flag.go b/flag.go index 37c9fdab21..050bb4b1d1 100644 --- a/flag.go +++ b/flag.go @@ -139,6 +139,12 @@ type CategorizableFlag interface { GetCategory() string } +// Countable is an interface to enable detection of flag values which support +// repetitive flags +type Countable interface { + Count() int +} + func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { set := flag.NewFlagSet(name, flag.ContinueOnError) diff --git a/flag_bool.go b/flag_bool.go index 3e19bde5b2..cb937ae65d 100644 --- a/flag_bool.go +++ b/flag_bool.go @@ -1,11 +1,63 @@ package cli import ( + "errors" "flag" "fmt" "strconv" ) +// boolValue needs to implement the boolFlag internal interface in flag +// to be able to capture bool fields and values +// +// type boolFlag interface { +// Value +// IsBoolFlag() bool +// } +type boolValue struct { + destination *bool + count *int +} + +func newBoolValue(val bool, p *bool, count *int) *boolValue { + *p = val + return &boolValue{ + destination: p, + count: count, + } +} + +func (b *boolValue) Set(s string) error { + v, err := strconv.ParseBool(s) + if err != nil { + err = errors.New("parse error") + return err + } + *b.destination = v + if b.count != nil { + *b.count = *b.count + 1 + } + return err +} + +func (b *boolValue) Get() interface{} { return *b.destination } + +func (b *boolValue) String() string { + if b.destination != nil { + return strconv.FormatBool(*b.destination) + } + return strconv.FormatBool(false) +} + +func (b *boolValue) IsBoolFlag() bool { return true } + +func (b *boolValue) Count() int { + if b.count != nil { + return *b.count + } + return 0 +} + // TakesValue returns true of the flag takes a value, otherwise false func (f *BoolFlag) TakesValue() bool { return false @@ -60,12 +112,19 @@ func (f *BoolFlag) Apply(set *flag.FlagSet) error { f.HasBeenSet = true } + count := f.Count + dest := f.Destination + + if count == nil { + count = new(int) + } + if dest == nil { + dest = new(bool) + } + for _, name := range f.Names() { - if f.Destination != nil { - set.BoolVar(f.Destination, name, f.Value, f.Usage) - continue - } - set.Bool(name, f.Value, f.Usage) + value := newBoolValue(f.Value, dest, count) + set.Var(value, name, f.Usage) } return nil diff --git a/flag_test.go b/flag_test.go index 001bc22d58..aee9b834b6 100644 --- a/flag_test.go +++ b/flag_test.go @@ -62,6 +62,53 @@ func TestBoolFlagValueFromContext(t *testing.T) { expect(t, ff.Get(ctx), false) } +func TestBoolFlagApply_SetsCount(t *testing.T) { + v := false + count := 0 + fl := BoolFlag{Name: "wat", Aliases: []string{"W", "huh"}, Destination: &v, Count: &count} + set := flag.NewFlagSet("test", 0) + err := fl.Apply(set) + expect(t, err, nil) + + err = set.Parse([]string{"--wat", "-W", "--huh"}) + expect(t, err, nil) + expect(t, v, true) + expect(t, count, 3) +} + +func TestBoolFlagCountFromContext(t *testing.T) { + + boolCountTests := []struct { + input []string + expectedVal bool + expectedCount int + }{ + { + input: []string{"-tf", "-w", "-huh"}, + expectedVal: true, + expectedCount: 3, + }, + { + input: []string{}, + expectedVal: false, + expectedCount: 0, + }, + } + + for _, bct := range boolCountTests { + set := flag.NewFlagSet("test", 0) + ctx := NewContext(nil, set, nil) + tf := &BoolFlag{Name: "tf", Aliases: []string{"w", "huh"}} + err := tf.Apply(set) + expect(t, err, nil) + + err = set.Parse(bct.input) + expect(t, err, nil) + expect(t, tf.Get(ctx), bct.expectedVal) + expect(t, ctx.Count("tf"), bct.expectedCount) + } +} + func TestFlagsFromEnv(t *testing.T) { newSetFloat64Slice := func(defaults ...float64) Float64Slice { s := NewFloat64Slice(defaults...) diff --git a/godoc-current.txt b/godoc-current.txt index f3be3c0393..12db90eea3 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -49,8 +49,8 @@ AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlagCategories}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{ $cv := offsetCommands .VisibleCommands 5}}{{range .VisibleCommands}} + {{$s := join .Names ", "}}{{$s}}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}}{{wrap .Usage $cv}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlagCategories}} GLOBAL OPTIONS:{{range .VisibleFlagCategories}} {{if .Name}}{{.Name}} @@ -157,8 +157,8 @@ DESCRIPTION: COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{ $cv := offsetCommands .VisibleCommands 5}}{{range .VisibleCommands}} + {{$s := join .Names ", "}}{{$s}}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}}{{wrap .Usage $cv}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} OPTIONS: {{range .VisibleFlags}}{{.}} @@ -300,6 +300,8 @@ type App struct { CommandNotFound CommandNotFoundFunc // Execute this function if a usage error occurs OnUsageError OnUsageErrorFunc + // Execute this function when an invalid flag is accessed from the context + InvalidFlagAccessHandler InvalidFlagAccessFunc // Compilation date Compiled time.Time // List of all authors who contributed @@ -450,6 +452,8 @@ type BoolFlag struct { Aliases []string EnvVars []string + + Count *int } BoolFlag is a flag with type bool @@ -629,6 +633,9 @@ func (cCtx *Context) Args() Args func (cCtx *Context) Bool(name string) bool Bool looks up the value of a local BoolFlag, returns false if not found +func (cCtx *Context) Count(name string) int + Count returns the num of occurences of this flag + func (cCtx *Context) Duration(name string) time.Duration Duration looks up the value of a local DurationFlag, returns 0 if not found @@ -701,6 +708,12 @@ func (cCtx *Context) Uint64(name string) uint64 func (cCtx *Context) Value(name string) interface{} Value returns the value of the flag corresponding to `name` +type Countable interface { + Count() int +} + Countable is an interface to enable detection of flag values which support + repetitive flags + type DocGenerationFlag interface { Flag @@ -1420,6 +1433,10 @@ func (f *IntSliceFlag) String() string func (f *IntSliceFlag) TakesValue() bool TakesValue returns true of the flag takes a value, otherwise false +type InvalidFlagAccessFunc func(*Context, string) + InvalidFlagAccessFunc is executed when an invalid flag is accessed from the + context. + type MultiError interface { error Errors() []error diff --git a/zz_generated.flags.go b/zz_generated.flags.go index f5b73ba245..bd2fc54a8d 100644 --- a/zz_generated.flags.go +++ b/zz_generated.flags.go @@ -327,6 +327,8 @@ type BoolFlag struct { Aliases []string EnvVars []string + + Count *int } // String returns a readable representation of this value (for usage defaults)