Skip to content

Commit 7ee4127

Browse files
authored
Allow escaping $ in default values (#128)
1 parent 562e9b1 commit 7ee4127

File tree

3 files changed

+61
-0
lines changed

3 files changed

+61
-0
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,24 @@ examples.
9393
}
9494
```
9595

96+
As a special case where the default value should contain a literal `$`,
97+
escape it with a backslash. Unfortunately this requires a double backslash
98+
in the struct tag:
99+
100+
```go
101+
type MyStruct struct {
102+
Amount string `env:"AMOUNT, default=\\$5.00"` // Default: $5.00
103+
}
104+
```
105+
106+
To have a literal backslash followed by a `$`, escape the backslash:
107+
108+
```go
109+
type MyStruct struct {
110+
Filepath string `env:"FILEPATH, default=C:\\Personal\\\\$name"` // Default: C:\Personal\$name
111+
}
112+
```
113+
96114
- `prefix` - sets the prefix to use for looking up environment variable keys
97115
on child structs and fields. This is useful for shared configurations:
98116

envconfig.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,18 @@ func lookup(key string, required bool, defaultValue string, l Lookuper) (string,
642642
}
643643

644644
if defaultValue != "" {
645+
// Handle escaped "$" by replacing the value with a character that is
646+
// invalid to have in an environment variable. A more perfect solution
647+
// would be to re-implement os.Expand to handle this case, but that's been
648+
// proposed and rejected in the stdlib. Additionally, the function is
649+
// dependent on other private functions in the [os] package, so
650+
// duplicating it is toilsome.
651+
//
652+
// While admittidly a hack, replacing the escaped values with invalid
653+
// characters (and then replacing later), is a reasonable solution.
654+
defaultValue = strings.ReplaceAll(defaultValue, "\\\\", "\u0000")
655+
defaultValue = strings.ReplaceAll(defaultValue, "\\$", "\u0008")
656+
645657
// Expand the default value. This allows for a default value that maps to
646658
// a different environment variable.
647659
val = os.Expand(defaultValue, func(i string) string {
@@ -657,6 +669,9 @@ func lookup(key string, required bool, defaultValue string, l Lookuper) (string,
657669
return ""
658670
})
659671

672+
val = strings.ReplaceAll(val, "\u0000", "\\")
673+
val = strings.ReplaceAll(val, "\u0008", "$")
674+
660675
return val, false, true, nil
661676
}
662677
}

envconfig_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,6 +1350,34 @@ func TestProcessWith(t *testing.T) {
13501350
"DEFAULT": "value",
13511351
}))),
13521352
},
1353+
{
1354+
name: "default/escaped_doesnt_interpolate",
1355+
target: &struct {
1356+
Field string `env:"FIELD,default=\\$DEFAULT"`
1357+
}{},
1358+
exp: &struct {
1359+
Field string `env:"FIELD,default=\\$DEFAULT"`
1360+
}{
1361+
Field: "$DEFAULT",
1362+
},
1363+
lookuper: MapLookuper(map[string]string{
1364+
"DEFAULT": "should-not-be-replaced",
1365+
}),
1366+
},
1367+
{
1368+
name: "default/escaped_escaped_keeps_escape",
1369+
target: &struct {
1370+
Field string `env:"FIELD,default=C:\\Personal\\\\$DEFAULT"`
1371+
}{},
1372+
exp: &struct {
1373+
Field string `env:"FIELD,default=C:\\Personal\\\\$DEFAULT"`
1374+
}{
1375+
Field: `C:\Personal\value`,
1376+
},
1377+
lookuper: MapLookuper(map[string]string{
1378+
"DEFAULT": "value",
1379+
}),
1380+
},
13531381
{
13541382
name: "default/slice",
13551383
target: &struct {

0 commit comments

Comments
 (0)