Skip to content

Commit

Permalink
Add an implementation of flag set into commands (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
FollowTheProcess authored Jul 31, 2024
1 parent 718af79 commit 93e6f37
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 3 deletions.
5 changes: 5 additions & 0 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"text/tabwriter"

"github.com/FollowTheProcess/cli/internal/flag"
"github.com/spf13/pflag"
)

Expand Down Expand Up @@ -42,6 +43,7 @@ func New(name string, options ...Option) (*Command, error) {
// Default implementation
cfg := config{
flags: pflag.NewFlagSet(name, pflag.ContinueOnError),
xflags: flag.NewSet(),
stdin: os.Stdin,
stdout: os.Stdout,
stderr: os.Stderr,
Expand Down Expand Up @@ -97,6 +99,9 @@ type Command struct {
// flags is the set of flags for this command.
flags *pflag.FlagSet

// xflags is my experimental flagset.
xflags *flag.Set

// versionFunc is the function thatgets called when the user calls -v/--version.
//
// It can be overridden by the user to customise their version output using
Expand Down
5 changes: 4 additions & 1 deletion internal/flag/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ const (
// should be the long hand version only e.g. --count, not -c/--count.
const NoShortHand = rune(-1)

var _ pflag.Value = Flag[string]{} // This will fail if we violate pflag.Value.
var (
_ pflag.Value = Flag[string]{} // This will fail if we violate pflag.Value
_ Value = Flag[string]{} // This one will fail if we violate our own Value interface
)

// Flaggable is a type constraint that defines any type capable of being parsed as a command line flag.
//
Expand Down
56 changes: 56 additions & 0 deletions internal/flag/set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package flag

import "fmt"

// Set is a set of command line flags.
type Set struct {
flags map[string]flagEntry // The actual stored flags, can lookup by name
shorthands map[rune]flagEntry // The flags by shorthand
}

// NewSet builds and returns a new set of flags.
func NewSet() *Set {
return &Set{
flags: make(map[string]flagEntry),
shorthands: make(map[rune]flagEntry),
}
}

// Add adds a flag to the Set.
func (s *Set) Add(name, usage string, shorthand rune, flag Value) error {
_, exists := s.flags[name]
if exists {
return fmt.Errorf("flag %q already defined", name)
}
var defaultValueNoArg string
if flag.Type() == "bool" {
// Boolean flags imply passing true, "--force" vs "--force true"
defaultValueNoArg = "true"
}
entry := flagEntry{
value: flag,
name: name,
usage: usage,
defaultValue: flag.String(),
defaultValueNoArg: defaultValueNoArg,
shorthand: shorthand,
}
s.flags[name] = entry

// Only add the shorthand if it wasn't opted out of
if shorthand != NoShortHand {
s.shorthands[shorthand] = entry
}

return nil
}

// flagEntry represents a single flag in the set.
type flagEntry struct {
value Value // The actual Flag[T]
name string // The full name of the flag e.g. "delete"
usage string // The flag's usage message
defaultValue string // String representation of the default flag value
defaultValueNoArg string // String representation of the default flag value if used without an arg, e.g. boolean flags "--force" implies "--force true"
shorthand rune // The optional shorthand e.g. 'd' or [NoShortHand]
}
8 changes: 8 additions & 0 deletions internal/flag/value.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package flag

// Value is an interface representing a Flag value that can be set from the command line.
type Value interface {
String() string // Print the stored value of a flag
Type() string // Return the string representation of the flag type e.g. "bool"
Set(str string) error // Set the stored value of a flag by parsing the string "str"
}
10 changes: 8 additions & 2 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type config struct {
stderr io.Writer
run func(cmd *Command, args []string) error
flags *pflag.FlagSet
xflags *flag.Set
versionFunc func(cmd *Command) error
parent *Command
argValidator ArgValidator
Expand All @@ -63,6 +64,7 @@ func (c *config) build() *Command {
stderr: c.stderr,
run: c.run,
flags: c.flags,
xflags: c.xflags,
versionFunc: c.versionFunc,
parent: c.parent,
argValidator: c.argValidator,
Expand Down Expand Up @@ -198,8 +200,6 @@ func Long(long string) Option {
//
// An arbitrary number of examples can be added to a [Command], and calls to [Example] are additive.
func Example(comment, command string) Option {
// TODO: Make sure both comment and command are not empty, then can ditch the
// complexity in example.String()
f := func(cfg *config) error {
errs := make([]error, 0, 2) //nolint:mnd // 2 here is because we have two arguments
if comment == "" {
Expand Down Expand Up @@ -353,6 +353,12 @@ func Flag[T Flaggable](p *T, name string, short rune, value T, usage string) Opt
return err
}

// Experimental flags
if err := cfg.xflags.Add(name, usage, short, f); err != nil {
// TODO: This error message is just for me debugging for now, make it more user friendly
return fmt.Errorf("xflags.Add: %w", err)
}

var defVal string
if f.Type() == "bool" {
defVal = "true"
Expand Down

0 comments on commit 93e6f37

Please sign in to comment.