Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add count option for bool flags #1257

Merged
merged 17 commits into from Sep 6, 2022
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