1// Package gotenv provides functionality to dynamically load the environment variables
2package gotenv
3
4import (
5	"bufio"
6	"fmt"
7	"io"
8	"os"
9	"regexp"
10	"strings"
11)
12
13const (
14	// Pattern for detecting valid line format
15	linePattern = `\A\s*(?:export\s+)?([\w\.]+)(?:\s*=\s*|:\s+?)('(?:\'|[^'])*'|"(?:\"|[^"])*"|[^#\n]+)?\s*(?:\s*\#.*)?\z`
16
17	// Pattern for detecting valid variable within a value
18	variablePattern = `(\\)?(\$)(\{?([A-Z0-9_]+)?\}?)`
19)
20
21// Env holds key/value pair of valid environment variable
22type Env map[string]string
23
24/*
25Load is a function to load a file or multiple files and then export the valid variables into environment variables if they do not exist.
26When it's called with no argument, it will load `.env` file on the current path and set the environment variables.
27Otherwise, it will loop over the filenames parameter and set the proper environment variables.
28*/
29func Load(filenames ...string) error {
30	return loadenv(false, filenames...)
31}
32
33/*
34OverLoad is a function to load a file or multiple files and then export and override the valid variables into environment variables.
35*/
36func OverLoad(filenames ...string) error {
37	return loadenv(true, filenames...)
38}
39
40/*
41Must is wrapper function that will panic when supplied function returns an error.
42*/
43func Must(fn func(filenames ...string) error, filenames ...string) {
44	if err := fn(filenames...); err != nil {
45		panic(err.Error())
46	}
47}
48
49/*
50Apply is a function to load an io Reader then export the valid variables into environment variables if they do not exist.
51*/
52func Apply(r io.Reader) error {
53	return parset(r, false)
54}
55
56/*
57OverApply is a function to load an io Reader then export and override the valid variables into environment variables.
58*/
59func OverApply(r io.Reader) error {
60	return parset(r, true)
61}
62
63func loadenv(override bool, filenames ...string) error {
64	if len(filenames) == 0 {
65		filenames = []string{".env"}
66	}
67
68	for _, filename := range filenames {
69		f, err := os.Open(filename)
70		if err != nil {
71			return err
72		}
73
74		err = parset(f, override)
75		if err != nil {
76			return err
77		}
78
79		f.Close()
80	}
81
82	return nil
83}
84
85// parse and set :)
86func parset(r io.Reader, override bool) error {
87	env, err := StrictParse(r)
88	if err != nil {
89		return err
90	}
91
92	for key, val := range env {
93		setenv(key, val, override)
94	}
95
96	return nil
97}
98
99func setenv(key, val string, override bool) {
100	if override {
101		os.Setenv(key, val)
102	} else {
103		if _, present := os.LookupEnv(key); !present {
104			os.Setenv(key, val)
105		}
106	}
107}
108
109// Parse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables.
110// It expands the value of a variable from the environment variable but does not set the value to the environment itself.
111// This function is skipping any invalid lines and only processing the valid one.
112func Parse(r io.Reader) Env {
113	env, _ := StrictParse(r)
114	return env
115}
116
117// StrictParse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables.
118// It expands the value of a variable from the environment variable but does not set the value to the environment itself.
119// This function is returning an error if there are any invalid lines.
120func StrictParse(r io.Reader) (Env, error) {
121	env := make(Env)
122	scanner := bufio.NewScanner(r)
123
124	i := 1
125	bom := string([]byte{239, 187, 191})
126
127	for scanner.Scan() {
128		line := scanner.Text()
129
130		if i == 1 {
131			line = strings.TrimPrefix(line, bom)
132		}
133
134		i++
135
136		err := parseLine(line, env)
137		if err != nil {
138			return env, err
139		}
140	}
141
142	return env, nil
143}
144
145func parseLine(s string, env Env) error {
146	rl := regexp.MustCompile(linePattern)
147	rm := rl.FindStringSubmatch(s)
148
149	if len(rm) == 0 {
150		return checkFormat(s, env)
151	}
152
153	key := rm[1]
154	val := rm[2]
155
156	// determine if string has quote prefix
157	hdq := strings.HasPrefix(val, `"`)
158
159	// determine if string has single quote prefix
160	hsq := strings.HasPrefix(val, `'`)
161
162	// trim whitespace
163	val = strings.Trim(val, " ")
164
165	// remove quotes '' or ""
166	rq := regexp.MustCompile(`\A(['"])(.*)(['"])\z`)
167	val = rq.ReplaceAllString(val, "$2")
168
169	if hdq {
170		val = strings.Replace(val, `\n`, "\n", -1)
171		val = strings.Replace(val, `\r`, "\r", -1)
172
173		// Unescape all characters except $ so variables can be escaped properly
174		re := regexp.MustCompile(`\\([^$])`)
175		val = re.ReplaceAllString(val, "$1")
176	}
177
178	rv := regexp.MustCompile(variablePattern)
179	fv := func(s string) string {
180		return varReplacement(s, hsq, env)
181	}
182
183	val = rv.ReplaceAllStringFunc(val, fv)
184	val = parseVal(val, env)
185
186	env[key] = val
187	return nil
188}
189
190func parseExport(st string, env Env) error {
191	if strings.HasPrefix(st, "export") {
192		vs := strings.SplitN(st, " ", 2)
193
194		if len(vs) > 1 {
195			if _, ok := env[vs[1]]; !ok {
196				return fmt.Errorf("line `%s` has an unset variable", st)
197			}
198		}
199	}
200
201	return nil
202}
203
204func varReplacement(s string, hsq bool, env Env) string {
205	if strings.HasPrefix(s, "\\") {
206		return strings.TrimPrefix(s, "\\")
207	}
208
209	if hsq {
210		return s
211	}
212
213	sn := `(\$)(\{?([A-Z0-9_]+)\}?)`
214	rn := regexp.MustCompile(sn)
215	mn := rn.FindStringSubmatch(s)
216
217	if len(mn) == 0 {
218		return s
219	}
220
221	v := mn[3]
222
223	replace, ok := env[v]
224	if !ok {
225		replace = os.Getenv(v)
226	}
227
228	return replace
229}
230
231func checkFormat(s string, env Env) error {
232	st := strings.TrimSpace(s)
233
234	if (st == "") || strings.HasPrefix(st, "#") {
235		return nil
236	}
237
238	if err := parseExport(st, env); err != nil {
239		return err
240	}
241
242	return fmt.Errorf("line `%s` doesn't match format", s)
243}
244
245func parseVal(val string, env Env) string {
246	if strings.Contains(val, "=") {
247		if !(val == "\n" || val == "\r") {
248			kv := strings.Split(val, "\n")
249
250			if len(kv) == 1 {
251				kv = strings.Split(val, "\r")
252			}
253
254			if len(kv) > 1 {
255				val = kv[0]
256
257				for i := 1; i < len(kv); i++ {
258					parseLine(kv[i], env)
259				}
260			}
261		}
262	}
263
264	return val
265}
266