Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"testing"

"go.followtheprocess.codes/cli"
"go.followtheprocess.codes/cli/flag"
"go.followtheprocess.codes/snapshot"
"go.followtheprocess.codes/test"
)
Expand Down Expand Up @@ -538,7 +539,7 @@ func TestHelp(t *testing.T) {
cli.OverrideArgs([]string{"--help"}),
cli.RequiredArg("src", "The file to copy"), // This one is required
cli.OptionalArg("dest", "Destination to copy to", "./dest"), // This one is optional
cli.Flag(new(cli.FlagCount), "verbosity", 'v', 0, "Increase the verbosity level"),
cli.Flag(new(flag.Count), "verbosity", 'v', 0, "Increase the verbosity level"),
cli.Run(func(cmd *cli.Command, args []string) error { return nil }),
},
wantErr: false,
Expand Down Expand Up @@ -599,9 +600,9 @@ func TestHelp(t *testing.T) {
cli.Long("A longer, probably multiline description"),
cli.SubCommands(sub1, sub2),
cli.Flag(new(bool), "delete", 'd', false, "Delete something"),
cli.Flag(new(int), "count", cli.NoShortHand, -1, "Count something"),
cli.Flag(new([]string), "things", cli.NoShortHand, nil, "Names of things"),
cli.Flag(new([]string), "more", cli.NoShortHand, []string{"one", "two"}, "Names of things with a default"),
cli.Flag(new(int), "count", flag.NoShortHand, -1, "Count something"),
cli.Flag(new([]string), "things", flag.NoShortHand, nil, "Names of things"),
cli.Flag(new([]string), "more", flag.NoShortHand, []string{"one", "two"}, "Names of things with a default"),
},
wantErr: false,
},
Expand Down
3 changes: 2 additions & 1 deletion examples/subcommands/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"go.followtheprocess.codes/cli"
"go.followtheprocess.codes/cli/flag"
)

