From ae9b592405472fff868421b8b7a41123419a6960 Mon Sep 17 00:00:00 2001 From: Dean Davidson Date: Tue, 2 Dec 2025 09:02:37 -0800 Subject: [PATCH 1/3] Allow bypass of newline decoding --- dotconfig.go | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/dotconfig.go b/dotconfig.go index d611d71..dcd47a2 100644 --- a/dotconfig.go +++ b/dotconfig.go @@ -16,15 +16,17 @@ import ( type DecodeOption int const ( - ReturnFileIOErrors DecodeOption = iota // Return file IO errors - EnforceStructTags // Make sure all fields in config struct have `env` struct tags - AllowWhitespace // Allow leading/trailing whitespace in string values + ReturnFileIOErrors DecodeOption = iota // Return file IO errors + EnforceStructTags // Make sure all fields in config struct have `env` struct tags + AllowWhitespace // Allow leading/trailing whitespace in string values + SkipNewlineDecoding // Don't turn "\n" into newlines ) type options struct { - ReturnFileIOErrors bool - EnforceStructTags bool - AllowWhitespace bool + ReturnFileIOErrors bool + EnforceStructTags bool + AllowWhitespace bool + SkipNewlineDecoding bool } func optsFromVariadic(opts []DecodeOption) options { @@ -37,6 +39,8 @@ func optsFromVariadic(opts []DecodeOption) options { v.EnforceStructTags = true case AllowWhitespace: v.AllowWhitespace = true + case SkipNewlineDecoding: + v.SkipNewlineDecoding = true } } return v @@ -91,6 +95,8 @@ func FromFileName[T any](name string, opts ...DecodeOption) (T, error) { // In the future might look in to more advanced escaping, etc. // but this suits our needs for the time being. func FromReader[T any](r io.Reader, opts ...DecodeOption) (T, error) { + // Get options struct from variadic input + decodedOpts := optsFromVariadic(opts) // First, parse all values in our reader and os.Setenv them. scanner := bufio.NewScanner(r) for scanner.Scan() { @@ -124,13 +130,15 @@ func FromReader[T any](r io.Reader, opts ...DecodeOption) (T, error) { // And trim starting double quote value = strings.TrimPrefix(value, `"`) } - // Turn \n into newlines - value = strings.ReplaceAll(value, `\n`, "\n") + // Optionally turn \n into newlines. + if !decodedOpts.SkipNewlineDecoding { + value = strings.ReplaceAll(value, `\n`, "\n") + } // Finally, set our env variable. os.Setenv(key, value) } // Next, populate config file based on struct tags and return populated config - return fromEnv[T](optsFromVariadic(opts)) + return fromEnv[T](decodedOpts) } var ( From 5285597666db28fa2f78b6c6cac625be57c1cad5 Mon Sep 17 00:00:00 2001 From: Dean Davidson Date: Tue, 2 Dec 2025 09:12:27 -0800 Subject: [PATCH 2/3] Add test --- dotconfig_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/dotconfig_test.go b/dotconfig_test.go index 2cdda0a..08f99a9 100644 --- a/dotconfig_test.go +++ b/dotconfig_test.go @@ -50,6 +50,21 @@ NUM_RETRIES=13`) } } +func TestFromReaderSkipDecodingNewlines(t *testing.T) { + type noNewlines struct { + StrVal string `env:"NO_NEWLINES"` + } + reader := strings.NewReader(`NO_NEWLINES='\n\n single line! \n\n'`) + config, err := dotconfig.FromReader[noNewlines](reader, dotconfig.SkipNewlineDecoding) + if err != nil { + t.Fatalf("Didn't expect error. Got %v.", err) + } + expected := `\n\n single line! \n\n` + if config.StrVal != expected { + t.Fatalf("Expected:\n%v\nGot:\n%v", expected, config.StrVal) + } +} + type moreAdvancedConfig struct { MaxBytesPerRequest int `env:"MAX_BYTES_PER_REQUEST"` APIVersion float64 `env:"API_VERSION"` From 236560925af141f01a6078db7b0ed35f101a8031 Mon Sep 17 00:00:00 2001 From: Dean Davidson Date: Tue, 2 Dec 2025 09:15:22 -0800 Subject: [PATCH 3/3] Tweak test just a tad --- dotconfig_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotconfig_test.go b/dotconfig_test.go index 08f99a9..eeab10c 100644 --- a/dotconfig_test.go +++ b/dotconfig_test.go @@ -54,12 +54,12 @@ func TestFromReaderSkipDecodingNewlines(t *testing.T) { type noNewlines struct { StrVal string `env:"NO_NEWLINES"` } - reader := strings.NewReader(`NO_NEWLINES='\n\n single line! \n\n'`) + reader := strings.NewReader(`NO_NEWLINES=\n single \n line! \n`) config, err := dotconfig.FromReader[noNewlines](reader, dotconfig.SkipNewlineDecoding) if err != nil { t.Fatalf("Didn't expect error. Got %v.", err) } - expected := `\n\n single line! \n\n` + expected := `\n single \n line! \n` if config.StrVal != expected { t.Fatalf("Expected:\n%v\nGot:\n%v", expected, config.StrVal) }