|
1 // Package gotenv provides functionality to dynamically load the environment variables |
|
2 package gotenv |
|
3 |
|
4 import ( |
|
5 "bufio" |
|
6 "fmt" |
|
7 "io" |
|
8 "os" |
|
9 "regexp" |
|
10 "strings" |
|
11 ) |
|
12 |
|
13 const ( |
|
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 |
|
22 type Env map[string]string |
|
23 |
|
24 /* |
|
25 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. |
|
26 When it's called with no argument, it will load `.env` file on the current path and set the environment variables. |
|
27 Otherwise, it will loop over the filenames parameter and set the proper environment variables. |
|
28 */ |
|
29 func Load(filenames ...string) error { |
|
30 return loadenv(false, filenames...) |
|
31 } |
|
32 |
|
33 /* |
|
34 OverLoad is a function to load a file or multiple files and then export and override the valid variables into environment variables. |
|
35 */ |
|
36 func OverLoad(filenames ...string) error { |
|
37 return loadenv(true, filenames...) |
|
38 } |
|
39 |
|
40 /* |
|
41 Must is wrapper function that will panic when supplied function returns an error. |
|
42 */ |
|
43 func Must(fn func(filenames ...string) error, filenames ...string) { |
|
44 if err := fn(filenames...); err != nil { |
|
45 panic(err.Error()) |
|
46 } |
|
47 } |
|
48 |
|
49 /* |
|
50 Apply is a function to load an io Reader then export the valid variables into environment variables if they do not exist. |
|
51 */ |
|
52 func Apply(r io.Reader) error { |
|
53 return parset(r, false) |
|
54 } |
|
55 |
|
56 /* |
|
57 OverApply is a function to load an io Reader then export and override the valid variables into environment variables. |
|
58 */ |
|
59 func OverApply(r io.Reader) error { |
|
60 return parset(r, true) |
|
61 } |
|
62 |
|
63 func 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 :) |
|
86 func 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 |
|
99 func 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. |
|
112 func 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. |
|
120 func 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 |
|
145 func 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 |
|
190 func 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 |
|
204 func 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 |
|
231 func 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 |
|
245 func 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 } |