Skip to content

Commit cbe83d6

Browse files
authored
Track pointer initialization for zero values (#73)
This fixes a bug where a pointer was initialized, but to the zero value. Like a *bool initialized to &false. Previously this would be considered "unset" because false is the zero value for a boolean. Now envconfig considers whether the pointer was initialized first, before checking whether the value is zero.
1 parent 63e4089 commit cbe83d6

File tree

2 files changed

+163
-9
lines changed

2 files changed

+163
-9
lines changed

envconfig.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ func processWith(ctx context.Context, i interface{}, l Lookuper, parentNoInit bo
304304
}
305305

306306
// Initialize pointer structs.
307+
pointerWasSet := false
307308
for ef.Kind() == reflect.Ptr {
308309
if ef.IsNil() {
309310
if ef.Type().Elem().Kind() != reflect.Struct {
@@ -316,6 +317,7 @@ func processWith(ctx context.Context, i interface{}, l Lookuper, parentNoInit bo
316317
// Use an empty struct of the type so we can traverse.
317318
ef = reflect.New(ef.Type().Elem()).Elem()
318319
} else {
320+
pointerWasSet = true
319321
ef = ef.Elem()
320322
}
321323
}
@@ -370,7 +372,7 @@ func processWith(ctx context.Context, i interface{}, l Lookuper, parentNoInit bo
370372

371373
// The field already has a non-zero value and overwrite is false, do not
372374
// overwrite.
373-
if !ef.IsZero() && !opts.Overwrite {
375+
if (pointerWasSet || !ef.IsZero()) && !opts.Overwrite {
374376
continue
375377
}
376378

@@ -382,7 +384,7 @@ func processWith(ctx context.Context, i interface{}, l Lookuper, parentNoInit bo
382384
// If the field already has a non-zero value and there was no value directly
383385
// specified, do not overwrite the existing field. We only want to overwrite
384386
// when the envvar was provided directly.
385-
if !ef.IsZero() && !found {
387+
if (pointerWasSet || !ef.IsZero()) && !found {
386388
continue
387389
}
388390

envconfig_test.go

Lines changed: 159 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,18 @@ type Button struct {
214214

215215
type Base64ByteSlice []Base64Bytes
216216

217+
func boolPtr(b bool) *bool {
218+
return &b
219+
}
220+
221+
func stringPtr(s string) *string {
222+
return &s
223+
}
224+
225+
func intPtr(i int) *int {
226+
return &i
227+
}
228+
217229
func TestProcessWith(t *testing.T) {
218230
t.Parallel()
219231

@@ -1445,35 +1457,49 @@ func TestProcessWith(t *testing.T) {
14451457

14461458
// Pointer pointers
14471459
{
1448-
name: "string_pointer",
1460+
name: "pointer_string",
14491461
input: &struct {
14501462
Field *string `env:"FIELD"`
14511463
}{},
14521464
exp: &struct {
14531465
Field *string `env:"FIELD"`
14541466
}{
1455-
Field: func() *string { s := "foo"; return &s }(),
1467+
Field: stringPtr("foo"),
14561468
},
14571469
lookuper: MapLookuper(map[string]string{
14581470
"FIELD": "foo",
14591471
}),
14601472
},
14611473
{
1462-
name: "string_pointer_pointer",
1474+
name: "pointer_pointer_string",
14631475
input: &struct {
14641476
Field **string `env:"FIELD"`
14651477
}{},
14661478
exp: &struct {
14671479
Field **string `env:"FIELD"`
14681480
}{
1469-
Field: func() **string { s := "foo"; ptr := &s; return &ptr }(),
1481+
Field: func() **string { ptr := stringPtr("foo"); return &ptr }(),
14701482
},
14711483
lookuper: MapLookuper(map[string]string{
14721484
"FIELD": "foo",
14731485
}),
14741486
},
14751487
{
1476-
name: "map_pointer",
1488+
name: "pointer_int",
1489+
input: &struct {
1490+
Field *int `env:"FIELD"`
1491+
}{},
1492+
exp: &struct {
1493+
Field *int `env:"FIELD"`
1494+
}{
1495+
Field: intPtr(5),
1496+
},
1497+
lookuper: MapLookuper(map[string]string{
1498+
"FIELD": "5",
1499+
}),
1500+
},
1501+
{
1502+
name: "pointer_map",
14771503
input: &struct {
14781504
Field *map[string]string `env:"FIELD"`
14791505
}{},
@@ -1490,7 +1516,7 @@ func TestProcessWith(t *testing.T) {
14901516
}),
14911517
},
14921518
{
1493-
name: "slice_pointer",
1519+
name: "pointer_slice",
14941520
input: &struct {
14951521
Field *[]string `env:"FIELD"`
14961522
}{},
@@ -1507,7 +1533,21 @@ func TestProcessWith(t *testing.T) {
15071533
}),
15081534
},
15091535
{
1510-
name: "bool_pointer",
1536+
name: "pointer_bool",
1537+
input: &struct {
1538+
Field *bool `env:"FIELD"`
1539+
}{},
1540+
exp: &struct {
1541+
Field *bool `env:"FIELD"`
1542+
}{
1543+
Field: boolPtr(true),
1544+
},
1545+
lookuper: MapLookuper(map[string]string{
1546+
"FIELD": "true",
1547+
}),
1548+
},
1549+
{
1550+
name: "pointer_bool_noinit",
15111551
input: &struct {
15121552
Field *bool `env:"FIELD,noinit"`
15131553
}{},
@@ -1518,6 +1558,118 @@ func TestProcessWith(t *testing.T) {
15181558
},
15191559
lookuper: MapLookuper(nil),
15201560
},
1561+
{
1562+
name: "pointer_bool_default_field_set_env_unset",
1563+
input: &struct {
1564+
Field *bool `env:"FIELD,default=true"`
1565+
}{
1566+
Field: boolPtr(false),
1567+
},
1568+
exp: &struct {
1569+
Field *bool `env:"FIELD,default=true"`
1570+
}{
1571+
Field: boolPtr(false),
1572+
},
1573+
lookuper: MapLookuper(nil),
1574+
},
1575+
{
1576+
name: "pointer_bool_default_field_set_env_set",
1577+
input: &struct {
1578+
Field *bool `env:"FIELD,default=true"`
1579+
}{
1580+
Field: boolPtr(false),
1581+
},
1582+
exp: &struct {
1583+
Field *bool `env:"FIELD,default=true"`
1584+
}{
1585+
Field: boolPtr(false),
1586+
},
1587+
lookuper: MapLookuper(map[string]string{
1588+
"FIELD": "true",
1589+
}),
1590+
},
1591+
{
1592+
name: "pointer_bool_default_field_unset_env_set",
1593+
input: &struct {
1594+
Field *bool `env:"FIELD,default=true"`
1595+
}{},
1596+
exp: &struct {
1597+
Field *bool `env:"FIELD,default=true"`
1598+
}{
1599+
Field: boolPtr(false),
1600+
},
1601+
lookuper: MapLookuper(map[string]string{
1602+
"FIELD": "false",
1603+
}),
1604+
},
1605+
{
1606+
name: "pointer_bool_default_field_unset_env_unset",
1607+
input: &struct {
1608+
Field *bool `env:"FIELD,default=true"`
1609+
}{},
1610+
exp: &struct {
1611+
Field *bool `env:"FIELD,default=true"`
1612+
}{
1613+
Field: boolPtr(true),
1614+
},
1615+
lookuper: MapLookuper(nil),
1616+
},
1617+
{
1618+
name: "pointer_bool_default_overwrite_field_set_env_unset",
1619+
input: &struct {
1620+
Field *bool `env:"FIELD,overwrite,default=true"`
1621+
}{
1622+
Field: boolPtr(false),
1623+
},
1624+
exp: &struct {
1625+
Field *bool `env:"FIELD,overwrite,default=true"`
1626+
}{
1627+
Field: boolPtr(false),
1628+
},
1629+
lookuper: MapLookuper(nil),
1630+
},
1631+
{
1632+
name: "pointer_bool_default_overwrite_field_set_env_set",
1633+
input: &struct {
1634+
Field *bool `env:"FIELD,overwrite,default=true"`
1635+
}{
1636+
Field: boolPtr(false),
1637+
},
1638+
exp: &struct {
1639+
Field *bool `env:"FIELD,overwrite,default=true"`
1640+
}{
1641+
Field: boolPtr(true),
1642+
},
1643+
lookuper: MapLookuper(map[string]string{
1644+
"FIELD": "true",
1645+
}),
1646+
},
1647+
{
1648+
name: "pointer_bool_default_overwrite_field_unset_env_set",
1649+
input: &struct {
1650+
Field *bool `env:"FIELD,overwrite,default=true"`
1651+
}{},
1652+
exp: &struct {
1653+
Field *bool `env:"FIELD,overwrite,default=true"`
1654+
}{
1655+
Field: boolPtr(false),
1656+
},
1657+
lookuper: MapLookuper(map[string]string{
1658+
"FIELD": "false",
1659+
}),
1660+
},
1661+
{
1662+
name: "pointer_bool_default_overwrite_field_unset_env_unset",
1663+
input: &struct {
1664+
Field *bool `env:"FIELD,overwrite,default=true"`
1665+
}{},
1666+
exp: &struct {
1667+
Field *bool `env:"FIELD,overwrite,default=true"`
1668+
}{
1669+
Field: boolPtr(true),
1670+
},
1671+
lookuper: MapLookuper(nil),
1672+
},
15211673

15221674
// Marshallers
15231675
{

0 commit comments

Comments
 (0)