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
18 changes: 18 additions & 0 deletions ffval/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,21 @@ func (v *reflectValue) Set(s string) error { return v.set(s) }
func (v *reflectValue) String() string { return v.get() }
func (v *reflectValue) IsBoolFlag() bool { return v.isBoolFlag }
func (v *reflectValue) GetPlaceholder() string { return v.placeholder }

//
//
//

// Func implements [flag.Value] for a function that takes a string and returns
// an error. It has essentially the same behavior as a stdlib [flag.Func].
type Func func(string) error

var _ flag.Value = (*Func)(nil)

func (fn Func) Set(s string) error {
return fn(s)
}

func (fn Func) String() string {
return ""
}
84 changes: 80 additions & 4 deletions flag_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,61 @@ import (

// FlagSet is a standard implementation of [Flags]. It's broadly similar to a
// flag.FlagSet, but with additional capabilities inspired by getopt(3).
//
// # Defining
//
// Flags can be defined in the flag set in a variety of ways, including:
//
// - [FlagSet.AddFlag] with a [FlagConfig]
// - [FlagSet.AddStruct] with a pointer to a "config" struct
// - [FlagSet.Value] with anything implementing the stdlib [flag.Value]
// - Helper methods like [FlagSet.Bool], [FlagSet.StringEnum], etc.
//
// See package [ffval] for more on flag value types and their implementations.
//
// Flags can have a short name, a long name, or both. Short names are single
// runes and specified by a single dash (-) prefix, like `-f`. Long names are
// strings and specified by a double dash (--) prefix, like `--foo`. Both short
// and long names must be unique within a flag set. See [FlagConfig] for the
// complete set of flag attributes.
//
// # Parsing
//
// Flag sets can be parsed with either [FlagSet.Parse], or by passing the flag
// set to [Parse]. Parse behavior can be customized with [Option] parameters.
// Notably, [WithEnvVarPrefix] allows flags to be set from (prefixed)
// environment variables.
//
// After a successful parse, the flag set is marked as parsed, and subsequent
// calls to parse fail with [ErrAlreadyParsed], until and unless the
// [FlagSet.Reset] method is called.
//
// # Help and usage
//
// Unlike the stdlib package flag, [FlagSet] does not automatically print help
// or usage information after a failed parse. Instead, it returns an error,
// which is expected to be handled by the caller. Package [ffhelp] provides a
// variety of helpers for defining and emitting help and usage information. Most
// users can call [ffhelp.Flags] with their flag set to get a good default help
// text. And it's easy for users with more sophisticated needs to define their
// own help text generators.
//
// Here is a common pattern for parsing a flag set:
//
// fs := ff.NewFlagSet("myprogram")
// // ...add/define flags...
// if err := fs.Parse(os.Args[1:], ff.WithEnvVarPrefix("MYPROGRAM")); err != nil {
// if errors.Is(err, ff.ErrHelp) {
// ffhelp.Flags(fs).WriteTo(os.Stdout)
// }
// return fmt.Errorf("parse flags: %w", err)
// }
//
// # Errata
//
// Like the stdlib package flag, the first `backtick quoted substring` in a
// flag's usage string is used as the flag's placeholder, if a placeholder is
// not otherwise explicitly provided.
type FlagSet struct {
name string
flags []*coreFlag
Expand Down Expand Up @@ -1157,11 +1212,17 @@ func (fs *FlagSet) DurationConfig(cfg FlagConfig, def time.Duration) *time.Durat
return &value
}

// Func defines a new flag in the flag set, and panics on any error.
// Func defines a new function flag in the flag set, and panics on any error.
//
// Function flags are different than normal flags in that they do not represent
// a value. Instead, they define a function, similar to a ParseFunc, that gets
// called every time the flag is set. That function receives the value of the
// flag as a string argument, and is expected to validate that value. If
// validation fails, the function should return an error, which will be returned
// to the user. If validation succeeds, the function should process the value
// and return nil.
func (fs *FlagSet) Func(short rune, long string, fn func(string) error, usage string) {
stdfs := flag.NewFlagSet("flagset-name", flag.ContinueOnError)
stdfs.Func("flag-name", "flag-usage", fn)
value := stdfs.Lookup("flag-name").Value
value := ffval.Func(fn)
fs.Value(short, long, value, usage)
}

Expand All @@ -1175,6 +1236,21 @@ func (fs *FlagSet) FuncLong(long string, fn func(string) error, usage string) {
fs.Func(0, long, fn, usage)
}

// FuncConfigVar defines a new flag in the flag set, and panics on any error.
// The Value field of the provided config is overwritten, and the NoDefault
// field is set to true.
//
// For more information on function flags, see [FlagSet.Func].
func (fs *FlagSet) FuncConfigVar(cfg FlagConfig, fn func(string) error) Flag {
cfg.Value = ffval.Func(fn)
cfg.NoDefault = true // function flags should not have a default value
f, err := fs.AddFlag(cfg)
if err != nil {
panic(err)
}
return f
}

//
//
//
Expand Down
96 changes: 96 additions & 0 deletions flag_set_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"flag"
"fmt"
"net/netip"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -641,3 +642,98 @@ func (ss *customStringSlice) String() string {
}

var _ flag.Value = (*customStringSlice)(nil)

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

var ipFlag netip.Addr

ipFlagFunc := func(s string) error {
ip, err := netip.ParseAddr(s)
if err != nil {
return err
}
ipFlag = ip
return nil
}

var (
longName = "ip"
placeholder = "IPADDR"
usage = "`IP` address to check" // explicit placeholder takes precedence
)

check := func(t *testing.T, fs *ff.FlagSet, f ff.Flag) {
t.Helper()

// Pre-parse flag checks.
if want, have := "", f.GetDefault(); want != have {
t.Errorf("GetDefault: want %q, have %q", want, have)
}

if want, have := placeholder, f.GetPlaceholder(); want != have {
t.Errorf("GetPlaceholder: want %q, have %q", want, have)
}

if want, have := usage, f.GetUsage(); want != have {
t.Errorf("GetUsage: want %q, have %q", want, have)
}

if want, have := false, f.IsSet(); want != have {
t.Errorf("IsSet: want %v, have %v", want, have)
}

// Parse.
if err := fs.Parse([]string{"--ip", "192.168.2.1"}); err != nil {
t.Fatalf("Parse: %v", err)
}

// Post-parse flag and ipAddr checks.
if want, have := true, f.IsSet(); want != have {
t.Errorf("IsSet: want %v, have %v", want, have)
}
if want, have := true, ipFlag.IsValid(); want != have {
t.Errorf("IsValid: want %v, have %v", want, have)
}
if want, have := "192.168.2.1", ipFlag.String(); want != have {
t.Errorf("String: want %q, have %q", want, have)
}
if want, have := true, ipFlag.Is4(); want != have {
t.Errorf("Is4: want %v, have %v", want, have)
}
if want, have := true, ipFlag.IsPrivate(); want != have {
t.Errorf("IsPrivate: want %v, have %v", want, have)
}
if want, have := false, ipFlag.IsLoopback(); want != have {
t.Errorf("IsLoopback: want %v, have %v", want, have)
}
}

t.Run("FuncConfigVar", func(t *testing.T) {
ipFlag = netip.Addr{}
fs := ff.NewFlagSet(t.Name())
flagConfig := ff.FlagConfig{
LongName: longName,
Placeholder: placeholder,
Usage: usage,
}
f := fs.FuncConfigVar(flagConfig, ipFlagFunc)
check(t, fs, f)
})

t.Run("AddFlag", func(t *testing.T) {
ipFlag = netip.Addr{}
fs := ff.NewFlagSet(t.Name())
flagConfig := ff.FlagConfig{
LongName: longName,
Placeholder: placeholder,
Usage: usage,
Value: ffval.Func(ipFlagFunc),
}
f, err := fs.AddFlag(flagConfig)
if err != nil {
t.Fatalf("AddFlag: %v", err)
}
check(t, fs, f)
})
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/peterbourgon/ff/v4

go 1.20
go 1.21.0

require (
github.com/pelletier/go-toml/v2 v2.0.9
Expand Down
Loading