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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Typed `FlagName` values that let `State.GetFlag` infer its return type

### Changed

- **BREAKING**: Replace the top-level `GetFlag` function with the generic `State.GetFlag` method
Expand Down
24 changes: 12 additions & 12 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ type FlagConfig struct {
Local bool
}

// FlagName ties a flag name to the type returned by [State.GetFlag].
type FlagName[T any] string

// State is the value passed to [Command.Exec]. It holds the parsed inputs the command needs to run.
type State struct {
// Args holds the positional arguments left after the command name and flags are parsed.
Expand Down Expand Up @@ -214,35 +217,32 @@ func FlagsFunc(fn func(f *flag.FlagSet)) (fset *flag.FlagSet) {
return fset
}

// GetFlag returns the value of a flag as type T. Call it from inside [Command.Exec] with the same
// Go type that was used when the flag was defined.
//
// GetFlag looks for the flag on the picked command first, then in its parent commands. A flag
// defined on the root command can be read from any subcommand. An unknown flag name or a wrong type
// is a programming error: GetFlag panics, and [Run] catches the panic and returns the error.
// GetFlag returns a flag value as T, searching the picked command before its parents. Unknown names
// and type mismatches are programming errors: GetFlag panics, and [Run] returns the error.
//
// verbose := s.GetFlag[bool]("verbose")
// count := s.GetFlag[int]("count")
// path := s.GetFlag[string]("path")
func (s *State) GetFlag[T any](name string) T {
// const count FlagName[int] = "count"
// n := s.GetFlag(count)
func (s *State) GetFlag[T any](name FlagName[T]) T {
if s == nil {
panic(&internalError{err: errors.New("state is nil")})
}
flagName := string(name)
// Try to find the flag in each command's flag set, starting from the current command
for i := len(s.path) - 1; i >= 0; i-- {
cmd := s.path[i]
if cmd.Flags == nil {
continue
}

if f := cmd.Flags.Lookup(name); f != nil {
if f := cmd.Flags.Lookup(flagName); f != nil {
if getter, ok := f.Value.(flag.Getter); ok {
value := getter.Get()
if v, ok := value.(T); ok {
return v
}
err := fmt.Errorf("type mismatch for flag %q in command %q: registered %T, requested %T",
formatFlagName(name),
formatFlagName(flagName),
getCommandPath(s.path),
value,
*new(T),
Expand All @@ -255,7 +255,7 @@ func (s *State) GetFlag[T any](name string) T {

// If flag not found anywhere in hierarchy, panic with helpful message
err := fmt.Errorf("flag %q not found in command %q flag set",
formatFlagName(name),
formatFlagName(flagName),
getCommandPath(s.path),
)
panic(&internalError{err: err})
Expand Down
14 changes: 14 additions & 0 deletions cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1840,6 +1840,20 @@ func TestStateGetFlag(t *testing.T) {
})
}

func TestStateGetFlagTypedName(t *testing.T) {
t.Parallel()

const verbose FlagName[bool] = "verbose"
cmd := &Command{
Name: "root",
Flags: FlagsFunc(func(f *flag.FlagSet) { f.Bool(string(verbose), false, "verbose output") }),
Exec: func(context.Context, *State) error { return nil },
}

require.NoError(t, Parse(cmd, []string{"--verbose"}))
require.True(t, cmd.state.GetFlag(verbose))
}

func TestStateCommandContext(t *testing.T) {
t.Parallel()

Expand Down
7 changes: 4 additions & 3 deletions examples/cmd/echo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ import (
"github.com/pressly/cli"
)

const capitalize cli.FlagName[bool] = "capitalize"

func main() {
root := &cli.Command{
Name: "echo",
Usage: "echo [flags] <text>...",
Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
f.Bool("capitalize", false, "capitalize the input")
f.Bool(string(capitalize), false, "capitalize the input")
}),
Exec: func(ctx context.Context, s *cli.State) error {
text := strings.Join(s.Args, " ")
// GetFlag uses generic methods, available in Go 1.27 or later.
if s.GetFlag[bool]("capitalize") {
if s.GetFlag(capitalize) {
text = strings.ToUpper(text)
}
fmt.Fprintln(s.Stdout, text)
Expand Down
Loading