diff --git a/README.md b/README.md index 49c2fe1..96242dc 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,24 @@ examples. } ``` + As a special case where the default value should contain a literal `$`, + escape it with a backslash. Unfortunately this requires a double backslash + in the struct tag: + + ```go + type MyStruct struct { + Amount string `env:"AMOUNT, default=\\$5.00"` // Default: $5.00 + } + ``` + + To have a literal backslash followed by a `$`, escape the backslash: + + ```go + type MyStruct struct { + Filepath string `env:"FILEPATH, default=C:\\Personal\\\\$name"` // Default: C:\Personal\$name + } + ``` + - `prefix` - sets the prefix to use for looking up environment variable keys on child structs and fields. This is useful for shared configurations: diff --git a/envconfig.go b/envconfig.go index ea29608..9cc4445 100644 --- a/envconfig.go +++ b/envconfig.go @@ -642,6 +642,18 @@ func lookup(key string, required bool, defaultValue string, l Lookuper) (string, } if defaultValue != "" { + // Handle escaped "$" by replacing the value with a character that is + // invalid to have in an environment variable. A more perfect solution + // would be to re-implement os.Expand to handle this case, but that's been + // proposed and rejected in the stdlib. Additionally, the function is + // dependent on other private functions in the [os] package, so + // duplicating it is toilsome. + // + // While admittidly a hack, replacing the escaped values with invalid + // characters (and then replacing later), is a reasonable solution. + defaultValue = strings.ReplaceAll(defaultValue, "\\\\", "\u0000") + defaultValue = strings.ReplaceAll(defaultValue, "\\$", "\u0008") + // Expand the default value. This allows for a default value that maps to // a different environment variable. val = os.Expand(defaultValue, func(i string) string { @@ -657,6 +669,9 @@ func lookup(key string, required bool, defaultValue string, l Lookuper) (string, return "" }) + val = strings.ReplaceAll(val, "\u0000", "\\") + val = strings.ReplaceAll(val, "\u0008", "$") + return val, false, true, nil } } diff --git a/envconfig_test.go b/envconfig_test.go index c1998f9..72520ba 100644 --- a/envconfig_test.go +++ b/envconfig_test.go @@ -1350,6 +1350,34 @@ func TestProcessWith(t *testing.T) { "DEFAULT": "value", }))), }, + { + name: "default/escaped_doesnt_interpolate", + target: &struct { + Field string `env:"FIELD,default=\\$DEFAULT"` + }{}, + exp: &struct { + Field string `env:"FIELD,default=\\$DEFAULT"` + }{ + Field: "$DEFAULT", + }, + lookuper: MapLookuper(map[string]string{ + "DEFAULT": "should-not-be-replaced", + }), + }, + { + name: "default/escaped_escaped_keeps_escape", + target: &struct { + Field string `env:"FIELD,default=C:\\Personal\\\\$DEFAULT"` + }{}, + exp: &struct { + Field string `env:"FIELD,default=C:\\Personal\\\\$DEFAULT"` + }{ + Field: `C:\Personal\value`, + }, + lookuper: MapLookuper(map[string]string{ + "DEFAULT": "value", + }), + }, { name: "default/slice", target: &struct {