4 import ( |
4 import ( |
5 "bufio" |
5 "bufio" |
6 "fmt" |
6 "fmt" |
7 "io" |
7 "io" |
8 "os" |
8 "os" |
|
9 "path/filepath" |
9 "regexp" |
10 "regexp" |
|
11 "sort" |
|
12 "strconv" |
10 "strings" |
13 "strings" |
11 ) |
14 ) |
12 |
15 |
13 const ( |
16 const ( |
14 // Pattern for detecting valid line format |
17 // Pattern for detecting valid line format |
15 linePattern = `\A\s*(?:export\s+)?([\w\.]+)(?:\s*=\s*|:\s+?)('(?:\'|[^'])*'|"(?:\"|[^"])*"|[^#\n]+)?\s*(?:\s*\#.*)?\z` |
18 linePattern = `\A\s*(?:export\s+)?([\w\.]+)(?:\s*=\s*|:\s+?)('(?:\'|[^'])*'|"(?:\"|[^"])*"|[^#\n]+)?\s*(?:\s*\#.*)?\z` |
16 |
19 |
17 // Pattern for detecting valid variable within a value |
20 // Pattern for detecting valid variable within a value |
18 variablePattern = `(\\)?(\$)(\{?([A-Z0-9_]+)?\}?)` |
21 variablePattern = `(\\)?(\$)(\{?([A-Z0-9_]+)?\}?)` |
|
22 |
|
23 // Byte order mark character |
|
24 bom = "\xef\xbb\xbf" |
19 ) |
25 ) |
20 |
26 |
21 // Env holds key/value pair of valid environment variable |
27 // Env holds key/value pair of valid environment variable |
22 type Env map[string]string |
28 type Env map[string]string |
23 |
29 |
24 /* |
30 // 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. |
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. |
31 // When it's called with no argument, it will load `.env` file on the current path and set the environment variables. |
26 When it's called with no argument, it will load `.env` file on the current path and set the environment variables. |
32 // Otherwise, it will loop over the filenames parameter and set the proper environment variables. |
27 Otherwise, it will loop over the filenames parameter and set the proper environment variables. |
|
28 */ |
|
29 func Load(filenames ...string) error { |
33 func Load(filenames ...string) error { |
30 return loadenv(false, filenames...) |
34 return loadenv(false, filenames...) |
31 } |
35 } |
32 |
36 |
33 /* |
37 // OverLoad is a function to load a file or multiple files and then export and override the valid variables into environment variables. |
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 { |
38 func OverLoad(filenames ...string) error { |
37 return loadenv(true, filenames...) |
39 return loadenv(true, filenames...) |
38 } |
40 } |
39 |
41 |
40 /* |
42 // Must is wrapper function that will panic when supplied function returns an error. |
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) { |
43 func Must(fn func(filenames ...string) error, filenames ...string) { |
44 if err := fn(filenames...); err != nil { |
44 if err := fn(filenames...); err != nil { |
45 panic(err.Error()) |
45 panic(err.Error()) |
46 } |
46 } |
47 } |
47 } |
48 |
48 |
49 /* |
49 // Apply is a function to load an io Reader then export the valid variables into environment variables if they do not exist. |
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 { |
50 func Apply(r io.Reader) error { |
53 return parset(r, false) |
51 return parset(r, false) |
54 } |
52 } |
55 |
53 |
56 /* |
54 // OverApply is a function to load an io Reader then export and override the valid variables into environment variables. |
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 { |
55 func OverApply(r io.Reader) error { |
60 return parset(r, true) |
56 return parset(r, true) |
61 } |
57 } |
62 |
58 |
63 func loadenv(override bool, filenames ...string) error { |
59 func loadenv(override bool, filenames ...string) error { |
108 |
103 |
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. |
104 // 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. |
105 // 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. |
106 // This function is skipping any invalid lines and only processing the valid one. |
112 func Parse(r io.Reader) Env { |
107 func Parse(r io.Reader) Env { |
113 env, _ := StrictParse(r) |
108 env, _ := strictParse(r, false) |
114 return env |
109 return env |
115 } |
110 } |
116 |
111 |
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. |
112 // 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. |
113 // 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. |
114 // This function is returning an error if there are any invalid lines. |
120 func StrictParse(r io.Reader) (Env, error) { |
115 func StrictParse(r io.Reader) (Env, error) { |
|
116 return strictParse(r, false) |
|
117 } |
|
118 |
|
119 // Read is a function to parse a file line by line and returns the valid Env key/value pair of valid variables. |
|
120 // It expands the value of a variable from the environment variable but does not set the value to the environment itself. |
|
121 // This function is skipping any invalid lines and only processing the valid one. |
|
122 func Read(filename string) (Env, error) { |
|
123 f, err := os.Open(filename) |
|
124 if err != nil { |
|
125 return nil, err |
|
126 } |
|
127 defer f.Close() |
|
128 return strictParse(f, false) |
|
129 } |
|
130 |
|
131 // Unmarshal reads a string line by line and returns the valid Env key/value pair of valid variables. |
|
132 // It expands the value of a variable from the environment variable but does not set the value to the environment itself. |
|
133 // This function is returning an error if there are any invalid lines. |
|
134 func Unmarshal(str string) (Env, error) { |
|
135 return strictParse(strings.NewReader(str), false) |
|
136 } |
|
137 |
|
138 // Marshal outputs the given environment as a env file. |
|
139 // Variables will be sorted by name. |
|
140 func Marshal(env Env) (string, error) { |
|
141 lines := make([]string, 0, len(env)) |
|
142 for k, v := range env { |
|
143 if d, err := strconv.Atoi(v); err == nil { |
|
144 lines = append(lines, fmt.Sprintf(`%s=%d`, k, d)) |
|
145 } else { |
|
146 lines = append(lines, fmt.Sprintf(`%s=%q`, k, v)) |
|
147 } |
|
148 } |
|
149 sort.Strings(lines) |
|
150 return strings.Join(lines, "\n"), nil |
|
151 } |
|
152 |
|
153 // Write serializes the given environment and writes it to a file |
|
154 func Write(env Env, filename string) error { |
|
155 content, err := Marshal(env) |
|
156 if err != nil { |
|
157 return err |
|
158 } |
|
159 // ensure the path exists |
|
160 if err := os.MkdirAll(filepath.Dir(filename), 0o775); err != nil { |
|
161 return err |
|
162 } |
|
163 // create or truncate the file |
|
164 file, err := os.Create(filename) |
|
165 if err != nil { |
|
166 return err |
|
167 } |
|
168 defer file.Close() |
|
169 _, err = file.WriteString(content + "\n") |
|
170 if err != nil { |
|
171 return err |
|
172 } |
|
173 |
|
174 return file.Sync() |
|
175 } |
|
176 |
|
177 func strictParse(r io.Reader, override bool) (Env, error) { |
121 env := make(Env) |
178 env := make(Env) |
122 scanner := bufio.NewScanner(r) |
179 scanner := bufio.NewScanner(r) |
123 |
180 |
124 i := 1 |
181 firstLine := true |
125 bom := string([]byte{239, 187, 191}) |
|
126 |
182 |
127 for scanner.Scan() { |
183 for scanner.Scan() { |
128 line := scanner.Text() |
184 line := strings.TrimSpace(scanner.Text()) |
129 |
185 |
130 if i == 1 { |
186 if firstLine { |
131 line = strings.TrimPrefix(line, bom) |
187 line = strings.TrimPrefix(line, bom) |
132 } |
188 firstLine = false |
133 |
189 } |
134 i++ |
190 |
135 |
191 if line == "" || line[0] == '#' { |
136 err := parseLine(line, env) |
192 continue |
|
193 } |
|
194 |
|
195 quote := "" |
|
196 // look for the delimiter character |
|
197 idx := strings.Index(line, "=") |
|
198 if idx == -1 { |
|
199 idx = strings.Index(line, ":") |
|
200 } |
|
201 // look for a quote character |
|
202 if idx > 0 && idx < len(line)-1 { |
|
203 val := strings.TrimSpace(line[idx+1:]) |
|
204 if val[0] == '"' || val[0] == '\'' { |
|
205 quote = val[:1] |
|
206 // look for the closing quote character within the same line |
|
207 idx = strings.LastIndex(strings.TrimSpace(val[1:]), quote) |
|
208 if idx >= 0 && val[idx] != '\\' { |
|
209 quote = "" |
|
210 } |
|
211 } |
|
212 } |
|
213 // look for the closing quote character |
|
214 for quote != "" && scanner.Scan() { |
|
215 l := scanner.Text() |
|
216 line += "\n" + l |
|
217 idx := strings.LastIndex(l, quote) |
|
218 if idx > 0 && l[idx-1] == '\\' { |
|
219 // foud a matching quote character but it's escaped |
|
220 continue |
|
221 } |
|
222 if idx >= 0 { |
|
223 // foud a matching quote |
|
224 quote = "" |
|
225 } |
|
226 } |
|
227 |
|
228 if quote != "" { |
|
229 return env, fmt.Errorf("missing quotes") |
|
230 } |
|
231 |
|
232 err := parseLine(line, env, override) |
137 if err != nil { |
233 if err != nil { |
138 return env, err |
234 return env, err |
139 } |
235 } |
140 } |
236 } |
141 |
237 |
142 return env, nil |
238 return env, nil |
143 } |
239 } |
144 |
240 |
145 func parseLine(s string, env Env) error { |
241 var ( |
146 rl := regexp.MustCompile(linePattern) |
242 lineRgx = regexp.MustCompile(linePattern) |
147 rm := rl.FindStringSubmatch(s) |
243 unescapeRgx = regexp.MustCompile(`\\([^$])`) |
|
244 varRgx = regexp.MustCompile(variablePattern) |
|
245 ) |
|
246 |
|
247 func parseLine(s string, env Env, override bool) error { |
|
248 rm := lineRgx.FindStringSubmatch(s) |
148 |
249 |
149 if len(rm) == 0 { |
250 if len(rm) == 0 { |
150 return checkFormat(s, env) |
251 return checkFormat(s, env) |
151 } |
252 } |
152 |
253 |
153 key := rm[1] |
254 key := strings.TrimSpace(rm[1]) |
154 val := rm[2] |
255 val := strings.TrimSpace(rm[2]) |
155 |
256 |
156 // determine if string has quote prefix |
257 var hsq, hdq bool |
157 hdq := strings.HasPrefix(val, `"`) |
258 |
158 |
259 // check if the value is quoted |
159 // determine if string has single quote prefix |
260 if l := len(val); l >= 2 { |
160 hsq := strings.HasPrefix(val, `'`) |
261 l -= 1 |
161 |
262 // has double quotes |
162 // trim whitespace |
263 hdq = val[0] == '"' && val[l] == '"' |
163 val = strings.Trim(val, " ") |
264 // has single quotes |
164 |
265 hsq = val[0] == '\'' && val[l] == '\'' |
165 // remove quotes '' or "" |
266 |
166 rq := regexp.MustCompile(`\A(['"])(.*)(['"])\z`) |
267 // remove quotes '' or "" |
167 val = rq.ReplaceAllString(val, "$2") |
268 if hsq || hdq { |
|
269 val = val[1:l] |
|
270 } |
|
271 } |
168 |
272 |
169 if hdq { |
273 if hdq { |
170 val = strings.Replace(val, `\n`, "\n", -1) |
274 val = strings.ReplaceAll(val, `\n`, "\n") |
171 val = strings.Replace(val, `\r`, "\r", -1) |
275 val = strings.ReplaceAll(val, `\r`, "\r") |
172 |
276 |
173 // Unescape all characters except $ so variables can be escaped properly |
277 // Unescape all characters except $ so variables can be escaped properly |
174 re := regexp.MustCompile(`\\([^$])`) |
278 val = unescapeRgx.ReplaceAllString(val, "$1") |
175 val = re.ReplaceAllString(val, "$1") |
279 } |
176 } |
280 |
177 |
281 if !hsq { |
178 rv := regexp.MustCompile(variablePattern) |
282 fv := func(s string) string { |
179 fv := func(s string) string { |
283 return varReplacement(s, hsq, env, override) |
180 return varReplacement(s, hsq, env) |
284 } |
181 } |
285 val = varRgx.ReplaceAllStringFunc(val, fv) |
182 |
286 val = parseVal(val, env, hdq, override) |
183 val = rv.ReplaceAllStringFunc(val, fv) |
287 } |
184 val = parseVal(val, env) |
|
185 |
288 |
186 env[key] = val |
289 env[key] = val |
187 return nil |
290 return nil |
188 } |
291 } |
189 |
292 |
199 } |
302 } |
200 |
303 |
201 return nil |
304 return nil |
202 } |
305 } |
203 |
306 |
204 func varReplacement(s string, hsq bool, env Env) string { |
307 var varNameRgx = regexp.MustCompile(`(\$)(\{?([A-Z0-9_]+)\}?)`) |
205 if strings.HasPrefix(s, "\\") { |
308 |
206 return strings.TrimPrefix(s, "\\") |
309 func varReplacement(s string, hsq bool, env Env, override bool) string { |
|
310 if s == "" { |
|
311 return s |
|
312 } |
|
313 |
|
314 if s[0] == '\\' { |
|
315 // the dollar sign is escaped |
|
316 return s[1:] |
207 } |
317 } |
208 |
318 |
209 if hsq { |
319 if hsq { |
210 return s |
320 return s |
211 } |
321 } |
212 |
322 |
213 sn := `(\$)(\{?([A-Z0-9_]+)\}?)` |
323 mn := varNameRgx.FindStringSubmatch(s) |
214 rn := regexp.MustCompile(sn) |
|
215 mn := rn.FindStringSubmatch(s) |
|
216 |
324 |
217 if len(mn) == 0 { |
325 if len(mn) == 0 { |
218 return s |
326 return s |
219 } |
327 } |
220 |
328 |
221 v := mn[3] |
329 v := mn[3] |
222 |
330 |
223 replace, ok := env[v] |
331 if replace, ok := os.LookupEnv(v); ok && !override { |
224 if !ok { |
332 return replace |
225 replace = os.Getenv(v) |
333 } |
226 } |
334 |
227 |
335 if replace, ok := env[v]; ok { |
228 return replace |
336 return replace |
|
337 } |
|
338 |
|
339 return os.Getenv(v) |
229 } |
340 } |
230 |
341 |
231 func checkFormat(s string, env Env) error { |
342 func checkFormat(s string, env Env) error { |
232 st := strings.TrimSpace(s) |
343 st := strings.TrimSpace(s) |
233 |
344 |
234 if (st == "") || strings.HasPrefix(st, "#") { |
345 if st == "" || st[0] == '#' { |
235 return nil |
346 return nil |
236 } |
347 } |
237 |
348 |
238 if err := parseExport(st, env); err != nil { |
349 if err := parseExport(st, env); err != nil { |
239 return err |
350 return err |
240 } |
351 } |
241 |
352 |
242 return fmt.Errorf("line `%s` doesn't match format", s) |
353 return fmt.Errorf("line `%s` doesn't match format", s) |
243 } |
354 } |
244 |
355 |
245 func parseVal(val string, env Env) string { |
356 func parseVal(val string, env Env, ignoreNewlines bool, override bool) string { |
246 if strings.Contains(val, "=") { |
357 if strings.Contains(val, "=") && !ignoreNewlines { |
247 if !(val == "\n" || val == "\r") { |
358 kv := strings.Split(val, "\r") |
248 kv := strings.Split(val, "\n") |
359 |
249 |
360 if len(kv) > 1 { |
250 if len(kv) == 1 { |
361 val = kv[0] |
251 kv = strings.Split(val, "\r") |
362 for _, l := range kv[1:] { |
252 } |
363 _ = parseLine(l, env, override) |
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 } |
364 } |
261 } |
365 } |
262 } |
366 } |
263 |
367 |
264 return val |
368 return val |