Skip to content

Commit 63f61ba

Browse files
authored
Merge pull request #13 from choria-io/12
(#12) Support UnNegatableBool()
2 parents 0c9abe5 + b04acc0 commit 63f61ba

File tree

9 files changed

+137
-14
lines changed

9 files changed

+137
-14
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,21 @@ Some historical points in time are kept:
2727
* Changes to make `staticcheck` happy
2828
* A new default template that shortens the help on large apps, old default preserved as `KingpinDefaultUsageTemplate`
2929
* Integration with [cheat](https://github.com/cheat/cheat) (see [below](#cheats))
30+
* Unnegatable booleans using a new `UnNegatableBool()` flag type, backwards compatibility kept
31+
32+
## UnNegatableBool
33+
34+
Fisk will add to all `Bool()` kind flags a negated version, in other words `--force` will also get `--no-force` added
35+
and the usage will show these negatable booleans.
36+
37+
Often though one does not want to have the negatable version of a boolean added, with fisk you can achieve this using
38+
our `UnNegatableBool()` which would just be the basic boolean flag with no negatable version.
3039

3140
## Cheats
3241

3342
I really like [cheat](https://github.com/cheat/cheat), a great little tool that gives access to bite-sized hints on what's great about a CLI tool.
3443

35-
Since `v0.2.0` Fisk supports cheats natively, you can get cheat formatted hints right from the app with no extra dependencies or export cheats into the `cheat` app for use via its interface and integrations.
44+
Since `v0.1.1` Fisk supports cheats natively, you can get cheat formatted hints right from the app with no extra dependencies or export cheats into the `cheat` app for use via its interface and integrations.
3645

3746
```nohighlight
3847
$ nats cheat pub

flags.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,12 @@ loop:
9494
if strings.HasPrefix(name, "no-") {
9595
name = name[3:]
9696
invert = true
97+
flag, ok = f.long[name]
98+
if ok {
99+
bf, bok := flag.value.(BoolFlag)
100+
ok = bok && bf.BoolFlagIsNegatable()
101+
}
97102
}
98-
flag, ok = f.long[name]
99103
}
100104
if !ok {
101105
return nil, fmt.Errorf("unknown long flag '%s'", flagToken)
@@ -111,8 +115,7 @@ loop:
111115

112116
flag.isSetByUser()
113117

114-
fb, ok := flag.value.(boolFlag)
115-
if ok && fb.IsBoolFlag() {
118+
if isBoolFlag(flag.value) {
116119
if invert {
117120
defaultValue = "false"
118121
} else {

flags_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package fisk
22

33
import (
4+
"bytes"
45
"io/ioutil"
56
"os"
67
"testing"
@@ -389,3 +390,45 @@ func TestIsSetByUser(t *testing.T) {
389390
assert.True(t, isSet)
390391
assert.False(t, isSet2)
391392
}
393+
394+
func TestNegatableBool(t *testing.T) {
395+
app := newTestApp()
396+
boolFlag := app.Flag("neg", "").Bool()
397+
unNegBoolFlag := app.Flag("unneg", "").UnNegatableBool()
398+
399+
_, err := app.Parse([]string{})
400+
assert.NoError(t, err)
401+
assert.False(t, *unNegBoolFlag)
402+
assert.False(t, *boolFlag)
403+
404+
_, err = app.Parse([]string{"--neg"})
405+
assert.NoError(t, err)
406+
assert.True(t, *boolFlag)
407+
assert.False(t, *unNegBoolFlag)
408+
409+
_, err = app.Parse([]string{"--no-neg"})
410+
assert.NoError(t, err)
411+
assert.False(t, *boolFlag)
412+
assert.False(t, *unNegBoolFlag)
413+
414+
_, err = app.Parse([]string{"--unneg"})
415+
assert.NoError(t, err)
416+
assert.False(t, *boolFlag)
417+
assert.True(t, *unNegBoolFlag)
418+
419+
_, err = app.Parse([]string{"--no-unneg"})
420+
assert.Error(t, err, "unknown long flag '--no-unneg'")
421+
}
422+
423+
func TestHelpForBoolFlags(t *testing.T) {
424+
app := newTestApp()
425+
app.Flag("yes", "").Default("true").Bool()
426+
app.Flag("no", "").Default("false").Bool()
427+
app.Flag("nonneg", "").UnNegatableBool()
428+
429+
w := bytes.NewBuffer(nil)
430+
app.Writer(w).Usage(nil)
431+
assert.Contains(t, w.String(), "--[no-]yes")
432+
assert.Contains(t, w.String(), "--[no-]no")
433+
assert.Contains(t, w.String(), "--nonneg")
434+
}

model.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@ func (f *FlagGroupModel) FlagSummary() string {
3434
}
3535

3636
if flag.Required {
37-
if flag.IsBoolFlag() && flag.Name != "help" && flag.Name != "version" {
38-
out = append(out, fmt.Sprintf("--[no-]%s", flag.Name))
39-
} else {
40-
out = append(out, fmt.Sprintf("--%s=%s", flag.Name, flag.FormatPlaceHolder()))
37+
if flag.IsBoolFlag() {
38+
if flag.IsNegatable() && flag.Name != "help" && flag.Name != "version" {
39+
out = append(out, fmt.Sprintf("--[no-]%s", flag.Name))
40+
} else {
41+
out = append(out, fmt.Sprintf("--%s=%s", flag.Name, flag.FormatPlaceHolder()))
42+
}
4143
}
4244
}
4345
}
@@ -67,10 +69,12 @@ func (f *FlagModel) String() string {
6769
}
6870

6971
func (f *FlagModel) IsBoolFlag() bool {
70-
if fl, ok := f.Value.(boolFlag); ok {
71-
return fl.IsBoolFlag()
72-
}
73-
return false
72+
return isBoolFlag(f.Value)
73+
}
74+
75+
func (f *FlagModel) IsNegatable() bool {
76+
bf, ok := f.Value.(BoolFlag)
77+
return ok && bf.BoolFlagIsNegatable()
7478
}
7579

7680
func (f *FlagModel) FormatPlaceHolder() string {

parser.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ func (p *ParseContext) Next() *Token {
199199
flag, ok := p.flags.short[short]
200200
// Not a known short flag, we'll just return it anyway.
201201
if !ok {
202-
} else if fb, ok := flag.value.(boolFlag); ok && fb.IsBoolFlag() {
202+
} else if isBoolFlag(flag.value) {
203203
// Bool short flag.
204204
} else {
205205
// Short flag with combined argument: -fARG

usage.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,11 @@ func formatCmdUsage(app *ApplicationModel, cmd *CmdModel) string {
7777
func formatFlag(haveShort bool, flag *FlagModel) string {
7878
flagString := ""
7979
flagName := flag.Name
80-
if flag.IsBoolFlag() && flag.Name != "help" && flag.Name != "version" {
80+
81+
if flag.IsNegatable() && flag.Name != "help" && flag.Name != "version" {
8182
flagName = "[no-]" + flagName
8283
}
84+
8385
if flag.Short != 0 {
8486
flagString += fmt.Sprintf("-%c, --%s", flag.Short, flagName)
8587
} else {

values.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,24 @@ func (a *accumulator) IsCumulative() bool {
128128
return true
129129
}
130130

131+
// BoolFlag is an optional interface to specify that a flag is a boolean flag.
132+
type BoolFlag interface {
133+
// BoolFlagIsNegatable Specify if the flag is negatable (ie. supports both --no-<name> and --name).
134+
BoolFlagIsNegatable() bool
135+
}
136+
137+
func isBoolFlag(f Value) bool {
138+
if bf, ok := f.(boolFlag); ok {
139+
return bf.IsBoolFlag()
140+
}
141+
_, ok := f.(BoolFlag)
142+
return ok
143+
}
144+
145+
func (b *boolValue) BoolFlagIsNegatable() bool { return true }
146+
147+
func (n *unNegatableBoolValue) BoolFlagIsNegatable() bool { return false }
148+
131149
func (b *boolValue) IsBoolFlag() bool { return true }
132150

133151
// -- time.Duration Value

values.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[
22
{"type": "bool", "parser": "strconv.ParseBool(s)"},
3+
{"name": "UnNegatableBool", "type": "bool", "parser": "strconv.ParseBool(s)"},
34
{"type": "string", "parser": "s, error(nil)", "format": "string(*f.v)", "plural": "Strings"},
45
{"type": "uint", "parser": "strconv.ParseUint(s, 0, 64)", "plural": "Uints"},
56
{"type": "uint8", "parser": "strconv.ParseUint(s, 0, 8)"},

values_generated.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,49 @@ func (p *parserMixin) BoolListVar(target *[]bool) {
5454
}))
5555
}
5656

57+
// -- bool Value
58+
type unNegatableBoolValue struct{ v *bool }
59+
60+
func newUnNegatableBoolValue(p *bool) *unNegatableBoolValue {
61+
return &unNegatableBoolValue{p}
62+
}
63+
64+
func (f *unNegatableBoolValue) Set(s string) error {
65+
v, err := strconv.ParseBool(s)
66+
if err == nil {
67+
*f.v = (bool)(v)
68+
}
69+
return err
70+
}
71+
72+
func (f *unNegatableBoolValue) Get() interface{} { return (bool)(*f.v) }
73+
74+
func (f *unNegatableBoolValue) String() string { return fmt.Sprintf("%v", *f.v) }
75+
76+
// UnNegatableBool parses the next command-line value as bool.
77+
func (p *parserMixin) UnNegatableBool() (target *bool) {
78+
target = new(bool)
79+
p.UnNegatableBoolVar(target)
80+
return
81+
}
82+
83+
func (p *parserMixin) UnNegatableBoolVar(target *bool) {
84+
p.SetValue(newUnNegatableBoolValue(target))
85+
}
86+
87+
// UnNegatableBoolList accumulates bool values into a slice.
88+
func (p *parserMixin) UnNegatableBoolList() (target *[]bool) {
89+
target = new([]bool)
90+
p.UnNegatableBoolListVar(target)
91+
return
92+
}
93+
94+
func (p *parserMixin) UnNegatableBoolListVar(target *[]bool) {
95+
p.SetValue(newAccumulator(target, func(v interface{}) Value {
96+
return newUnNegatableBoolValue(v.(*bool))
97+
}))
98+
}
99+
57100
// -- string Value
58101
type stringValue struct{ v *string }
59102

0 commit comments

Comments
 (0)