diff --git a/command_test.go b/command_test.go index 297079f..1157dd7 100644 --- a/command_test.go +++ b/command_test.go @@ -11,6 +11,7 @@ import ( "testing" "go.followtheprocess.codes/cli" + "go.followtheprocess.codes/cli/flag" "go.followtheprocess.codes/snapshot" "go.followtheprocess.codes/test" ) @@ -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, @@ -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, }, diff --git a/examples/subcommands/cli.go b/examples/subcommands/cli.go index 475588a..fb7549c 100644 --- a/examples/subcommands/cli.go +++ b/examples/subcommands/cli.go @@ -6,6 +6,7 @@ import ( "time" "go.followtheprocess.codes/cli" + "go.followtheprocess.codes/cli/flag" ) func BuildCLI() (*cli.Command, error) { @@ -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 } diff --git a/examples/subcommands/main.go b/examples/subcommands/main.go index 79899b3..683c78c 100644 --- a/examples/subcommands/main.go +++ b/examples/subcommands/main.go @@ -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 } diff --git a/flag/flag.go b/flag/flag.go new file mode 100644 index 0000000..3454d4f --- /dev/null +++ b/flag/flag.go @@ -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 +} diff --git a/internal/flag/flag.go b/internal/flag/flag.go index 6992075..456b96a 100644 --- a/internal/flag/flag.go +++ b/internal/flag/flag.go @@ -14,6 +14,8 @@ import ( "unicode" "unicode/utf8" "unsafe" + + "go.followtheprocess.codes/cli/flag" ) const ( @@ -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" @@ -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) } @@ -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) @@ -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 @@ -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) @@ -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 @@ -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) @@ -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 } @@ -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, @@ -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, @@ -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) } @@ -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: diff --git a/internal/flag/flag_test.go b/internal/flag/flag_test.go index f4fa79b..dea7640 100644 --- a/internal/flag/flag_test.go +++ b/internal/flag/flag_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + publicflag "go.followtheprocess.codes/cli/flag" "go.followtheprocess.codes/cli/internal/flag" "go.followtheprocess.codes/test" ) @@ -156,21 +157,21 @@ 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") @@ -178,13 +179,13 @@ func TestFlaggableTypes(t *testing.T) { // 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) @@ -1038,7 +1039,7 @@ func TestFlagValidation(t *testing.T) { { name: "no shorthand", flagName: "delete", - short: flag.NoShortHand, + short: publicflag.NoShortHand, wantErr: false, }, { diff --git a/internal/flag/flaggable.go b/internal/flag/flaggable.go deleted file mode 100644 index 951812e..0000000 --- a/internal/flag/flaggable.go +++ /dev/null @@ -1,42 +0,0 @@ -package flag - -import ( - "net" - "time" -) - -// 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 -} diff --git a/internal/flag/set.go b/internal/flag/set.go index 799f5c9..790c467 100644 --- a/internal/flag/set.go +++ b/internal/flag/set.go @@ -7,6 +7,7 @@ import ( "slices" "strings" + "go.followtheprocess.codes/cli/flag" "go.followtheprocess.codes/cli/internal/style" "go.followtheprocess.codes/hue/tabwriter" ) @@ -31,31 +32,31 @@ func NewSet() *Set { } // AddToSet adds a flag to the given Set. -func AddToSet[T Flaggable](set *Set, flag Flag[T]) error { +func AddToSet[T flag.Flaggable](set *Set, f Flag[T]) error { if set == nil { return errors.New("cannot add flag to a nil set") } - name := flag.Name() - short := flag.Short() + name := f.Name() + short := f.Short() _, exists := set.flags[name] if exists { return fmt.Errorf("flag %q already defined", name) } - if short != NoShortHand { - f, exists := set.shorthands[short] + if short != flag.NoShortHand { + existingFlag, exists := set.shorthands[short] if exists { - return fmt.Errorf("shorthand %q already in use for flag %q", string(short), f.Name()) + return fmt.Errorf("shorthand %q already in use for flag %q", string(short), existingFlag.Name()) } } - set.flags[name] = flag + set.flags[name] = f // Only add the shorthand if it wasn't opted out of - if short != NoShortHand { - set.shorthands[short] = flag + if short != flag.NoShortHand { + set.shorthands[short] = f } return nil @@ -202,14 +203,14 @@ func (s *Set) Usage() (string, error) { tw := tabwriter.NewWriter(buf, style.MinWidth, style.TabWidth, style.Padding, style.PadChar, style.Flags) for _, name := range names { - flag := s.flags[name] - if flag == nil { + f := s.flags[name] + if f == nil { return "", fmt.Errorf("Value stored against key %s was nil", name) // Should never happen } var shorthand string - if flag.Short() != NoShortHand { - shorthand = "-" + string(flag.Short()) + if f.Short() != flag.NoShortHand { + shorthand = "-" + string(f.Short()) } else { shorthand = "N/A" } @@ -219,8 +220,8 @@ func (s *Set) Usage() (string, error) { " %s\t--%s\t%s\t%s\n", style.Bold.Text(shorthand), style.Bold.Text(name), - flag.Type(), - flag.Usage(), + f.Type(), + f.Usage(), ) } diff --git a/internal/flag/set_test.go b/internal/flag/set_test.go index 65f56db..4d491a1 100644 --- a/internal/flag/set_test.go +++ b/internal/flag/set_test.go @@ -6,6 +6,7 @@ import ( "slices" "testing" + publicflag "go.followtheprocess.codes/cli/flag" "go.followtheprocess.codes/cli/internal/flag" "go.followtheprocess.codes/snapshot" "go.followtheprocess.codes/test" @@ -748,7 +749,7 @@ func TestParse(t *testing.T) { name: "no shorthand use long", newSet: func(t *testing.T) *flag.Set { set := flag.NewSet() - f, err := flag.New(new(int), "count", flag.NoShortHand, 0, "Count something") + f, err := flag.New(new(int), "count", publicflag.NoShortHand, 0, "Count something") test.Ok(t, err) err = flag.AddToSet(set, f) @@ -776,7 +777,7 @@ func TestParse(t *testing.T) { name: "no shorthand use short", newSet: func(t *testing.T) *flag.Set { set := flag.NewSet() - f, err := flag.New(new(int), "count", flag.NoShortHand, 0, "Count something") + f, err := flag.New(new(int), "count", publicflag.NoShortHand, 0, "Count something") test.Ok(t, err) err = flag.AddToSet(set, f) @@ -805,7 +806,7 @@ func TestParse(t *testing.T) { name: "valid count long", newSet: func(t *testing.T) *flag.Set { set := flag.NewSet() - f, err := flag.New(new(flag.Count), "count", 'c', 0, "Count something") + f, err := flag.New(new(publicflag.Count), "count", 'c', 0, "Count something") test.Ok(t, err) err = flag.AddToSet(set, f) @@ -827,7 +828,7 @@ func TestParse(t *testing.T) { name: "valid count short", newSet: func(t *testing.T) *flag.Set { set := flag.NewSet() - f, err := flag.New(new(flag.Count), "count", 'c', 0, "Count something") + f, err := flag.New(new(publicflag.Count), "count", 'c', 0, "Count something") test.Ok(t, err) err = flag.AddToSet(set, f) @@ -849,7 +850,7 @@ func TestParse(t *testing.T) { name: "valid count super short", newSet: func(t *testing.T) *flag.Set { set := flag.NewSet() - f, err := flag.New(new(flag.Count), "count", 'c', 0, "Count something") + f, err := flag.New(new(publicflag.Count), "count", 'c', 0, "Count something") test.Ok(t, err) err = flag.AddToSet(set, f) @@ -1241,7 +1242,7 @@ func TestUsage(t *testing.T) { version, err := flag.New(new(bool), "version", 'V', false, "Show version info for test") test.Ok(t, err) - up, err := flag.New(new(bool), "update", flag.NoShortHand, false, "Update something") + up, err := flag.New(new(bool), "update", publicflag.NoShortHand, false, "Update something") test.Ok(t, err) set := flag.NewSet() @@ -1267,7 +1268,7 @@ func TestUsage(t *testing.T) { version, err := flag.New(new(bool), "version", 'V', false, "Show version info for test") test.Ok(t, err) - up, err := flag.New(new(bool), "update", flag.NoShortHand, false, "Update something") + up, err := flag.New(new(bool), "update", publicflag.NoShortHand, false, "Update something") test.Ok(t, err) count, err := flag.New(new(int), "count", 'c', 0, "Count things") diff --git a/option.go b/option.go index c0f209f..0935079 100644 --- a/option.go +++ b/option.go @@ -7,35 +7,11 @@ import ( "slices" "strings" - "go.followtheprocess.codes/cli/internal/flag" + "go.followtheprocess.codes/cli/flag" + internalflag "go.followtheprocess.codes/cli/internal/flag" "go.followtheprocess.codes/hue" ) -// NoShortHand should be passed as the "short" argument to [Flag] if the desired flag -// should be the long hand version only e.g. --count, not -c/--count. -const NoShortHand = flag.NoShortHand - -// Flaggable is a type constraint that defines any type capable of being parsed as a command line flag. -type Flaggable flag.Flaggable - -// Note: this must be a type alias (FlagCount = flag.Count), not a newtype (FlagCount flag.Count) -// otherwise parsing does not work correctly as the flag package does not know how to parse -// a new type declared here. - -// FlagCount 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 FlagCount = flag.Count - // Option is a functional option for configuring a [Command]. type Option interface { // Apply the option to the config, returning an error if the @@ -64,7 +40,7 @@ type config struct { stdout io.Writer stderr io.Writer run func(cmd *Command, args []string) error - flags *flag.Set + flags *internalflag.Set parent *Command argValidator ArgValidator name string @@ -455,18 +431,18 @@ func Allow(validator ArgValidator) Option { // // Add a force flag // var force bool // cli.New("rm", cli.Flag(&force, "force", 'f', false, "Force deletion without confirmation")) -func Flag[T Flaggable](p *T, name string, short rune, value T, usage string) Option { +func Flag[T flag.Flaggable](p *T, name string, short rune, value T, usage string) Option { f := func(cfg *config) error { if _, ok := cfg.flags.Get(name); ok { return fmt.Errorf("flag %q already defined", name) } - f, err := flag.New(p, name, short, value, usage) + f, err := internalflag.New(p, name, short, value, usage) if err != nil { return err } - if err := flag.AddToSet(cfg.flags, f); err != nil { + if err := internalflag.AddToSet(cfg.flags, f); err != nil { return fmt.Errorf("could not add flag %q to command %q: %w", name, cfg.name, err) }