1
2 package gotenv
3
4 import (
5 "bufio"
6 "bytes"
7 "fmt"
8 "io"
9 "os"
10 "path/filepath"
11 "regexp"
12 "sort"
13 "strconv"
14 "strings"
15
16 "golang.org/x/text/encoding/unicode"
17 "golang.org/x/text/transform"
18 )
19
20 const (
21
22 linePattern = `\A\s*(?:export\s+)?([\w\.]+)(?:\s*=\s*|:\s+?)('(?:\'|[^'])*'|"(?:\"|[^"])*"|[^#\n]+)?\s*(?:\s*\#.*)?\z`
23
24
25 variablePattern = `(\\)?(\$)(\{?([A-Z0-9_]+)?\}?)`
26 )
27
28
29 var (
30 bomUTF8 = []byte("\xEF\xBB\xBF")
31 bomUTF16LE = []byte("\xFF\xFE")
32 bomUTF16BE = []byte("\xFE\xFF")
33 )
34
35
36 type Env map[string]string
37
38
39
40
41 func Load(filenames ...string) error {
42 return loadenv(false, filenames...)
43 }
44
45
46 func OverLoad(filenames ...string) error {
47 return loadenv(true, filenames...)
48 }
49
50
51 func Must(fn func(filenames ...string) error, filenames ...string) {
52 if err := fn(filenames...); err != nil {
53 panic(err.Error())
54 }
55 }
56
57
58 func Apply(r io.Reader) error {
59 return parset(r, false)
60 }
61
62
63 func OverApply(r io.Reader) error {
64 return parset(r, true)
65 }
66
67 func loadenv(override bool, filenames ...string) error {
68 if len(filenames) == 0 {
69 filenames = []string{".env"}
70 }
71
72 for _, filename := range filenames {
73 f, err := os.Open(filename)
74 if err != nil {
75 return err
76 }
77
78 err = parset(f, override)
79 f.Close()
80 if err != nil {
81 return err
82 }
83 }
84
85 return nil
86 }
87
88
89 func parset(r io.Reader, override bool) error {
90 env, err := strictParse(r, override)
91 if err != nil {
92 return err
93 }
94
95 for key, val := range env {
96 setenv(key, val, override)
97 }
98
99 return nil
100 }
101
102 func setenv(key, val string, override bool) {
103 if override {
104 os.Setenv(key, val)
105 } else {
106 if _, present := os.LookupEnv(key); !present {
107 os.Setenv(key, val)
108 }
109 }
110 }
111
112
113
114
115 func Parse(r io.Reader) Env {
116 env, _ := strictParse(r, false)
117 return env
118 }
119
120
121
122
123 func StrictParse(r io.Reader) (Env, error) {
124 return strictParse(r, false)
125 }
126
127
128
129
130 func Read(filename string) (Env, error) {
131 f, err := os.Open(filename)
132 if err != nil {
133 return nil, err
134 }
135 defer f.Close()
136 return strictParse(f, false)
137 }
138
139
140
141
142 func Unmarshal(str string) (Env, error) {
143 return strictParse(strings.NewReader(str), false)
144 }
145
146
147
148 func Marshal(env Env) (string, error) {
149 lines := make([]string, 0, len(env))
150 for k, v := range env {
151 if d, err := strconv.Atoi(v); err == nil {
152 lines = append(lines, fmt.Sprintf(`%s=%d`, k, d))
153 } else {
154 lines = append(lines, fmt.Sprintf(`%s=%q`, k, v))
155 }
156 }
157 sort.Strings(lines)
158 return strings.Join(lines, "\n"), nil
159 }
160
161
162 func Write(env Env, filename string) error {
163 content, err := Marshal(env)
164 if err != nil {
165 return err
166 }
167
168 if err := os.MkdirAll(filepath.Dir(filename), 0o775); err != nil {
169 return err
170 }
171
172 file, err := os.Create(filename)
173 if err != nil {
174 return err
175 }
176 defer file.Close()
177 _, err = file.WriteString(content + "\n")
178 if err != nil {
179 return err
180 }
181
182 return file.Sync()
183 }
184
185
186
187 func splitLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
188 if atEOF && len(data) == 0 {
189 return 0, nil, bufio.ErrFinalToken
190 }
191
192 idx := bytes.IndexAny(data, "\r\n")
193 switch {
194 case atEOF && idx < 0:
195 return len(data), data, bufio.ErrFinalToken
196
197 case idx < 0:
198 return 0, nil, nil
199 }
200
201
202 eol := idx + 1
203
204 if len(data) > eol && data[eol-1] == '\r' && data[eol] == '\n' {
205 eol++
206 }
207
208 return eol, data[:idx], nil
209 }
210
211 func strictParse(r io.Reader, override bool) (Env, error) {
212 env := make(Env)
213
214 buf := new(bytes.Buffer)
215 tee := io.TeeReader(r, buf)
216
217
218 bomByteBuffer := make([]byte, 3)
219 _, err := tee.Read(bomByteBuffer)
220 if err != nil && err != io.EOF {
221 return env, err
222 }
223
224 z := io.MultiReader(buf, r)
225
226
227 var scanner *bufio.Scanner
228
229 if bytes.HasPrefix(bomByteBuffer, bomUTF8) {
230 scanner = bufio.NewScanner(transform.NewReader(z, unicode.UTF8BOM.NewDecoder()))
231 } else if bytes.HasPrefix(bomByteBuffer, bomUTF16LE) {
232 scanner = bufio.NewScanner(transform.NewReader(z, unicode.UTF16(unicode.LittleEndian, unicode.ExpectBOM).NewDecoder()))
233 } else if bytes.HasPrefix(bomByteBuffer, bomUTF16BE) {
234 scanner = bufio.NewScanner(transform.NewReader(z, unicode.UTF16(unicode.BigEndian, unicode.ExpectBOM).NewDecoder()))
235 } else {
236 scanner = bufio.NewScanner(z)
237 }
238
239 scanner.Split(splitLines)
240
241 for scanner.Scan() {
242 if err := scanner.Err(); err != nil {
243 return env, err
244 }
245
246 line := strings.TrimSpace(scanner.Text())
247 if line == "" || line[0] == '#' {
248 continue
249 }
250
251 quote := ""
252
253 idx := strings.Index(line, "=")
254 if idx == -1 {
255 idx = strings.Index(line, ":")
256 }
257
258 if idx > 0 && idx < len(line)-1 {
259 val := strings.TrimSpace(line[idx+1:])
260 if val[0] == '"' || val[0] == '\'' {
261 quote = val[:1]
262
263 idx = strings.LastIndex(strings.TrimSpace(val[1:]), quote)
264 if idx >= 0 && val[idx] != '\\' {
265 quote = ""
266 }
267 }
268 }
269
270 for quote != "" && scanner.Scan() {
271 l := scanner.Text()
272 line += "\n" + l
273 idx := strings.LastIndex(l, quote)
274 if idx > 0 && l[idx-1] == '\\' {
275
276 continue
277 }
278 if idx >= 0 {
279
280 quote = ""
281 }
282 }
283
284 if quote != "" {
285 return env, fmt.Errorf("missing quotes")
286 }
287
288 err := parseLine(line, env, override)
289 if err != nil {
290 return env, err
291 }
292 }
293
294 return env, scanner.Err()
295 }
296
297 var (
298 lineRgx = regexp.MustCompile(linePattern)
299 unescapeRgx = regexp.MustCompile(`\\([^$])`)
300 varRgx = regexp.MustCompile(variablePattern)
301 )
302
303 func parseLine(s string, env Env, override bool) error {
304 rm := lineRgx.FindStringSubmatch(s)
305
306 if len(rm) == 0 {
307 return checkFormat(s, env)
308 }
309
310 key := strings.TrimSpace(rm[1])
311 val := strings.TrimSpace(rm[2])
312
313 var hsq, hdq bool
314
315
316 if l := len(val); l >= 2 {
317 l -= 1
318
319 hdq = val[0] == '"' && val[l] == '"'
320
321 hsq = val[0] == '\'' && val[l] == '\''
322
323
324 if hsq || hdq {
325 val = val[1:l]
326 }
327 }
328
329 if hdq {
330 val = strings.ReplaceAll(val, `\n`, "\n")
331 val = strings.ReplaceAll(val, `\r`, "\r")
332
333
334 val = unescapeRgx.ReplaceAllString(val, "$1")
335 }
336
337 if !hsq {
338 fv := func(s string) string {
339 return varReplacement(s, hsq, env, override)
340 }
341 val = varRgx.ReplaceAllStringFunc(val, fv)
342 }
343
344 env[key] = val
345 return nil
346 }
347
348 func parseExport(st string, env Env) error {
349 if strings.HasPrefix(st, "export") {
350 vs := strings.SplitN(st, " ", 2)
351
352 if len(vs) > 1 {
353 if _, ok := env[vs[1]]; !ok {
354 return fmt.Errorf("line `%s` has an unset variable", st)
355 }
356 }
357 }
358
359 return nil
360 }
361
362 var varNameRgx = regexp.MustCompile(`(\$)(\{?([A-Z0-9_]+)\}?)`)
363
364 func varReplacement(s string, hsq bool, env Env, override bool) string {
365 if s == "" {
366 return s
367 }
368
369 if s[0] == '\\' {
370
371 return s[1:]
372 }
373
374 if hsq {
375 return s
376 }
377
378 mn := varNameRgx.FindStringSubmatch(s)
379
380 if len(mn) == 0 {
381 return s
382 }
383
384 v := mn[3]
385
386 if replace, ok := os.LookupEnv(v); ok && !override {
387 return replace
388 }
389
390 if replace, ok := env[v]; ok {
391 return replace
392 }
393
394 return os.Getenv(v)
395 }
396
397 func checkFormat(s string, env Env) error {
398 st := strings.TrimSpace(s)
399
400 if st == "" || st[0] == '#' {
401 return nil
402 }
403
404 if err := parseExport(st, env); err != nil {
405 return err
406 }
407
408 return fmt.Errorf("line `%s` doesn't match format", s)
409 }
410
View as plain text