func BuildCLI() (*cli.Command, error) {
Expand Down Expand Up @@ -48,7 +49,7 @@ func buildSayCommand() (*cli.Command, error) {
type doOptions struct {
count int
fast bool
verbosity cli.FlagCount
verbosity flag.Count
duration time.Duration
}

Expand Down
1 change: 1 addition & 0 deletions examples/subcommands/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ func run() error {
if err := cmd.Execute(); err != nil {
return fmt.Errorf("could not execute root command: %w", err)
}

return nil
}
61 changes: 61 additions & 0 deletions flag/flag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Package flag provides mechanisms for defining and configuring command line flags.
package flag

import (
"net"
"time"
)

// NoShortHand should be passed as the "short" argument to [New] if the desired flag
// should be the long hand version only e.g. --count, not -c/--count.
const NoShortHand = rune(-1)

// Count is a type used for a flag who's job is to increment a counter, e.g. a "verbosity"
// flag may be used like so "-vvv" which should increase the verbosity level to 3.
//
// Count flags may be used in the following ways:
// - -vvv
// - --verbose --verbose --verbose (although not sure why you'd do this)
// - --verbose=3
//
// All have the same effect of increasing the verbosity level to 3.
//
// --verbose 3 however is not supported, this is due to an internal parsing
// implementation detail.
type Count uint

// Flaggable is a type constraint that defines any type capable of being parsed as a command line flag.
type Flaggable interface {
int |
int8 |
int16 |
int32 |
int64 |
uint |
uint8 |
uint16 |
uint32 |
uint64 |
uintptr |
float32 |
float64 |
string |
bool |
[]byte |
Count |
time.Time |
time.Duration |
net.IP |
[]int |
[]int8 |
[]int16 |
[]int32 |
[]int64 |
[]uint |
[]uint16 |
[]uint32 |
[]uint64 |
[]float32 |
[]float64 |
[]string
}
40 changes: 17 additions & 23 deletions internal/flag/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"unicode"
"unicode/utf8"
"unsafe"

"go.followtheprocess.codes/cli/flag"
)

const (
Expand Down Expand Up @@ -63,18 +65,10 @@ const (
boolTrue = "true"
)

// NoShortHand should be passed as the "short" argument to [New] if the desired flag
// should be the long hand version only e.g. --count, not -c/--count.
const NoShortHand = rune(-1)

var _ Value = Flag[string]{} // This will fail if we violate our Value interface

// Count is a type used for a flag who's job is to increment a counter, e.g. a "verbosity"
// flag may be passed "-vvv" which should increase the verbosity level to 3.
type Count uint

// Flag represents a single command line flag.
type Flag[T Flaggable] struct {
type Flag[T flag.Flaggable] struct {
value *T // The actual stored value
name string // The name of the flag as appears on the command line, e.g. "force" for a --force flag
usage string // One line description of the flag, e.g. "Force deletion without confirmation"
Expand All @@ -90,7 +84,7 @@ type Flag[T Flaggable] struct {
//
// var force bool
// flag.New(&force, "force", 'f', false, "Force deletion without confirmation")
func New[T Flaggable](p *T, name string, short rune, value T, usage string) (Flag[T], error) {
func New[T flag.Flaggable](p *T, name string, short rune, value T, usage string) (Flag[T], error) {
if err := validateFlagName(name); err != nil {
return Flag[T]{}, fmt.Errorf("invalid flag name %q: %w", name, err)
}
Expand Down Expand Up @@ -173,7 +167,7 @@ func (f Flag[T]) String() string { //nolint:cyclop // No other way of doing this
return formatInt(typ)
case int64:
return formatInt(typ)
case Count:
case flag.Count:
return formatUint(typ)
case uint:
return formatUint(typ)
Expand Down Expand Up @@ -249,7 +243,7 @@ func (f Flag[T]) Type() string { //nolint:cyclop // No other way of doing this r
return typeInt32
case int64:
return typeInt64
case Count:
case flag.Count:
return typeCount
case uint:
return typeUint
Expand Down Expand Up @@ -360,11 +354,11 @@ func (f Flag[T]) Set(str string) error { //nolint:gocognit,cyclop // No other wa
*f.value = *cast[T](&val)

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

newValue := current + Count(val)
newValue := current + flag.Count(val)
*f.value = *cast[T](&newValue)

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

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

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

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

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

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

Expand Down Expand Up @@ -911,15 +905,15 @@ func formatStringSlice(slice []string) string {
// zero value being nil. The primary use of isZeroIsh is to determine whether or not
// a default value is worth displaying to the user in the help text, and an empty slice
// is probably not.
func isZeroIsh[T Flaggable](value T) bool { //nolint:cyclop // Not much else we can do here
func isZeroIsh[T flag.Flaggable](value T) bool { //nolint:cyclop // Not much else we can do here
// Note: all the slice values ([]T) are in their own separate branches because if you
// combine them, the resulting value in the body of the case block is 'any' and
// you cannot do len(any)
switch typ := any(value).(type) {
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64:
return typ == 0
case Count:
return typ == Count(0)
case flag.Count:
return typ == flag.Count(0)
case string:
return typ == ""
case bool:
Expand Down
13 changes: 7 additions & 6 deletions internal/flag/flag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"
"time"

publicflag "go.followtheprocess.codes/cli/flag"
"go.followtheprocess.codes/cli/internal/flag"
"go.followtheprocess.codes/test"
)
Expand Down Expand Up @@ -156,35 +157,35 @@ func TestFlaggableTypes(t *testing.T) {
})

t.Run("count valid", func(t *testing.T) {
var c flag.Count
var c publicflag.Count

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

err = countFlag.Set("1")
test.Ok(t, err)
test.Equal(t, c, flag.Count(1))
test.Equal(t, c, publicflag.Count(1))
test.Equal(t, countFlag.Type(), "count")
test.Equal(t, countFlag.String(), "1")

// Setting it again should increment to 2
err = countFlag.Set("1")
test.Ok(t, err)
test.Equal(t, c, flag.Count(2))
test.Equal(t, c, publicflag.Count(2))
test.Equal(t, countFlag.Type(), "count")
test.Equal(t, countFlag.String(), "2")

// Should also be able to set an explicit number e.g. --verbosity=3
// so should now be 5
err = countFlag.Set("3")
test.Ok(t, err)
test.Equal(t, c, flag.Count(5))
test.Equal(t, c, publicflag.Count(5))
test.Equal(t, countFlag.Type(), "count")
test.Equal(t, countFlag.String(), "5")
})

t.Run("count invalid", func(t *testing.T) {
var c flag.Count
var c publicflag.Count

countFlag, err := flag.New(&c, "count", 'c', 0, "Count something")
test.Ok(t, err)
Expand Down Expand Up @@ -1038,7 +1039,7 @@ func TestFlagValidation(t *testing.T) {
{
name: "no shorthand",
flagName: "delete",
short: flag.NoShortHand,
short: publicflag.NoShortHand,
wantErr: false,
},
{
Expand Down
42 changes: 0 additions & 42 deletions internal/flag/flaggable.go

This file was deleted.

Loading
Loading