Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 13, 19:15 UTC
cmd: Enhance .env (dotenv) file parsing
Basic support for quoted values, newlines in quoted values, and comments. Does not support variable or command expansion.
Matthew Holt committed
Apr 13, 2022 at 11:35 UTC
30b6d1f47a35d00fca7cff0daa2ff59a98c5a85e
2 files changed
+212
-16
cmd/main.go
+42
-16
@@ -368,42 +368,68 @@ func loadEnvFromFile(envFile string) error {
368
return nil
369
}
370
371
+// parseEnvFile parses an env file from KEY=VALUE format.
372
+// It's pretty naive. Limited value quotation is supported,
373
+// but variable and command expansions are not supported.
374
func parseEnvFile(envInput io.Reader) (map[string]string, error) {
375
envMap := make(map[string]string)
376
377
scanner := bufio.NewScanner(envInput)
375
- var line string
376
- lineNumber := 0
378
+ var lineNumber int
379
380
for scanner.Scan() {
379
- line = strings.TrimSpace(scanner.Text())
381
+ line := strings.TrimSpace(scanner.Text())
382
lineNumber++
383
382
- // skip lines starting with comment
383
- if strings.HasPrefix(line, "#") {
384
- continue
385
- }
386
-
387
- // skip empty line
388
- if len(line) == 0 {
384
+ // skip empty lines and lines starting with comment
385
+ if line == "" || strings.HasPrefix(line, "#") {
386
continue
387
}
388
389
+ // split line into key and value
390
fields := strings.SplitN(line, "=", 2)
391
if len(fields) != 2 {
392
return nil, fmt.Errorf("can't parse line %d; line should be in KEY=VALUE format", lineNumber)
393
}
394
+ key, val := fields[0], fields[1]
395
397
- if strings.Contains(fields[0], " ") {
398
- return nil, fmt.Errorf("bad key on line %d: contains whitespace", lineNumber)
399
- }
400
-
401
- key := fields[0]
402
- val := fields[1]
396
+ // sometimes keys are prefixed by "export " so file can be sourced in bash; ignore it here
397
+ key = strings.TrimPrefix(key, "export ")
398
399
+ // validate key and value
400
if key == "" {
401
return nil, fmt.Errorf("missing or empty key on line %d", lineNumber)
402
}
403
+ if strings.Contains(key, " ") {
404
+ return nil, fmt.Errorf("invalid key on line %d: contains whitespace: %s", lineNumber, key)
405
+ }
406
+ if strings.HasPrefix(val, " ") || strings.HasPrefix(val, "\t") {
407
+ return nil, fmt.Errorf("invalid value on line %d: whitespace before value: '%s'", lineNumber, val)
408
+ }
409
+
410
+ // remove any trailing comment after value
411
+ if commentStart := strings.Index(val, "#"); commentStart > 0 {
412
+ before := val[commentStart-1]
413
+ if before == '\t' || before == ' ' {
414
+ val = strings.TrimRight(val[:commentStart], " \t")
415
+ }
416
+ }
417
+
418
+ // quoted value: support newlines
419
+ if strings.HasPrefix(val, `"`) {
420
+ for !(strings.HasSuffix(line, `"`) && !strings.HasSuffix(line, `\"`)) {
421
+ val = strings.ReplaceAll(val, `\"`, `"`)
422
+ if !scanner.Scan() {
423
+ break
424
+ }
425
+ lineNumber++
426
+ line = strings.ReplaceAll(scanner.Text(), `\"`, `"`)
427
+ val += "\n" + line
428
+ }
429
+ val = strings.TrimPrefix(val, `"`)
430
+ val = strings.TrimSuffix(val, `"`)
431
+ }
432
+
433
envMap[key] = val
434
}
435
cmd/main_test.go
new
+170
@@ -0,0 +1,170 @@
1
+package caddycmd
2
+
3
+import (
4
+ "reflect"
5
+ "strings"
6
+ "testing"
7
+)
8
+
9
+func TestParseEnvFile(t *testing.T) {
10
+ for i, tc := range []struct {
11
+ input string
12
+ expect map[string]string
13
+ shouldErr bool
14
+ }{
15
+ {
16
+ input: `KEY=value`,
17
+ expect: map[string]string{
18
+ "KEY": "value",
19
+ },
20
+ },
21
+ {
22
+ input: `
23
+ KEY=value
24
+ OTHER_KEY=Some Value
25
+ `,
26
+ expect: map[string]string{
27
+ "KEY": "value",
28
+ "OTHER_KEY": "Some Value",
29
+ },
30
+ },
31
+ {
32
+ input: `
33
+ KEY=value
34
+ INVALID KEY=asdf
35
+ OTHER_KEY=Some Value
36
+ `,
37
+ shouldErr: true,
38
+ },
39
+ {
40
+ input: `
41
+ KEY=value
42
+ SIMPLE_QUOTED="quoted value"
43
+ OTHER_KEY=Some Value
44
+ `,
45
+ expect: map[string]string{
46
+ "KEY": "value",
47
+ "SIMPLE_QUOTED": "quoted value",
48
+ "OTHER_KEY": "Some Value",
49
+ },
50
+ },
51
+ {
52
+ input: `
53
+ KEY=value
54
+ NEWLINES="foo
55
+ bar"
56
+ OTHER_KEY=Some Value
57
+ `,
58
+ expect: map[string]string{
59
+ "KEY": "value",
60
+ "NEWLINES": "foo\n\tbar",
61
+ "OTHER_KEY": "Some Value",
62
+ },
63
+ },
64
+ {
65
+ input: `
66
+ KEY=value
67
+ ESCAPED="\"escaped quotes\"
68
+here"
69
+ OTHER_KEY=Some Value
70
+ `,
71
+ expect: map[string]string{
72
+ "KEY": "value",
73
+ "ESCAPED": "\"escaped quotes\"\nhere",
74
+ "OTHER_KEY": "Some Value",
75
+ },
76
+ },
77
+ {
78
+ input: `
79
+ export KEY=value
80
+ OTHER_KEY=Some Value
81
+ `,
82
+ expect: map[string]string{
83
+ "KEY": "value",
84
+ "OTHER_KEY": "Some Value",
85
+ },
86
+ },
87
+ {
88
+ input: `
89
+ =value
90
+ OTHER_KEY=Some Value
91
+ `,
92
+ shouldErr: true,
93
+ },
94
+ {
95
+ input: `
96
+ EMPTY=
97
+ OTHER_KEY=Some Value
98
+ `,
99
+ expect: map[string]string{
100
+ "EMPTY": "",
101
+ "OTHER_KEY": "Some Value",
102
+ },
103
+ },
104
+ {
105
+ input: `
106
+ EMPTY=""
107
+ OTHER_KEY=Some Value
108
+ `,
109
+ expect: map[string]string{
110
+ "EMPTY": "",
111
+ "OTHER_KEY": "Some Value",
112
+ },
113
+ },
114
+ {
115
+ input: `
116
+ KEY=value
117
+ #OTHER_KEY=Some Value
118
+ `,
119
+ expect: map[string]string{
120
+ "KEY": "value",
121
+ },
122
+ },
123
+ {
124
+ input: `
125
+ KEY=value
126
+ COMMENT=foo bar # some comment here
127
+ OTHER_KEY=Some Value
128
+ `,
129
+ expect: map[string]string{
130
+ "KEY": "value",
131
+ "COMMENT": "foo bar",
132
+ "OTHER_KEY": "Some Value",
133
+ },
134
+ },
135
+ {
136
+ input: `
137
+ KEY=value
138
+ WHITESPACE= foo
139
+ OTHER_KEY=Some Value
140
+ `,
141
+ shouldErr: true,
142
+ },
143
+ {
144
+ input: `
145
+ KEY=value
146
+ WHITESPACE=" foo bar "
147
+ OTHER_KEY=Some Value
148
+ `,
149
+ expect: map[string]string{
150
+ "KEY": "value",
151
+ "WHITESPACE": " foo bar ",
152
+ "OTHER_KEY": "Some Value",
153
+ },
154
+ },
155
+ } {
156
+ actual, err := parseEnvFile(strings.NewReader(tc.input))
157
+ if err != nil && !tc.shouldErr {
158
+ t.Errorf("Test %d: Got error but shouldn't have: %v", i, err)
159
+ }
160
+ if err == nil && tc.shouldErr {
161
+ t.Errorf("Test %d: Did not get error but should have", i)
162
+ }
163
+ if tc.shouldErr {
164
+ continue
165
+ }
166
+ if !reflect.DeepEqual(tc.expect, actual) {
167
+ t.Errorf("Test %d: Expected %v but got %v", i, tc.expect, actual)
168
+ }
169
+ }
170
+}