Skip to content

Commit 0da4d16

Browse files
authored
Merge pull request #15 from choria-io/14
(#14) Expand duration handling
2 parents 63f61ba + d53154c commit 0da4d16

File tree

7 files changed

+148
-13
lines changed

7 files changed

+148
-13
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,17 @@ Some historical points in time are kept:
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))
3030
* Unnegatable booleans using a new `UnNegatableBool()` flag type, backwards compatibility kept
31+
* Extended parsing for durations that include weeks (`w`, `W`), months (`M`), years (`y`, `Y`) and days (`d`, `D`) units
3132

32-
## UnNegatableBool
33+
### UnNegatableBool
3334

3435
Fisk will add to all `Bool()` kind flags a negated version, in other words `--force` will also get `--no-force` added
3536
and the usage will show these negatable booleans.
3637

3738
Often though one does not want to have the negatable version of a boolean added, with fisk you can achieve this using
3839
our `UnNegatableBool()` which would just be the basic boolean flag with no negatable version.
3940

40-
## Cheats
41+
### Cheats
4142

4243
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.
4344

app.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,13 @@ func New(name, help string) *Application {
7777
a.argGroup = newArgGroup()
7878
a.cmdGroup = newCmdGroup(a)
7979
a.HelpFlag = a.Flag("help", "Show context-sensitive help")
80-
a.HelpFlag.Bool()
80+
a.HelpFlag.UnNegatableBool()
8181

82-
a.Flag("help-long", "Generate long help.").Hidden().PreAction(a.generateLongHelp).Bool()
83-
a.Flag("help-man", "Generate a man page.").Hidden().PreAction(a.generateManPage).Bool()
84-
a.Flag("completion-bash", "Output possible completions for the given args.").Hidden().BoolVar(&a.completion)
85-
a.Flag("completion-script-bash", "Generate completion script for bash.").Hidden().PreAction(a.generateBashCompletionScript).Bool()
86-
a.Flag("completion-script-zsh", "Generate completion script for ZSH.").Hidden().PreAction(a.generateZSHCompletionScript).Bool()
82+
a.Flag("help-long", "Generate long help.").Hidden().PreAction(a.generateLongHelp).UnNegatableBool()
83+
a.Flag("help-man", "Generate a man page.").Hidden().PreAction(a.generateManPage).UnNegatableBool()
84+
a.Flag("completion-bash", "Output possible completions for the given args.").Hidden().UnNegatableBoolVar(&a.completion)
85+
a.Flag("completion-script-bash", "Generate completion script for bash.").Hidden().PreAction(a.generateBashCompletionScript).UnNegatableBool()
86+
a.Flag("completion-script-zsh", "Generate completion script for ZSH.").Hidden().PreAction(a.generateZSHCompletionScript).UnNegatableBool()
8787

8888
return a
8989
}
@@ -402,7 +402,7 @@ and by saving the output using --save these cheats become accessible within that
402402
403403
See https://github.com/cheat/cheat for more details`)
404404
a.CheatCommand.Arg("label", "The cheat to show").StringVar(&cheat)
405-
a.CheatCommand.Flag("list", "List available cheats").BoolVar(&list)
405+
a.CheatCommand.Flag("list", "List available cheats").UnNegatableBoolVar(&list)
406406
a.CheatCommand.Flag("save", "Saves the cheats to the given directory").PlaceHolder("DIRECTORY").StringVar(&dir)
407407

408408
return a
@@ -445,7 +445,7 @@ func (a *Application) Version(version string) *Application {
445445
a.terminate(0)
446446
return nil
447447
})
448-
a.VersionFlag.Bool()
448+
a.VersionFlag.UnNegatableBool()
449449
return a
450450
}
451451

duration_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package fisk
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestDurationParser(t *testing.T) {
12+
cases := []struct {
13+
s string
14+
d time.Duration
15+
err error
16+
}{
17+
{"-1m1d", (time.Minute + (24 * time.Hour)) * -1, nil},
18+
{"1m1.1w", (184 * time.Hour) + time.Minute, nil},
19+
{"1M", 24 * 30 * time.Hour, nil},
20+
{"1Y1M", (365 * 24 * time.Hour) + (24 * 30 * time.Hour), nil},
21+
{"1xX", 0, fmt.Errorf("%w: invalid unit xX", errInvalidDuration)},
22+
}
23+
24+
for _, c := range cases {
25+
d, err := ParseDuration(c.s)
26+
if c.err == nil {
27+
assert.NoError(t, err)
28+
} else {
29+
assert.Error(t, err, c.err)
30+
}
31+
assert.Equal(t, c.d, d, c.s)
32+
}
33+
}

