diff --git a/go.mod b/go.mod index 126e61d..776fdee 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/joho/godotenv -go 1.12 +go 1.16 \ No newline at end of file diff --git a/godotenv.go b/godotenv.go index 466f2eb..1405310 100644 --- a/godotenv.go +++ b/godotenv.go @@ -15,6 +15,7 @@ package godotenv import ( "bufio" + "embed" "errors" "fmt" "io" @@ -51,6 +52,50 @@ func Load(filenames ...string) (err error) { return } +func LoadFS(fsys embed.FS, filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFileFS(fsys, filename, false) + if err != nil { + return // return early on a spazout + } + } + return +} + +func loadFileFS(fsys embed.FS, filename string, overload bool) error { + envMap, err := readFileFS(fsys, filename) + if err != nil { + return err + } + + currentEnv := map[string]bool{} + rawEnv := os.Environ() + for _, rawEnvLine := range rawEnv { + key := strings.Split(rawEnvLine, "=")[0] + currentEnv[key] = true + } + + for key, value := range envMap { + if !currentEnv[key] || overload { + os.Setenv(key, value) + } + } + + return nil +} + +func readFileFS(fsys embed.FS, filename string) (envMap map[string]string, err error) { + file, err := fsys.Open(filename) + if err != nil { + return + } + defer file.Close() + + return Parse(file) +} + // Overload will read your env file(s) and load them into ENV for this process. // // Call this function as close as possible to the start of your program (ideally in main) @@ -74,6 +119,18 @@ func Overload(filenames ...string) (err error) { return } +func OverloadFS(fsys embed.FS, filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFileFS(fsys, filename, true) + if err != nil { + return // return early on a spazout + } + } + return +} + // Read all env (with same file loading semantics as Load) but return values as // a map rather than automatically writing values into env func Read(filenames ...string) (envMap map[string]string, err error) { diff --git a/godotenv_test.go b/godotenv_test.go index 7274c14..0f3c5e6 100644 --- a/godotenv_test.go +++ b/godotenv_test.go @@ -2,6 +2,7 @@ package godotenv import ( "bytes" + "embed" "fmt" "os" "reflect" @@ -11,6 +12,9 @@ import ( var noopPresets = make(map[string]string) +//go:embed fixtures/* +var content embed.FS + func parseAndCompare(t *testing.T, rawEnvLine string, expectedKey string, expectedValue string) { key, value, _ := parseLine(rawEnvLine, noopPresets) if key != expectedKey || value != expectedValue { @@ -40,6 +44,28 @@ func loadEnvAndCompareValues(t *testing.T, loader func(files ...string) error, e } } +func loadFSEnvAndCompareValues(t *testing.T, loader func(fsys embed.FS, files ...string) error, fsys embed.FS, envFileName string, expectedValues map[string]string, presets map[string]string) { + // first up, clear the env + os.Clearenv() + + for k, v := range presets { + os.Setenv(k, v) + } + + err := loader(fsys, envFileName) + if err != nil { + t.Fatalf("Error loading %v", envFileName) + } + + for k := range expectedValues { + envValue := os.Getenv(k) + v := expectedValues[k] + if envValue != v { + t.Errorf("Mismatch for key '%v': expected '%v' got '%v'", k, v, envValue) + } + } +} + func TestLoadWithNoArgsLoadsDotEnv(t *testing.T) { err := Load() pathError := err.(*os.PathError) @@ -48,6 +74,14 @@ func TestLoadWithNoArgsLoadsDotEnv(t *testing.T) { } } +func TestLoadFSWithNoArgsLoadsDotEnv(t *testing.T) { + err := LoadFS(content) + pathError := err.(*os.PathError) + if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" { + t.Errorf("Didn't try and open .env by default") + } +} + func TestOverloadWithNoArgsOverloadsDotEnv(t *testing.T) { err := Overload() pathError := err.(*os.PathError) @@ -56,6 +90,14 @@ func TestOverloadWithNoArgsOverloadsDotEnv(t *testing.T) { } } +func TestOverloadFSWithNoArgsOverloadsDotEnv(t *testing.T) { + err := OverloadFS(content) + pathError := err.(*os.PathError) + if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" { + t.Errorf("Didn't try and open .env by default") + } +} + func TestLoadFileNotFound(t *testing.T) { err := Load("somefilethatwillneverexistever.env") if err == nil { @@ -63,6 +105,13 @@ func TestLoadFileNotFound(t *testing.T) { } } +func TestLoadFSFileNotFound(t *testing.T) { + err := LoadFS(content, "somefilethatwillneverexistever.env") + if err == nil { + t.Error("File wasn't found but LoadFS didn't return an error") + } +} + func TestOverloadFileNotFound(t *testing.T) { err := Overload("somefilethatwillneverexistever.env") if err == nil { @@ -70,6 +119,13 @@ func TestOverloadFileNotFound(t *testing.T) { } } +func TestOverloadFSFileNotFound(t *testing.T) { + err := OverloadFS(content,"somefilethatwillneverexistever.env") + if err == nil { + t.Error("File wasn't found but Overload didn't return an error") + } +} + func TestReadPlainEnv(t *testing.T) { envFileName := "fixtures/plain.env" expectedValues := map[string]string{ @@ -131,6 +187,22 @@ func TestLoadDoesNotOverride(t *testing.T) { loadEnvAndCompareValues(t, Load, envFileName, expectedValues, presets) } +func TestLoadFSDoesNotOverride(t *testing.T) { + envFileName := "fixtures/plain.env" + + // ensure NO overload + presets := map[string]string{ + "OPTION_A": "do_not_override", + "OPTION_B": "", + } + + expectedValues := map[string]string{ + "OPTION_A": "do_not_override", + "OPTION_B": "", + } + loadFSEnvAndCompareValues(t, LoadFS, content, envFileName, expectedValues, presets) +} + func TestOveroadDoesOverride(t *testing.T) { envFileName := "fixtures/plain.env" @@ -145,6 +217,20 @@ func TestOveroadDoesOverride(t *testing.T) { loadEnvAndCompareValues(t, Overload, envFileName, expectedValues, presets) } +func TestOveroadFSDoesOverride(t *testing.T) { + envFileName := "fixtures/plain.env" + + // ensure NO overload + presets := map[string]string{ + "OPTION_A": "do_not_override", + } + + expectedValues := map[string]string{ + "OPTION_A": "1", + } + loadFSEnvAndCompareValues(t, OverloadFS, content, envFileName, expectedValues, presets) +} + func TestLoadPlainEnv(t *testing.T) { envFileName := "fixtures/plain.env" expectedValues := map[string]string{ @@ -158,6 +244,19 @@ func TestLoadPlainEnv(t *testing.T) { loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) } +func TestLoadFSPlainEnv(t *testing.T) { + envFileName := "fixtures/plain.env" + expectedValues := map[string]string{ + "OPTION_A": "1", + "OPTION_B": "2", + "OPTION_C": "3", + "OPTION_D": "4", + "OPTION_E": "5", + } + + loadFSEnvAndCompareValues(t, LoadFS, content, envFileName, expectedValues, noopPresets) +} + func TestLoadExportedEnv(t *testing.T) { envFileName := "fixtures/exported.env" expectedValues := map[string]string{ @@ -168,6 +267,16 @@ func TestLoadExportedEnv(t *testing.T) { loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) } +func TestLoadFSExportedEnv(t *testing.T) { + envFileName := "fixtures/exported.env" + expectedValues := map[string]string{ + "OPTION_A": "2", + "OPTION_B": "\\n", + } + + loadFSEnvAndCompareValues(t, LoadFS, content, envFileName, expectedValues, noopPresets) +} + func TestLoadEqualsEnv(t *testing.T) { envFileName := "fixtures/equals.env" expectedValues := map[string]string{ @@ -177,6 +286,15 @@ func TestLoadEqualsEnv(t *testing.T) { loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) } +func TestLoadFSEqualsEnv(t *testing.T) { + envFileName := "fixtures/equals.env" + expectedValues := map[string]string{ + "OPTION_A": "postgres://localhost:5432/database?sslmode=disable", + } + + loadFSEnvAndCompareValues(t, LoadFS, content, envFileName, expectedValues, noopPresets) +} + func TestLoadQuotedEnv(t *testing.T) { envFileName := "fixtures/quoted.env" expectedValues := map[string]string{ @@ -194,6 +312,23 @@ func TestLoadQuotedEnv(t *testing.T) { loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) } +func TestLoadFSQuotedEnv(t *testing.T) { + envFileName := "fixtures/quoted.env" + expectedValues := map[string]string{ + "OPTION_A": "1", + "OPTION_B": "2", + "OPTION_C": "", + "OPTION_D": "\\n", + "OPTION_E": "1", + "OPTION_F": "2", + "OPTION_G": "", + "OPTION_H": "\n", + "OPTION_I": "echo 'asd'", + } + + loadFSEnvAndCompareValues(t, LoadFS, content, envFileName, expectedValues, noopPresets) +} + func TestSubstitutions(t *testing.T) { envFileName := "fixtures/substitutions.env" expectedValues := map[string]string{ @@ -207,6 +342,19 @@ func TestSubstitutions(t *testing.T) { loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) } +func TestFSSubstitutions(t *testing.T) { + envFileName := "fixtures/substitutions.env" + expectedValues := map[string]string{ + "OPTION_A": "1", + "OPTION_B": "1", + "OPTION_C": "1", + "OPTION_D": "11", + "OPTION_E": "", + } + + loadFSEnvAndCompareValues(t, LoadFS, content, envFileName, expectedValues, noopPresets) +} + func TestExpanding(t *testing.T) { tests := []struct { name string @@ -281,6 +429,16 @@ func TestActualEnvVarsAreLeftAlone(t *testing.T) { } } +func TestFSActualEnvVarsAreLeftAlone(t *testing.T) { + os.Clearenv() + os.Setenv("OPTION_A", "actualenv") + _ = LoadFS(content, "fixtures/plain.env") + + if os.Getenv("OPTION_A") != "actualenv" { + t.Error("An ENV var set earlier was overwritten") + } +} + func TestParsing(t *testing.T) { // unquoted values parseAndCompare(t, "FOO=bar", "FOO", "bar")