Skip to content

Commit 39a551b

Browse files
committed
Added methods for working on env slices.
## Changes * Added methods for working on env slices. * Added tests * Load and Overload uses Read (code reuse) * Avoid using return arguments `raw return` (antipattern) * Exec does not pollute current process environment variables when invoked. This allows easy use with with os/exec package. Signed-off-by: Bartlomiej Plotka <[email protected]>
1 parent f562099 commit 39a551b

File tree

2 files changed

+105
-41
lines changed

2 files changed

+105
-41
lines changed

godotenv.go

+75-41
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,19 @@ const doubleQuoteSpecialChars = "\\\n\r\"!$`"
3939
// godotenv.Load("fileone", "filetwo")
4040
//
4141
// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults
42-
func Load(filenames ...string) (err error) {
43-
filenames = filenamesOrDefault(filenames)
42+
func Load(filenames ...string) error {
43+
envMap, err := Read(filenames...)
44+
if err != nil {
45+
return err
46+
}
4447

45-
for _, filename := range filenames {
46-
err = loadFile(filename, false)
47-
if err != nil {
48-
return // return early on a spazout
48+
for _, e := range MergeEnvSlices(EnvMapToSortedSlice(envMap), os.Environ()) {
49+
// We assume env slice always has key and some value (even empty).
50+
if err := os.Setenv(strings.Split(e, "=")[0], strings.SplitN(e, "=", 2)[1]); err != nil {
51+
return err
4952
}
5053
}
51-
return
54+
return nil
5255
}
5356

5457
// Overload will read your env file(s) and load them into ENV for this process.
@@ -63,15 +66,18 @@ func Load(filenames ...string) (err error) {
6366
//
6467
// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars.
6568
func Overload(filenames ...string) (err error) {
66-
filenames = filenamesOrDefault(filenames)
69+
envMap, err := Read(filenames...)
70+
if err != nil {
71+
return err
72+
}
6773

68-
for _, filename := range filenames {
69-
err = loadFile(filename, true)
70-
if err != nil {
71-
return // return early on a spazout
74+
for _, e := range MergeEnvSlices(os.Environ(), EnvMapToSortedSlice(envMap)) {
75+
// We assume env slice always has key and some value (even empty).
76+
if err := os.Setenv(strings.Split(e, "=")[0], strings.SplitN(e, "=", 2)[1]); err != nil {
77+
return err
7278
}
7379
}
74-
return
80+
return nil
7581
}
7682

7783
// Read all env (with same file loading semantics as Load) but return values as
@@ -82,18 +88,15 @@ func Read(filenames ...string) (envMap map[string]string, err error) {
8288

8389
for _, filename := range filenames {
8490
individualEnvMap, individualErr := readFile(filename)
85-
8691
if individualErr != nil {
87-
err = individualErr
88-
return // return early on a spazout
92+
return nil, individualErr // Return early on a spazout.
8993
}
9094

9195
for key, value := range individualEnvMap {
9296
envMap[key] = value
9397
}
9498
}
95-
96-
return
99+
return envMap, nil
97100
}
98101

99102
// Parse reads an env file from io.Reader, returning a map of keys and values.
@@ -107,39 +110,55 @@ func Parse(r io.Reader) (envMap map[string]string, err error) {
107110
}
108111

109112
if err = scanner.Err(); err != nil {
110-
return
113+
return nil, err
111114
}
112115

113116
for _, fullLine := range lines {
114117
if !isIgnoredLine(fullLine) {
115118
var key, value string
116119
key, value, err = parseLine(fullLine, envMap)
117-
118120
if err != nil {
119-
return
121+
return nil, err
120122
}
121123
envMap[key] = value
122124
}
123125
}
124126
return
125127
}
126128

127-
//Unmarshal reads an env file from a string, returning a map of keys and values.
129+
// Unmarshal reads an env file from a string, returning a map of keys and values.
128130
func Unmarshal(str string) (envMap map[string]string, err error) {
129131
return Parse(strings.NewReader(str))
130132
}
131133

132-
// Exec loads env vars from the specified filenames (empty map falls back to default)
133-
// then executes the cmd specified.
134+
// EnvMapToSortedSlice converts env map that you can get from `Read` or `Parse` into env sorted string
135+
// slice commonly used by `exec.Command`. This allows to execute new process with relevant
136+
// variables without changing current process environment.
137+
// See https://golang.org/pkg/os/exec/#Cmd `Env` field to read more about slice format.
138+
func EnvMapToSortedSlice(m map[string]string) []string {
139+
s := make([]string, 0, len(m))
140+
for k, v := range m {
141+
s = append(s, fmt.Sprintf("%s=%s", k, v))
142+
}
143+
sort.Strings(s)
144+
return s
145+
}
146+
147+
// Exec executes given command with args in separate process in environment that consists
148+
// of env vars from the specified filenames (empty map falls back to default) and current process env vars on top.
134149
//
135150
// Simply hooks up os.Stdin/err/out to the command and calls Run()
136151
//
137152
// If you want more fine grained control over your command it's recommended
138153
// that you use `Load()` or `Read()` and the `os/exec` package yourself.
139154
func Exec(filenames []string, cmd string, cmdArgs []string) error {
140-
Load(filenames...)
155+
envMap, err := Read(filenames...)
156+
if err != nil {
157+
return err
158+
}
141159

142160
command := exec.Command(cmd, cmdArgs...)
161+
command.Env = MergeEnvSlices(EnvMapToSortedSlice(envMap), os.Environ())
143162
command.Stdin = os.Stdin
144163
command.Stdout = os.Stdout
145164
command.Stderr = os.Stderr
@@ -187,26 +206,41 @@ func filenamesOrDefault(filenames []string) []string {
187206
return filenames
188207
}
189208

190-
func loadFile(filename string, overload bool) error {
191-
envMap, err := readFile(filename)
192-
if err != nil {
193-
return err
194-
}
209+
// MergeEnvSlices merges two slices into single sorted one by applying `over` slice into `base`.
210+
// The `over` slice will be used if the key overlaps.
211+
func MergeEnvSlices(base, over []string) (merged []string) {
212+
sort.Strings(base)
213+
sort.Strings(over)
195214

196-
currentEnv := map[string]bool{}
197-
rawEnv := os.Environ()
198-
for _, rawEnvLine := range rawEnv {
199-
key := strings.Split(rawEnvLine, "=")[0]
200-
currentEnv[key] = true
201-
}
215+
var b, o int
216+
for b < len(base) || o < len(over) {
202217

203-
for key, value := range envMap {
204-
if !currentEnv[key] || overload {
205-
os.Setenv(key, value)
218+
if b >= len(base) {
219+
merged = append(merged, over[o])
220+
o++
221+
continue
206222
}
207-
}
208223

209-
return nil
224+
if o >= len(over) {
225+
merged = append(merged, base[b])
226+
b++
227+
continue
228+
}
229+
230+
switch strings.Compare(strings.Split(base[b], "=")[0], strings.Split(over[o], "=")[0]) {
231+
case 0:
232+
// Same keys. Instead of picking over element, ignore base one. This ensure correct behaviour if base
233+
// has duplicate elements.
234+
b++
235+
case 1:
236+
merged = append(merged, over[o])
237+
o++
238+
case -1:
239+
merged = append(merged, base[b])
240+
b++
241+
}
242+
}
243+
return merged
210244
}
211245

212246
func readFile(filename string) (envMap map[string]string, err error) {

godotenv_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,36 @@ func TestRoundtrip(t *testing.T) {
469469
if !reflect.DeepEqual(env, roundtripped) {
470470
t.Errorf("Expected '%s' to roundtrip as '%v', got '%v' instead", fixtureFilename, env, roundtripped)
471471
}
472+
}
473+
}
472474

475+
func TestEnvMapToSlice(t *testing.T) {
476+
expected := []string{
477+
"OPTION_A=1",
478+
"OPTION_B=2",
479+
"OPTION_C=",
480+
"OPTION_D=\\n",
481+
"OPTION_E=1",
482+
"OPTION_F=2",
483+
"OPTION_G=",
484+
"OPTION_H=\n",
485+
"OPTION_I=echo 'asd'",
486+
"OPTION_J=postgres://localhost:5432/database?sslmode=disable",
487+
}
488+
489+
converted := EnvMapToSortedSlice(map[string]string{
490+
"OPTION_I": "echo 'asd'",
491+
"OPTION_A": "1",
492+
"OPTION_B": "2",
493+
"OPTION_E": "1",
494+
"OPTION_C": "",
495+
"OPTION_D": "\\n",
496+
"OPTION_H": "\n",
497+
"OPTION_F": "2",
498+
"OPTION_G": "",
499+
"OPTION_J": "postgres://localhost:5432/database?sslmode=disable",
500+
})
501+
if !reflect.DeepEqual(expected, converted) {
502+
t.Errorf("Expected '%s' from EnvMapToSortedSlice got '%v' instead", expected, converted)
473503
}
474504
}

0 commit comments

Comments
 (0)