From 23ed87b7ac381522ba84811b8a5fa494ad085f22 Mon Sep 17 00:00:00 2001 From: Peter Bourgon Date: Sun, 20 Jul 2025 19:24:30 +0200 Subject: [PATCH] Bring Func flags up to date --- ffval/value.go | 18 +++++++++ flag_set.go | 84 ++++++++++++++++++++++++++++++++++++++++-- flag_set_test.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- 4 files changed, 195 insertions(+), 5 deletions(-) diff --git a/ffval/value.go b/ffval/value.go index c7607dc..ea0c74d 100644 --- a/ffval/value.go +++ b/ffval/value.go @@ -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 "" +} diff --git a/flag_set.go b/flag_set.go index 310f34b..4b9e56a 100644 --- a/flag_set.go +++ b/flag_set.go @@ -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 @@ -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) } @@ -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 +} + // // // diff --git a/flag_set_test.go b/flag_set_test.go index cb03a65..209e47f 100644 --- a/flag_set_test.go +++ b/flag_set_test.go @@ -4,6 +4,7 @@ import ( "errors" "flag" "fmt" + "net/netip" "reflect" "strings" "testing" @@ -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) + }) +} diff --git a/go.mod b/go.mod index 60108a5..d3ca464 100644 --- a/go.mod +++ b/go.mod @@ -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