Repository: caarlos0/env (go)

Issue:
Non-existing variable of type slice of structs is mapped to an empty slice instead of nil
Hey there

Thanks for your awesome library :+1: 

I wanted to update to v11.2.0 and I noticed that the recent change (https://github.com/caarlos0/env/pull/312) changed the semantics regarding slices of structs. The problem is that a non-existing of type `[]someStruct` is mapped to an empty slice instead of the prior behavior (as well as with slices of primitive types) where it is mapped to `nil`. 

Pull request description:
fix: do not init empty arrays
fix #320 

Patch:
diff --git a/LICENSE.md b/LICENSE.md
index 3a59b6b..2846340 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2015-2022 Carlos Alexandro Becker
+Copyright (c) 2015-2024 Carlos Alexandro Becker
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/env.go b/env.go
index 9585321..5b0be18 100644
--- a/env.go
+++ b/env.go
@@ -416,12 +416,14 @@ func doParseSlice(ref reflect.Value, processField processFieldFn, opts Options)
 			}
 		}
 
-		if reflect.Ptr == ref.Kind() {
-			resultPtr := reflect.New(sliceType)
-			resultPtr.Elem().Set(result)
-			result = resultPtr
+		if result.Len() > 0 {
+			if reflect.Ptr == ref.Kind() {
+				resultPtr := reflect.New(sliceType)
+				resultPtr.Elem().Set(result)
+				result = resultPtr
+			}
+			ref.Set(result)
 		}
-		ref.Set(result)
 	}
 
 	return nil
diff --git a/go.mod b/go.mod
index 215c28b..affcbd4 100644
--- a/go.mod
+++ b/go.mod
@@ -2,4 +2,6 @@ module github.com/caarlos0/env/v11
 
 retract v11.0.1 // v11.0.1 accidentally introduced a breaking change regarding the behavior of nil pointers. You can now chose to auto-initialize them by setting the `init` tag option.
 
+retract v11.2.0 // v11.2.0 accidentally introduced a breaking change regarding the behavior of nil slices of complex types.
+
 go 1.18


Test patch:
diff --git a/env_test.go b/env_test.go
index a24e2e1..1a567b4 100644
--- a/env_test.go
+++ b/env_test.go
@@ -1354,70 +1354,6 @@ func TestParseInvalidURL(t *testing.T) {
 	isTrue(t, errors.Is(err, ParseError{}))
 }
 