durations.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package fisk
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strconv"
7+
"time"
8+
)
9+
10+
var (
11+
durationMatcher = regexp.MustCompile(`([-+]?)(([\d\.]+)([a-zA-Z]+))`)
12+
errInvalidDuration = fmt.Errorf("invalid duration")
13+
)
14+
15+
// ParseDuration parse durations with additional units over those from
16+
// standard go parser.
17+
//
18+
// In addition to normal go parser time units it also supports
19+
// these.
20+
//
21+
// The reason these are not in go standard lib is due to precision around
22+
// how many days in a month and about leap years and leap seconds. This
23+
// function does nothing to try and correct for those.
24+
//
25+
// * "w", "W" - a week based on 7 days of exactly 24 hours
26+
// * "d", "D" - a day based on 24 hours
27+
// * "M" - a month made of 30 days of 24 hours
28+
// * "y", "Y" - a year made of 365 days of 24 hours each
29+
//
30+
// Valid duration strings can be -1y1d1µs
31+
func ParseDuration(d string) (time.Duration, error) {
32+
var (
33+
r time.Duration
34+
neg = 1
35+
)
36+
37+
if len(d) == 0 {
38+
return r, errInvalidDuration
39+
}
40+
41+
parts := durationMatcher.FindAllStringSubmatch(d, -1)
42+
if len(parts) == 0 {
43+
return r, errInvalidDuration
44+
}
45+
46+
for i, p := range parts {
47+
if len(p) != 5 {
48+
return 0, errInvalidDuration
49+
}
50+
51+
if i == 0 && p[1] == "-" {
52+
neg = -1
53+
}
54+
55+
switch p[4] {
56+
case "w", "W":
57+
val, err := strconv.ParseFloat(p[3], 32)
58+
if err != nil {
59+
return 0, fmt.Errorf("%w: %v", errInvalidDuration, err)
60+
}
61+
62+
r += time.Duration(val*7*24) * time.Hour
63+
64+
case "d", "D":
65+
val, err := strconv.ParseFloat(p[3], 32)
66+
if err != nil {
67+
return 0, fmt.Errorf("%w: %v", errInvalidDuration, err)
68+
}
69+
70+
r += time.Duration(val*24) * time.Hour
71+
72+
case "M":
73+
val, err := strconv.ParseFloat(p[3], 32)
74+
if err != nil {
75+
return 0, fmt.Errorf("%w: %v", errInvalidDuration, err)
76+
}
77+
78+
r += time.Duration(val*24*30) * time.Hour
79+
80+
case "Y", "y":
81+
val, err := strconv.ParseFloat(p[3], 32)
82+
if err != nil {
83+
return 0, fmt.Errorf("%w: %v", errInvalidDuration, err)
84+
}
85+
86+
r += time.Duration(val*24*365) * time.Hour
87+
88+
case "ns", "us", "µs", "ms", "s", "m", "h":
89+
dur, err := time.ParseDuration(p[2])
90+
if err != nil {
91+
return 0, fmt.Errorf("%w: %v", errInvalidDuration, err)
92+
}
93+
94+
r += dur
95+
default:
96+
return 0, fmt.Errorf("%w: invalid unit %v", errInvalidDuration, p[4])
97+
}
98+
}
99+
100+
return time.Duration(neg) * r, nil
101+
}

model.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func (f *FlagGroupModel) FlagSummary() string {
3535

3636
if flag.Required {
3737
if flag.IsBoolFlag() {
38-
if flag.IsNegatable() && flag.Name != "help" && flag.Name != "version" {
38+
if flag.IsNegatable() {
3939
out = append(out, fmt.Sprintf("--[no-]%s", flag.Name))
4040
} else {
4141
out = append(out, fmt.Sprintf("--%s=%s", flag.Name, flag.FormatPlaceHolder()))

usage.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func formatFlag(haveShort bool, flag *FlagModel) string {
7878
flagString := ""
7979
flagName := flag.Name
8080

81-
if flag.IsNegatable() && flag.Name != "help" && flag.Name != "version" {
81+
if flag.IsNegatable() {
8282
flagName = "[no-]" + flagName
8383
}
8484

values.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ func newDurationValue(p *time.Duration) *durationValue {
156156
}
157157

158158
func (d *durationValue) Set(s string) error {
159-
v, err := time.ParseDuration(s)
159+
v, err := ParseDuration(s)
160160
*d = durationValue(v)
161161
return err
162162
}

0 commit comments

Comments
 (0)