Repository: alecthomas/kong
Pull request #199 (2021-08-27 22:55:13)

Add ability to ignore fields that match given regex pattern(s)
@alecthomas this adds functionality to allow kong to ignore fields that match a certain pattern.

I needed this in order to be able to ensure that kong does not parse fields like `XXX_` that are automatically generated via protoc.

The regexp is kind of a big hammer but since this is for CLI generation, I figure the perf hit is OK. LMK.

alecthomas: Thanks for this, I love getting contributions! :)
dselans: > Thanks for this, I love getting contributions! :)

Thank YOU - you did all the hard work! I added an extra test for options - noticed something was untested when running a coverage report for my own stuff.

Patch:
diff --git a/build.go b/build.go
index 374897c..85e18dc 100644
--- a/build.go
+++ b/build.go
@@ -24,6 +24,7 @@ func build(k *Kong, ast interface{}) (app *Application, err error) {
 	for _, flag := range extraFlags {
 		seenFlags[flag.Name] = true
 	}
+
 	node, err := buildNode(k, iv, ApplicationNode, seenFlags)
 	if err != nil {
 		return nil, err
@@ -112,7 +113,15 @@ func buildNode(k *Kong, v reflect.Value, typ NodeType, seenFlags map[string]bool
 	if err != nil {
 		return nil, err
 	}
+
+MAIN:
 	for _, field := range fields {
+		for _, r := range k.ignoreFieldsRegex {
+			if r.MatchString(v.Type().Name() + "." + field.field.Name) {
+				continue MAIN
+			}
+		}
+
 		ft := field.field
 		fv := field.value
 
diff --git a/kong.go b/kong.go
index dd4b966..14f5c2d 100644
--- a/kong.go
+++ b/kong.go
@@ -6,6 +6,7 @@ import (
 	"os"
 	"path/filepath"
 	"reflect"
+	"regexp"
 	"strings"
 )
 
@@ -48,10 +49,11 @@ type Kong struct {
 	Stdout io.Writer
 	Stderr io.Writer
 
-	bindings  bindings
-	loader    ConfigurationLoader
-	resolvers []Resolver
-	registry  *Registry
+	bindings          bindings
+	loader            ConfigurationLoader
+	resolvers         []Resolver
+	registry          *Registry
+	ignoreFieldsRegex []*regexp.Regexp
 
 	noDefaultHelp bool
 	usageOnError  usageOnError
@@ -73,13 +75,14 @@ type Kong struct {
 // See the README (https://github.com/alecthomas/kong) for usage instructions.
 func New(grammar interface{}, options ...Option) (*Kong, error) {
 	k := &Kong{
-		Exit:          os.Exit,
-		Stdout:        os.Stdout,
-		Stderr:        os.Stderr,
-		registry:      NewRegistry().RegisterDefaults(),
-		vars:          Vars{},
-		bindings:      bindings{},
-		helpFormatter: DefaultHelpValueFormatter,
+		Exit:              os.Exit,
+		Stdout:            os.Stdout,
+		Stderr:            os.Stderr,
+		registry:          NewRegistry().RegisterDefaults(),
+		vars:              Vars{},
+		bindings:          bindings{},
+		helpFormatter:     DefaultHelpValueFormatter,
+		ignoreFieldsRegex: make([]*regexp.Regexp, 0),
 	}
 
 	options = append(options, Bind(k))
diff --git a/options.go b/options.go
index aec57d6..6036d91 100644
--- a/options.go
+++ b/options.go
@@ -6,6 +6,7 @@ import (
 	"os/user"
 	"path/filepath"
 	"reflect"
+	"regexp"
 	"strings"
 
 	"github.com/pkg/errors"
@@ -319,6 +320,31 @@ func Resolvers(resolvers ...Resolver) Option {
 	})
 }
 
+// IgnoreFieldsRegex will cause kong.New() to skip field names that match any
+// of the provided regex patterns. This is useful if you are not able to add a
+// kong="-" struct tag to a struct/element before the call to New.
+//
+// Example: When referencing protoc generated structs, you will likely want to
+// ignore/skip XXX_* fields.
+func IgnoreFieldsRegex(regexes ...string) Option {
+	return OptionFunc(func(k *Kong) error {
+		for _, r := range regexes {
+			if r == "" {
+				return errors.New("regex input cannot be empty")
+			}
+
+			re, err := regexp.Compile(r)
+			if err != nil {
+				return errors.Wrap(err, "unable to compile regex")
+			}
+
+			k.ignoreFieldsRegex = append(k.ignoreFieldsRegex, re)
+		}
+
+		return nil
+	})
+}
+
 // ConfigurationLoader is a function that builds a resolver from a file.
 type ConfigurationLoader func(r io.Reader) (Resolver, error)
 


Test patch:
diff --git a/kong_test.go b/kong_test.go
index ca8bafe..7d80d40 100644
--- a/kong_test.go
+++ b/kong_test.go
@@ -1297,3 +1297,65 @@ func TestHydratePointerCommands(t *testing.T) {
 	require.NoError(t, err)
 	require.Equal(t, &cmd{Flag: true}, cli.Cmd)
 }
+
+// nolint
+type testIgnoreFields struct {
+	Foo struct {
+		Bar bool
+		Sub struct {
+			SubFlag1     bool `kong:"name=subflag1"`
+			XXX_SubFlag2 bool `kong:"name=subflag2"`
+		} `kong:"cmd"`
+	} `kong:"cmd"`
+	XXX_Baz struct {
+		Boo bool
+	} `kong:"cmd,name=baz"`
+}
+
+func TestIgnoreRegex(t *testing.T) {
+	cli := testIgnoreFields{}
+
+	k, err := kong.New(&cli, kong.IgnoreFieldsRegex(`.*\.XXX_.+`))
+	require.NoError(t, err)
+
+	_, err = k.Parse([]string{"foo", "sub"})
+	require.NoError(t, err)
+
+	_, err = k.Parse([]string{"foo", "sub", "--subflag1"})
+	require.NoError(t, err)
+
+	_, err = k.Parse([]string{"foo", "sub", "--subflag2"})
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "unknown flag --subflag2")
+
+	_, err = k.Parse([]string{"baz"})
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "unexpected argument baz")
+}
+
+// Verify that passing a nil regex will work
+func TestIgnoreRegexEmpty(t *testing.T) {
+	cli := testIgnoreFields{}
+
+	_, err := kong.New(&cli, kong.IgnoreFieldsRegex(""))
+	require.Error(t, err)
+	require.Contains(t, "regex input cannot be empty", err.Error())
+}
+
+type optionWithErr struct{}
+
+func (o *optionWithErr) Apply(k *kong.Kong) error {
+	return errors.New("option returned err")
+}
+
+func TestOptionReturnsErr(t *testing.T) {
+	cli := struct {
+		Test bool
+	}{}
+
+	optWithError := &optionWithErr{}
+
+	_, err := kong.New(cli, optWithError)
+	require.Error(t, err)
+	require.Equal(t, "option returned err", err.Error())
+}