-func ExampleParse() {
-	type inner struct {
-		Foo string `env:"FOO" envDefault:"foobar"`
-	}
-	type config struct {
-		Home         string         `env:"HOME,required"`
-		Port         int            `env:"PORT" envDefault:"3000"`
-		IsProduction bool           `env:"PRODUCTION"`
-		TempFolder   string         `env:"TEMP_FOLDER,expand" envDefault:"${HOME}/.tmp"`
-		StringInts   map[string]int `env:"MAP_STRING_INT" envDefault:"k1:1,k2:2"`
-		Inner        inner
-	}
-	os.Setenv("HOME", "/tmp/fakehome")
-	var cfg config
-	if err := Parse(&cfg); err != nil {
-		fmt.Println("failed:", err)
-	}
-	fmt.Printf("%+v", cfg)
-	// Output:  {Home:/tmp/fakehome Port:3000 IsProduction:false TempFolder:/tmp/fakehome/.tmp StringInts:map[k1:1 k2:2] Inner:{Foo:foobar}}
-}
-
-func ExampleParse_onSet() {
-	type config struct {
-		Home         string `env:"HOME,required"`
-		Port         int    `env:"PORT" envDefault:"3000"`
-		IsProduction bool   `env:"PRODUCTION"`
-		NoEnvTag     bool
-		Inner        struct{} `envPrefix:"INNER_"`
-	}
-	os.Setenv("HOME", "/tmp/fakehome")
-	var cfg config
-	if err := ParseWithOptions(&cfg, Options{
-		OnSet: func(tag string, value interface{}, isDefault bool) {
-			fmt.Printf("Set %s to %v (default? %v)\n", tag, value, isDefault)
-		},
-	}); err != nil {
-		fmt.Println("failed:", err)
-	}
-	fmt.Printf("%+v", cfg)
-	// Output: Set HOME to /tmp/fakehome (default? false)
-	// Set PORT to 3000 (default? true)
-	// Set PRODUCTION to  (default? false)
-	// {Home:/tmp/fakehome Port:3000 IsProduction:false NoEnvTag:false Inner:{}}
-}
-
-func ExampleParse_defaults() {
-	type config struct {
-		A string `env:"FOO" envDefault:"foo"`
-		B string `env:"FOO"`
-	}
-
-	// env FOO is not set
-
-	cfg := config{
-		A: "A",
-		B: "B",
-	}
-	if err := Parse(&cfg); err != nil {
-		fmt.Println("failed:", err)
-	}
-	fmt.Printf("%+v", cfg)
-	// Output: {A:foo B:B}
-}
-
 func TestIgnoresUnexported(t *testing.T) {
 	type unexportedConfig struct {
 		home  string `env:"HOME"`
@@ -1467,32 +1403,6 @@ func TestPrecedenceUnmarshalText(t *testing.T) {
 	isEqual(t, []LogLevel{DebugLevel, InfoLevel}, cfg.LogLevels)
 }
 
-func ExampleParseWithOptions() {
-	type thing struct {
-		desc string
-	}
-
-	type conf struct {
-		Thing thing `env:"THING"`
-	}
-
-	os.Setenv("THING", "my thing")
-
-	c := conf{}
-
-	err := ParseWithOptions(&c, Options{FuncMap: map[reflect.Type]ParserFunc{
-		reflect.TypeOf(thing{}): func(v string) (interface{}, error) {
-			return thing{desc: v}, nil
-		},
-	}})
-	if err != nil {
-		fmt.Println(err)
-	}
-	fmt.Println(c.Thing.desc)
-	// Output:
-	// my thing
-}
-
 func TestFile(t *testing.T) {
 	type config struct {
 		SecretKey string `env:"SECRET_KEY,file"`
@@ -2215,3 +2125,23 @@ func TestIssue298ErrorNestedFieldRequiredNotSet(t *testing.T) {
 	isErrorWithMessage(t, err, `env: required environment variable "FOO_0_STR" is not set`)
 	isTrue(t, errors.Is(err, EnvVarIsNotSetError{}))
 }
+
+func TestIssue320(t *testing.T) {
+	type Test struct {
+		Str string `env:"STR"`
+		Num int    `env:"NUM"`
+	}
+	type ComplexConfig struct {
+		Foo *[]Test `envPrefix:"FOO_"`
+		Bar []Test  `envPrefix:"BAR"`
+		Baz []Test  `env:",init"`
+	}
+
+	cfg := ComplexConfig{}
+
+	isNoErr(t, Parse(&cfg))
+
+	isEqual(t, cfg.Foo, nil)
+	isEqual(t, cfg.Bar, nil)
+	isEqual(t, cfg.Baz, nil)
+}
diff --git a/example_test.go b/example_test.go
new file mode 100644
index 0000000..3a3ea93
--- /dev/null
+++ b/example_test.go
@@ -0,0 +1,456 @@
+package env
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"reflect"
+)
+
+// Basic package usage example.
+func Example() {
+	type Config struct {
+		Foo string `env:"FOO"`
+	}
+
+	os.Setenv("FOO", "bar")
+
+	// parse:
+	var cfg1 Config
+	_ = Parse(&cfg1)
+
+	// parse with generics:
+	cfg2, _ := ParseAs[Config]()
+
+	fmt.Print(cfg1.Foo, cfg2.Foo)
+	// Output: barbar
+}
+
+// Parse the environment into a struct.
+func ExampleParse() {
+	type Config struct {
+		Home string `env:"HOME"`
+	}
+	os.Setenv("HOME", "/tmp/fakehome")
+	var cfg Config
+	if err := Parse(&cfg); err != nil {
+		fmt.Println(err)
+	}
+	fmt.Printf("%+v", cfg)
+	// Output:  {Home:/tmp/fakehome}
+}
+
+// Parse the environment into a struct using generics.
+func ExampleParseAs() {
+	type Config struct {
+		Home string `env:"HOME"`
+	}
+	os.Setenv("HOME", "/tmp/fakehome")
+	cfg, err := ParseAs[Config]()
+	if err != nil {
+		fmt.Println(err)
+	}
+	fmt.Printf("%+v", cfg)
+	// Output:  {Home:/tmp/fakehome}
+}
+
+func ExampleParse_required() {
+	type Config struct {
+		Nope string `env:"NOPE,required"`
+	}
+	var cfg Config
+	if err := Parse(&cfg); err != nil {
+		fmt.Println(err)
+	}
+	fmt.Printf("%+v", cfg)
+	// Output: env: required environment variable "NOPE" is not set
+	// {Nope:}
+}
+
+// While `required` demands the environment variable to be set, it doesn't check
+// its value. If you want to make sure the environment is set and not empty, you
+// need to use the `notEmpty` tag option instead (`env:"SOME_ENV,notEmpty"`).
+func ExampleParse_notEmpty() {
+	type Config struct {
+		Nope string `env:"NOPE,notEmpty"`
+	}
+	os.Setenv("NOPE", "")
+	var cfg Config
+	if err := Parse(&cfg); err != nil {
+		fmt.Println(err)
+	}
+	fmt.Printf("%+v", cfg)
+	// Output: env: environment variable "NOPE" should not be empty
+	// {Nope:}
+}
+
+// The `env` tag option `unset` (e.g., `env:"tagKey,unset"`) can be added
+// to ensure that some environment variable is unset after reading it.
+func ExampleParse_unset() {
+	type Config struct {
+		Secret string `env:"SECRET,unset"`
+	}
+	os.Setenv("SECRET", "1234")
+	var cfg Config
+	if err := Parse(&cfg); err != nil {
+		fmt.Println(err)
+	}
+	fmt.Printf("%+v - %s", cfg, os.Getenv("SECRET"))
+	// Output: {Secret:1234} -
+}
+
+// You can use `envSeparator` to define which character should be used to
+// separate array items in a string.
+// Similarly, you can use `envKeyValSeparator` to define which character should
+// be used to separate a key from a value in a map.
+// The defaults are `,` and `:`, respectively.
+func ExampleParse_seprator() {
+	type Config struct {
+		Map map[string]string `env:"CUSTOM_MAP" envSeparator:"-" envKeyValSeparator:"|"`
+	}
+	os.Setenv("CUSTOM_MAP", "k1|v1-k2|v2")
+	var cfg Config
+	if err := Parse(&cfg); err != nil {
+		fmt.Println(err)
+	}
+	fmt.Printf("%+v", cfg)
+	// Output: {Map:map[k1:v1 k2:v2]}
+}
+
+// If you set the `expand` option, environment variables (either in `${var}` or
+// `$var` format) in the string will be replaced according with the actual
+// value of the variable. For example:
+func ExampleParse_expand() {
+	type Config struct {
+		Expand1 string `env:"EXPAND_1,expand"`
+		Expand2 string `env:"EXPAND_2,expand" envDefault:"ABC_${EXPAND_1}"`
+	}
+	os.Setenv("EXPANDING", "HI")
+	os.Setenv("EXPAND_1", "HELLO_${EXPANDING}")
+	var cfg Config
+	if err := Parse(&cfg); err != nil {
+		fmt.Println(err)
+	}
+	fmt.Printf("%+v", cfg)
+	// Output: {Expand1:HELLO_HI Expand2:ABC_HELLO_HI}
+}
+
+// You can automatically initialize `nil` pointers regardless of if a variable
+// is set for them or not.
+// This behavior can be enabled by using the `init` tag option.
+func ExampleParse_init() {
+	type Inner struct {
+		A string `env:"OLA" envDefault:"HI"`
+	}
+	type Config struct {
+		NilInner  *Inner
+		InitInner *Inner `env:",init"`
+	}
+	var cfg Config
+	if err := Parse(&cfg); err != nil {
+		fmt.Println(err)
+	}
+	fmt.Print(cfg.NilInner, cfg.InitInner)
+	// Output: <nil> &{HI}
+}
+
+// You can define the default value for a field by either using the
+// `envDefault` tag, or when initializing the `struct`.
+//
+// Default values defined as `struct` tags will overwrite existing values
+// during `Parse`.
+func ExampleParse_setDefaults() {
+	type Config struct {
+		Foo string `env:"DEF_FOO"`
+		Bar string `env:"DEF_BAR" envDefault:"bar"`
+	}
+	cfg := Config{
+		Foo: "foo",
+	}
+	if err := Parse(&cfg); err != nil {
+		fmt.Println(err)
+	}
+	fmt.Printf("%+v", cfg)
+	// Output: {Foo:foo Bar:bar}
+}
+
+// You might want to listen to value sets and, for example, log something or do
+// some other kind of logic.
+func ExampleParseWithOptions_onSet() {
+	type config struct {
+		Home         string `env:"HOME,required"`
+		Port         int    `env:"PORT" envDefault:"3000"`
+		IsProduction bool   `env:"PRODUCTION"`
+		NoEnvTag     bool
+		Inner        struct{} `envPrefix:"INNER_"`
+	}
+	os.Setenv("HOME", "/tmp/fakehome")
+	var cfg config
+	if err := ParseWithOptions(&cfg, Options{
+		OnSet: func(tag string, value interface{}, isDefault bool) {
+			fmt.Printf("Set %s to %v (default? %v)\n", tag, value, isDefault)
+		},
+	}); err != nil {
+		fmt.Println("failed:", err)
+	}
+	fmt.Printf("%+v", cfg)
+	// Output: Set HOME to /tmp/fakehome (default? false)
+	// Set PORT to 3000 (default? true)
+	// Set PRODUCTION to  (default? false)
+	// {Home:/tmp/fakehome Port:3000 IsProduction:false NoEnvTag:false Inner:{}}
+}
+
+// By default, env supports anything that implements the `TextUnmarshaler`
+// interface, which includes `time.Time`.
+//
+// The upside is that depending on the format you need, you don't need to change
+// anything.
+//
+// The downside is that if you do need time in another format, you'll need to
+// create your own type and implement `TextUnmarshaler`.
+func ExampleParse_customTimeFormat() {
+	// type MyTime time.Time
+	//
+	// func (t *MyTime) UnmarshalText(text []byte) error {
+	// 	tt, err := time.Parse("2006-01-02", string(text))
+	// 	*t = MyTime(tt)
+	// 	return err
+	// }
+
+	type Config struct {
+		SomeTime MyTime `env:"SOME_TIME"`
+	}
+	os.Setenv("SOME_TIME", "2021-05-06")
+	var cfg Config
+	if err := Parse(&cfg); err != nil {
+		fmt.Println(err)
+	}
+	fmt.Print(cfg.SomeTime)
+	// Output: {0 63755856000 <nil>}
+}
+
+// Parse using extra options.
+func ExampleParseWithOptions_customTypes() {
+	type Thing struct {
+		desc string
+	}
+
+	type Config struct {
+		Thing Thing `env:"THING"`
+	}
+
+	os.Setenv("THING", "my thing")
+
+	c := Config{}
+	err := ParseWithOptions(&c, Options{
+		FuncMap: map[reflect.Type]ParserFunc{
+			reflect.TypeOf(Thing{}): func(v string) (interface{}, error) {
+				return Thing{desc: v}, nil
+			},
+		},
+	})
+	if err != nil {
+		fmt.Println(err)
+	}
+	fmt.Print(c.Thing.desc)
+	// Output: my thing
+}
+
+// Make all fields required by default.
+func ExampleParseWithOptions_allFieldsRequired() {
+	type Config struct {
+		Username string `env:"EX_USERNAME" envDefault:"admin"`
+		Password string `env:"EX_PASSWORD"`
+	}
+
+	var cfg Config
+	if err := ParseWithOptions(&cfg, Options{
+		RequiredIfNoDef: true,
+	}); err != nil {
+		fmt.Println(err)
+	}
+	fmt.Printf("%+v\n", cfg)
+	// Output: env: required environment variable "EX_PASSWORD" is not set
+	// {Username:admin Password:}
+}
+
+// Set a custom environment.
+// By default, `os.Environ()` is used.
+func ExampleParseWithOptions_setEnv() {
+	type Config struct {
+		Username string `env:"EX_USERNAME" envDefault:"admin"`
+		Password string `env:"EX_PASSWORD"`
+	}
+
+	var cfg Config
+	if err := ParseWithOptions(&cfg, Options{
+		Environment: map[string]string{
+			"EX_USERNAME": "john",
+			"EX_PASSWORD": "cena",
+		},
+	}); err != nil {
+		fmt.Println(err)
+	}
+	fmt.Printf("%+v\n", cfg)
+	// Output: {Username:john Password:cena}
+}
+
+// Handling slices of complex types.
+func ExampleParse_complexSlices() {
+	type Test struct {
+		Str string `env:"STR"`
+		Num int    `env:"NUM"`
+	}
+	type Config struct {
+		Foo []Test `envPrefix:"FOO"`
+	}
+
+	os.Setenv("FOO_0_STR", "a")
+	os.Setenv("FOO_0_NUM", "1")
+	os.Setenv("FOO_1_STR", "b")
+	os.Setenv("FOO_1_NUM", "2")
+
+	var cfg Config
+	if err := Parse(&cfg); err != nil {
+		fmt.Println(err)
+	}
+	fmt.Printf("%+v\n", cfg)
+	// Output: {Foo:[{Str:a Num:1} {Str:b Num:2}]}
+}
+
+// Setting prefixes for inner types.
+func ExampleParse_prefix() {
+	type Inner struct {
+		Foo string `env:"FOO,required"`
+	}
+	type Config struct {
+		A Inner `envPrefix:"A_"`
+		B Inner `envPrefix:"B_"`
+	}
+	os.Setenv("A_FOO", "a")
+	os.Setenv("B_FOO", "b")
+	var cfg Config
+	if err := Parse(&cfg); err != nil {
+		fmt.Println(err)
+	}
+	fmt.Printf("%+v", cfg)
+	// Output: {A:{Foo:a} B:{Foo:b}}
+}
+
+// Setting prefixes for the entire config.
+func ExampleParseWithOptions_prefix() {
+	type Config struct {
+		Foo string `env:"FOO"`
+	}
+	os.Setenv("MY_APP_FOO", "a")
+	var cfg Config
+	if err := ParseWithOptions(&cfg, Options{
+		Prefix: "MY_APP_",
+	}); err != nil {
+		fmt.Println(err)
+	}
+	fmt.Printf("%+v", cfg)
+	// Output: {Foo:a}
+}
+
+// Use a different tag name than `env`.
+func ExampleParseWithOptions_tagName() {
+	type Config struct {
+		Home string `json:"HOME"`
+	}
+	os.Setenv("HOME", "hello")
+	var cfg Config
+	if err := ParseWithOptions(&cfg, Options{
+		TagName: "json",
+	}); err != nil {
+		fmt.Println(err)
+	}
+	fmt.Printf("%+v", cfg)
+	// Output: {Home:hello}
+}
+
+// If you don't want to set the `env` tag on every field, you can use the
+// `UseFieldNameByDefault` option.
+//
+// It will use the field name to define the environment variable name.
+// So, `Foo` becomes `FOO`, `FooBar` becomes `FOO_BAR`, and so on.
+func ExampleParseWithOptions_useFieldName() {
+	type Config struct {
+		Foo string
+	}
+	os.Setenv("FOO", "bar")
+	var cfg Config
+	if err := ParseWithOptions(&cfg, Options{
+		UseFieldNameByDefault: true,
+	}); err != nil {
+		fmt.Println(err)
+	}
+	fmt.Printf("%+v", cfg)
+	// Output: {Foo:bar}
+}
+
+// The `env` tag option `file` (e.g., `env:"tagKey,file"`) can be added in
+// order to indicate that the value of the variable shall be loaded from a
+// file.
+//
+// The path of that file is given by the environment variable associated with
+// it.
+func ExampleParse_fromFile() {
+	f, _ := os.CreateTemp("", "")
+	_, _ = io.WriteString(f, "super secret")
+	_ = f.Close()
+
+	type Config struct {
+		Secret string `env:"SECRET,file"`
+	}
+	os.Setenv("SECRET", f.Name())
+	var cfg Config
+	if err := Parse(&cfg); err != nil {
+		fmt.Println(err)
+	}
+	fmt.Printf("%+v", cfg)
+	// Output: {Secret:super secret}
+}
+
+// TODO: envSeperator
+//
+
+func ExampleParse_errorHandling() {
+	type Config struct {
+		Username string `env:"EX_ERR_USERNAME" envDefault:"admin"`
+		Password string `env:"EX_ERR_PASSWORD,notEmpty"`
+	}
+
+	var cfg Config
+	if err := Parse(&cfg); err != nil {
+		if errors.Is(err, EmptyEnvVarError{}) {
+			fmt.Println("oopsie")
+		}
+		aggErr := AggregateError{}
+		if ok := errors.As(err, &aggErr); ok {
+			for _, er := range aggErr.Errors {
+				switch v := er.(type) {
+				// Handle the error types you need:
+				// ParseError
+				// NotStructPtrError
+				// NoParserError
+				// NoSupportedTagOptionError
+				// EnvVarIsNotSetError
+				// EmptyEnvVarError
+				// LoadFileContentError
+				// ParseValueError
+				case EmptyEnvVarError:
+					fmt.Println("daisy")
+				default:
+					fmt.Printf("Unknown error type %v", v)
+				}
+			}
+		}
+	}
+
+	fmt.Printf("%+v", cfg)
+	// Output: oopsie
+	// daisy
+	// {Username:admin Password:}
+}