Repository: alecthomas/kong
Pull request #273 (2022-02-17 09:01:39)

Switch to the standard errors API, that was introduced in 1.13


Patch:
diff --git a/README.md b/README.md
index 48418c4..372db71 100644
--- a/README.md
+++ b/README.md
@@ -556,8 +556,8 @@ type Validatable interface {
  }
  ```
 
-+If one of these nodes is in the active command-line it will be called during
-+normal validation.
+If one of these nodes is in the active command-line it will be called during
+normal validation.
 
 ## Modifying Kong's behaviour
 
diff --git a/bin/.go@latest.pkg b/bin/.go-1.17.pkg
similarity index 100%
rename from bin/.go@latest.pkg
rename to bin/.go-1.17.pkg
diff --git a/bin/go b/bin/go
index 142d6ff..cdc9b02 120000
--- a/bin/go
+++ b/bin/go
@@ -1,1 +1,1 @@
-.go@latest.pkg
\ No newline at end of file
+.go-1.17.pkg
\ No newline at end of file
diff --git a/bin/gofmt b/bin/gofmt
index 142d6ff..cdc9b02 120000
--- a/bin/gofmt
+++ b/bin/gofmt
@@ -1,1 +1,1 @@
-.go@latest.pkg
\ No newline at end of file
+.go-1.17.pkg
\ No newline at end of file
diff --git a/callbacks.go b/callbacks.go
index 8b78741..1cfaccf 100644
--- a/callbacks.go
+++ b/callbacks.go
@@ -4,8 +4,6 @@ import (
 	"fmt"
 	"reflect"
 	"strings"
-
-	"github.com/pkg/errors"
 )
 
 type bindings map[reflect.Type]func() (reflect.Value, error)
@@ -35,7 +33,7 @@ func (b bindings) addProvider(provider interface{}) error {
 	pv := reflect.ValueOf(provider)
 	t := pv.Type()
 	if t.Kind() != reflect.Func || t.NumIn() != 0 || t.NumOut() != 2 || t.Out(1) != reflect.TypeOf((*error)(nil)).Elem() {
-		return errors.Errorf("%T must be a function with the signature func()(T, error)", provider)
+		return fmt.Errorf("%T must be a function with the signature func()(T, error)", provider)
 	}
 	rt := pv.Type().Out(0)
 	b[rt] = func() (reflect.Value, error) {
diff --git a/context.go b/context.go
index 1e26ff5..45c412a 100644
--- a/context.go
+++ b/context.go
@@ -1,14 +1,13 @@
 package kong
 
 import (
+	"errors"
 	"fmt"
 	"os"
 	"reflect"
 	"sort"
 	"strconv"
 	"strings"
-
-	"github.com/pkg/errors"
 )
 
 // Path records the nodes and parsed values from the current command-line.
@@ -209,9 +208,8 @@ func (c *Context) Validate() error { // nolint: gocyclo
 			desc = node.Path()
 		}
 		if validate := isValidatable(value); validate != nil {
-			err := validate.Validate()
-			if err != nil {
-				return errors.Wrap(err, desc)
+			if err := validate.Validate(); err != nil {
+				return fmt.Errorf("%s: %w", desc, err)
 			}
 		}
 	}
@@ -558,7 +556,7 @@ func (c *Context) Resolve() error {
 			for _, resolver := range resolvers {
 				s, err := resolver.Resolve(c, path, flag)
 				if err != nil {
-					return errors.Wrap(err, flag.ShortSummary())
+					return fmt.Errorf("%s: %w", flag.ShortSummary(), err)
 				}
 				if s == nil {
 					continue
@@ -683,8 +681,9 @@ func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
 		}
 		err := flag.Parse(c.scan, c.getValue(flag.Value))
 		if err != nil {
-			if e, ok := errors.Cause(err).(*expectedError); ok && e.token.InferredType().IsAny(FlagToken, ShortFlagToken) {
-				return errors.Errorf("%s; perhaps try %s=%q?", err, flag.ShortSummary(), e.token)
+			var expected *expectedError
+			if errors.As(err, &expected) && expected.token.InferredType().IsAny(FlagToken, ShortFlagToken) {
+				return fmt.Errorf("%s; perhaps try %s=%q?", err.Error(), flag.ShortSummary(), expected.token)
 			}
 			return err
 		}
@@ -888,19 +887,18 @@ func checkEnum(value *Value, target reflect.Value) error {
 		return nil
 
 	case reflect.Map, reflect.Struct:
-		return errors.Errorf("enum can only be applied to a slice or value")
+		return errors.New("enum can only be applied to a slice or value")
 
 	default:
-		enumMap := value.EnumMap()
+		enumSlice := value.EnumSlice()
 		v := fmt.Sprintf("%v", target)
-		if enumMap[v] {
-			return nil
-		}
 		enums := []string{}
-		for enum := range enumMap {
+		for _, enum := range enumSlice {
+			if enum == v {
+				return nil
+			}
 			enums = append(enums, fmt.Sprintf("%q", enum))
 		}
-		sort.Strings(enums)
 		return fmt.Errorf("%s must be one of %s but got %q", value.ShortSummary(), strings.Join(enums, ","), target.Interface())
 	}
 }
diff --git a/error.go b/error.go
index 30b8858..18225ef 100644
--- a/error.go
+++ b/error.go
@@ -8,5 +8,5 @@ type ParseError struct {
 	Context *Context
 }
 
-// Cause returns the original cause of the error.
-func (p *ParseError) Cause() error { return p.error }
+// Unwrap returns the original cause of the error.
+func (p *ParseError) Unwrap() error { return p.error }
diff --git a/go.mod b/go.mod
index bdd8bb2..e5c0722 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,6 @@ module github.com/alecthomas/kong
 require (
 	github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/pkg/errors v0.9.1
 	github.com/stretchr/testify v1.7.0
 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
 )
diff --git a/go.sum b/go.sum
index 12908f3..e787c1f 100644
--- a/go.sum
+++ b/go.sum
@@ -3,8 +3,6 @@ github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
diff --git a/kong.go b/kong.go
index 3123dc2..eeeaa8c 100644
--- a/kong.go
+++ b/kong.go
@@ -1,6 +1,7 @@
 package kong
 
 import (
+	"errors"
 	"fmt"
 	"io"
 	"os"
@@ -378,13 +379,14 @@ func (k *Kong) FatalIfErrorf(err error, args ...interface{}) {
 		msg = fmt.Sprintf(args[0].(string), args[1:]...) + ": " + err.Error()
 	}
 	// Maybe display usage information.
-	if err, ok := err.(*ParseError); ok {
+	var parseErr *ParseError
+	if errors.As(err, &parseErr) {
 		switch k.usageOnError {
 		case fullUsage:
-			_ = k.help(k.helpOptions, err.Context)
+			_ = k.help(k.helpOptions, parseErr.Context)
 			fmt.Fprintln(k.Stdout)
 		case shortUsage:
-			_ = k.shortHelp(k.helpOptions, err.Context)
+			_ = k.shortHelp(k.helpOptions, parseErr.Context)
 			fmt.Fprintln(k.Stdout)
 		}
 	}
diff --git a/mapper.go b/mapper.go
index 88fe1a5..139b2df 100644
--- a/mapper.go
+++ b/mapper.go
@@ -3,6 +3,7 @@ package kong
 import (
 	"encoding"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"math/bits"
@@ -12,8 +13,6 @@ import (
 	"strconv"
 	"strings"
 	"time"
-
-	"github.com/pkg/errors"
 )
 
 var (
@@ -295,14 +294,14 @@ func (boolMapper) Decode(ctx *DecodeContext, target reflect.Value) error {
 				target.SetBool(false)
 
 			default:
-				return errors.Errorf("bool value must be true, 1, yes, false, 0 or no but got %q", v)
+				return fmt.Errorf("bool value must be true, 1, yes, false, 0 or no but got %q", v)
 			}
 
 		case bool:
 			target.SetBool(v)
 
 		default:
-			return errors.Errorf("expected bool but got %q (%T)", token.Value, token.Value)
+			return fmt.Errorf("expected bool but got %q (%T)", token.Value, token.Value)
 		}
 	} else {
 		target.SetBool(true)
@@ -322,12 +321,12 @@ func durationDecoder() MapperFunc {
 		case string:
 			d, err = time.ParseDuration(v)
 			if err != nil {
-				return errors.Errorf("expected duration but got %q: %s", v, err)
+				return fmt.Errorf("expected duration but got %q: %v", v, err)
 			}
 		case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
 			d = reflect.ValueOf(v).Convert(reflect.TypeOf(time.Duration(0))).Interface().(time.Duration) // nolint: forcetypeassert
 		default:
-			return errors.Errorf("expected duration but got %q", v)
+			return fmt.Errorf("expected duration but got %q", v)
 		}
 		target.Set(reflect.ValueOf(d))
 		return nil
@@ -368,11 +367,11 @@ func intDecoder(bits int) MapperFunc { // nolint: dupl
 			sv = fmt.Sprintf("%v", v)
 
 		default:
-			return errors.Errorf("expected an int but got %q (%T)", t, t.Value)
+			return fmt.Errorf("expected an int but got %q (%T)", t, t.Value)
 		}
 		n, err := strconv.ParseInt(sv, 10, bits)
 		if err != nil {
-			return errors.Errorf("expected a valid %d bit int but got %q", bits, sv)
+			return fmt.Errorf("expected a valid %d bit int but got %q", bits, sv)
 		}
 		target.SetInt(n)
 		return nil
@@ -394,11 +393,11 @@ func uintDecoder(bits int) MapperFunc { // nolint: dupl
 			sv = fmt.Sprintf("%v", v)
 
 		default:
-			return errors.Errorf("expected an int but got %q (%T)", t, t.Value)
+			return fmt.Errorf("expected an int but got %q (%T)", t, t.Value)
 		}
 		n, err := strconv.ParseUint(sv, 10, bits)
 		if err != nil {
-			return errors.Errorf("expected a valid %d bit uint but got %q", bits, sv)
+			return fmt.Errorf("expected a valid %d bit uint but got %q", bits, sv)
 		}
 		target.SetUint(n)
 		return nil
@@ -415,7 +414,7 @@ func floatDecoder(bits int) MapperFunc {
 		case string:
 			n, err := strconv.ParseFloat(v, bits)
 			if err != nil {
-				return errors.Errorf("expected a float but got %q (%T)", t, t.Value)
+				return fmt.Errorf("expected a float but got %q (%T)", t, t.Value)
 			}
 			target.SetFloat(n)
 
@@ -429,7 +428,7 @@ func floatDecoder(bits int) MapperFunc {
 			target.Set(reflect.ValueOf(v))
 
 		default:
-			return errors.Errorf("expected an int but got %q (%T)", t, t.Value)
+			return fmt.Errorf("expected an int but got %q (%T)", t, t.Value)
 		}
 		return nil
 	}
@@ -447,7 +446,7 @@ func mapDecoder(r *Registry) MapperFunc {
 			t := ctx.Scan.Pop()
 			// If decoding a flag, we need an argument.
 			if t.IsEOL() {
-				return errors.Errorf("unexpected EOL")
+				return errors.New("unexpected EOL")
 			}
 			switch v := t.Value.(type) {
 			case string:
@@ -457,7 +456,7 @@ func mapDecoder(r *Registry) MapperFunc {
 				for _, m := range v {
 					err := jsonTranscode(m, target.Addr().Interface())
 					if err != nil {
-						return errors.WithStack(err)
+						return err
 					}
 				}
 				return nil
@@ -466,7 +465,7 @@ func mapDecoder(r *Registry) MapperFunc {
 				return jsonTranscode(v, target.Addr().Interface())
 
 			default:
-				return errors.Errorf("invalid map value %q (of type %T)", t, t.Value)
+				return fmt.Errorf("invalid map value %q (of type %T)", t, t.Value)
 			}
 		} else {
 			tokens := ctx.Scan.PopWhile(func(t Token) bool { return t.IsValue() })
@@ -480,7 +479,7 @@ func mapDecoder(r *Registry) MapperFunc {
 			}
 			parts := strings.SplitN(token, "=", 2)
 			if len(parts) != 2 {
-				return errors.Errorf("expected \"<key>=<value>\" but got %q", token)
+				return fmt.Errorf("expected \"<key>=<value>\" but got %q", token)
 			}
 			key, value := parts[0], parts[1]
 
@@ -488,7 +487,7 @@ func mapDecoder(r *Registry) MapperFunc {
 			if typ := ctx.Value.Tag.Type; typ != "" {
 				parts := strings.Split(typ, ":")
 				if len(parts) != 2 {
-					return errors.Errorf("type:\"\" on map field must be in the form \"[<keytype>]:[<valuetype>]\"")
+					return errors.New("type:\"\" on map field must be in the form \"[<keytype>]:[<valuetype>]\"")
 				}
 				keyTypeName, valueTypeName = parts[0], parts[1]
 			}
@@ -497,14 +496,14 @@ func mapDecoder(r *Registry) MapperFunc {
 			keyDecoder := r.ForNamedType(keyTypeName, el.Key())
 			keyValue := reflect.New(el.Key()).Elem()
 			if err := keyDecoder.Decode(ctx.WithScanner(keyScanner), keyValue); err != nil {
-				return errors.Errorf("invalid map key %q", key)
+				return fmt.Errorf("invalid map key %q", key)
 			}
 
 			valueScanner := Scan(value)
 			valueDecoder := r.ForNamedType(valueTypeName, el.Elem())
 			valueValue := reflect.New(el.Elem()).Elem()
 			if err := valueDecoder.Decode(ctx.WithScanner(valueScanner), valueValue); err != nil {
-				return errors.Errorf("invalid map value %q", value)
+				return fmt.Errorf("invalid map value %q", value)
 			}
 
 			target.SetMapIndex(keyValue, valueValue)
@@ -522,7 +521,7 @@ func sliceDecoder(r *Registry) MapperFunc {
 			t := ctx.Scan.Pop()
 			// If decoding a flag, we need an argument.
 			if t.IsEOL() {
-				return errors.Errorf("unexpected EOL")
+				return errors.New("unexpected EOL")
 			}
 			switch v := t.Value.(type) {
 			case string:
@@ -541,13 +540,13 @@ func sliceDecoder(r *Registry) MapperFunc {
 		}
 		childDecoder := r.ForNamedType(ctx.Value.Tag.Type, el)
 		if childDecoder == nil {
-			return errors.Errorf("no mapper for element type of %s", target.Type())
+			return fmt.Errorf("no mapper for element type of %s", target.Type())
 		}
 		for !childScanner.Peek().IsEOL() {
 			childValue := reflect.New(el).Elem()
 			err := childDecoder.Decode(ctx.WithScanner(childScanner), childValue)
 			if err != nil {
-				return errors.WithStack(err)
+				return err
 			}
 			target.Set(reflect.Append(target, childValue))
 		}
@@ -561,7 +560,7 @@ func pathMapper(r *Registry) MapperFunc {
 			return sliceDecoder(r)(ctx, target)
 		}
 		if target.Kind() != reflect.String {
-			return errors.Errorf("\"path\" type must be applied to a string not %s", target.Type())
+			return fmt.Errorf("\"path\" type must be applied to a string not %s", target.Type())
 		}
 		var path string
 		err := ctx.Scan.PopValueInto("file", &path)
@@ -607,7 +606,7 @@ func existingFileMapper(r *Registry) MapperFunc {
 			return sliceDecoder(r)(ctx, target)
 		}
 		if target.Kind() != reflect.String {
-			return errors.Errorf("\"existingfile\" type must be applied to a string not %s", target.Type())
+			return fmt.Errorf("\"existingfile\" type must be applied to a string not %s", target.Type())
 		}
 		var path string
 		err := ctx.Scan.PopValueInto("file", &path)
@@ -621,7 +620,7 @@ func existingFileMapper(r *Registry) MapperFunc {
 				return err
 			}
 			if stat.IsDir() {
-				return errors.Errorf("%q exists but is a directory", path)
+				return fmt.Errorf("%q exists but is a directory", path)
 			}
 		}
 		target.SetString(path)
@@ -635,7 +634,7 @@ func existingDirMapper(r *Registry) MapperFunc {
 			return sliceDecoder(r)(ctx, target)
 		}
 		if target.Kind() != reflect.String {
-			return errors.Errorf("\"existingdir\" must be applied to a string not %s", target.Type())
+			return fmt.Errorf("\"existingdir\" must be applied to a string not %s", target.Type())
 		}
 		var path string
 		err := ctx.Scan.PopValueInto("file", &path)
@@ -648,7 +647,7 @@ func existingDirMapper(r *Registry) MapperFunc {
 			return err
 		}
 		if !stat.IsDir() {
-			return errors.Errorf("%q exists but is not a directory", path)
+			return fmt.Errorf("%q exists but is not a directory", path)
 		}
 		target.SetString(path)
 		return nil
@@ -666,7 +665,7 @@ func counterMapper() MapperFunc {
 			case string:
 				n, err := strconv.ParseInt(v, 10, 64)
 				if err != nil {
-					return errors.Errorf("expected a counter but got %q (%T)", t, t.Value)
+					return fmt.Errorf("expected a counter but got %q (%T)", t, t.Value)
 				}
 				target.SetInt(n)
 
@@ -674,7 +673,7 @@ func counterMapper() MapperFunc {
 				target.Set(reflect.ValueOf(v))
 
 			default:
-				return errors.Errorf("expected a counter but got %q (%T)", t, t.Value)
+				return fmt.Errorf("expected a counter but got %q (%T)", t, t.Value)
 			}
 			return nil
 		}
@@ -690,7 +689,7 @@ func counterMapper() MapperFunc {
 			target.SetFloat(target.Float() + 1)
 
 		default:
-			return errors.Errorf("type:\"counter\" must be used with a numeric field")
+			return fmt.Errorf("type:\"counter\" must be used with a numeric field")
 		}
 		return nil
 	}
@@ -705,7 +704,7 @@ func urlMapper() MapperFunc {
 		}
 		url, err := url.Parse(urlStr)
 		if err != nil {
-			return errors.WithStack(err)
+			return err
 		}
 		target.Set(reflect.ValueOf(url))
 		return nil
@@ -775,7 +774,7 @@ func (f *NamedFileContentFlag) Decode(ctx *DecodeContext) error { // nolint: rev
 	filename = ExpandPath(filename)
 	data, err := ioutil.ReadFile(filename) // nolint: gosec
 	if err != nil {
-		return errors.Errorf("failed to open %q: %s", filename, err)
+		return fmt.Errorf("failed to open %q: %v", filename, err)
 	}
 	f.Contents = data
 	f.Filename = filename
@@ -799,7 +798,7 @@ func (f *FileContentFlag) Decode(ctx *DecodeContext) error { // nolint: revive
 	filename = ExpandPath(filename)
 	data, err := ioutil.ReadFile(filename) // nolint: gosec
 	if err != nil {
-		return errors.Errorf("failed to open %q: %s", filename, err)
+		return fmt.Errorf("failed to open %q: %v", filename, err)
 	}
 	*f = data
 	return nil
@@ -808,7 +807,10 @@ func (f *FileContentFlag) Decode(ctx *DecodeContext) error { // nolint: revive
 func jsonTranscode(in, out interface{}) error {
 	data, err := json.Marshal(in)
 	if err != nil {
-		return errors.WithStack(err)
+		return err
+	}
+	if err = json.Unmarshal(data, out); err != nil {
+		return fmt.Errorf("%#v -> %T: %v", in, out, err)
 	}
-	return errors.Wrapf(json.Unmarshal(data, out), "%#v -> %T", in, out)
+	return nil
 }
diff --git a/model.go b/model.go
index 1013d93..af2cdaf 100644
--- a/model.go
+++ b/model.go
@@ -7,8 +7,6 @@ import (
 	"reflect"
 	"strconv"
 	"strings"
-
-	"github.com/pkg/errors"
 )
 
 // A Visitable component in the model.
@@ -257,6 +255,16 @@ func (v *Value) EnumMap() map[string]bool {
 	return out
 }
 
+// EnumSlice returns a slice of the enums in this value.
+func (v *Value) EnumSlice() []string {
+	parts := strings.Split(v.Enum, ",")
+	out := make([]string, len(parts))
+	for i, part := range parts {
+		out[i] = strings.TrimSpace(part)
+	}
+	return out
+}
+
 // ShortSummary returns a human-readable summary of the value, not including any placeholders/defaults.
 func (v *Value) ShortSummary() string {
 	if v.Flag != nil {
@@ -325,7 +333,7 @@ func (v *Value) Parse(scan *Scanner, target reflect.Value) (err error) {
 	}
 	err = v.Mapper.Decode(&DecodeContext{Value: v, Scan: scan}, target)
 	if err != nil {
-		return errors.Wrap(err, v.ShortSummary())
+		return fmt.Errorf("%s: %w", v.ShortSummary(), err)
 	}
 	v.Set = true
 	return nil
diff --git a/options.go b/options.go
index 4c240ed..02d33fd 100644
--- a/options.go
+++ b/options.go
@@ -1,6 +1,8 @@
 package kong
 
 import (
+	"errors"
+	"fmt"
 	"io"
 	"os"
 	"os/user"
@@ -8,8 +10,6 @@ import (
 	"reflect"
 	"regexp"
 	"strings"
-
-	"github.com/pkg/errors"
 )
 
 // An Option applies optional changes to the Kong application.
@@ -240,6 +240,21 @@ func ConfigureHelp(options HelpOptions) Option {
 	})
 }
 
+// AutoGroup automatically assigns groups to flags.
+func AutoGroup(format func(parent Visitable, flag *Flag) *Group) Option {
+	return PostBuild(func(kong *Kong) error {
+		parents := []Visitable{kong.Model}
+		return Visit(kong.Model, func(node Visitable, next Next) error {
+			if flag, ok := node.(*Flag); ok && flag.Group == nil {
+				flag.Group = format(parents[len(parents)-1], flag)
+			}
+			parents = append(parents, node)
+			defer func() { parents = parents[:len(parents)-1] }()
+			return next(nil)
+		})
+	})
+}
+
 // Groups associates `group` field tags with group metadata.
 //
 // This option is used to simplify Kong tags while providing
@@ -329,7 +344,7 @@ func IgnoreFields(regexes ...string) Option {
 
 			re, err := regexp.Compile(r)
 			if err != nil {
-				return errors.Wrap(err, "unable to compile regex")
+				return fmt.Errorf("unable to compile regex: %v", err)
 			}
 
 			k.ignoreFields = append(k.ignoreFields, re)
@@ -365,7 +380,7 @@ func Configuration(loader ConfigurationLoader, paths ...string) Option {
 
 			resolver, err := k.LoadConfig(path)
 			if err != nil {
-				return errors.Wrap(err, path)
+				return fmt.Errorf("%s: %v", path, err)
 			}
 			if resolver != nil {
 				k.resolvers = append(k.resolvers, resolver)


Test patch:
diff --git a/help_test.go b/help_test.go
index e0e9043..2018b0f 100644
--- a/help_test.go
+++ b/help_test.go
@@ -494,6 +494,81 @@ func TestCustomValueFormatter(t *testing.T) {
 	require.Contains(t, w.String(), "A flag.")
 }
 
+func TestAutoGroup(t *testing.T) {
+	var cli struct {
+		GroupedAString string `help:"A string flag grouped in A."`
+		FreeString     string `help:"A non grouped string flag."`
+		GroupedBString string `help:"A string flag grouped in B."`
+		FreeBool       bool   `help:"A non grouped bool flag."`
+		GroupedABool   bool   `help:"A bool flag grouped in A."`
+
+		One struct {
+			Flag string `help:"Nested flag."`
+			// Group is inherited from the parent command
+			Thing struct {
+				Arg string `arg help:"argument"`
+			} `cmd help:"subcommand thing"`
+			Other struct {
+				Other string `arg help:"other arg"`
+			} `arg help:"subcommand other"`
+			// ... but a subcommand can override it
+			Stuff struct {
+				Stuff string `arg help:"argument"`
+			} `arg help:"subcommand stuff"`
+		} `cmd help:"A subcommand grouped in A."`
+
+		Two struct {
+			Grouped1String  string `help:"A string flag grouped in 1."`
+			AFreeString     string `help:"A non grouped string flag."`
+			Grouped2String  string `help:"A string flag grouped in 2."`
+			AGroupedAString bool   `help:"A string flag grouped in A."`
+			Grouped1Bool    bool   `help:"A bool flag grouped in 1."`
+		} `cmd help:"A non grouped subcommand."`
+
+		Four struct {
+			Flag string `help:"Nested flag."`
+		} `cmd help:"Another subcommand grouped in B."`
+
+		Three struct {
+			Flag string `help:"Nested flag."`
+		} `cmd help:"Another subcommand grouped in A."`
+	}
+	w := bytes.NewBuffer(nil)
+	app := mustNew(t, &cli,
+		kong.Writers(w, w),
+		kong.Exit(func(int) {}),
+		kong.AutoGroup(func(parent kong.Visitable, flag *kong.Flag) *kong.Group {
+			if node, ok := parent.(*kong.Node); ok {
+				return &kong.Group{
+					Key:   node.Name,
+					Title: strings.Title(node.Name) + " flags:",
+				}
+			}
+			return nil
+		}),
+	)
+	_, _ = app.Parse([]string{"--help", "two"})
+	require.Equal(t, `Usage: test two
+
+A non grouped subcommand.
+
+Flags:
+  -h, --help                       Show context-sensitive help.
+      --grouped-a-string=STRING    A string flag grouped in A.
+      --free-string=STRING         A non grouped string flag.
+      --grouped-b-string=STRING    A string flag grouped in B.
+      --free-bool                  A non grouped bool flag.
+      --grouped-a-bool             A bool flag grouped in A.
+
+Two flags:
+  --grouped-1-string=STRING    A string flag grouped in 1.
+  --a-free-string=STRING       A non grouped string flag.
+  --grouped-2-string=STRING    A string flag grouped in 2.
+  --a-grouped-a-string         A string flag grouped in A.
+  --grouped-1-bool             A bool flag grouped in 1.
+`, w.String())
+}
+
 func TestHelpGrouping(t *testing.T) {
 	var cli struct {
 		GroupedAString string `help:"A string flag grouped in A." group:"Group A"`
diff --git a/kong_test.go b/kong_test.go
index e7568b2..6ce73f5 100644
--- a/kong_test.go
+++ b/kong_test.go
@@ -2,11 +2,11 @@ package kong_test
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"strings"
 	"testing"
 
-	"github.com/pkg/errors"
 	"github.com/stretchr/testify/require"
 
 	"github.com/alecthomas/kong"
@@ -663,6 +663,7 @@ func TestInterpolationIntoModel(t *testing.T) {
 	require.Equal(t, "Some default value.", flag.Default)
 	require.Equal(t, "Help, I need chickens!", flag.Help)
 	require.Equal(t, map[string]bool{"a": true, "b": true, "c": true, "d": true}, flag.EnumMap())
+	require.Equal(t, []string{"a", "b", "c", "d"}, flag.EnumSlice())
 	require.Equal(t, "One of a,b", flag2.Help)
 	require.Equal(t, "SAVE_THE_QUEEN", flag3.Env)
 	require.Equal(t, "God SAVE_THE_QUEEN", flag3.Help)
@@ -787,6 +788,14 @@ func TestEnum(t *testing.T) {
 	require.EqualError(t, err, "--flag must be one of \"a\",\"b\",\"c\" but got \"d\"")
 }
 
+func TestEnumMeaningfulOrder(t *testing.T) {
+	var cli struct {
+		Flag string `enum:"first,second,third,fourth,fifth" required:""`
+	}
+	_, err := mustNew(t, &cli).Parse([]string{"--flag", "sixth"})
+	require.EqualError(t, err, "--flag must be one of \"first\",\"second\",\"third\",\"fourth\",\"fifth\" but got \"sixth\"")
+}
+
 type commandWithHook struct {
 	value string
 }