New Upstream Release - golang-github-kelseyhightower-envconfig-dev

Ready changes

Summary

Merged new upstream version: 1.4.0 (was: 1.3.0).

Resulting package

Built on 2022-09-29T23:55 (took 4m16s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases golang-github-kelseyhightower-envconfig-dev

Lintian Result

Diff

diff --git a/.travis.yml b/.travis.yml
index e15301a..04b97ae 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,13 @@
 language: go
 
 go:
-  - 1.4
-  - 1.5
-  - 1.6
+  - 1.4.x
+  - 1.5.x
+  - 1.6.x
+  - 1.7.x
+  - 1.8.x
+  - 1.9.x
+  - 1.10.x
+  - 1.11.x
+  - 1.12.x
   - tip
diff --git a/README.md b/README.md
index b6c65a8..33408d6 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # envconfig
 
-[![Build Status](https://travis-ci.org/kelseyhightower/envconfig.png)](https://travis-ci.org/kelseyhightower/envconfig)
+[![Build Status](https://travis-ci.org/kelseyhightower/envconfig.svg)](https://travis-ci.org/kelseyhightower/envconfig)
 
 ```Go
 import "github.com/kelseyhightower/envconfig"
@@ -54,7 +54,7 @@ func main() {
         log.Fatal(err.Error())
     }
     format := "Debug: %v\nPort: %d\nUser: %s\nRate: %f\nTimeout: %s\n"
-    _, err = fmt.Printf(format, s.Debug, s.Port, s.User, s.Rate)
+    _, err = fmt.Printf(format, s.Debug, s.Port, s.User, s.Rate, s.Timeout)
     if err != nil {
         log.Fatal(err.Error())
     }
@@ -103,6 +103,7 @@ type Specification struct {
     RequiredVar     string `required:"true"`
     IgnoredVar      string `ignored:"true"`
     AutoSplitVar    string `split_words:"true"`
+    RequiredAndAutoSplitVar    string `required:"true" split_words:"true"`
 }
 ```
 
@@ -128,7 +129,8 @@ If envconfig can't find an environment variable value for `MYAPP_DEFAULTVAR`,
 it will populate it with "foobar" as a default value.
 
 If envconfig can't find an environment variable value for `MYAPP_REQUIREDVAR`,
-it will return an error when asked to process the struct.
+it will return an error when asked to process the struct.  If
+`MYAPP_REQUIREDVAR` is present but empty, envconfig will not return an error.
 
 If envconfig can't find an environment variable in the form `PREFIX_MYVAR`, and there
 is a struct tag defined, it will try to populate your variable with an environment
@@ -150,7 +152,7 @@ environment variable is set.
 
 ## Supported Struct Field Types
 
-envconfig supports supports these struct field types:
+envconfig supports these struct field types:
 
   * string
   * int8, int16, int32, int64
@@ -159,6 +161,8 @@ envconfig supports supports these struct field types:
   * slices of any supported type
   * maps (keys and values of any supported type)
   * [encoding.TextUnmarshaler](https://golang.org/pkg/encoding/#TextUnmarshaler)
+  * [encoding.BinaryUnmarshaler](https://golang.org/pkg/encoding/#BinaryUnmarshaler)
+  * [time.Duration](https://golang.org/pkg/time/#Duration)
 
 Embedded structs using these fields are also supported.
 
diff --git a/debian/changelog b/debian/changelog
index f853c9d..538b891 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+golang-github-kelseyhightower-envconfig-dev (1.4.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Thu, 29 Sep 2022 23:51:00 -0000
+
 golang-github-kelseyhightower-envconfig-dev (1.3.0-2) unstable; urgency=medium
 
   * Build with golang-any and drop dependency on golang-go
diff --git a/env_os.go b/env_os.go
index a6a014a..eba07a6 100644
--- a/env_os.go
+++ b/env_os.go
@@ -1,4 +1,4 @@
-// +build appengine
+// +build appengine go1.5
 
 package envconfig
 
diff --git a/env_syscall.go b/env_syscall.go
index 9d98085..4254540 100644
--- a/env_syscall.go
+++ b/env_syscall.go
@@ -1,4 +1,4 @@
-// +build !appengine
+// +build !appengine,!go1.5
 
 package envconfig
 
diff --git a/envconfig.go b/envconfig.go
index 892d746..3f16108 100644
--- a/envconfig.go
+++ b/envconfig.go
@@ -8,6 +8,7 @@ import (
 	"encoding"
 	"errors"
 	"fmt"
+	"os"
 	"reflect"
 	"regexp"
 	"strconv"
@@ -18,6 +19,9 @@ import (
 // ErrInvalidSpecification indicates that a specification is of the wrong type.
 var ErrInvalidSpecification = errors.New("specification must be a struct pointer")
 
+var gatherRegexp = regexp.MustCompile("([^A-Z]+|[A-Z]+[^A-Z]+|[A-Z]+)")
+var acronymRegexp = regexp.MustCompile("([A-Z]+)([A-Z][^A-Z]+)")
+
 // A ParseError occurs when an environment variable cannot be converted to
 // the type required by a struct field during assignment.
 type ParseError struct {
@@ -55,7 +59,6 @@ type varInfo struct {
 
 // GatherInfo gathers information about the specified struct
 func gatherInfo(prefix string, spec interface{}) ([]varInfo, error) {
-	expr := regexp.MustCompile("([^A-Z]+|[A-Z][^A-Z]+|[A-Z]+)")
 	s := reflect.ValueOf(spec)
 
 	if s.Kind() != reflect.Ptr {
@@ -72,7 +75,7 @@ func gatherInfo(prefix string, spec interface{}) ([]varInfo, error) {
 	for i := 0; i < s.NumField(); i++ {
 		f := s.Field(i)
 		ftype := typeOfSpec.Field(i)
-		if !f.CanSet() || ftype.Tag.Get("ignored") == "true" {
+		if !f.CanSet() || isTrue(ftype.Tag.Get("ignored")) {
 			continue
 		}
 
@@ -100,12 +103,16 @@ func gatherInfo(prefix string, spec interface{}) ([]varInfo, error) {
 		info.Key = info.Name
 
 		// Best effort to un-pick camel casing as separate words
-		if ftype.Tag.Get("split_words") == "true" {
-			words := expr.FindAllStringSubmatch(ftype.Name, -1)
+		if isTrue(ftype.Tag.Get("split_words")) {
+			words := gatherRegexp.FindAllStringSubmatch(ftype.Name, -1)
 			if len(words) > 0 {
 				var name []string
 				for _, words := range words {
-					name = append(name, words[0])
+					if m := acronymRegexp.FindStringSubmatch(words[0]); len(m) == 3 {
+						name = append(name, m[1], m[2])
+					} else {
+						name = append(name, words[0])
+					}
 				}
 
 				info.Key = strings.Join(name, "_")
@@ -122,7 +129,7 @@ func gatherInfo(prefix string, spec interface{}) ([]varInfo, error) {
 
 		if f.Kind() == reflect.Struct {
 			// honor Decode if present
-			if decoderFrom(f) == nil && setterFrom(f) == nil && textUnmarshaler(f) == nil {
+			if decoderFrom(f) == nil && setterFrom(f) == nil && textUnmarshaler(f) == nil && binaryUnmarshaler(f) == nil {
 				innerPrefix := prefix
 				if !ftype.Anonymous {
 					innerPrefix = info.Key
@@ -142,6 +149,37 @@ func gatherInfo(prefix string, spec interface{}) ([]varInfo, error) {
 	return infos, nil
 }
 
+// CheckDisallowed checks that no environment variables with the prefix are set
+// that we don't know how or want to parse. This is likely only meaningful with
+// a non-empty prefix.
+func CheckDisallowed(prefix string, spec interface{}) error {
+	infos, err := gatherInfo(prefix, spec)
+	if err != nil {
+		return err
+	}
+
+	vars := make(map[string]struct{})
+	for _, info := range infos {
+		vars[info.Key] = struct{}{}
+	}
+
+	if prefix != "" {
+		prefix = strings.ToUpper(prefix) + "_"
+	}
+
+	for _, env := range os.Environ() {
+		if !strings.HasPrefix(env, prefix) {
+			continue
+		}
+		v := strings.SplitN(env, "=", 2)[0]
+		if _, found := vars[v]; !found {
+			return fmt.Errorf("unknown environment variable %s", v)
+		}
+	}
+
+	return nil
+}
+
 // Process populates the specified struct based on environment variables
 func Process(prefix string, spec interface{}) error {
 	infos, err := gatherInfo(prefix, spec)
@@ -164,13 +202,17 @@ func Process(prefix string, spec interface{}) error {
 
 		req := info.Tags.Get("required")
 		if !ok && def == "" {
-			if req == "true" {
-				return fmt.Errorf("required key %s missing value", info.Key)
+			if isTrue(req) {
+				key := info.Key
+				if info.Alt != "" {
+					key = info.Alt
+				}
+				return fmt.Errorf("required key %s missing value", key)
 			}
 			continue
 		}
 
-		err := processField(value, info.Field)
+		err = processField(value, info.Field)
 		if err != nil {
 			return &ParseError{
 				KeyName:   info.Key,
@@ -209,6 +251,10 @@ func processField(value string, field reflect.Value) error {
 		return t.UnmarshalText([]byte(value))
 	}
 
+	if b := binaryUnmarshaler(field); b != nil {
+		return b.UnmarshalBinary([]byte(value))
+	}
+
 	if typ.Kind() == reflect.Ptr {
 		typ = typ.Elem()
 		if field.IsNil() {
@@ -256,34 +302,41 @@ func processField(value string, field reflect.Value) error {
 		}
 		field.SetFloat(val)
 	case reflect.Slice:
-		vals := strings.Split(value, ",")
-		sl := reflect.MakeSlice(typ, len(vals), len(vals))
-		for i, val := range vals {
-			err := processField(val, sl.Index(i))
-			if err != nil {
-				return err
+		sl := reflect.MakeSlice(typ, 0, 0)
+		if typ.Elem().Kind() == reflect.Uint8 {
+			sl = reflect.ValueOf([]byte(value))
+		} else if len(strings.TrimSpace(value)) != 0 {
+			vals := strings.Split(value, ",")
+			sl = reflect.MakeSlice(typ, len(vals), len(vals))
+			for i, val := range vals {
+				err := processField(val, sl.Index(i))
+				if err != nil {
+					return err
+				}
 			}
 		}
 		field.Set(sl)
 	case reflect.Map:
-		pairs := strings.Split(value, ",")
 		mp := reflect.MakeMap(typ)
-		for _, pair := range pairs {
-			kvpair := strings.Split(pair, ":")
-			if len(kvpair) != 2 {
-				return fmt.Errorf("invalid map item: %q", pair)
-			}
-			k := reflect.New(typ.Key()).Elem()
-			err := processField(kvpair[0], k)
-			if err != nil {
-				return err
-			}
-			v := reflect.New(typ.Elem()).Elem()
-			err = processField(kvpair[1], v)
-			if err != nil {
-				return err
+		if len(strings.TrimSpace(value)) != 0 {
+			pairs := strings.Split(value, ",")
+			for _, pair := range pairs {
+				kvpair := strings.Split(pair, ":")
+				if len(kvpair) != 2 {
+					return fmt.Errorf("invalid map item: %q", pair)
+				}
+				k := reflect.New(typ.Key()).Elem()
+				err := processField(kvpair[0], k)
+				if err != nil {
+					return err
+				}
+				v := reflect.New(typ.Elem()).Elem()
+				err = processField(kvpair[1], v)
+				if err != nil {
+					return err
+				}
+				mp.SetMapIndex(k, v)
 			}
-			mp.SetMapIndex(k, v)
 		}
 		field.Set(mp)
 	}
@@ -317,3 +370,13 @@ func textUnmarshaler(field reflect.Value) (t encoding.TextUnmarshaler) {
 	interfaceFrom(field, func(v interface{}, ok *bool) { t, *ok = v.(encoding.TextUnmarshaler) })
 	return t
 }
+
+func binaryUnmarshaler(field reflect.Value) (b encoding.BinaryUnmarshaler) {
+	interfaceFrom(field, func(v interface{}, ok *bool) { b, *ok = v.(encoding.BinaryUnmarshaler) })
+	return b
+}
+
+func isTrue(s string) bool {
+	b, _ := strconv.ParseBool(s)
+	return b
+}
diff --git a/envconfig_1.8_test.go b/envconfig_1.8_test.go
new file mode 100644
index 0000000..8dfcc6c
--- /dev/null
+++ b/envconfig_1.8_test.go
@@ -0,0 +1,68 @@
+// +build go1.8
+
+package envconfig
+
+import (
+	"errors"
+	"net/url"
+	"os"
+	"testing"
+)
+
+type SpecWithURL struct {
+	UrlValue   url.URL
+	UrlPointer *url.URL
+}
+
+func TestParseURL(t *testing.T) {
+	var s SpecWithURL
+
+	os.Clearenv()
+	os.Setenv("ENV_CONFIG_URLVALUE", "https://github.com/kelseyhightower/envconfig")
+	os.Setenv("ENV_CONFIG_URLPOINTER", "https://github.com/kelseyhightower/envconfig")
+
+	err := Process("env_config", &s)
+	if err != nil {
+		t.Fatal("unexpected error:", err)
+	}
+
+	u, err := url.Parse("https://github.com/kelseyhightower/envconfig")
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	if s.UrlValue != *u {
+		t.Errorf("expected %q, got %q", u, s.UrlValue.String())
+	}
+
+	if *s.UrlPointer != *u {
+		t.Errorf("expected %q, got %q", u, s.UrlPointer)
+	}
+}
+
+func TestParseURLError(t *testing.T) {
+	var s SpecWithURL
+
+	os.Clearenv()
+	os.Setenv("ENV_CONFIG_URLPOINTER", "http_://foo")
+
+	err := Process("env_config", &s)
+
+	v, ok := err.(*ParseError)
+	if !ok {
+		t.Fatalf("expected ParseError, got %T %v", err, err)
+	}
+	if v.FieldName != "UrlPointer" {
+		t.Errorf("expected %s, got %v", "UrlPointer", v.FieldName)
+	}
+
+	expectedUnerlyingError := url.Error{
+		Op:  "parse",
+		URL: "http_://foo",
+		Err: errors.New("first path segment in URL cannot contain colon"),
+	}
+
+	if v.Err.Error() != expectedUnerlyingError.Error() {
+		t.Errorf("expected %q, got %q", expectedUnerlyingError, v.Err)
+	}
+}
diff --git a/envconfig_test.go b/envconfig_test.go
index e754058..2ca765d 100644
--- a/envconfig_test.go
+++ b/envconfig_test.go
@@ -7,7 +7,9 @@ package envconfig
 import (
 	"flag"
 	"fmt"
+	"net/url"
 	"os"
+	"strings"
 	"testing"
 	"time"
 )
@@ -21,6 +23,16 @@ func (h *HonorDecodeInStruct) Decode(env string) error {
 	return nil
 }
 
+type CustomURL struct {
+	Value *url.URL
+}
+
+func (cu *CustomURL) UnmarshalBinary(data []byte) error {
+	u, err := url.Parse(string(data))
+	cu.Value = u
+	return err
+}
+
 type Specification struct {
 	Embedded                     `desc:"can we document a struct"`
 	EmbeddedButIgnored           `ignored:"true"`
@@ -32,16 +44,19 @@ type Specification struct {
 	Timeout                      time.Duration
 	AdminUsers                   []string
 	MagicNumbers                 []int
+	EmptyNumbers                 []int
+	ByteSlice                    []byte
 	ColorCodes                   map[string]int
 	MultiWordVar                 string
 	MultiWordVarWithAutoSplit    uint32 `split_words:"true"`
+	MultiWordACRWithAutoSplit    uint32 `split_words:"true"`
 	SomePointer                  *string
 	SomePointerWithDefault       *string `default:"foo2baz" desc:"foorbar is the word"`
 	MultiWordVarWithAlt          string  `envconfig:"MULTI_WORD_VAR_WITH_ALT" desc:"what alt"`
 	MultiWordVarWithLowerCaseAlt string  `envconfig:"multi_word_var_with_lower_case_alt"`
 	NoPrefixWithAlt              string  `envconfig:"SERVICE_HOST"`
 	DefaultVar                   string  `default:"foobar"`
-	RequiredVar                  string  `required:"true"`
+	RequiredVar                  string  `required:"True"`
 	NoPrefixDefault              string  `envconfig:"BROKER" default:"127.0.0.1"`
 	RequiredDefault              string  `required:"true" default:"foo2bar"`
 	Ignored                      string  `ignored:"true"`
@@ -52,6 +67,9 @@ type Specification struct {
 	AfterNested  string
 	DecodeStruct HonorDecodeInStruct `envconfig:"honor"`
 	Datetime     time.Time
+	MapField     map[string]string `default:"one:two,three:four"`
+	UrlValue     CustomURL
+	UrlPointer   *CustomURL
 }
 
 type Embedded struct {
@@ -78,6 +96,8 @@ func TestProcess(t *testing.T) {
 	os.Setenv("ENV_CONFIG_TIMEOUT", "2m")
 	os.Setenv("ENV_CONFIG_ADMINUSERS", "John,Adam,Will")
 	os.Setenv("ENV_CONFIG_MAGICNUMBERS", "5,10,20")
+	os.Setenv("ENV_CONFIG_EMPTYNUMBERS", "")
+	os.Setenv("ENV_CONFIG_BYTESLICE", "this is a test value")
 	os.Setenv("ENV_CONFIG_COLORCODES", "red:1,green:2,blue:3")
 	os.Setenv("SERVICE_HOST", "127.0.0.1")
 	os.Setenv("ENV_CONFIG_TTL", "30")
@@ -88,6 +108,9 @@ func TestProcess(t *testing.T) {
 	os.Setenv("ENV_CONFIG_HONOR", "honor")
 	os.Setenv("ENV_CONFIG_DATETIME", "2016-08-16T18:57:05Z")
 	os.Setenv("ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT", "24")
+	os.Setenv("ENV_CONFIG_MULTI_WORD_ACR_WITH_AUTO_SPLIT", "25")
+	os.Setenv("ENV_CONFIG_URLVALUE", "https://github.com/kelseyhightower/envconfig")
+	os.Setenv("ENV_CONFIG_URLPOINTER", "https://github.com/kelseyhightower/envconfig")
 	err := Process("env_config", &s)
 	if err != nil {
 		t.Error(err.Error())
@@ -128,6 +151,13 @@ func TestProcess(t *testing.T) {
 		s.MagicNumbers[2] != 20 {
 		t.Errorf("expected %#v, got %#v", []int{5, 10, 20}, s.MagicNumbers)
 	}
+	if len(s.EmptyNumbers) != 0 {
+		t.Errorf("expected %#v, got %#v", []int{}, s.EmptyNumbers)
+	}
+	expected := "this is a test value"
+	if string(s.ByteSlice) != expected {
+		t.Errorf("expected %v, got %v", expected, string(s.ByteSlice))
+	}
 	if s.Ignored != "" {
 		t.Errorf("expected empty string, got %#v", s.Ignored)
 	}
@@ -170,6 +200,23 @@ func TestProcess(t *testing.T) {
 	if s.MultiWordVarWithAutoSplit != 24 {
 		t.Errorf("expected %q, got %q", 24, s.MultiWordVarWithAutoSplit)
 	}
+
+	if s.MultiWordACRWithAutoSplit != 25 {
+		t.Errorf("expected %d, got %d", 25, s.MultiWordACRWithAutoSplit)
+	}
+
+	u, err := url.Parse("https://github.com/kelseyhightower/envconfig")
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	if *s.UrlValue.Value != *u {
+		t.Errorf("expected %q, got %q", u, s.UrlValue.Value.String())
+	}
+
+	if *s.UrlPointer.Value != *u {
+		t.Errorf("expected %q, got %q", u, s.UrlPointer.Value.String())
+	}
 }
 
 func TestParseErrorBool(t *testing.T) {
@@ -326,6 +373,16 @@ func TestRequiredVar(t *testing.T) {
 	}
 }
 
+func TestRequiredMissing(t *testing.T) {
+	var s Specification
+	os.Clearenv()
+
+	err := Process("env_config", &s)
+	if err == nil {
+		t.Error("no failure when missing required variable")
+	}
+}
+
 func TestBlankDefaultVar(t *testing.T) {
 	var s Specification
 	os.Clearenv()
@@ -422,6 +479,24 @@ func TestPointerFieldBlank(t *testing.T) {
 	}
 }
 
+func TestEmptyMapFieldOverride(t *testing.T) {
+	var s Specification
+	os.Clearenv()
+	os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo")
+	os.Setenv("ENV_CONFIG_MAPFIELD", "")
+	if err := Process("env_config", &s); err != nil {
+		t.Error(err.Error())
+	}
+
+	if s.MapField == nil {
+		t.Error("expected empty map, got <nil>")
+	}
+
+	if len(s.MapField) != 0 {
+		t.Errorf("expected empty map, got map of size %d", len(s.MapField))
+	}
+}
+
 func TestMustProcess(t *testing.T) {
 	var s Specification
 	os.Clearenv()
@@ -640,7 +715,7 @@ func TestTextUnmarshalerError(t *testing.T) {
 		t.Errorf("expected ParseError, got %v", v)
 	}
 	if v.FieldName != "Datetime" {
-		t.Errorf("expected %s, got %v", "Debug", v.FieldName)
+		t.Errorf("expected %s, got %v", "Datetime", v.FieldName)
 	}
 
 	expectedLowLevelError := time.ParseError{
@@ -653,8 +728,84 @@ func TestTextUnmarshalerError(t *testing.T) {
 	if v.Err.Error() != expectedLowLevelError.Error() {
 		t.Errorf("expected %s, got %s", expectedLowLevelError, v.Err)
 	}
-	if s.Debug != false {
-		t.Errorf("expected %v, got %v", false, s.Debug)
+}
+
+func TestBinaryUnmarshalerError(t *testing.T) {
+	var s Specification
+	os.Clearenv()
+	os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo")
+	os.Setenv("ENV_CONFIG_URLPOINTER", "http://%41:8080/")
+
+	err := Process("env_config", &s)
+
+	v, ok := err.(*ParseError)
+	if !ok {
+		t.Fatalf("expected ParseError, got %T %v", err, err)
+	}
+	if v.FieldName != "UrlPointer" {
+		t.Errorf("expected %s, got %v", "UrlPointer", v.FieldName)
+	}
+
+	// To be compatible with go 1.5 and lower we should do a very basic check,
+	// because underlying error message varies in go 1.5 and go 1.6+.
+
+	ue, ok := v.Err.(*url.Error)
+	if !ok {
+		t.Errorf("expected error type to be \"*url.Error\", got %T", v.Err)
+	}
+
+	if ue.Op != "parse" {
+		t.Errorf("expected error op to be \"parse\", got %q", ue.Op)
+	}
+}
+
+func TestCheckDisallowedOnlyAllowed(t *testing.T) {
+	var s Specification
+	os.Clearenv()
+	os.Setenv("ENV_CONFIG_DEBUG", "true")
+	os.Setenv("UNRELATED_ENV_VAR", "true")
+	err := CheckDisallowed("env_config", &s)
+	if err != nil {
+		t.Errorf("expected no error, got %s", err)
+	}
+}
+
+func TestCheckDisallowedMispelled(t *testing.T) {
+	var s Specification
+	os.Clearenv()
+	os.Setenv("ENV_CONFIG_DEBUG", "true")
+	os.Setenv("ENV_CONFIG_ZEBUG", "false")
+	err := CheckDisallowed("env_config", &s)
+	if experr := "unknown environment variable ENV_CONFIG_ZEBUG"; err.Error() != experr {
+		t.Errorf("expected %s, got %s", experr, err)
+	}
+}
+
+func TestCheckDisallowedIgnored(t *testing.T) {
+	var s Specification
+	os.Clearenv()
+	os.Setenv("ENV_CONFIG_DEBUG", "true")
+	os.Setenv("ENV_CONFIG_IGNORED", "false")
+	err := CheckDisallowed("env_config", &s)
+	if experr := "unknown environment variable ENV_CONFIG_IGNORED"; err.Error() != experr {
+		t.Errorf("expected %s, got %s", experr, err)
+	}
+}
+
+func TestErrorMessageForRequiredAltVar(t *testing.T) {
+	var s struct {
+		Foo    string `envconfig:"BAR" required:"true"`
+	}
+
+	os.Clearenv()
+	err := Process("env_config", &s)
+
+	if err == nil {
+		t.Error("no failure when missing required variable")
+	}
+
+	if !strings.Contains(err.Error(), " BAR ") {
+		t.Errorf("expected error message to contain BAR, got \"%v\"", err)
 	}
 }
 
@@ -686,3 +837,28 @@ func (ss *setterStruct) Set(value string) error {
 	ss.Inner = fmt.Sprintf("setterstruct{%q}", value)
 	return nil
 }
+
+func BenchmarkGatherInfo(b *testing.B) {
+	os.Clearenv()
+	os.Setenv("ENV_CONFIG_DEBUG", "true")
+	os.Setenv("ENV_CONFIG_PORT", "8080")
+	os.Setenv("ENV_CONFIG_RATE", "0.5")
+	os.Setenv("ENV_CONFIG_USER", "Kelsey")
+	os.Setenv("ENV_CONFIG_TIMEOUT", "2m")
+	os.Setenv("ENV_CONFIG_ADMINUSERS", "John,Adam,Will")
+	os.Setenv("ENV_CONFIG_MAGICNUMBERS", "5,10,20")
+	os.Setenv("ENV_CONFIG_COLORCODES", "red:1,green:2,blue:3")
+	os.Setenv("SERVICE_HOST", "127.0.0.1")
+	os.Setenv("ENV_CONFIG_TTL", "30")
+	os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo")
+	os.Setenv("ENV_CONFIG_IGNORED", "was-not-ignored")
+	os.Setenv("ENV_CONFIG_OUTER_INNER", "iamnested")
+	os.Setenv("ENV_CONFIG_AFTERNESTED", "after")
+	os.Setenv("ENV_CONFIG_HONOR", "honor")
+	os.Setenv("ENV_CONFIG_DATETIME", "2016-08-16T18:57:05Z")
+	os.Setenv("ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT", "24")
+	for i := 0; i < b.N; i++ {
+		var s Specification
+		gatherInfo("env_config", &s)
+	}
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..1561d1e
--- /dev/null
+++ b/go.mod
@@ -0,0 +1 @@
+module github.com/kelseyhightower/envconfig
diff --git a/testdata/custom.txt b/testdata/custom.txt
index 243e82c..04d2f5d 100644
--- a/testdata/custom.txt
+++ b/testdata/custom.txt
@@ -11,9 +11,12 @@ ENV_CONFIG_TTL=
 ENV_CONFIG_TIMEOUT=
 ENV_CONFIG_ADMINUSERS=
 ENV_CONFIG_MAGICNUMBERS=
+ENV_CONFIG_EMPTYNUMBERS=
+ENV_CONFIG_BYTESLICE=
 ENV_CONFIG_COLORCODES=
 ENV_CONFIG_MULTIWORDVAR=
 ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT=
+ENV_CONFIG_MULTI_WORD_ACR_WITH_AUTO_SPLIT=
 ENV_CONFIG_SOMEPOINTER=
 ENV_CONFIG_SOMEPOINTERWITHDEFAULT=foorbar.is.the.word
 ENV_CONFIG_MULTI_WORD_VAR_WITH_ALT=what.alt
@@ -28,3 +31,6 @@ ENV_CONFIG_OUTER_PROPERTYWITHDEFAULT=
 ENV_CONFIG_AFTERNESTED=
 ENV_CONFIG_HONOR=
 ENV_CONFIG_DATETIME=
+ENV_CONFIG_MAPFIELD=
+ENV_CONFIG_URLVALUE=
+ENV_CONFIG_URLPOINTER=
diff --git a/testdata/default_list.txt b/testdata/default_list.txt
index bc29211..fb0eced 100644
--- a/testdata/default_list.txt
+++ b/testdata/default_list.txt
@@ -66,6 +66,16 @@ ENV_CONFIG_MAGICNUMBERS
 ..[type]........Comma-separated.list.of.Integer
 ..[default].....
 ..[required]....
+ENV_CONFIG_EMPTYNUMBERS
+..[description].
+..[type]........Comma-separated.list.of.Integer
+..[default].....
+..[required]....
+ENV_CONFIG_BYTESLICE
+..[description].
+..[type]........String
+..[default].....
+..[required]....
 ENV_CONFIG_COLORCODES
 ..[description].
 ..[type]........Comma-separated.list.of.String:Integer.pairs
@@ -81,6 +91,11 @@ ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT
 ..[type]........Unsigned.Integer
 ..[default].....
 ..[required]....
+ENV_CONFIG_MULTI_WORD_ACR_WITH_AUTO_SPLIT
+..[description].
+..[type]........Unsigned.Integer
+..[default].....
+..[required]....
 ENV_CONFIG_SOMEPOINTER
 ..[description].
 ..[type]........String
@@ -151,3 +166,18 @@ ENV_CONFIG_DATETIME
 ..[type]........Time
 ..[default].....
 ..[required]....
+ENV_CONFIG_MAPFIELD
+..[description].
+..[type]........Comma-separated.list.of.String:String.pairs
+..[default].....one:two,three:four
+..[required]....
+ENV_CONFIG_URLVALUE
+..[description].
+..[type]........CustomURL
+..[default].....
+..[required]....
+ENV_CONFIG_URLPOINTER
+..[description].
+..[type]........CustomURL
+..[default].....
+..[required]....
diff --git a/testdata/default_table.txt b/testdata/default_table.txt
index f3cf945..65c9b44 100644
--- a/testdata/default_table.txt
+++ b/testdata/default_table.txt
@@ -1,34 +1,40 @@
 This.application.is.configured.via.the.environment..The.following.environment
 variables.can.be.used:
 
-KEY..............................................TYPE............................................DEFAULT...........REQUIRED....DESCRIPTION
-ENV_CONFIG_ENABLED...............................True.or.False.................................................................some.embedded.value
-ENV_CONFIG_EMBEDDEDPORT..........................Integer.......................................................................
-ENV_CONFIG_MULTIWORDVAR..........................String........................................................................
-ENV_CONFIG_MULTI_WITH_DIFFERENT_ALT..............String........................................................................
-ENV_CONFIG_EMBEDDED_WITH_ALT.....................String........................................................................
-ENV_CONFIG_DEBUG.................................True.or.False.................................................................
-ENV_CONFIG_PORT..................................Integer.......................................................................
-ENV_CONFIG_RATE..................................Float.........................................................................
-ENV_CONFIG_USER..................................String........................................................................
-ENV_CONFIG_TTL...................................Unsigned.Integer..............................................................
-ENV_CONFIG_TIMEOUT...............................Duration......................................................................
-ENV_CONFIG_ADMINUSERS............................Comma-separated.list.of.String................................................
-ENV_CONFIG_MAGICNUMBERS..........................Comma-separated.list.of.Integer...............................................
-ENV_CONFIG_COLORCODES............................Comma-separated.list.of.String:Integer.pairs..................................
-ENV_CONFIG_MULTIWORDVAR..........................String........................................................................
-ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT........Unsigned.Integer..............................................................
-ENV_CONFIG_SOMEPOINTER...........................String........................................................................
-ENV_CONFIG_SOMEPOINTERWITHDEFAULT................String..........................................foo2baz.......................foorbar.is.the.word
-ENV_CONFIG_MULTI_WORD_VAR_WITH_ALT...............String........................................................................what.alt
-ENV_CONFIG_MULTI_WORD_VAR_WITH_LOWER_CASE_ALT....String........................................................................
-ENV_CONFIG_SERVICE_HOST..........................String........................................................................
-ENV_CONFIG_DEFAULTVAR............................String..........................................foobar........................
-ENV_CONFIG_REQUIREDVAR...........................String............................................................true........
-ENV_CONFIG_BROKER................................String..........................................127.0.0.1.....................
-ENV_CONFIG_REQUIREDDEFAULT.......................String..........................................foo2bar...........true........
-ENV_CONFIG_OUTER_INNER...........................String........................................................................
-ENV_CONFIG_OUTER_PROPERTYWITHDEFAULT.............String..........................................fuzzybydefault................
-ENV_CONFIG_AFTERNESTED...........................String........................................................................
-ENV_CONFIG_HONOR.................................HonorDecodeInStruct...........................................................
-ENV_CONFIG_DATETIME..............................Time..........................................................................
+KEY..............................................TYPE............................................DEFAULT...............REQUIRED....DESCRIPTION
+ENV_CONFIG_ENABLED...............................True.or.False.....................................................................some.embedded.value
+ENV_CONFIG_EMBEDDEDPORT..........................Integer...........................................................................
+ENV_CONFIG_MULTIWORDVAR..........................String............................................................................
+ENV_CONFIG_MULTI_WITH_DIFFERENT_ALT..............String............................................................................
+ENV_CONFIG_EMBEDDED_WITH_ALT.....................String............................................................................
+ENV_CONFIG_DEBUG.................................True.or.False.....................................................................
+ENV_CONFIG_PORT..................................Integer...........................................................................
+ENV_CONFIG_RATE..................................Float.............................................................................
+ENV_CONFIG_USER..................................String............................................................................
+ENV_CONFIG_TTL...................................Unsigned.Integer..................................................................
+ENV_CONFIG_TIMEOUT...............................Duration..........................................................................
+ENV_CONFIG_ADMINUSERS............................Comma-separated.list.of.String....................................................
+ENV_CONFIG_MAGICNUMBERS..........................Comma-separated.list.of.Integer...................................................
+ENV_CONFIG_EMPTYNUMBERS..........................Comma-separated.list.of.Integer...................................................
+ENV_CONFIG_BYTESLICE.............................String............................................................................
+ENV_CONFIG_COLORCODES............................Comma-separated.list.of.String:Integer.pairs......................................
+ENV_CONFIG_MULTIWORDVAR..........................String............................................................................
+ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT........Unsigned.Integer..................................................................
+ENV_CONFIG_MULTI_WORD_ACR_WITH_AUTO_SPLIT........Unsigned.Integer..................................................................
+ENV_CONFIG_SOMEPOINTER...........................String............................................................................
+ENV_CONFIG_SOMEPOINTERWITHDEFAULT................String..........................................foo2baz...........................foorbar.is.the.word
+ENV_CONFIG_MULTI_WORD_VAR_WITH_ALT...............String............................................................................what.alt
+ENV_CONFIG_MULTI_WORD_VAR_WITH_LOWER_CASE_ALT....String............................................................................
+ENV_CONFIG_SERVICE_HOST..........................String............................................................................
+ENV_CONFIG_DEFAULTVAR............................String..........................................foobar............................
+ENV_CONFIG_REQUIREDVAR...........................String................................................................true........
+ENV_CONFIG_BROKER................................String..........................................127.0.0.1.........................
+ENV_CONFIG_REQUIREDDEFAULT.......................String..........................................foo2bar...............true........
+ENV_CONFIG_OUTER_INNER...........................String............................................................................
+ENV_CONFIG_OUTER_PROPERTYWITHDEFAULT.............String..........................................fuzzybydefault....................
+ENV_CONFIG_AFTERNESTED...........................String............................................................................
+ENV_CONFIG_HONOR.................................HonorDecodeInStruct...............................................................
+ENV_CONFIG_DATETIME..............................Time..............................................................................
+ENV_CONFIG_MAPFIELD..............................Comma-separated.list.of.String:String.pairs.....one:two,three:four................
+ENV_CONFIG_URLVALUE..............................CustomURL.........................................................................
+ENV_CONFIG_URLPOINTER............................CustomURL.........................................................................
diff --git a/testdata/fault.txt b/testdata/fault.txt
index 30e28ce..b525ff1 100644
--- a/testdata/fault.txt
+++ b/testdata/fault.txt
@@ -28,3 +28,9 @@
 {.Key}
 {.Key}
 {.Key}
+{.Key}
+{.Key}
+{.Key}
+{.Key}
+{.Key}
+{.Key}
diff --git a/usage.go b/usage.go
index 1846353..1e6d0a8 100644
--- a/usage.go
+++ b/usage.go
@@ -27,7 +27,7 @@ variables can be used:
   [default]     {{usage_default .}}
   [required]    {{usage_required .}}{{end}}
 `
-	// DefaultTableFormat constant to use to display usage in a tabluar format
+	// DefaultTableFormat constant to use to display usage in a tabular format
 	DefaultTableFormat = `This application is configured via the environment. The following environment
 variables can be used:
 
@@ -37,9 +37,10 @@ KEY	TYPE	DEFAULT	REQUIRED	DESCRIPTION
 )
 
 var (
-	decoderType     = reflect.TypeOf((*Decoder)(nil)).Elem()
-	setterType      = reflect.TypeOf((*Setter)(nil)).Elem()
-	unmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
+	decoderType           = reflect.TypeOf((*Decoder)(nil)).Elem()
+	setterType            = reflect.TypeOf((*Setter)(nil)).Elem()
+	textUnmarshalerType   = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
+	binaryUnmarshalerType = reflect.TypeOf((*encoding.BinaryUnmarshaler)(nil)).Elem()
 )
 
 func implementsInterface(t reflect.Type) bool {
@@ -47,14 +48,19 @@ func implementsInterface(t reflect.Type) bool {
 		reflect.PtrTo(t).Implements(decoderType) ||
 		t.Implements(setterType) ||
 		reflect.PtrTo(t).Implements(setterType) ||
-		t.Implements(unmarshalerType) ||
-		reflect.PtrTo(t).Implements(unmarshalerType)
+		t.Implements(textUnmarshalerType) ||
+		reflect.PtrTo(t).Implements(textUnmarshalerType) ||
+		t.Implements(binaryUnmarshalerType) ||
+		reflect.PtrTo(t).Implements(binaryUnmarshalerType)
 }
 
 // toTypeDescription converts Go types into a human readable description
 func toTypeDescription(t reflect.Type) string {
 	switch t.Kind() {
 	case reflect.Array, reflect.Slice:
+		if t.Elem().Kind() == reflect.Uint8 {
+			return "String"
+		}
 		return fmt.Sprintf("Comma-separated list of %s", toTypeDescription(t.Elem()))
 	case reflect.Map:
 		return fmt.Sprintf(
@@ -103,7 +109,7 @@ func toTypeDescription(t reflect.Type) string {
 	return fmt.Sprintf("%+v", t)
 }
 
-// Usage writes usage information to stderr using the default header and table format
+// Usage writes usage information to stdout using the default header and table format
 func Usage(prefix string, spec interface{}) error {
 	// The default is to output the usage information as a table
 	// Create tabwriter instance to support table output

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/gocode/src/github.com/kelseyhightower/envconfig/envconfig_1.8_test.go
-rw-r--r--  root/root   /usr/share/gocode/src/github.com/kelseyhightower/envconfig/go.mod

No differences were encountered in the control files

More details

Full run details