@@ -39,16 +39,19 @@ const doubleQuoteSpecialChars = "\\\n\r\"!$`"
39
39
// godotenv.Load("fileone", "filetwo")
40
40
//
41
41
// 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
+ }
44
47
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 ( EnvMapToSlice ( 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
49
52
}
50
53
}
51
- return
54
+ return nil
52
55
}
53
56
54
57
// 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) {
63
66
//
64
67
// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars.
65
68
func Overload (filenames ... string ) (err error ) {
66
- filenames = filenamesOrDefault (filenames )
69
+ envMap , err := Read (filenames ... )
70
+ if err != nil {
71
+ return err
72
+ }
67
73
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 (), EnvMapToSlice ( 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
72
78
}
73
79
}
74
- return
80
+ return nil
75
81
}
76
82
77
83
// 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) {
82
88
83
89
for _ , filename := range filenames {
84
90
individualEnvMap , individualErr := readFile (filename )
85
-
86
91
if individualErr != nil {
87
- err = individualErr
88
- return // return early on a spazout
92
+ return nil , individualErr // Return early on a spazout.
89
93
}
90
94
91
95
for key , value := range individualEnvMap {
92
96
envMap [key ] = value
93
97
}
94
98
}
95
-
96
- return
99
+ return envMap , nil
97
100
}
98
101
99
102
// 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) {
107
110
}
108
111
109
112
if err = scanner .Err (); err != nil {
110
- return
113
+ return nil , err
111
114
}
112
115
113
116
for _ , fullLine := range lines {
114
117
if ! isIgnoredLine (fullLine ) {
115
118
var key , value string
116
119
key , value , err = parseLine (fullLine , envMap )
117
-
118
120
if err != nil {
119
- return
121
+ return nil , err
120
122
}
121
123
envMap [key ] = value
122
124
}
123
125
}
124
126
return
125
127
}
126
128
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.
128
130
func Unmarshal (str string ) (envMap map [string ]string , err error ) {
129
131
return Parse (strings .NewReader (str ))
130
132
}
131
133
132
- // Exec loads env vars from the specified filenames (empty map falls back to default)
133
- // then executes the cmd specified.
134
+ // EnvMapToSlice 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 EnvMapToSlice (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.
134
149
//
135
150
// Simply hooks up os.Stdin/err/out to the command and calls Run()
136
151
//
137
152
// If you want more fine grained control over your command it's recommended
138
153
// that you use `Load()` or `Read()` and the `os/exec` package yourself.
139
154
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
+ }
141
159
142
160
command := exec .Command (cmd , cmdArgs ... )
161
+ command .Env = MergeEnvSlices (EnvMapToSlice (envMap ), os .Environ ())
143
162
command .Stdin = os .Stdin
144
163
command .Stdout = os .Stdout
145
164
command .Stderr = os .Stderr
@@ -187,26 +206,41 @@ func filenamesOrDefault(filenames []string) []string {
187
206
return filenames
188
207
}
189
208
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 )
195
214
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 ) {
202
217
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
206
222
}
207
- }
208
223
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
210
244
}
211
245
212
246
func readFile (filename string ) (envMap map [string ]string , err error ) {
0 commit comments