Skip to content

Commit

Permalink
Merge pull request #1257 from dearchap/bool_count
Browse files Browse the repository at this point in the history
Add count option for bool flags
  • Loading branch information
dearchap committed Sep 6, 2022
2 parents b1b1b27 + 869f29a commit 6ccecf2
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 13 deletions.
2 changes: 1 addition & 1 deletion cmd/urfave-cli-genflags/generated.gotmpl
Expand Up @@ -23,7 +23,7 @@ type {{.TypeName}} struct {
EnvVars []string

{{range .StructFields}}
{{.Name}} {{.Type}}
{{.Name}} {{if .Pointer}}*{{end}}{{.Type}}
{{end}}
}

Expand Down
5 changes: 3 additions & 2 deletions cmd/urfave-cli-genflags/main.go
Expand Up @@ -230,8 +230,9 @@ type FlagTypeConfig struct {
}

type FlagStructField struct {
Name string
Type string
Name string
Type string
Pointer bool
}

type FlagType struct {
Expand Down
10 changes: 10 additions & 0 deletions context.go
Expand Up @@ -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 {
Expand Down
40 changes: 40 additions & 0 deletions docs/v2/examples/flags.md
Expand Up @@ -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)

<!-- {
"args": ["&#45;&#45;foo", "&#45;&#45;foo"],
"output": "count 2"
} -->
```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.
Expand Down
4 changes: 3 additions & 1 deletion flag-spec.yaml
Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions flag.go
Expand Up @@ -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)

Expand Down
69 changes: 64 additions & 5 deletions 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
Expand Down Expand Up @@ -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
Expand Down
47 changes: 47 additions & 0 deletions flag_test.go
Expand Up @@ -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...)
Expand Down
25 changes: 21 additions & 4 deletions godoc-current.txt
Expand Up @@ -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}}
Expand Down Expand Up @@ -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}}{{.}}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -450,6 +452,8 @@ type BoolFlag struct {

Aliases []string
EnvVars []string

Count *int
}
BoolFlag is a flag with type bool

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

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

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions zz_generated.flags.go
Expand Up @@ -327,6 +327,8 @@ type BoolFlag struct {

Aliases []string
EnvVars []string

Count *int
}

// String returns a readable representation of this value (for usage defaults)
Expand Down

0 comments on commit 6ccecf2

Please sign in to comment.