From 997419b19b0eee3b829380a4cdd98e8b893ebd27 Mon Sep 17 00:00:00 2001 From: rodkevich Date: Sun, 24 Nov 2024 01:16:04 +0300 Subject: [PATCH 1/2] Introduce raw literal option --- README.md | 14 ++++++++++++++ envconfig.go | 30 +++++++++++++++++++++++++----- envconfig_doc_test.go | 10 +++++++++- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 49c2fe1..59c4075 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,20 @@ examples. } ``` +- `default.raw` - sets a literal default value for the environment variable + if it is not set, without applying any transformations or expansions. + This allows for exact values to be used, including special characters. + This option ensures that the exact value provided in the `default.raw` tag is used. + + ```go + type MyStruct struct { + Token string `env:"TOKEN, default.raw=this^will$be&used|as-is"` + } + ``` + + If the `PORT` environment variable is unset, the field `Port` will be assigned the literal value `expand^makes$no&problems`, without interpreting `$no` or other special characters. + + - `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 9715bc8..481ae04 100644 --- a/envconfig.go +++ b/envconfig.go @@ -75,6 +75,7 @@ const ( optPrefix = "prefix=" optRequired = "required" optSeparator = "separator=" + optLiteral = "default.raw=" ) // internalError is a custom error type for errors returned by envconfig. @@ -225,6 +226,7 @@ type options struct { Overwrite bool DecodeUnset bool Required bool + Literal bool } // Config represent inputs to the envconfig decoding. @@ -454,7 +456,7 @@ func processWith(ctx context.Context, c *Config) error { // Lookup the value, ignoring an error if the key isn't defined. This is // required for nested structs that don't declare their own `env` keys, // but have internal fields with an `env` defined. - val, found, usedDefault, err := lookup(key, required, opts.Default, l) + val, found, usedDefault, err := lookup(key, required, opts.Default, opts.Literal, l) if err != nil && !errors.Is(err, ErrMissingKey) { return fmt.Errorf("%s: %w", tf.Name, err) } @@ -509,7 +511,7 @@ func processWith(ctx context.Context, c *Config) error { continue } - val, found, usedDefault, err := lookup(key, required, opts.Default, l) + val, found, usedDefault, err := lookup(key, required, opts.Default, opts.Literal, l) if err != nil { return fmt.Errorf("%s: %w", tf.Name, err) } @@ -599,9 +601,15 @@ LOOP: opts.Delimiter = strings.TrimPrefix(o, optDelimiter) case strings.HasPrefix(search, optSeparator): opts.Separator = strings.TrimPrefix(o, optSeparator) + case strings.HasPrefix(search, optLiteral): + opts.Literal = true + o = strings.TrimLeft(strings.Join(tagOpts[i:], ","), " ") + // Use opts.Default for the literal value. No need for a new string option. + opts.Default = strings.TrimPrefix(o, optDefault) + break LOOP case strings.HasPrefix(search, optDefault): // If a default value was given, assume everything after is the provided - // value, including comma-seprated items. + // value, including comma-separated items. o = strings.TrimLeft(strings.Join(tagOpts[i:], ","), " ") opts.Default = strings.TrimPrefix(o, optDefault) break LOOP @@ -617,7 +625,7 @@ LOOP: // first boolean parameter indicates whether the value was found in the // lookuper. The second boolean parameter indicates whether the default value // was used. -func lookup(key string, required bool, defaultValue string, l Lookuper) (string, bool, bool, error) { +func lookup(key string, required bool, defaultValue string, literal bool, l Lookuper) (string, bool, bool, error) { if key == "" { // The struct has something like `env:",required"`, which is likely a // mistake. We could try to infer the envvar from the field name, but that @@ -642,7 +650,19 @@ func lookup(key string, required bool, defaultValue string, l Lookuper) (string, } if defaultValue != "" { - // Expand the default value. This allows for a default value that maps to + if literal { + // If the "literal" option is set, the default value is treated as-is + // without any modification or expansion. This allows for exact values + // to be used, including special characters or reserved formatting. + // Here, we check if the defaultValue contains the literal prefix and + // extract the portion after the prefix. + parts := strings.Split(defaultValue, optLiteral) + if len(parts) > 1 { + return parts[1], false, true, nil + } + } + // If literal option is not set, we can 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 { lookuper := l diff --git a/envconfig_doc_test.go b/envconfig_doc_test.go index 98ccc74..d70d6fe 100644 --- a/envconfig_doc_test.go +++ b/envconfig_doc_test.go @@ -110,7 +110,9 @@ func Example_defaults() { type MyStruct struct { Port int `env:"PORT, default=8080"` - Username string `env:"USERNAME, default=$OTHER_ENV"` + Username string `env:"USERNAME, default=$OTHER_ENV"` // expands to empty + ApiKey string `env:"API_KEY, default=expand^makes$no&problems"` // $no expands to empty without .raw + Secret string `env:"SECRET, default.raw=expand^makes$no&problems|i,hope|!@#$%^&*()_+~{}[]:;,.<>?/|\\"` } var s MyStruct @@ -119,9 +121,15 @@ func Example_defaults() { } fmt.Printf("port: %d\n", s.Port) + fmt.Printf("username: [%s]\n", s.Username) // for empty brackets + fmt.Printf("api-key: %s\n", s.ApiKey) + fmt.Printf("secret: %s\n", s.Secret) // Output: // port: 8080 + // username: [] + // api-key: expand^makes&problems + // secret: expand^makes$no&problems|i,hope|!@#$%^&*()_+~{}[]:;,.<>?/|\ } func Example_prefix() { From 3a82bb36a77aeefc24a5f6405d572f6fec6b1d25 Mon Sep 17 00:00:00 2001 From: rodkevich Date: Sun, 24 Nov 2024 01:30:44 +0300 Subject: [PATCH 2/2] default.raw description --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 59c4075..e1e6408 100644 --- a/README.md +++ b/README.md @@ -104,9 +104,6 @@ examples. } ``` - If the `PORT` environment variable is unset, the field `Port` will be assigned the literal value `expand^makes$no&problems`, without interpreting `$no` or other special characters. - - - `prefix` - sets the prefix to use for looking up environment variable keys on child structs and fields. This is useful for shared configurations: