1 // Package gotenv provides functionality to dynamically load the environment variables
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 // Pattern for detecting valid line format
22 linePattern = `\A\s*(?:export\s+)?([\w\.]+)(?:\s*=\s*|:\s+?)('(?:\'|[^'])*'|"(?:\"|[^"])*"|[^#\n]+)?\s*(?:\s*\#.*)?\z`
23 24 // Pattern for detecting valid variable within a value
25 variablePattern = `(\\)?(\$)(\{?([A-Z0-9_]+)?\}?)`
26 )
27 28 // Byte order mark character
29 var (
30 bomUTF8 = []byte("\xEF\xBB\xBF")
31 bomUTF16LE = []byte("\xFF\xFE")
32 bomUTF16BE = []byte("\xFE\xFF")
33 )
34 35 // Env holds key/value pair of valid environment variable
36 type Env map[string]string
37 38 // Load is a function to load a file or multiple files and then export the valid variables into environment variables if they do not exist.
39 // When it's called with no argument, it will load `.env` file on the current path and set the environment variables.
40 // Otherwise, it will loop over the filenames parameter and set the proper environment variables.
41 func Load(filenames ...string) error {
42 return loadenv(false, filenames...)
43 }
44 45 // OverLoad is a function to load a file or multiple files and then export and override the valid variables into environment variables.
46 func OverLoad(filenames ...string) error {
47 return loadenv(true, filenames...)
48 }
49 50 // Must is wrapper function that will panic when supplied function returns an error.
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 // Apply is a function to load an io Reader then export the valid variables into environment variables if they do not exist.
58 func Apply(r io.Reader) error {
59 return parset(r, false)
60 }
61 62 // OverApply is a function to load an io Reader then export and override the valid variables into environment variables.
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 // parse and set :)
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 // Parse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables.
113 // It expands the value of a variable from the environment variable but does not set the value to the environment itself.
114 // This function is skipping any invalid lines and only processing the valid one.
115 func Parse(r io.Reader) Env {
116 env, _ := strictParse(r, false)
117 return env
118 }
119 120 // StrictParse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables.
121 // It expands the value of a variable from the environment variable but does not set the value to the environment itself.
122 // This function is returning an error if there are any invalid lines.
123 func StrictParse(r io.Reader) (Env, error) {
124 return strictParse(r, false)
125 }
126 127 // Read is a function to parse a file line by line and returns the valid Env key/value pair of valid variables.
128 // It expands the value of a variable from the environment variable but does not set the value to the environment itself.
129 // This function is skipping any invalid lines and only processing the valid one.
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 // Unmarshal reads a string line by line and returns the valid Env key/value pair of valid variables.
140 // It expands the value of a variable from the environment variable but does not set the value to the environment itself.
141 // This function is returning an error if there are any invalid lines.
142 func Unmarshal(str string) (Env, error) {
143 return strictParse(strings.NewReader(str), false)
144 }
145 146 // Marshal outputs the given environment as a env file.
147 // Variables will be sorted by name.
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 // Write serializes the given environment and writes it to a file
162 func Write(env Env, filename string) error {
163 content, err := Marshal(env)
164 if err != nil {
165 return err
166 }
167 // ensure the path exists
168 if err := os.MkdirAll(filepath.Dir(filename), 0o775); err != nil {
169 return err
170 }
171 // create or truncate the file
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 // splitLines is a valid SplitFunc for a bufio.Scanner. It will split lines on CR ('\r'), LF ('\n') or CRLF (any of the three sequences).
186 // If a CR is immediately followed by a LF, it is treated as a CRLF (one single line break).
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 // consume CR or LF
202 eol := idx + 1
203 // detect CRLF
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 // There can be a maximum of 3 BOM bytes.
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 // We chooes a different scanner depending on file encoding.
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 // look for the delimiter character
253 idx := strings.Index(line, "=")
254 if idx == -1 {
255 idx = strings.Index(line, ":")
256 }
257 // look for a quote character
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 // look for the closing quote character within the same line
263 idx = strings.LastIndex(strings.TrimSpace(val[1:]), quote)
264 if idx >= 0 && val[idx] != '\\' {
265 quote = ""
266 }
267 }
268 }
269 // look for the closing quote character
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 // foud a matching quote character but it's escaped
276 continue
277 }
278 if idx >= 0 {
279 // foud a matching quote
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 // check if the value is quoted
316 if l := len(val); l >= 2 {
317 l -= 1
318 // has double quotes
319 hdq = val[0] == '"' && val[l] == '"'
320 // has single quotes
321 hsq = val[0] == '\'' && val[l] == '\''
322 323 // remove quotes '' or ""
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 // Unescape all characters except $ so variables can be escaped properly
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 // the dollar sign is escaped
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