Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added LoadFS from embed files #137

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/joho/godotenv

go 1.12
go 1.16
57 changes: 57 additions & 0 deletions godotenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package godotenv

import (
"bufio"
"embed"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
158 changes: 158 additions & 0 deletions godotenv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package godotenv

import (
"bytes"
"embed"
"fmt"
"os"
"reflect"
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -56,20 +90,42 @@ 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 {
t.Error("File wasn't found but Load didn't return an error")
}
}

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 {
t.Error("File wasn't found but Overload didn't return an error")
}
}

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{
Expand Down Expand Up @@ -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"

Expand All @@ -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{
Expand All @@ -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{
Expand All @@ -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{
Expand All @@ -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{
Expand All @@ -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{
Expand All @@ -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
Expand Down Expand Up @@ -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")
Expand Down