Skip to content

Commit 5c8675f

Browse files
Breaking: Move FlagCount and Flaggable to a public flag package (#177)
1 parent 0b31428 commit 5c8675f

10 files changed

Lines changed: 123 additions & 128 deletions

File tree

command_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"testing"
1212

1313
"go.followtheprocess.codes/cli"
14+
"go.followtheprocess.codes/cli/flag"
1415
"go.followtheprocess.codes/snapshot"
1516
"go.followtheprocess.codes/test"
1617
)
@@ -538,7 +539,7 @@ func TestHelp(t *testing.T) {
538539
cli.OverrideArgs([]string{"--help"}),
539540
cli.RequiredArg("src", "The file to copy"), // This one is required
540541
cli.OptionalArg("dest", "Destination to copy to", "./dest"), // This one is optional
541-
cli.Flag(new(cli.FlagCount), "verbosity", 'v', 0, "Increase the verbosity level"),
542+
cli.Flag(new(flag.Count), "verbosity", 'v', 0, "Increase the verbosity level"),
542543
cli.Run(func(cmd *cli.Command, args []string) error { return nil }),
543544
},
544545
wantErr: false,
@@ -599,9 +600,9 @@ func TestHelp(t *testing.T) {
599600
cli.Long("A longer, probably multiline description"),
600601
cli.SubCommands(sub1, sub2),
601602
cli.Flag(new(bool), "delete", 'd', false, "Delete something"),
602-
cli.Flag(new(int), "count", cli.NoShortHand, -1, "Count something"),
603-
cli.Flag(new([]string), "things", cli.NoShortHand, nil, "Names of things"),
604-
cli.Flag(new([]string), "more", cli.NoShortHand, []string{"one", "two"}, "Names of things with a default"),
603+
cli.Flag(new(int), "count", flag.NoShortHand, -1, "Count something"),
604+
cli.Flag(new([]string), "things", flag.NoShortHand, nil, "Names of things"),
605+
cli.Flag(new([]string), "more", flag.NoShortHand, []string{"one", "two"}, "Names of things with a default"),
605606
},
606607
wantErr: false,
607608
},

examples/subcommands/cli.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
"go.followtheprocess.codes/cli"
9+
"go.followtheprocess.codes/cli/flag"
910
)
1011

1112
func BuildCLI() (*cli.Command, error) {
@@ -48,7 +49,7 @@ func buildSayCommand() (*cli.Command, error) {
4849
type doOptions struct {
4950
count int
5051
fast bool
51-
verbosity cli.FlagCount
52+
verbosity flag.Count
5253
duration time.Duration
5354
}
5455

examples/subcommands/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ func run() error {
2222
if err := cmd.Execute(); err != nil {
2323
return fmt.Errorf("could not execute root command: %w", err)
2424
}
25+
2526
return nil
2627
}

flag/flag.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Package flag provides mechanisms for defining and configuring command line flags.
2+
package flag
3+
4+
import (
5+
"net"
6+
"time"
7+
)
8+
9+
// NoShortHand should be passed as the "short" argument to [New] if the desired flag
10+
// should be the long hand version only e.g. --count, not -c/--count.
11+
const NoShortHand = rune(-1)
12+
13+
// Count is a type used for a flag who's job is to increment a counter, e.g. a "verbosity"
14+
// flag may be used like so "-vvv" which should increase the verbosity level to 3.
15+
//
16+
// Count flags may be used in the following ways:
17+
// - -vvv
18+
// - --verbose --verbose --verbose (although not sure why you'd do this)
19+
// - --verbose=3
20+
//
21+
// All have the same effect of increasing the verbosity level to 3.
22+
//
23+
// --verbose 3 however is not supported, this is due to an internal parsing
24+
// implementation detail.
25+
type Count uint
26+
27+
// Flaggable is a type constraint that defines any type capable of being parsed as a command line flag.
28+
type Flaggable interface {
29+
int |
30+
int8 |
31+
int16 |
32+
int32 |
33+
int64 |
34+
uint |
35+
uint8 |
36+
uint16 |
37+
uint32 |
38+
uint64 |
39+
uintptr |
40+
float32 |
41+
float64 |
42+
string |
43+
bool |
44+
[]byte |
45+
Count |
46+
time.Time |
47+
time.Duration |
48+
net.IP |
49+
[]int |
50+
[]int8 |
51+
[]int16 |
52+
[]int32 |
53+
[]int64 |
54+
[]uint |
55+
[]uint16 |
56+
[]uint32 |
57+
[]uint64 |
58+
[]float32 |
59+
[]float64 |
60+
[]string
61+
}

internal/flag/flag.go

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"unicode"
1515
"unicode/utf8"
1616
"unsafe"
17+
18+
"go.followtheprocess.codes/cli/flag"
1719
)
1820

1921
const (
@@ -63,18 +65,10 @@ const (
6365
boolTrue = "true"
6466
)
6567

66-
// NoShortHand should be passed as the "short" argument to [New] if the desired flag
67-
// should be the long hand version only e.g. --count, not -c/--count.
68-
const NoShortHand = rune(-1)
69-
7068
var _ Value = Flag[string]{} // This will fail if we violate our Value interface
7169

72-
// Count is a type used for a flag who's job is to increment a counter, e.g. a "verbosity"
73-
// flag may be passed "-vvv" which should increase the verbosity level to 3.
74-
type Count uint
75-
7670
// Flag represents a single command line flag.
77-
type Flag[T Flaggable] struct {
71+
type Flag[T flag.Flaggable] struct {
7872
value *T // The actual stored value
7973
name string // The name of the flag as appears on the command line, e.g. "force" for a --force flag
8074
usage string // One line description of the flag, e.g. "Force deletion without confirmation"
@@ -90,7 +84,7 @@ type Flag[T Flaggable] struct {
9084
//
9185
// var force bool
9286
// flag.New(&force, "force", 'f', false, "Force deletion without confirmation")
93-
func New[T Flaggable](p *T, name string, short rune, value T, usage string) (Flag[T], error) {
87+
func New[T flag.Flaggable](p *T, name string, short rune, value T, usage string) (Flag[T], error) {
9488
if err := validateFlagName(name); err != nil {
9589
return Flag[T]{}, fmt.Errorf("invalid flag name %q: %w", name, err)
9690
}
@@ -173,7 +167,7 @@ func (f Flag[T]) String() string { //nolint:cyclop // No other way of doing this
173167
return formatInt(typ)
174168
case int64:
175169
return formatInt(typ)
176-
case Count:
170+
case flag.Count:
177171
return formatUint(typ)
178172
case uint:
179173
return formatUint(typ)
@@ -249,7 +243,7 @@ func (f Flag[T]) Type() string { //nolint:cyclop // No other way of doing this r
249243
return typeInt32
250244
case int64:
251245
return typeInt64
252-
case Count:
246+
case flag.Count:
253247
return typeCount
254248
case uint:
255249
return typeUint
@@ -360,11 +354,11 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa
360354
*f.value = *cast[T](&val)
361355

362356
return nil
363-
case Count:
357+
case flag.Count:
364358
// We have to do a bit of custom stuff here as an increment is a read and write op
365359
// First read the current value of the flag and cast it to a Count so we
366360
// can increment it
367-
current, ok := any(*f.value).(Count)
361+
current, ok := any(*f.value).(flag.Count)
368362
if !ok {
369363
// This basically shouldn't ever happen but it's easy enough to handle nicely
370364
return errBadType(*f.value)
@@ -378,7 +372,7 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa
378372
return errParse(f.name, str, typ, err)
379373
}
380374

381-
newValue := current + Count(val)
375+
newValue := current + flag.Count(val)
382376
*f.value = *cast[T](&newValue)
383377

384378
return nil
@@ -698,7 +692,7 @@ type signed interface {
698692
// unsigned is the same as constraints.Unsigned (with Count mixed in) but we don't have to depend
699693
// on golang/x/exp.
700694
type unsigned interface {
701-
uint | uint8 | uint16 | uint32 | uint64 | uintptr | Count
695+
uint | uint8 | uint16 | uint32 | uint64 | uintptr | flag.Count
702696
}
703697

704698
// cast converts a *T1 to a *T2, we use it here when we know (via generics and compile time checks)
@@ -774,7 +768,7 @@ func validateFlagName(name string) error {
774768
// it enforces only a single character, so all we have to do is make sure it's a valid ASCII character.
775769
func validateFlagShort(short rune) error {
776770
// If it's the marker for long hand only, this is fine
777-
if short == NoShortHand {
771+
if short == flag.NoShortHand {
778772
return nil
779773
}
780774

@@ -791,7 +785,7 @@ func validateFlagShort(short rune) error {
791785

792786
// errParse is a helper to quickly return a consistent error in the face of flag
793787
// value parsing errors.
794-
func errParse[T Flaggable](name, str string, typ T, err error) error {
788+
func errParse[T flag.Flaggable](name, str string, typ T, err error) error {
795789
return fmt.Errorf(
796790
"flag %q received invalid value %q (expected %T), detail: %w",
797791
name,
@@ -803,7 +797,7 @@ func errParse[T Flaggable](name, str string, typ T, err error) error {
803797

804798
// errParseSlice is like errParse but for []T flags where the error message
805799
// needs to be a bit more specific.
806-
func errParseSlice[T Flaggable](name, str string, typ T, err error) error {
800+
func errParseSlice[T flag.Flaggable](name, str string, typ T, err error) error {
807801
return fmt.Errorf(
808802
"flag %q (type %T) cannot append element %q: %w",
809803
name,
@@ -815,7 +809,7 @@ func errParseSlice[T Flaggable](name, str string, typ T, err error) error {
815809

816810
// errBadType makes a consistent error in the face of a bad type
817811
// assertion.
818-
func errBadType[T Flaggable](value T) error {
812+
func errBadType[T flag.Flaggable](value T) error {
819813
return fmt.Errorf("bad value %v, could not cast to %T", value, value)
820814
}
821815

@@ -911,15 +905,15 @@ func formatStringSlice(slice []string) string {
911905
// zero value being nil. The primary use of isZeroIsh is to determine whether or not
912906
// a default value is worth displaying to the user in the help text, and an empty slice
913907
// is probably not.
914-
func isZeroIsh[T Flaggable](value T) bool { //nolint:cyclop // Not much else we can do here
908+
func isZeroIsh[T flag.Flaggable](value T) bool { //nolint:cyclop // Not much else we can do here
915909
// Note: all the slice values ([]T) are in their own separate branches because if you
916910
// combine them, the resulting value in the body of the case block is 'any' and
917911
// you cannot do len(any)
918912
switch typ := any(value).(type) {
919913
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64:
920914
return typ == 0
921-
case Count:
922-
return typ == Count(0)
915+
case flag.Count:
916+
return typ == flag.Count(0)
923917
case string:
924918
return typ == ""
925919
case bool:

internal/flag/flag_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88
"time"
99

10+
publicflag "go.followtheprocess.codes/cli/flag"
1011
"go.followtheprocess.codes/cli/internal/flag"
1112
"go.followtheprocess.codes/test"
1213
)
@@ -156,35 +157,35 @@ func TestFlaggableTypes(t *testing.T) {
156157
})
157158

158159
t.Run("count valid", func(t *testing.T) {
159-
var c flag.Count
160+
var c publicflag.Count
160161

161162
countFlag, err := flag.New(&c, "count", 'c', 0, "Count something")
162163
test.Ok(t, err)
163164

164165
err = countFlag.Set("1")
165166
test.Ok(t, err)
166-
test.Equal(t, c, flag.Count(1))
167+
test.Equal(t, c, publicflag.Count(1))
167168
test.Equal(t, countFlag.Type(), "count")
168169
test.Equal(t, countFlag.String(), "1")
169170

170171
// Setting it again should increment to 2
171172
err = countFlag.Set("1")
172173
test.Ok(t, err)
173-
test.Equal(t, c, flag.Count(2))
174+
test.Equal(t, c, publicflag.Count(2))
174175
test.Equal(t, countFlag.Type(), "count")
175176
test.Equal(t, countFlag.String(), "2")
176177

177178
// Should also be able to set an explicit number e.g. --verbosity=3
178179
// so should now be 5
179180
err = countFlag.Set("3")
180181
test.Ok(t, err)
181-
test.Equal(t, c, flag.Count(5))
182+
test.Equal(t, c, publicflag.Count(5))
182183
test.Equal(t, countFlag.Type(), "count")
183184
test.Equal(t, countFlag.String(), "5")
184185
})
185186

186187
t.Run("count invalid", func(t *testing.T) {
187-
var c flag.Count
188+
var c publicflag.Count
188189

189190
countFlag, err := flag.New(&c, "count", 'c', 0, "Count something")
190191
test.Ok(t, err)
@@ -1038,7 +1039,7 @@ func TestFlagValidation(t *testing.T) {
10381039
{
10391040
name: "no shorthand",
10401041
flagName: "delete",
1041-
short: flag.NoShortHand,
1042+
short: publicflag.NoShortHand,
10421043
wantErr: false,
10431044
},
10441045
{

internal/flag/flaggable.go

Lines changed: 0 additions & 42 deletions
This file was deleted.

0 commit comments

Comments
 (0)